目录
- 1 缓存基本思想
- 2 Redis基础
- 2.1 Redis介绍
- 2.1 Redis单机版安装和使用
- 2.3 Redis数据类型和应用场景
- 3 Redis持久化
- 3.1 为什么要持久化
- 3.2 应用场景
- 4 Redis高级
- 4.1 发布与订阅
- 4.2 事务
- 4.3 Lua脚本
- 5 高可用方案
- 5.1 哨兵模式
- 5.2 集群与分区
- 5.3 官方cluster分区
- 5.4 容灾(failover)
1 缓存基本思想
什么是缓存?
缓存原指CPU上的一种高速存储器,它先于内存与CPU交换数据,速度很快
现在泛指存储在计算机上的原始数据的复制集,便于快速访问。
以空间换时间的一种技术。
缓存的使用场景
DB缓存,减轻DB服务器压力
一般情况下数据存在数据库中,应用程序直接操作数据库。
当访问量上万,数据库压力增大,可以采取的方案有:
读写分离,分库分表
当访问量达到10万、百万,需要引入缓存。
将已经访问过的内容或数据存储起来,当再次访问时先找缓存,缓存命中返回数据。不命中再找数据库,并回填缓存。
- 提高系统响应
数据库的数据是存在文件里,也就是硬盘。与内存做交换(swap)
在大量瞬间访问时(高并发)MySQL单机会因为频繁IO而造成无法响应。MySQL的InnoDB是有行锁,将数据缓存在Redis中,也就是存在了内存中。
内存天然支持高并发访问。可以瞬间处理大量请求。
qps读请求达到11万/S,写请求8万/S - 做Session分离
传统的session是由tomcat自己进行维护和管理。
集群或分布式环境,不同的tomcat管理各自的session。只能在各个tomcat之间,通过网络和Io进行session的复制,极大的影响了系统的性能。
1、各个Tomcat间复制session,性能损耗
2、不能保证各个Tomcat的Session数据同步
将登录成功后的Session信息,存放在Redis中,这样多个服务器(Tomcat)可以共享Session信息。 - Redis的作用是数据的临时存储
- 做分布式锁(Redis)
- 做乐观锁(Redis)
缓存的优势、代价
提升用户体验
提升系统性能
减轻服务器压力
使用缓存的代价
额外的硬件支出
高并发缓存失效
缓存与数据库数据同步
缓存并发竞争
缓存的读写模式
缓存有三种读写模式:
- Cache Aside Pattern(常用)
Cache Aside Pattern(旁路缓存),是最经典的缓存+数据库读写模式。
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
更新的时候,先更新数据库,然后再删除缓存。
为什么是删除缓存,而不是更新缓存呢?
1、缓存的值是一个结构:hash、list,更新数据需要遍历
先遍历(耗时)后修改
2、懒加载,使用的时候才更新缓存
使用的时候才从DB中加载,也可以采用异步的方式填充缓存,开启一个线程定时将DB的数据刷到缓存中。
高并发脏读的三种情况
- 先更新数据库,再更新缓存
update与commit之间,更新缓存,commit失败
则DB与缓存数据不一致 - 先删除缓存,再更新数据库
update与commit之间,有新的读,缓存空,读DB数据到缓存 数据是旧的数据
commit后 DB为新数据
则DB与缓存数据不一致 - 先更新数据库,再删除缓存(推荐)
update与commit之间,有新的读,缓存空,读DB数据到缓存 数据是旧的数据commit后 DB为新数据,则DB与缓存数据不一致。
此时可采用延时双删策略。
- Read/Write Through Pattern
应用程序只操作缓存,缓存操作数据库。
Read-Through(穿透读模式/直读模式):应用程序读缓存,缓存没有,由缓存回源到数据库,并写入缓存。(guavacache)
Write-Through(穿透写模式/直写模式):应用程序写缓存,缓存写数据库。
该种模式需要提供数据库的handler,开发较为复杂。 - Write Behind Caching Pattern
应用程序只更新缓存。
缓存通过异步的方式将数据批量或合并后更新到DB中。
不能时时同步,甚至会丢数据。
2 Redis基础
2.1 Redis介绍
什么是Redis
- Redis (Remote Dictionary Server)远程字典服务器,是用C语言开发的一个开源的高性能键值对( key-value )内存数据库。
- 它提供了五种数据类型来存储值:字符串类型、散列类型、列表类型、集合类型、有序集合类型。
- 它是一种 NoSQL 数据存储。
Redis应用场景
- 缓存使用,减轻DB压力
- DB使用,用于临时存储数据(字典表,购买记录)
- 解决分布式场景下Session分离问题(登录信息)
- 任务队列(秒杀、抢红包等等) 乐观锁
- 应用排行榜 zset
- 签到 bitmap
- 分布式锁
- 冷热数据交换
2.1 Redis单机版安装和使用
wget http://download.redis.io/releases/redis-5.0.5.tar.gz
tar -zxf redis-5.0.5.tar.gz
cd redis-5.0.5/src
make
mkdir /usr/redis -p
make install PREFIX=/usr/redis
Redis启动
启动命令: redis-server ,直接运行 bin/redis-server 将以前端模式启动
关闭命令: ctrl+c
启动缺点:客户端窗口关闭则 redis-server 程序结束,不推荐使用此方法
后端启动(守护进程启动)
cp redis.conf /usr/redis/bin/
vim redis.conf
# 将`daemonize`由`no`改为`yes`
daemonize yes
# 默认绑定的是回环地址,默认不能被其他机器访问
# bind 127.0.0.1
# 是否开启保护模式,由yes该为no
protected-mode no
# 启动服务
./redis-server redis.conf
# 后端启动的关闭方式
/redis-cli shutdown
命令说明
- redis-server :启动 redis 服务
- redis-cli :进入 redis 命令客户端
- redis-benchmark : 性能测试的工具
- redis-check-aof : aof 文件进行检查的工具
- redis-check-dump : rdb 文件进行检查的工具
- redis-sentinel : 启动哨兵监控服务
Redis命令行客户端
# -h:redis服务器的ip地址
# -p:redis实例的端口号
# 不写的话默认本机ip访问 端口为6379
./redis-cli -h 127.0.0.1 -p 6379
2.3 Redis数据类型和应用场景
Redis是一个Key-Value的存储系统,使用ANSI C语言编写。
key的类型是字符串。
value的数据类型有:
常用的:string字符串类型、list列表类型、set集合类型、sortedset(zset)有序集合类型、hash类型。
不常见的:bitmap位图类型、geo地理位置类型。
Redis5.0新增一种:stream类型
注意:Redis中命令是忽略大小写,(set SET),key是不忽略大小写的 (NAME name)
**Redis的Key的设计 **
1. 用:分割
2. 把表名转换为key前缀, 比如: user:
3. 第二段放置主键值
4. 第三段放置列名
比如:用户表user, 转换为redis的key-value存储
string字符串类型
Redis的String能表达3种值的类型:字符串、整数、浮点数 100.01 是个六位的串
常见操作命令如下表:
应用场景:
1、key和命令是字符串
2、普通的赋值
3、incr用于乐观锁
incr:递增数字,可用于实现乐观锁 watch(事务)
4、setnx用于分布式锁
当value不存在时采用赋值,可用于实现分布式锁list列表类型
list列表类型可以存储有序、可重复的元素,获取头部或尾部附近的记录是极快的。list的元素个数最多为2^32-1个(40亿)。
常见操作命令如下表:
set集合类型
Set:无序、唯一元素
集合中最大的成员数为 2^32 - 1
常见操作命令如下表:
geo地理位置类型
geo是Redis用来处理位置信息的。在Redis3.2中正式使用。主要是利用了Z阶曲线、Base32编码和geohash算法。
Z阶曲线
在x轴和y轴上将十进制数转化为二进制数,采用x轴和y轴对应的二进制数依次交叉后得到一个六位数编码。把数字从小到大依次连起来的曲线称为Z阶曲线,Z阶曲线是把多维转换成一维的一种方法。
Redis常用命令
官方命令大全网址:http://www.redis.cn/commands.htmlRedis的Java客户端—Jedis
1、关闭RedisServer端的防火墙
systemctl stop firewalld(默认)
systemctl disable firewalld.service(设置开启不启动)
2、新建maven项目后导入Jedis包
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
3、写程序
@Test
public void testConn(){
//与Redis建立连接 IP+port
Jedis redis = new Jedis("centos7-1", 6379);
//在Redis中写字符串 key value
redis.set("jedis:name:1","jd-zhangfei");
//获得Redis中字符串的值
System.out.println(redis.get("jedis:name:1"));
//在Redis中写list
redis.lpush("jedis:list:1","1","2","3","4","5");
//获得list的长度
System.out.println(redis.llen("jedis:list:1"));
}
缓存过期和淘汰策略
Redis性能高:
官方数据
读:110000次/s
写:81000次/s
长期使用,key会不断增加,Redis作为缓存使用,物理内存也会满,内存与硬盘交换(swap) 虚拟内存 ,频繁IO 性能急剧下降。
- maxmemory
不设置的场景
Redis的key是固定的,不会增加
Redis作为DB使用,保证数据的完整性,不能淘汰 , 可以做集群,横向扩展
缓存淘汰策略:禁止驱逐 (默认)
设置的场景
Redis是作为缓存使用,不断增加Key
maxmemory : 默认为0 无限大
问题:达到物理内存后性能急剧下架,甚至崩溃
内存与硬盘交换(swap) 虚拟内存 ,频繁IO 性能急剧下降
设置多少?
与业务有关,1个Redis实例,保证系统运行 1 G ,剩下的就都可以设置Redis物理内存的3/4
3 Redis持久化
3.1 为什么要持久化
Redis是内存数据库,宕机后数据会消失。
Redis重启后快速恢复数据,要提供持久化机制。
Redis持久化是为了快速的恢复数据而不是为了存储数据。
Redis有两种持久化方式:RDB和AOF注意:Redis持久化不保证数据的完整性。
当Redis用作DB时,DB数据要完整,所以一定要有一个完整的数据源(文件、mysql)
在系统启动时,从这个完整的数据源中将数据load到Redis中
数据量较小,不易改变,比如:字典库(xml、Table)
通过info命令可以查看关于持久化的信息。
- RDB
RDB(Redis DataBase),是redis默认的存储方式,RDB方式是通过快照(snapshotting )完成的。
触发快照的方式:
- 符合自定义配置的快照规则
- 执行save或者bgsave命令
- 执行flushall命令
- 执行主从复制操作 (第一次)
配置参数定期执行:
save "" # 不使用RDB存储 不能主从
save 900 1 # 表示15分钟(900秒钟)内至少1个键被更改则进行快照。
save 300 10 # 表示5分钟(300秒)内至少10个键被更改则进行快照。
save 60 10000 # 表示1分钟内至少10000个键被更改则进行快照。
RDB执行流程(原理):
1. Redis父进程首先判断:当前是否在执行save,或bgsave/bgrewriteaof(aof文件重写命令)的子进程,如果在执行则bgsave命令直接返回。
2. 父进程执行fork(调用OS函数复制主进程)操作创建子进程,这个复制过程中父进程是阻塞的,Redis不能执行来自客户端的任何命令。
3. 父进程fork后,bgsave命令返回”Background saving started”信息并不再阻塞父进程,并可以响应其他命令。
4. 子进程创建RDB文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换。(RDB始终完整)
5. 子进程发送信号给父进程表示完成,父进程更新统计信息。
6. 父进程fork子进程后,继续工作。
RDB文件结构
RDB的优缺点
优点:
RDB是二进制压缩文件,占用空间小,便于传输(传给slaver)
主进程fork子进程,可以最大化Redis性能,主进程不能太大,Redis的数据量不能太大,复制过程中主进程阻塞
缺点:
不保证数据完整性,会丢失最后一次快照以后更改的所有数据
- AOF
AOF(append only file)是Redis的另一种持久化方式。Redis默认情况下是不开启的。开启AOF持久化后,Redis 将所有对数据库进行过写入的命令(及其参数)(RESP)记录到 AOF 文件, 以此达到记录数据库状态的目的,这样当Redis重启后只要按顺序回放这些命令就会恢复到原始状态了。
AOF会记录过程,RDB只管结果
AOF持久化实现:
配置 redis.conf
# 可以通过修改redis.conf配置文件中的appendonly参数开启
appendonly yes
# AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的。
dir ./
# 默认的文件名是appendonly.aof,可以通过appendfilename参数修改
appendfilename appendonly.aof
AOF原理:
AOF文件中存储的是redis的命令,同步命令到 AOF 文件的整个过程可以分为三个阶段:
命令传播: 当一个 Redis 客户端需要执行命令时, 它通过网络连接, 将协议文本发送给 Redis 服务器。服务器在接到客户端的请求之后, 它会根据协议文本的内容, 选择适当的命令函数, 并将各个参数从字符串文本转换为 Redis 字符串对象( StringObject )。每当命令函数成功执行之后, 命令参数都会被传播到AOF 程序。
缓存追加: 当命令被传播到 AOF 程序之后, 程序会根据命令以及命令的参数, 将命令从字符串对象转换回原来的协议文本。协议文本生成之后, 它会被追加到 redis.h/redisServer 结构的 aof_buf 末尾。redisServer 结构维持着 Redis 服务器的状态, aof_buf 域则保存着所有等待写入到 AOF 文件的协议文本(RESP)。
文件写入和保存: AOF 缓存中的内容被写入到 AOF 文件末尾,如果设定的 AOF 保
存条件被满足的话, fsync 函数或者 fdatasync 函数会被调用,将写入的内容真正地保存到磁盘中。
每当服务器常规任务函数被执行、 或者事件处理器被执行时, aof.c/flushAppendOnlyFile 函数都会被调用, 这个函数执行以下两个工作:
WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。
SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。
AOF 保存模式:
Redis 目前支持三种 AOF 保存模式,它们分别是:
AOF_FSYNC_NO :不保存。 AOF_FSYNC_EVERYSEC :每一秒钟保存一次。(默认)
AOF_FSYNC_ALWAYS :每执行一个命令保存一次。
1. 不保存
在这种模式下, 每次调用 flushAppendOnlyFile 函数, WRITE 都会被执行, 但 SAVE 会被略过。
在这种模式下, SAVE 只会在以下任意一种情况中被执行:
Redis 被关闭 AOF 功能被关闭 系统的写缓存被刷新(可能是缓存已经被写满,或者定期保存操作被执行) 这三种情况下的 SAVE 操作都会引起 Redis 主进程阻塞。
2. 每一秒钟保存一次(推荐)
在这种模式中, SAVE 原则上每隔一秒钟就会执行一次, 因为 SAVE 操作是由后台子线程(fork)调用的, 所以它不会引起服务器主进程阻塞。
3. 每执行一个命令保存一次
在这种模式下,每次执行完一个命令之后, WRITE 和 SAVE 都会被执行。另外,因为 SAVE 是由 Redis 主进程执行的,所以在 SAVE 执行期间,主进程会被阻塞,不能接受命令请求。
AOF重写、触发方式、混合持久化:
AOF记录数据的变化过程,越来越大,需要重写"瘦身",当前的命令集的最小数据集。
Redis可以在 AOF体积变得过大时,自动地在后台(Fork子进程)对 AOF进行重写。重写后的新 AOF文件包含了恢复当前数据集所需的最小命令集合。 所谓的“重写”其实是一个有歧义的词语, 实际上,AOF 重写并不需要对原有的 AOF 文件进行任何写入和读取, 它针对的是数据库中键的当前值。
举例如下:
set s1 11
set s1 22 ------- > set s1 33
set s1 33
没有优化的:
set s1 11
set s1 22
set s1 33
优化后:
set s1 33
Redis 不希望 AOF 重写造成服务器无法处理请求, 所以 Redis 决定将 AOF 重写程序放到(后台)子进程里执行, 这样处理的最大好处是:
1、子进程进行 AOF 重写期间,主进程可以继续处理命令请求。
2、子进程带有主进程的数据副本,使用子进程而不是线程,可以在避免锁的情况下,保证数据的安全性。
不过, 使用子进程也有一个问题需要解决: 因为子进程在进行 AOF 重写期间, 主进程还需要继续处理命令, 而新的命令可能对现有的数据进行修改, 这会让当前数据库的数据和重写后的 AOF 文件中的数据不一致。
为了解决这个问题, Redis 增加了一个 AOF 重写缓存, 这个缓存在 fork 出子进程之后开始启用,Redis 主进程在接到新的写命令之后, 除了会将这个写命令的协议内容追加到现有的 AOF 文件之外,还会追加到这个缓存中。
重写过程分析(整个重写操作是绝对安全的):
Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
当子进程在执行 AOF 重写时, 主进程需要执行以下三个工作:
处理命令请求。 将写命令追加到现有的 AOF 文件中。 将写命令追加到 AOF 重写缓存中。 这样一来可以保证:
现有的 AOF 功能会继续执行,即使在 AOF 重写期间发生停机,也不会有任何数据丢失。 所有对数据库进行修改的命令都会被记录到 AOF 重写缓存中。 当子进程完成 AOF 重写之后, 它会向父进程发送一个完成信号, 父进程在接到完成信号之后, 会调用一个信号处理函数, 并完成以下工作:
将 AOF 重写缓存中的内容全部写入到新 AOF 文件中。 对新的 AOF 文件进行改名,覆盖原有的 AOF 文件。
Redis数据库里的+AOF重写过程中的命令------->新的AOF文件---->覆盖老的
当步骤 1 执行完毕之后, 现有 AOF 文件、新 AOF 文件和数据库三者的状态就完全一致了。
当步骤 2 执行完毕之后, 程序就完成了新旧两个 AOF 文件的交替。
这个信号处理函数执行完毕之后, 主进程就可以继续像往常一样接受命令请求了。 在整个 AOF 后台重写过程中, 只有最后的写入缓存和改名操作会造成主进程阻塞, 在其他时候, AOF 后台重写都不会对主进程造成阻塞, 这将 AOF 重写对性能造成的影响降到了最低。
触发方式:
1、配置触发
在redis.conf中配置
# 表示当前aof文件大小超过上一次aof文件大小的百分之多少的时候会进行重写。如果之前没有重写过,以
启动时aof文件大小为准
auto-aof-rewrite-percentage 100
# 限制允许重写最小aof文件大小,也就是文件大小小于64mb的时候,不需要进行优化
auto-aof-rewrite-min-size 64mb
2、执行bgrewriteaof命令
127.0.0.1:6379> bgrewriteaof
Background append only file rewriting started
- 混合持久化
RDB和AOF各有优缺点,Redis 4.0 开始支持 rdb 和 aof 的混合持久化。如果把混合持久化打开,aofrewrite 的时候就直接把 rdb 的内容写到 aof 文件开头。
RDB的头+AOF的身体---->appendonly.aof
开启混合持久化:
aof-use-rdb-preamble yes
AOF文件的载入与数据还原
因为AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍AOF文件里面保存的写命令,就可以还原服务器关闭之前的数据库状态 Redis读取AOF文件并还原数据库状态的详细步骤如下:
1、创建一个不带网络连接的伪客户端(fake client):因为Redis的命令只能在客户端上下文中执行,而载入AOF文件时所使用的命令直接来源于AOF文件而不是网络连接,所以服 务器使用了一个没有网络连接的伪客户端来执行AOF文件保存的写命令,伪客户端执行命令 的效果和带网络连接的客户端执行命令的效果完全一样
2、从AOF文件中分析并读取出一条写命令
3、使用伪客户端执行被读出的写命令
4、一直执行步骤2和步骤3,直到AOF文件中的所有写命令都被处理完毕为止 当完成以上步骤之后,AOF文件所保存的数据库状态就会被完整地还原出来,整个过程如下图所示:
RDB与AOF对比:
1、RDB存某个时刻的数据快照,采用二进制压缩存储,AOF存操作命令,采用文本存储(混合)
2、RDB性能高、AOF性能较低
3、RDB在配置触发状态会丢失最后一次快照以后更改的所有数据,AOF设置为每秒保存一次,则最多丢2秒的数据
4、Redis以主服务器模式运行,RDB不会保存过期键值对数据,Redis以从服务器模式运行,RDB会保存过期键值对,当主服务器向从服务器同步时,再清空过期键值对。
AOF写入文件时,对过期的key会追加一条del命令,当执行AOF重写时,会忽略过期key和del命令。
3.2 应用场景
内存数据库 rdb+aof 数据不容易丢。
有原始数据源: 每次启动时都从原始数据源中初始化 ,则 不用开启持久化 (数据量较小)
缓存服务器 rdb 一般 性能高。
在数据还原时,有rdb+aof 则还原aof,因为RDB会造成文件的丢失,AOF相对数据要完整。
只有rdb,则还原rdb。
lg的配置策略
追求高性能:都不开 redis宕机 从数据源恢复
字典库 : 不驱逐,保证数据完整性 不开持久化
用作DB 不能主从 数据量小
做缓存 较高性能: 开rdb
Redis数据量存储过大,性能突然下降,fork 时间过长,阻塞主进程,则只开启AOF。
4 Redis高级
4.1 发布与订阅
Redis提供了发布订阅功能,可以用于消息的传输
Redis的发布订阅机制包括三个部分,publisher,subscriber和Channel
发布者和订阅者都是Redis客户端,Channel则为Redis服务器端。
发布者将消息发送到某个的频道,订阅了这个频道的订阅者就能接收到这条消息。
频道/模式的订阅与退订
subscribe:订阅 subscribe channel1 channel2 …
Redis客户端1订阅频道1和频道2
127.0.0.1:6379> subscribe ch1 ch2
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "ch1"
3) (integer) 1
1) "subscribe"
2) "ch2"
3) (integer) 2
publish:发布消息 publish channel message
Redis客户端2将消息发布在频道1和频道2上
127.0.0.1:6379> publish ch1 hello
(integer) 1
127.0.0.1:6379> publish ch2 world
(integer) 1
# Redis客户端1接收到频道1和频道2的消息
1) "message"
2) "ch1"
3) "hello"
1) "message"
2) "ch2"
3) "world"
unsubscribe:退订 channel
发布订阅的机制:
订阅某个频道或模式:
客户端(client):
- 属性为pubsub_channels,该属性表明了该客户端订阅的所有频道
- 属性为pubsub_patterns,该属性表示该客户端订阅的所有模式
服务器端(RedisServer):
- 属性为pubsub_channels,该服务器端中的所有频道以及订阅了这个频道的客户端
- 属性为pubsub_patterns,该服务器端中的所有模式和订阅了这些模式的客户端
当客户端向某个频道发送消息时,Redis首先在redisServer中的pubsub_channels中找出键为该频道的结点,遍历该结点的值,即遍历订阅了该频道的所有客户端,将消息发送给这些客户端。
然后,遍历结构体redisServer中的pubsub_patterns,找出包含该频道的模式的结点,将消息发送给订阅了该模式的客户端。
4.2 事务
- ACID回顾
- Atomicity(原子性):构成事务的的所有操作必须是一个逻辑单元,要么全部执行,要么全部不执行。
Redis:一个队列中的命令 执行或不执行 - Consistency(一致性):数据库在事务执行前后状态都必须是稳定的或者是一致的。
Redis: 集群中不能保证时时的一致性,只能是最终一致性 - Isolation(隔离性):事务之间不会相互影响。
Redis: 命令是顺序执行的,在一个事务中,有可能被执行其他客户端的命令的 - Durability(持久性):事务执行成功后必须全部写入磁盘。
Redis有持久化但不保证 数据的完整性
- Redis事务
Redis的事务是通过multi、exec、discard和watch这四个命令来完成的。
Redis的单个命令都是原子性的,所以这里需要确保事务性的对象是命令集合。
Redis将命令集合序列化并确保处于同一事务的命令集合连续且不被打断的执行
Redis不支持回滚操作 - 事务命令
multi:用于标记事务块的开始,Redis会将后续的命令逐个放入队列中,然后使用exec原子化地执行这个命令队列
exec:执行命令队列
discard:清除命令队列
watch:监视key 版本改变后 exec就会提交失败
unwatch:清除监视key - 事务的执行
- 事务开始
在RedisClient中,有属性flags,用来表示是否在事务中flags=REDIS_MULTI - 命令入队
RedisClient将命令存放在事务队列中(EXEC,DISCARD,WATCH,MULTI除外) - 事务队列
multiCmd *commands 用于存放命令 - 执行事务
RedisClient向服务器端发送exec命令,RedisServer会遍历事务队列,执行队列中的命令,最后将执行的结果一次性返回给客户端。
4.3 Lua脚本
lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
Lua应用场景:游戏开发、独立应用脚本、Web应用脚本、扩展和数据库插件。
nginx上使用lua 实现高并发
OpenRestry:一个可伸缩的基于Nginx的Web平台,是在nginx之上集成了lua模块的第三方服务器
OpenRestry是一个通过Lua扩展Nginx实现的可伸缩的Web平台,内部集成了大量精良的Lua库、第三方模块以及大多数的依赖项。 用于方便地搭建能够处理超高并发(日活千万级别)、扩展性极高的动态Web应用、Web服务和动态网关。 功能和nginx类似,就是由于支持lua动态脚本,所以更加灵活。
OpenRestry通过Lua脚本扩展nginx功能,可提供负载均衡、请求路由、安全认证、服务鉴权、流量控制与日志监控等服务。
类似的还有Kong(Api Gateway)、tengine(阿里)
创建并修改lua环境
下载:
地址:http://www.lua.org/download.html 可以本地下载上传到linux,也可以使用curl命令在linux系统中进行在线下载
curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz
安装:
yum -y install readline-devel ncurses-devel
tar -zxvf lua-5.3.5.tar.gz
#在src目录下
make linux
或make install
如果报错,说找不到readline/readline.h, 可以通过yum命令安装:
yum -y install readline-devel ncurses-devel
# 安装后再
make linux / make install
Lua环境协作组件
从Redis2.6.0版本开始,通过内置的lua编译/解释器,可以使用EVAL命令对lua脚本进行求值。
脚本的命令是原子的,RedisServer在执行脚本命令中,不允许插入新的命令
脚本的命令可以复制,RedisServer在获得脚本后不执行,生成标识返回,Client根据标识就可以随时执行
5 高可用方案
“高可用性”(High Availability)通常来描述一个系统经过专门的设计,从而减少停工时间,而保持其服务的高度可用性。CAP的A AP模型
单机的Redis是无法保证高可用性的,当Redis服务器宕机后,即使在有持久化的机制下也无法保证不丢失数据。
所以我们采用Redis多机和集群的方式来保证Redis的高可用性。
redis主从+sentinel监视。
当redis主节点挂掉后,进行选举,从节点中升为主节点。此时当原主节点重新启动后,会直接变为从节点。
5.1 哨兵模式
哨兵(sentinel)是Redis的高可用性(High Availability)的解决方案:
由一个或多个sentinel实例组成sentinel集群可以监视一个或多个主服务器和多个从服务器。
当主服务器进入下线状态时,sentinel可以将该主服务器下的某一从服务器升级为主服务器继续提供服务,从而保证redis的高可用性。
获取主服务器信息
Sentinel默认每10s一次,向被监控的主服务器发送info命令,获取主服务器和其下属从服务器的信息。
检测主观下线状态
自己连接不上认为监听服务器下线了。
Sentinel每秒一次向所有与它建立了命令连接的实例(主服务器、从服务器和其他Sentinel)发送PING命令
实例在down-after-milliseconds毫秒内返回无效回复(除了+PONG、-LOADING、-MASTERDOWN外)实例在down-after-milliseconds毫秒内无回复(超时)Sentinel就会认为该实例主观下线(SDown)
检查客观下线状态
当一个Sentinel将一个主服务器判断为主观下线后,Sentinel会向同时监控这个主服务器的所有其他Sentinel发送查询命令,判断它们是否也认为主服务器下线。如果达到Sentinel配置中的quorum数量的Sentinel实例都判断主服务器为主观下线,则该主服务器就会被判定为客观下线(ODown)。
选举Leader Sentinel
当一个主服务器被判定为客观下线后,监视这个主服务器的所有Sentinel会通过选举算法(raft),选出一个Leader Sentinel去执行failover(故障转移)操作。
Raft算法:
Raft协议是用来解决分布式系统一致性问题的协议。
Raft协议描述的节点共有三种状态:Leader, Follower, Candidate。
term:Raft协议将时间切分为一个个的Term(任期),可以认为是一种“逻辑时间”。
选举流程:
Raft采用心跳机制触发Leader选举
系统启动后,全部节点初始化为Follower,term为0。
节点如果收到了RequestVote或者AppendEntries,就会保持自己的Follower身份。节点如果一段时间内没收到AppendEntries消息,在该节点的超时时间内还没发现Leader,Follower就会转换成Candidate,自己开始竞选Leader。一旦转化为Candidate,该节点立即开始下面几件事情:
- 增加自己的term。
- 启动一个新的定时器。
- 给自己投一票。
- 向所有其他节点发送RequestVote,并等待其他节点的回复。
如果在计时器超时前,节点收到多数节点的同意投票,就转换成Leader。同时向所有其他节点发送AppendEntries,告知自己成为了Leader。每个节点在一个term内只能投一票,采取先到先得的策略,Candidate前面说到已经投给了自己,Follower会投给第一个收到RequestVote的节点。Raft协议的定时器采取随机超时时间,这是选举Leader的关键。在同一个term内,先转为Candidate的节点会先发起投票,从而获得多数票。
Sentinel的leader选举流程
1、某Sentinel认定master客观下线后,该Sentinel会先看看自己有没有投过票,如果自己已经投过票给其他Sentinel了,在一定时间内自己就不会成为Leader。
2、如果该Sentinel还没投过票,那么它就成为Candidate。
3、Sentinel需要完成几件事情:
- 更新故障转移状态为start
- 当前epoch加1,相当于进入一个新term,在Sentinel中epoch就是Raft协议中的term。
- 向其他节点发送 is-master-down-by-addr 命令请求投票。命令会带上自己的epoch。
- 给自己投一票(leader、leader_epoch)
4、当其它哨兵收到此命令时,可以同意或者拒绝它成为领导者;(通过判断epoch)
5、Candidate会不断的统计自己的票数,直到他发现认同他成为Leader的票数超过一半而且超过它配置的quorum,这时它就成为了Leader。
6、其他Sentinel等待Leader从slave选出master后,检测到新的master正常工作后,就会去掉客观下线的标识。
故障转移
当选举出Leader Sentinel后,Leader Sentinel会对下线的主服务器执行故障转移操作,主要有三个步骤:
- 它会将失效 Master 的其中一个 Slave 升级为新的 Master , 并让失效 Master 的其他 Slave 改为复制新的 Master ;
- 当客户端试图连接失效的 Master 时,集群也会向客户端返回新 Master 的地址,使得集群可以使用现在的 Master 替换失效 Master 。
- Master 和 Slave 服务器切换后, Master 的 redis.conf 、 Slave 的 redis.conf 和sentinel.conf 的配置文件的内容都会发生相应的改变,即, Master 主服务器的 redis.conf配置文件中会多一行 replicaof 的配置, sentinel.conf 的监控目标会随之调换。
主服务器的选择
哨兵leader根据以下规则从客观下线的主服务器的从服务器中选择出新的主服务器。
- 过滤掉主观下线的节点
- 选择slave-priority最高的节点,如果由则返回没有就继续选择
- 选择出复制偏移量最大的系节点,因为复制偏移量越大则数据复制的越完整,如果由就返回了,没有就继续
- 选择run_id最小的节点,因为run_id越小说明重启次数越少
5.2 集群与分区
分区是将数据分布在多个Redis实例(Redis主机)上,以至于每个实例只包含一部分数据。
分区的意义
- 性能的提升
单机Redis的网络I/O能力和计算资源是有限的,将请求分散到多台机器,充分利用多台机器的计算能力可网络带宽,有助于提高Redis总体的服务能力。 - 存储能力的横向扩展
即使Redis的服务能力能够满足应用需求,但是随着存储数据的增加,单台机器受限于机器本身的存储容量,将数据分散到多台机器上存储使得Redis服务可以横向扩展。
分区的方式
根据分区键(id)进行分区:
- 范围分区
根据id数字的范围比如1–10000、100001–20000…90001-100000,每个范围分到不同的Redis实例中。
好处:实现简单,方便迁移和扩展
缺陷:热点数据分布不均,性能损失 - hash分区
利用简单的hash算法即可:Redis实例=hash(key)%N
key:要进行分区的键,比如user_id
N:Redis实例个数(Redis主机)
好处:支持任何类型的key,热点分布较均匀,性能较好。
缺陷:迁移复杂,需要重新计算,扩展较差(利用一致性hash环) - client端分区
对于一个给定的key,客户端直接选择正确的节点来进行读写。许多Redis客户端都实现了客户端分区(JedisPool),也可以自行编程实现。 - 普通Hash的优势
实现简单,热点数据分布均匀
普通Hash的缺陷
节点数固定,扩展的话需要重新计算,查询时必须用分片的key来查,一旦key改变,数据就查不出了,所以要使用不易改变的key进行分片。
一致性hash
基本概念
普通hash是对主机数量取模,而一致性hash是对2-32(4 294 967 296)取模。我们把2-32想象成一个圆,就像钟表一样,钟表的圆可以理解成由60个点组成的圆,而此处我们把这个圆想象成由2-32个点组成的圆,示意图如下:
圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、5、6……直到2-32-1,也就是说0点左侧的第一个点代表2-32-1 。我们把这个由2的32次方个点组成的圆环称为hash环。
假设我们有3台缓存服务器,服务器A、服务器B、服务器C,那么,在生产环境中,这三台服务器肯定有自己的IP地址,我们使用它们各自的IP地址进行哈希计算,使用哈希后的结果对2-32取模,可以使用
如下公式:
hash(服务器的IP地址) % 2-32
通过上述公式算出的结果一定是一个0到2-32-1之间的一个整数,我们就用算出的这个整数,代表服务器A、服务器B、服务器C,既然这个整数肯定处于0到2-32-1之间,那么,上图中的hash环上必定有一个点与这个整数对应,也就是服务器A、服务器B、服务C就可以映射到这个环上,如下图:
现在服务器与数据都被映射到了hash环上,上图中的数据将会被缓存到服务器A上,因为从数据的位置开始,沿顺时针方向遇到的第一个服务器就是A服务器,所以,上图中的数据将会被缓存到服务器A上。如图:
将缓存服务器与被缓存对象都映射到hash环上以后,从被缓存对象的位置出发,沿顺时针方向遇到的第一个服务器,就是当前对象将要缓存于的服务器,由于被缓存对象与服务器hash后的值是固定的,所以,在服务器不变的情况下,数据必定会被缓存到固定的服务器上,那么,当下次想要访问这个数据时,只要再次使用相同的算法进行计算,即可算出这个数据被缓存在哪个服务器上,直接去对应的服务器查找对应的数据即可。
优点
添加或移除节点时,数据只需要做部分的迁移,比如上图中把C服务器移除,则数据4迁移到服务器A中,而其他的数据保持不变。添加效果是一样的。hash环偏移
在介绍一致性哈希的概念时,我们理想化的将3台服务器均匀的映射到了hash环上。也就是说数据的范围是2^32/N。但实际情况往往不是这样的。有可能某个服务器的数据会很多,某个服务器的数据会很少,造成服务器性能不平均。这种现象称为hash环偏移。
理论上我们可以通过增加服务器的方式来减少偏移,但这样成本较高,所以我们可以采用虚拟节点的方式,也就是虚拟服务器,如图:
“虚拟节点"是"实际节点”(实际的物理服务器)在hash环上的复制品,一个实际节点可以对应多个虚拟节点。从上图可以看出,A、B、C三台服务器分别虚拟出了一个虚拟节点,当然,如果你需要,也可以虚拟出更多的虚拟节点。引入虚拟节点的概念后,缓存的分布就均衡多了,上图中,1号、3号数据被缓存在服务器A中,5号、4号数据被缓存在服务器B中,6号、2号数据被缓存在服务器C中,如果你还不放心,可以虚拟出更多的虚拟节点,以便减小hash环偏斜所带来的影响,虚拟节点越多,hash环上的节点就越多,缓存被均匀分布的概率就越大。
缺点
复杂度高,客户端需要自己处理数据路由、高可用、故障转移等问题。使用分区,数据的处理会变得复杂,不得不对付多个redis数据库和AOF文件,不得在多个实例和主机之间持久化你的数据。
5.3 官方cluster分区
Redis3.0之后,Redis官方提供了完整的集群解决方案。
方案采用去中心化的方式,包括:sharding(分区)、replication(复制)、failover(故障转移)。称为RedisCluster。
Redis5.0前采用redis-trib进行集群的创建和管理,需要ruby支持。
Redis5.0可以直接使用Redis-cli进行集群的创建和管理。
去中心化
RedisCluster由多个Redis节点组构成,是一个P2P无中心节点的集群架构,依靠Gossip协议传播的集群。
Gossip协议
Gossip协议是一个通信协议,一种传播消息的方式。
起源于:病毒传播
Gossip协议基本思想就是:
一个节点周期性(每秒)随机选择一些节点,并把信息传递给这些节点。这些收到信息的节点接下来会做同样的事情,即把这些信息传递给其他一些随机选择的节点。信息会周期性的传递给N个目标节点。这个N被称为fanout(扇出)
slot
redis-cluster把所有的物理节点映射到[0-16383]个slot上,基本上采用平均分配和连续分配的方式。比如上图中有5个主节点,这样在RedisCluster创建时,slot槽可按下表分配:
RedisCluster的优势
- 高性能
Redis Cluster 的性能与单节点部署是同级别的。多主节点、负载均衡、读写分离 - 高可用
Redis Cluster 支持标准的 主从复制配置来保障高可用和高可靠。
failover
Redis Cluster 也实现了一个类似 Raft 的共识方式,来保障整个集群的可用性。 - 易扩展
向 Redis Cluster 中添加新节点,或者移除节点,都是透明的,不需要停机。水平、垂直方向都非常容易扩展。数据分区,海量数据,数据存储 - 原生
部署 Redis Cluster 不需要其他的代理或者工具,而且 Redis Cluster 和单机 Redis 几乎完全兼
容。
创建Redis集群(创建时Redis里不要有数据)
./redis-cli --cluster create 192.168.80.10:7001 192.168.80.10:7002 192.168.80.10:7003 192.168.80.10:7004 192.168.80.10:7005 192.168.80.10:7006 --cluster-replicas 1
# 命令客户端连接集群
./redis-cli -h 127.0.0.1 -p 7001 -c
# 查看集群的命令
## 查看集群状态
cluster info
# 查看集群中的节点:
cluster nodes
分片
不同节点分组服务于相互无交集的分片(sharding),Redis Cluster 不存在单独的proxy或配置服务器,所以需要将客户端路由到目标的分片。
客户端路由
Redis Cluster的客户端相比单机Redis 需要具备路由语义的识别能力,且具备一定的路由缓存能力。
moved重定向
1.每个节点通过通信都会共享Redis Cluster中槽和集群中对应节点的关系
2.客户端向Redis Cluster的任意节点发送命令,接收命令的节点会根据CRC16规则进行hash运算与16384取余,计算自己的槽和对应节点
3.如果保存数据的槽被分配给当前节点,则去槽中执行命令,并把命令执行结果返回给客户端
4.如果保存数据的槽不在当前节点的管理范围内,则向客户端返回moved重定向异常
5.客户端接收到节点返回的结果,如果是moved异常,则从moved异常中获取目标节点的信息
6.客户端向目标节点发送命令,获取命令执行结果
ask重定向
在对集群进行扩容和缩容时,需要对槽及槽中数据进行迁移.
当客户端向某个节点发送命令,节点向客户端返回moved异常,告诉客户端数据对应的槽的节点信息如果此时正在进行集群扩展或者缩空操作,当客户端向正确的节点发送命令时,槽及槽中数据已经被迁移到别的节点了,就会返回ask,这就是ask重定向机制
1.客户端向目标节点发送命令,目标节点中的槽已经迁移支别的节点上了,此时目标节点会返回ask转向给客户端
2.客户端向新的节点发送Asking命令给新的节点,然后再次向新节点发送命令
3.新节点执行命令,把命令执行结果返回给客户端
扩容
添加主节点
mkdir redis-cluster/7007
make install PREFIX=/var/redis-cluster/7007
# 添加7007结点作为新节点,并启动
# 执行命令:
/redis-cli --cluster add-node 192.168.80.10:7007 192.168.80.10:7001
# 查看集群结点发现7007已添加到集群中
cluster nodes
# hash槽重新分配(数据迁移)
# 添加完主节点需要对主节点进行hash槽分配,这样该主节才可以存储数据。
cluster nodes
# 给刚添加的7007结点分配槽
./redis-cli --cluster reshard 192.168.80.10:7007
# 输入:3000,表示要给目标节点分配3000个槽
How many slots do you want to move (from 1 to 16384)? 3000
# 输入接收槽的结点id
What is the receiving node ID?
# PS:这里准备给7007分配槽,通过cluster nodes查看7007结点id
# 输入源结点id
# 输入:all
# 输入yes开始移动槽到目标结点id
5.4 容灾(failover)
故障检测
集群中的每个节点都会定期地(每秒)向集群中的其他节点发送PING消息。如果在一定时间内(cluster-node-timeout),发送ping的节点A没有收到某节点B的pong回应,则A将B标识为pfail。
A在后续发送ping时,会带上B的pfail信息, 通知给其他节点。如果B被标记为pfail的个数大于集群主节点个数的一半(N/2 + 1)时,B会被标记为fail,A向整个集群广播,该节点已经下线。其他节点收到广播,标记B为fail。
从节点选举
raft,每个从节点,都根据自己对master复制数据的offset,来设置一个选举时间,offset越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。
slave 通过向其他master发送FAILVOER_AUTH_REQUEST 消息发起竞选,
master 收到后回复FAILOVER_AUTH_ACK 消息告知是否同意。
slave 发送FAILOVER_AUTH_REQUEST 前会将currentEpoch 自增,并将最新的Epoch 带入到FAILOVER_AUTH_REQUEST 消息中,如果自己未投过票,则回复同意,否则回复拒绝。