文章目录
1. Redis常见的数据结构
string(字符串),hash(哈希),list(列表),set(集合),zset(有序集合)
2. Redis为什么单线程还那么快
- Redis是纯内存数据库,数据读写直接操作内存
- 单线程模式,节省线程切换的开销
- Redis使用的是非阻塞IO,IO多路复用(延伸:什么是IO多路服用?)
3. Redis的过期策略
在Redis中过期的key不会立刻从内存中删除,而是会同时以下面两种策略进行删除
策略 | 说明 |
---|---|
惰性删除 | 当查询这个key的时候判断过期时间,如果过期则会触发del |
定期删除 | 每隔一段时间随机抽查设置了过期时间的key进行删除 |
redis 每秒进行10次定期删除策略
- 随机取20个设置了过期策略的key
- 检查20个key中过期时间中已过期的key并删除
- 如果有超过25%的key已过期则重复第一步
4. 关于Redis的事务
将要执行的命令放在multi和exec之间,构成了redis的简单事务,Redis的事务很不完善,只支持回滚命令错误的情况,运行时错误则放弃执行。
有些情况,需要确保事务开启后,某一个key没被修改过才会执行事务,负责不执行,此时可以使用watch命令
时间点 | client-1 | client-2 |
---|---|---|
T1 | set key hello | |
T2 | watch key | |
T3 | mutli | |
T4 | append key world | |
T5 | append key redis | |
T6 | exec | |
T7 | get key |
在T7时刻,redis的返回值为helloworld,由此可见,事务没有被执行。
5. Redis持久化
Redis持久化分为RDB和AOF两种
5.1 RDB
把当前进程数据生成快照保存到磁盘,可手动触发和自动触发
5.1.1 bgsave的执行过程
- 父进程判断当前正在执行的子进程中,如果有bgsave命令直接返回
- 父进程执行fork操作创建子进程,该操作会阻塞其他命令
- fork完成后,Redis服务器可以继续响应客户端的内容
- 子进程创建RDB文件,根据父进程内存生成快照文件,完成后替换原有文件
- 发送信号给父进程表示完成,父进程更新统计信息。
5.2 AOF
以独立日志的方式记录每次写命令,重启时再重新执行AOF文件的命令用来恢复数据。
5.2.1 工作流程
- 命令写入:所有的写入命令追加到aof_buf区(aof缓冲区)
- 文件同步:命令写入到缓冲区后会根据不同的策略,同步aof文件(always,everysec,no)
- 文件重写:随着指令不断写入AOF,文件会越来越大,Redis引入了AOF重写机制压缩文件体积。AOF重写是把Redis进程内的数据转化为写命令同步到新的AOF文件的过程。
- 重启加载:AOF和RDB都可以用于服务重启时恢复数据。AOF开启且存在AOF文件,优先加载AOF文件,AOF关闭或者不存在AOF,则加载RDB文件。
5.2.2 文件同步策略
配置值 | 说明 |
---|---|
always | 命令写入到aof_buf之后,调用系统的fsync操作同步到aof文件,fsync完成后线程返回 |
everysec | 命令写入aof_buf之后,调用系统的write命令,write命令之后线程返回,fsync命令由专门的线程每秒执行一次 |
no | 命令写入aof_buf之后,调用系统的write命令,write命令之后线程返回,不对aof文件执行fsync操作,fsync操作由操作系统负责,通常同步周期最长为30s |
write操作:触发延迟写机制,数据只写入linux的系统缓冲区后直接返回,此时数据仍然在内存中,具体的同步硬盘机制依赖于操作系统的调度机制。
fsync操作:针对单个文件,做强制硬盘同步,fsync将阻塞直到写入硬盘完成后返回,保证了持久化。
5.2.3 文件重写机制
- 文件重写后为什么会变小?
a. 进程内已过期数据不需要再写入
b. 被删除的key不需要被写入
c. 多条命令可以合并为单条 - 文件重写有什么好处?
a. 重写后提高了空间利用率
b. 更小的文件更容易被加载 - 文件重写的流程?
a. 执行aof重写请求,如果当前由子进程正在aof重写,则命令不执行,如果当前由子进程正在执行bgsave,则等待bgsave后执行。
b. fork创建子进程(fork过程会阻塞)
c. 父进程继续响应其他命令,所有写命令依然写入aof_buf区,但是重写的子进程只能使用fork操作时的内存数据,故此还需要把新进入的写命令同时写入aof_rewrite_buf,以保证AOF重写过程中的数据不被丢失
d. 子进程根据内存快照,按照命令合并规则写入新的aof文件。
e. 新aof文件写入完成后,给父进程发送信号,父进程更新统计信息。
f. 父进程把aof_rewrite_buf去的数据写入到新的aof文件。
g. 使用新的aof文件替换老文件。
5.3 RDB和AOF的对比
1. RDB是一个紧凑压缩的二进制,代表Redis在某个时间的数据快照,非常适合做备份和全量复制等。
2. RDB的加载速度远远快于AOF
3. RDB生成的持久化文件通常比AOF生成的持久化文件小
4. 通常开启AOF会降低Redis服务器的QPS,AOF常用的同步策略没1s执行一次fsync
5. RDB没法做到秒级持久化,fork操作相对是一个高开销的操作,RDB每次持久化都会fork数据,有数据丢失的风险
6. 由于RDB存储的是二进制格式,低版本可能不兼容高版本的RDB文件
4. Redis的应用场景
- 分布式锁
- 热点数据缓存
- 计数器,限流等
- 用户登陆管理
- 队列
- 点赞,好友关系
- 抽奖活动
- 排行榜
5. Redis慢查询统计
慢查询日志只记录命令的执行时间,不记录网络耗时,到达服务端的排队时间等。
config set slowlog-log-slower-than 10000 //大于多少微秒计做慢查询
config set slowlog-max-len 1000//慢查询存储条数
config rewrite//如果需要持久化到配置文件,需要执行这个
慢查询的统计结果会存储到一个list当中,可以通过命令slowlog get查看,返回的属性依次为标示id,发生的时间戳,命令耗时(微秒),命令和参数
6. Redis中的Pipeline机制
一条命令的执行要经过一下四个过程:发送命令
→
\rightarrow
→命令排队
→
\rightarrow
→命令执行
→
\rightarrow
→返回结果,其中发送命令耗时+返回结果耗时,被称作往返时间(RTT)。
当有100个命令需要执行的时候,按照普通命令来,需要100个RTT的时间,大大降低了应用程序的性能。
Pipeline机制用于改善上述问题,他会将一系列命令打包,把原本需要n次RTT的命令,简化为通过一次RTT与Redis服务端进行交互。
6.1 Pipeline与原声批量处理的比较
- Pipeline可以任意的组长各种命令
- 原声的批量处理是原子的,Pipeline是非原子的
7. Redis的发布订阅模式
消息内容不会被持久化
订阅消息:subscribe testChannel
发布消息:public testChannel 123456 (返回值为多少个订阅者)
按照模式订阅:psubscribe test* 订阅所有test开头的channel
查询活跃的渠道(至少有一个订阅者的):pubsub channels [pattern]
查看某一个频道的订阅数:pubsub number testChannel
8. 主从配置
8.1 相关配置
slaveof 127.0.0.1 6379 //master配置
masterauth xxxxxx //如果master设置了密码,在此处配置
命令:slaveof 127.0.0.1 6379
注:从节点的同步是异步进行的,数据在主节点更新后就会返回成功。
断开连接: slaveof no one
断开后不会丢弃之前的数据,只是不会再继续同步了
8.2 常见拓扑结构
结构 | 说明 |
---|---|
一主一从 | 常用于主节点宕机后的故障转移,可以只在从节点开启AOF,但要避免主节点宕机后的自动重启 |
一主多从 | 适用于并发读较多的情况,使用多个从节点实现读写分离,但是处理写请求时,主节点需要同步多个从节点,从而加大了主节点的网络压力 |
树状 | 从节点从主节点同步数据后,又可以作为主节点同步给其他的从节点,缓解主节点的压力 |
8.3 常见运维问题
- 数据延迟 可以通过外部程序,定期检查复制的偏移量来进行及时发现,info replication命令查看master_repl_offset,例如大于10M时报警
- 主从配置不一致 有些配置主从是可以不一致的,比如AOF开关,但是有些配置主从是必须一致的,比如maxmemory等。
- 规避全量复制
- 第一次建立复制关系时的全量复制无可避免,当主节点数据量较大时,尽量选择业务低峰期扩展从节点。
- 节点运行id不匹配,从节点跟主节点建立连接后会保存主节点的运行ID,如主节点宕机重启后,运行ID发生了改变,此时从节点会认为更换了一个新的主节点,从而引起全量复制,这种问题应从架构上避免,如主节点宕机后,能够迅速将某一从节点提升为主节点,或者采用自动故障转移的哨兵或集群方案
- 主节点从网络中断开重连后,从节点会发送psync命令请求部分数据,当请求的偏移量不再主节点的缓冲区时,会将部分复制退化为全量复制。针对这个问题,需要根据网络中断时长,写命令数据量来合理规划缓冲区。
- 规避复制风暴 复制风暴是指大量从节点对同一主节点或者对统一机器的多个主节点短时间内发起全量复制的过程,从而导致大量的网络开销,CPU,内存消耗等。
- 尽量不要在一个master上挂在过多的slave,可以加入中间节点来对master进行保护
- 尽量不要把多台master部署在同一个机器上
9. Redis内存
9.1 查看内存统计
命令 info memory
属性 | 说明 |
---|---|
used_memory | 所有数据内存占用量 |
user_memory_rss | 占用的物理内存总量 |
mem_fragmentation_ratio | 碎片率,等于user_memory_rss/used_memory |
used_memory_peak | 巅峰内存使用量 |
- mem_fragmentation_ratio > 1时,表示可能存在内存碎片,值越大说明内存碎片越严重
- mem_fragmentation_ratio < 1时,说明很可能发生了内存交换,这种情况可能大幅降低redis的吞吐量
9.2 内存模型
Redis进程的内存消耗主要包括:自身内存+对象内存+缓冲内存+内存碎片
内存 | 说明 |
---|---|
自身内存 | 一个空的进程,占用的内存很小,此部分可以忽略不记 |
对象内存 | 对象内存时占用最大的一部分内存,用户所有的数据都存储在对象内存中,redis数据以key-value的形式存储,每新增一个key都至少新增两个对象,key丢像和value对象。 |
缓冲内存 | 分为客户端缓冲区,复制挤压缓冲区,AOF缓冲区 |
内存碎片 | 可以理解为user_memory_rss - used_memory部分 |
9.3 内存管理
9.3.1 设置内存上限
maxmemory参数可以用于设置内存上限,该参数设置的是实际使用的内存(不包含内存碎片),所以Redis占用的物理内存(used_memory_rss)实际有可能超出这个值。
设置内存上限的目的
- 用于缓存时,采用LRU淘汰算法释放空间
- 避免Redis的使用内存超过物理机内存
9.3.2 内存回收策略
策略 | 说明 | 版本 |
---|---|---|
noeviction | 直接报错 | |
allkeys-lru | 扫描所有key,淘汰部分最近未使用的 | |
volatile-lru | 扫描设置了过期时间的key,淘汰部分最近未使用的 | |
allkeys-random | 随机淘汰一部分key | |
volatile-random | 随机淘汰一部分设置了过期时间的key | |
allkeys-lfu | 扫描所有key,淘汰一些最近最少使用的 | Redis4.0之后 |
volatile-lfu | 扫描所有设置了过期时间的key,淘汰一些最近最少使用的 | Redis4.0之后 |
volatile-ttl | 扫描所有设置了过期时间的key,淘汰一些即将过期的key |
9.3.3 RedisObject对象
Redos存储的数据都使用RedisObject来封装,包括string,hash,list,set,zset在内的所有数据类型。
RedisObject的内部结构
字段 | 介绍 |
---|---|
type | 表示当前对象使用的数据类型,可以使用type key来查看 |
encoding | 表示Redis内部编码类型,代表当前对象内部使用的哪种数据结构 |
lru | 记录对象最后一次被访问的时间,用于辅助lru算法删除键数据 |
refcount | 记录当前对象被引用的次数,用于通过引用次数回收内存,当refcount=0时,可以安全的回收该对象 |
*ptr | 与对象的数据内容相关,如果时整数,直接存储数据,否则表示指向数据的指针,Redis3.0之后,如果值对象是字符串类型且长度<=39,内部编码为embstr,字符串sds与redisObject一起分配 |
9.3.4 Redis内存优化
- 共享对象池,Redis内部维护了[0,9999]的整数变量池,但是当设置了maxmemory并且内存淘汰策略为*-lru后,Redis会禁止使用共享对象池
- 字符串优化,Redis没有使用C语言原生的字符串,而是自己实现了字符串结构,简单动态字符串(SDS)
- 编码优化
9.3.5 简单动态字符串(SDS)
SDS结构体
字段 | 说明 |
---|---|
int len | 已用字节长度 |
int free | 未用字节长度 |
char buf[] | 字节数组 |
SDS特点
- O(1)时间复杂度获取:字符串长度,已用长度,未用长度
- 可用于保存字节数组,支持安全的二进制数据存储
- 内部实现空间预分配机制,降低内存再分配次数
- 惰性删除机制,字符串缩减后的空间不释放,作为预分配空间保留
空间预分配规则
- 第一次创建,len属性等于实际大小,free=0,不做预分配
- 修改后如果已有的free空间不足且数据小于1M,每次预分配一倍的容量
- 修改后如果已有的free空间不足且数据大于1M,每次预分配1M空间
9.3.6 编码
类型 | 编码方式 | 数据结构 | 编码转换规则 |
---|---|---|---|
字符串 | raw | 动态字符串编码 | 长度>39 |
embstr | 优化内存分配的字符串编码 | 长度<=39,非整数 | |
int | 整数编码 | 整数 | |
hash | hashtable | 散列表编码 | value的字节数 > hash-max-ziplist-value 或 filed个数 > hash-max-ziplist-entries |
ziplist | 压缩列表编码 | value的字节数 <= hash-max-ziplist-value 且 filed个数 <= hash-max-ziplist-entries | |
list | linkedlist | 双向链表编码 | value的字节数 > list-max-ziplist-value 或 元素个数 > list-max-ziplist-entries |
ziplist | 压缩列表编码 | value的字节数 <= list-max-ziplist-value 且 元素个数 <= list-max-ziplist-entries | |
quicklist | 3.2版本新的列表编码 | 废弃list-max-ziplist-value和list-max-ziplist-entries list-max-ziplist-size:表示最大压缩空间长度,最大空间使用[-5,-1]范围配置,默认-2,表示8KB list-compress-depth:表示最大压缩深度,默认=0,不压缩 | |
set | hashtable | 散列表编码 | 元素非整数 或 元素个数 > hash-max-ziplist-entries |
intset | 整数集合编码 | 元素为整数 且 元素个数 <= hash-max-ziplist-entries | |
zset | skiplist | 跳表编码 | value字节数 > zset-max-ziplist-value 或 元素个数 > zset-max-ziplist-entries |
ziplist | 压缩列表编码 | value字节数 <= zset-max-ziplist-value 且 元素个数 <= zset-max-ziplist-entries |
注: Redis的编码转换只能从小内存到大内存转换,不可逆
10. 哨兵(Redis Sentinel)
10.1 重要配置
配置名称 | 解释 |
---|---|
sentinel monitor <master-name> <ip> <port> <quorum> | master-name 主节点的别名 ip 主节点的ip地址 prot 主节点端口号 quorum 用于故障判断,至少有quorum个sentinel节点认为主机诶单不可达时才会认为主节点故障 |
sentinel down-after-milliseconds <master-name> <times> | 每个Sentinel节点都会对其他的Sentinel节点和Redis数据节点发送ping命令,如果超过times的时间没有回复则会认为节点不可达 |
sentinel parallel-syncs <master-name> <nums> | 完成一次故障转移之后,每次想新的主节点发起复制的从节点个数,过大会导致主节点io和网络开销超载 |
sentinel failover-timeout <master-name> <times> | 故障转移超市时间,作用与故障转移各个阶段 |
sentinel auth-pass <master-name> <password> | 如果Redis主节点设置了密码,需要使用这个配置添加主节点的密码 |
Sentinel配置注意事项:
- sentinel set命令只对当前Sentinel节点生效,不会自动同步给其他节点
- sentinel set命令命令执行成功会会立刻刷新配置文件,区别于Redis的config set命令
10.2 Sentinel常用API
API | 介绍 |
---|---|
sentinel masters | 获取监控的所有master节点 |
sentinel master <master-name> | 通过别名获取master节点 |
sentinel slaves <master-name> | 获取某个主节点下的所有从节点 |
sentinel sentinels <master-name> | 获取监控某个主节点的其他sentinel节点(不含本身) |
sentinel get-master-addr-by-name <master-name> | 获取主节点的IP和端口号 |
sentinel reset <pattern> | 对符合pattern风格的主节点进行重置 |
sentinel failover <master-name> | 强制故障转移,不与其他sentinel节点协商,故障转移完成后,其他sentinel节点按照故障转移结果更新本身 |
sentinel chquorum | 检查当前可达的sentinel节点是否达到quorum配置的个数 |
sentinel remove <master-name> | 取消对某个Redis的监控 |
sentinel monitor <master-name> <ip> <port> <quorum> | 新增对某个主节点的监控 |
sentinel is-master-down-by-adddr <ip> <port> <current_epoch> <runid> | 获取节点对主节点状态的判断,当runid为*时,直接获取主节点状态,如果时具体的runid时,则是希望谬表sentinel同意自己作为Sentinel领导者选举 |
10.3 三个定时任务
- 任务一:每隔10s向Redis的数据节点发送一条info命令获取最新拓扑结构
- 任务二:每隔2s,向__sentinel__:hello频道发送Sentinel节点的自身信息和对主节点的判断,同时又会订阅这个频道,来了解其他节点的信息以及其他节点对主节点状态的判断,同时如果有新的sentinel节点加入,保存起来
- 任务三:每隔1s,Sentinel节点会向其他的sentinel节点和所有的Redis数据节点发送一个ping命令,用来判断这些节点是否可达。
10.4 主观下线和客观下线
主观下线:通过上述的任务三,当Sentinel节点对其他的Sentinel节点,Redis的数据节点发送ping命令后,如果超过down-after-milliseconds配置的时间还没有收到回复,此时该Sentinel节点会认为这个节点不可达。主观下线有很大的误判的可能
客观下线:当sentinel判定主观下线的节点为主节点时,该节点会通过sentinel is-master-down-by-add命令向其他的sentinel节点询问对主节点的判断,当超过<quorum>配置的数量的sentinel节点认为该主节点下线后,这时该sentinel节点才会作出客观下线的决定。
10.5 领导者Sentinel选举
步骤:
- 每个在线的Sentinel都有资格成为领导者,当他确认主节点主观下线后,会向其他的Sentinel节点发送sentinel is-master-down-by-addr命令,要求将自己设置为领导者。
- 收到命令的Sentinel节点,如果没有同意过其他Sentinel节点的sentinel is-master-down-by-addr命令,则同意该请求,负责拒绝。
- 如果该sentinel节点发现自己的票数已经过半并且大于quorum,则成为领导者。
- 如果经过一轮之后没有选出领导者,则进入下一轮。
10.6 故障转移
步骤
- 选择从节点作为新的主节点
- 对新选出来的主节点做slaveof no one操作,使其成为主节点
- 向剩余的从节点发出命令,让他们成为新的主节点的从节点,
- 所有的sentinel节点将原来的主节点更新为从节点,等其恢复后,命令它去复制新的主节点
选取从节点为新的主节点的规则:
- 过滤掉不健康的从节点(主动下线、断线),过滤掉5s内没回复过sentinel节点ping命令的节点,过滤掉与主节点失联超过down-after-milliseconds*10的节点
- 选择slave-priority最高的从节点列表,确定则返回,不确定继续
- 选择复制偏移量最大的从节点,确定则返回,不确定继续
- 选择runid最小的从节点
11. 集群
11.1 虚拟槽分区
使用分散度良好的哈希函数把所有的数据映射到一个固定范围的整数集合当中,这些整数定义为槽,这个范围一定要远远大于节点个数,每个节点负责一定数量的槽。
在Redis中,使用CRC16(key)&16383来计算属于哪个槽。
如果key中包含{},则只使用{}中的内容来计算槽,该功能一般用作批量处理和pipeline处理的优化
11.2 集群功能限制
- key的批量操作支持有限,目前只支持具有相同槽的key执行批量操作。
- 事务支持有限,事务只支持具有相同槽的key
- key作为数据分区的最小力度,不能将大对象如hash,list等映射到不同的分区
- 不支持多数据库操作,单机下可以支持16个库,集群模式下只能支持一个库
- 复制结构只支持一层,从节点只能复制主节点,不支持树状结构
11.3 集群通信
Redis集群采用P2P的Gossip(留言)协议作为通信协议,工作原理是节点直接不断交换信息,一段时间后所有的节点都会知道集群的完整信息。
Gossip消息分类
消息类型 | 描述 |
---|---|
meet消息 | 用于通知新节点加入。消息发送者通知(或者说邀请)接收者加入到当前集群,消息通讯正常完成后,接收节点会加入当集群中并进行周期性的ping,pong消息交换 |
ping消息 | 集群内最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换信息,ping消息封装了自身节点和部分其他节点的状态数据 |
pong消息 | 当收到ping,meet消息后,作为响应信息回复给发送方确认消息正常通信。pong消息封装了自身状态数据。节点也可以主动向集群内广播自身pong消息来通知整个集群对自身状态进行更新 |
fail消息 | 当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到后会把对应节点更新为下线状态 |
通讯流程
ping消息每个节点默认每秒执行10次,每秒会随机选取5个节点,从中找出最长没有通讯的节点发送ping消息。
每100毫秒扫描一下每一个节点,如果发现最后接收该节点pong消息的时间>[cluster_node_timeout/2],则立即发送ping消息。
每次通讯携带的节点数据量为总节点数据量的1/10,小于3取3,大于总数-2,取总数减2
11.4 集群伸缩
迁移数据的过程 Redis的数据迁移是逐个槽进行的
- 对目标节点发送cluster setslot [slot] importing [sourceNodeId]命令,让目标节点准备好倒入槽数据
- 对源节点发送cluster setslot [slot] migrating [targetNodeId]命令,让源节点准备迁出槽数据
- 源节点顺序执行cluster getkeysinslot [solt] [count] 命令,获取count数量的属于这个slot的键
- 在源节点上执行migrate [targetIp] [targetPort] “” 0 [timeout] [keys…]命令,把获取到的键通过pipeline机制批量迁移到目标节点。批量命令是3.0.6之后提供的,之前只能单个key进行迁移
- 重复3,4知道槽下所有的键都迁移到目标节点
- 想集群内所有主节点发送cluster setslot [solt] node [targetNodeId]命令,通知槽分配给目标节点。此处为了变更的及时性,会对集群内所有节点进行广播。
上述操作步骤是为了方便了解原理,实际操作会使用redis-trib.rb命令
redis-trib.rb reshard host:port --form \<arg> --to \<arg> --slots \<arg> --yes --timeout \<arg> --pipeline \<arg>
参数 | 说明 |
---|---|
host:port | 必传,集群内任意节点地址,用来获取整个集群信息 |
from | 源节点id,如果有多个用逗号隔开,不传会在执行过程中提示用户输入 |
to | 只能有一个,表示目标节点id,不传会在执行过程中提示用户输入 |
slots | 需要迁移的槽总量,在执行过程中提示用户输入 |
yes | 打印执行计划时,是否需要用户输入yes再执行 |
timeout | 每次migrate操作的执行时间 |
pipeline | 每次批量迁移键的大小,默认为10 |
cluster forget nodeId命令,用于忘记节点,执行改命令后,会把该nodeId加入到禁用列表中,禁用有效期为60s,也就是说我们有60s的时间让集群内所有节点进行forget
实际生产环节中,一般不会直接食用forget命令,该命令容易遗漏节点,而是用redis-trib.rb del-node {host:port} {downNodeId}
redis-trib.rb del-node在下线过程中,如果发现下线的节点有从节点,会将该从节点指向其他节点然后再进行下线,所以如果主从都下线的情况下,要先下线从节点,避免不必要的全量复制
11.5 重定向
MOVED重定向: 请求某个节点时,通过key计算出槽,发现槽不存在于这个节点,则返回MVOED重定向
ASK重定向: 请求某个节点时,通过key计算出槽,发现这个槽属于当前节点,但是正在被迁移,切这个key不在当前节点时返回ASK重定向
11.6 故障转移
11.6.1 故障发现
主观下线:某一个节点认为另一个节点不可用,即下线状态,这个状态并不是最终的故障判断,只能代表一个节点的意见,可能存在误判。
客观下线:指标记一个节点整整的下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。如果是持有槽的主节点故障,则需要进行故障转移。
主观下线判定流程:
- 节点A向节点B发送ping消息,如果能正常收到节点B的pong消息,则认为节点B正常,节点A更新节点B的最后通信时间
- 如果节点A和节点B通讯出现问题则断开连接,下次会进行重连,如果一直通讯失败,则节点A不会更新与节点B的最后通信时间
- 节点A的定时任务监测到节点B的最后通讯时间超过cluster-node-timeout时,更新本地对节点B的状态为主观下线(pfail)。
客观下线判断流程:
- 节点A判定节点B主观下线后,相应的节点状态会随着节点A发出的ping/pong消息在集群内传播。
- 其他节点收到包含pfail的消息时,判断消息的发送者是不是主节点,如果是主节点,则更新本地对应的pfail节点的下线报告。
- 根据更新后的下线报告尝试进行客观下线(并不一定客观下线成功)
- 如果尝试客观下线成功,即计算有效下线报告数量大于<font color="#dd0000>有槽主节点数量的一半,则将节点标记为客观下线,并在集群内广播fail消息。
如果下线报告在cluter-node-timeout * 2没有更新,则该下线报告将会过期,入股主观下线上报的速度赶不上下线报告过期的速度,那么故障节点将永远无法被客观下线,从而导致故障转移失败,因此不建议将cluter-node-timeout设置过下。
11.6.2 故障恢复
客观下线的节点如果是包含槽的主节点,就需要从他的从节点当中选择一个替换他,从而保证集群的高可用。
恢复流程:
- 资格检查:每个从节点检查最后与主节点断线时间,判断是否具有替换主节点的资格。如果断线时间超过cluster-node-timeout * cluster-slave-validity-factor,则当前从节点不具备故障转移资格。
- 准备选举时间:从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程。选举时间根据偏移量排名计算,偏移量越小,选举时间越小。
- 发起选举:当从节点定时任务发现到达故障选举时间后,发起选举
- 选举投票:只有持有槽的主节点才会参与处理故障选举消息,每个持有槽的主节点在每个纪元内只有一个选票,当接收到第一个请求投票的从节点消息时进行投票,之后其他接单发来相同纪元的消息时将会忽略。如果在开始投票的cluster-node-timeout * 2时间内没有选择出替换主节点的从节点,本次投票作废,开启下一纪元。
- 替换主节点:从节点收集到足够的选票之后,触发替换主节点的操作。
替换主节点流程:
- 取消从节点复制,变为主节点
- 执行clusterDelSolt槽走撤销故障主节点负责的槽,并执行clusterAddSolt操作,将这些槽委派给自己
- 向集群广播自己的pong消息,通知集群内所有节点,自己从从节点变为了主节点,并接管了故障节点的槽信息
附1:Redis常用命令
全局命令
keys * //查看所有的键(生产环境慎用,由于redis为单线程的,如果key较多,会导致其他请求等待)
dbsize //查看数据库中key的总个数,这个命令不会遍历所有key,redis用一个变量来维护
exists hello//查询数据库中是否存在hello这个key,0表示不存在,1表示存在
del hello// 删除hello这个key,也可以批量,del key1 key2 key3
expire hello 10//给hello这个键添加过期时间,单位秒
ttl hello//查看hello这个键的过期时间,有过期时间时返回过期时间,返回-1时表示未设置过期时间,-2表示key不存在
type hello//查看键的数据结构类型
字符串
命令 | 介绍 |
---|---|
set hello world | 设置一个值,key为hello,值为world |
get hello | 查看hello这个key对应的值 |
setnx key value | 当键不存在时才可以设置成功 |
set key value xx | 当键存在时在可以设置成功 |
mset key1 value1 key2 value2 | 批量设置 |
mget key1 key2 key3 | 批量获取 |
incr key | 自增(不是整数会报错,是整数自增,不存在设置为1) |
append key value | 在字符串末尾追加 |
strlen key | 获取字符串长度 |
getset key vlues | 设置并返回原值 |
哈希
命令 | 介绍 |
---|---|
hset user:1 name abc | 设置值,key为user:1,属性为name,值为abc |
hget user:1 name | 获取值 |
hdel user:1 name | 删除user:1中的name属性 |
hlen user:1 | 查看user:1 共有多少属性 |
hmset user:1 name abc age 18 sex 1 | 批量设置属性 |
hmget user:1 name age sex | 批量获取属性 |
hexists user:1 name | 判断user:1下是否存在name属性 |
hkeys user:1 | 获取所有user:1下所有的属性 |
hvals user:1 | 获取user:1下所有的属性值 |
hgetall user:1 | 获取所有的属性和属性值 |
hincby user:1 age | 自增 |
hstrlen user:1 name | 查看user:1 name的长度 |
列表
命令 | 介绍 |
---|---|
blpop key1 [key2 ] timeout | 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 |
brpop key1 [key2 ] timeout | 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 |
brpoplpush source destination timeout | 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 |
lindex key index | 通过索引获取列表中的元素 |
LINSERT key BEFORE|AFTER pivot value | 在列表的元素前或者后插入元素 |
LLEN key | 获取列表长度素 |
LPOP key | 移出并获取列表的第一个元素 |
LPUSH key value1 [value2] | 将一个或多个值插入到列表头部 |
LPUSHX key value | 将一个值插入到已存在的列表头部 |
LRANGE key start stop | 获取列表指定范围内的元素 |
LREM key count value | 移除列表元素 |
LSET key index value | 通过索引设置列表元素的值 |
LTRIM key start stop | 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。 |
RPOP key | 移除列表的最后一个元素,返回值为移除的元素。 |
RPOPLPUSH source destination | 移除列表的最后一个元素,并将该元素添加到另一个列表并返回 |
RPUSH key value1 [value2] | 在列表中添加一个或多个值 |
RPUSHX key value | 为已存在的列表添加值 |
集合
命令 | 介绍 |
---|---|
SADD key member1 [member2] | 向集合添加一个或多个成员 |
SCARD key | 获取集合的成员数 |
SDIFF key1 [key2] | 返回第一个集合与其他集合之间的差异 |
SDIFFSTORE destination key1 [key2] | 返回给定所有集合的差集并存储在 destination 中 |
SINTER key1 [key2] | 返回给定所有集合的交集 |
SINTERSTORE destination key1 [key2] | 返回给定所有集合的交集并存储在 destination 中 |
SISMEMBER key member | 判断 member 元素是否是集合 key 的成员 |
SMEMBERS key | 返回集合中的所有成员 |
SMOVE source destination member | 将 member 元素从 source 集合移动到 destination 集合 |
SPOP key | 移除并返回集合中的一个随机元素 |
SRANDMEMBER key [count] | 返回集合中一个或多个随机数 |
SREM key member1 [member2] | 移除集合中一个或多个成员 |
SUNION key1 [key2] | 返回所有给定集合的并集 |
SUNIONSTORE destination key1 [key2] | 所有给定集合的并集存储在 destination 集合中 |
SSCAN key cursor [MATCH pattern] [COUNT count] | 迭代集合中的元素 |
有序集合
命令 | 介绍 |
---|---|
ZADD key score1 member1 [score2 member2] | 向有序集合添加一个或多个成员,或者更新已存在成员的分数 |
ZCARD key | 获取有序集合的成员数 |
ZCOUNT key min max | 计算在有序集合中指定区间分数的成员数 |
ZINCRBY key increment member | 有序集合中对指定成员的分数加上增量 increment |
ZINTERSTORE destination numkeys key [key …] | 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 destination 中 |
ZLEXCOUNT key min max | 在有序集合中计算指定字典区间内成员数量 |
ZRANGE key start stop [WITHSCORES] | 通过索引区间返回有序集合指定区间内的成员 |
ZRANGEBYLEX key min max [LIMIT offset count] | 通过字典区间返回有序集合的成员 |
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT] | 通过分数返回有序集合指定区间内的成员 |
ZRANK key member | 返回有序集合中指定成员的索引 |
ZREM key member [member …] | 移除有序集合中的一个或多个成员 |
ZREMRANGEBYLEX key min max | 移除有序集合中给定的字典区间的所有成员 |
ZREMRANGEBYRANK key start stop | 移除有序集合中给定的排名区间的所有成员 |
ZREMRANGEBYSCORE key min max | 移除有序集合中给定的分数区间的所有成员 |
ZREVRANGE key start stop [WITHSCORES] | 返回有序集中指定区间内的成员,通过索引,分数从高到低 |
ZREVRANGEBYSCORE key max min [WITHSCORES] | 返回有序集中指定分数区间内的成员,分数从高到低排序 |
ZREVRANK key member | 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序 |
ZSCORE key member | 返回有序集中,成员的分数值 |
ZUNIONSTORE destination numkeys key [key …] | 计算给定的一个或多个有序集的并集,并存储在新的 key 中 |
ZSCAN key cursor [MATCH pattern] [COUNT count] | 迭代有序集合中的元素(包括元素成员和元素分值) |
附2: Redis使用Lua脚本
Redis执行Lua可以使用eval和evalsha
eval script(Lua脚本) keynums(key的数量) key value
eval ‘return "hello " … KEYS[1] … ARGV[1]’ 1 redis java
evalsha的执行方式,先将Lua脚本加载到服务端,服务端给客户端返回sha1的校验和,后续客户端可以通过校验和来代替脚本,减少每次传递Lua脚本的开销,同时使脚本得到复用。
加载到内存的命令:script load 'return "hello " .. KEYS[1] .. ARGV[1]'
服务端返回校验和:7413dc2440db1fea7c0a0bde841fa68eefaf149c
使用命令执行脚本 evlasha 7413dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis java
在redis重启后,已加载的脚本会消失
其他命令
判断是否存在 script exists 7413dc2440db1fea7c0a0bde841fa68eefaf149c
清除内存中已存在的lua脚本 script flush
清除正在执行的lua脚本 script kill
调用Redis的命令
eval ‘return redis.call(“get”, “key1”)’ 0
redis.call和redis.pcall区别
redis.call执行出错会停止执行
redis.pcall执行出错会忽略错误继续执行
Lua脚本是原子的,整个脚本的执行过程中不会插入其他命令,带来好处的同时也要注意,不要创建耗时过长的Lua脚本