# redis
Redis 是一种基于 key-value 的 NoSQL 数据库
Redis 的全称是 REmote Dictionary Server,是一个开源(BSD 许可)的内存数据存储系统,它可以用作数据库、缓存和消息中间件。它支持多种类型的数据结构,如字符串(string)、哈希(hash)、列表(list)、集合(set)、有序集合(sorted set),同时在这些基础数据结构的基础之上演变出了位图(bitmap)、HyperLogLog、GEO 等数据结构。Redis 内置了复制(replication),LUA 脚本(Lua scripting),LRU 淘汰策略(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence),并通过 Redis 哨兵(Sentinel)和自动分区(Cluster)提供高可用性。
# redis 作用
1、内存存储、持久化 (RDB,AOF)。
2、效率高,用于高速缓存
3、发布订阅系统
4、地图信息分析
5、计时器,计数器
- 提供了键过期功能,可以用来实现缓存。
- 提供了发布订阅功能,可以用来实现基本消息队列。
- 提供了简单的事务功能(不支持回滚),能在一定程度上保证事务特性。
- 支持 Lua 脚本功能,可以利用 Lua 创造出新的原子命令,可以处理一些复杂的业务逻辑。
- 提供了流水线 (Pipeline) 功能,能将一批命令一次性传到 Redis,减少了网络的开销。
# redis 特性
多样的数据类型
持久化
事务
集群
Redis 高可用篇
# redis 速度快
正常情况下,Redis 执行命令的速度非常快,官方给出的数字是读写性能可以达到 10 万 / 秒,造就了 Redis 如此之快的原因可大致归纳为以下三点
- Redis 为内存数据库,相较于传统的硬盘存储的数据库具有更高的读写性能。
- 底层实现为 C 语言,针对不同的数据结构做了专门的设计和优化。
- 使用单线程结构 + I/O 多路复用的形式(Redis6.0 后使用不同的线程分别处理网络 I/O 和命令执行),不需要频繁地执行线程间的切换。且无需考虑加锁的问题,不存在死锁导致的性能消耗。
# 安装 redis
1 | wget https://github.com/redis/redis/archive/refs/tags/7.4.1.tar.gz |
redis 默认 16 个数据库,默认使用第 0 个数据
1 | 127.0.0.1:6379> SELECT 3 |
redis 是单线程的,6 开始支持多线程
redis 的瓶颈是机器的内存和网络带宽
# 数据类型
1 | 127.0.0.1:6379> set name ssss |
# string
1 | 127.0.0.1:6379> APPEND name wwww # 存在追加,不存在创建 |
# list
1 | # list 可以做栈,队列,阻塞队列 |
实际是一个链表。移除了所有值的聊表,也代表不存在
在链表两端改动或插入,效率最高。中间效率较低
也可以用作消息队列
# set
1 | set中值不能重复 |
# hash
1 | 127.0.0.1:6379> HSET myhash haha xixi |
hash 存变更的数据。hash 更适合对象存储,string 更适合字符串
# zset
1 | set基础上增加一个值,有序集合 |
# geospatial
用于推算地理位置信息,两地距离,方圆的人
1 | 127.0.0.1:6379> GEOADD city:china 116.4 39.9 bj |
geo 底层实现原理是 zset,可以使用 zset 命令操作 geo
1 | 127.0.0.1:6379> ZRANGE city:china 0 -1 withscores |
# hyperloglog
1 | # 基数,不重复的元素,可以有误差 |
# bitmap
1 | bitmaps位图,都是操作二进制位来记录,只有1和0两个状态 |
# 事务
redis 单条命令保证原子性。但是事务不保证原子性
一次性,顺序性,排他性
redis 事务没有隔离级别的概念。命令在事务中没有被直接执行,只有发起执行命令的时候才执行
1 | 127.0.0.1:6379> MULTI |
编译型异常,所有事务都不会执行
运行时异常,只有错误命令不会被执行
1 | 127.0.0.1:6379> set ooo ppp |
# 乐观锁
悲观锁:什么时候都会出问题,做什么都会加锁,影响性能
乐观锁:认为什么时候都不会出现问题,更新数据的时候去判断下在此期间是否有人修改过这个数据。mysql 中用 version 字段。获取 version,更新的时候比较 version。redis 中使用 watch
1 | # redis监视测试 |
# redis.conf
1 | include /path/to/local.conf |
# redis 持久化
Redis 是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以 Redis 提供了持久化功能
Redis4.0 版本之后还提供了 RDB-AOF 混合持久化格式,这种持久化方式混合了两者的优点。
# RDB(redis database)
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是 Snapshot 快照,它恢复时是将快照文件直接读到内存里。
Redis 会单独创建 (fork) 一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文 件替换上次持久化好的文件。整个过程中,主进程是不进行任何 lD 操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。RDB 的缺点是最后一次持久化后的数据可能丢失。我们默认的就是 RDB, 一般情况下不需要修改这个配置!
主从复制中,rdb 是在从机上面备用

rdb 保存文件名 dump.rdb
1 | save 900 1 # 900秒内修改1次key,触发rdb操作 |
触发机制
手动触发
- save:阻塞当前 redis 服务器进程,直至持久化完成,对于内存数据较大的情况,会造成长时间的阻塞,严重影响线上其它客户端获取数据。
- bgsave:Redis 进程执行 fork 操作创建子进程,RDB 持久化过程由子进程负责,完成后自动结束。阻塞只发生在 fork 阶段,一般时间很短。
自动触发
- 使用 save 相关配置,如 “save m n”。表示 m 秒内数据集存在 n 次修改时,自动触发 bgsave。(save 规则满足,自动触发 rdb)
- 如果从节点执行全量复制操作,主节点自动执行 bgsave 生成 RDB 文件并发送给从节点。
- 执行 debug reload 命令重新加载 Redis 时,也会自动触发 save 操作。
- 默认情况下执行 shutdown 命令时,如果没有开启 AOF 持久化功能则自动执行 bgsave。
执行 flushall,手动触发 rdb
退出 redis,也会产生 RDB
恢复 RDB 文件
放到 redis 配置目录,redis 启动时会自动检查 dump.rdb ,恢复其中数据
1 | 127.0.0.1:6379> CONFIG get dir |
优点:
1、适合大规模数据恢复
2、对数据完整性高俅不高
3、RDB 是一个紧凑压缩的二进制文件,占用体积小。
- 4、Redis 加载 RDB 进行数据恢复远快于 AOF 的方式。
缺点:
需要一定时间间隔。redis 宕机后最后一次修改丢失
fork 进程占用一定资源
RDB 文件使用特定二进制格式保存,Redis 版本演进过程中有多个格式的 RDB 版本,存在老版本 Redis 服务无法兼容新版 RDB 格式的问题。
# AOF
将所有命令都记录下来,恢复的时候把这个文件全量执行一遍
以日志的形式来记录每个写操作,将 Redis 执行过的所有指令记录下来 (读操作不记录), 只许追加文件但不可以改写文件,redis 启动之初会读取该文件重新构建数据,换言之,redis 重启的活就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
aof 保存文件名 append.aof

1 | appendonly yes # 默认不开启 |
aof 文件如果有问题,redis-server 无法启动。可以使用 redis-check-aof 修复 aof 文件
1 | redis-check-aof --fix appendonly.aof |
# 文件重写
当 AOF 文件过大的时候,会触发自动重写机制。除此之外,也可以手动执行 bgrewriteaof 进行重写。
AOF 的重写机制基本与 RDB 的持久化操作相同,通过 fork 一个进程来执行相应的操作。不同之处在于,在重写过程中,后续到来的命令可以添加到 AOF 重写缓冲区(aof_rewrite_buf)中,当新的 AOF 文件重写完成后,可以将这部分命令添加到新的 AOF 文件中,见图中操作 5.2。
1、相对于数据文件来说,aof 文件远远大于 rbd,修复速度也比 rdb 慢
2、AOF 运行效率也比 rdb 慢,所以 redis 默认持久化是用的 rbd
# redis 发布订阅
redis 发布订阅是一种消息通信模式,发送者发送消息 (pub),订阅者接受消息 (sub)
redis 可以订阅任何数量频道
1 | 订阅段 |
通过 SUBSCRIBE 命令订阅某频道后,redis-server 里维护了 - 一个字典,字典的键就是一个个频道,而字典的值则是一个链表,链表中保存了所有订阅这个 channel 的客户端。SUBSCRIBE 命令的关键,就是将客户端添加到给定 channel 的订阅链表中 2。
Pub/Sub 从字面上理解就是发布 (Publish) 与订阅 (Subscribe), 在 Redis 中,你可以设定对某一个 key 值进行消息发布及消息订阅,当一个 key 值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。
使用场景:
1、实时消息系统
2、实时聊天,频道当做聊天室
3、订阅,关注系统
或者使用消息队列
# redis 主从复制
将 Redis 服务器的数据,复制到其他的 redis 服务器。复制是单向的,只能主复制到从。主从复制可以进行读写分离,减轻服务器压力。
最低配 1 主 + 2 从。
1、数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
2、故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
3、负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 (即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点), 分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高 Redis 服务器的并发量。
4、高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是 Redis 高可用的基础。
单台 redis 内存利用不应该超过 20G。默认情况下每台 redis 都是主节点,一个从节点只能有一个主节点
1 | info replication |
从机只读,主机读写。没有配置哨兵的情况下,从机不会自动成为主机
主机断开连接,但是没有写操作时。主机恢复,从机仍然可以读写信息。
从机断开连接,如果没有写入到配置文件,再次连接会变为主机。又变回从机时,数据会从主机中获取。
Slave 启动成功连接到 master 后会发送一个 sync 命令
Master 接到命令,启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令,在后台进程执行完毕之后,master 将传送整个数据文件到 slave, 并完成一次完全同步。
全量复制:而 slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中。
增量复制:Master 继续将新的所有收集到的修改命令依次传给 slave, 完成同步
但是只要是重新连接 master, 一次完全同步 (全量复制) 将被自动执行
主从复制可以
M->(M/S)->S,此时中间的主机是 slave,是只读的,但是对于最后的 slave,他是 master。
1 | slaveof no one # 没有master的时候,自己变成master |
# 哨兵模式
哨兵模式是一种特殊的模式,首先 Redis 提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待 Redis 服务器响应,从而监控运行的多个 Reedis 实例
主机故障后,根据投票数自动将从库转为主库
Redis sentinel 仅仅是在普通的 redis 主从复制模式增加了一系列的 sentinel 节点,用于监控主从模式中的 redis 数据节点,并没有对 redis 的数据节点进行额外的处理。
这里的哨兵有两个作用
通过发送命令,让 Redis 服务器返回监控其运行状态,包括主服务器和从服务器
当哨兵监测到 master 宕机,会自动将 slave 切换成 master, 然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。
然而一个哨兵进程对 Redis 服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
# 主观 / 客观下线
假设主服务器宕机,哨兵 1 先检测到这个结果,系统并不会马上进行 failover 过程,仅仅是哨兵 1 主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,每票的结果由一个哨兵发起,进行 failover [故障转移] 操作。切换成功后,就就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。
- 主观下线:利用第三个定时任务,可以对所有节点进行故障检测。当 sentinel 向其它节点发送 ping 后,超过参数 down-after-millliseconds 配置的时间后仍没有收到有效的回复,就会判定这个节点为故障的,并标记这个节点下线,这个行为就叫做主观下线。主观下线是某一个 sentinel 节点对另外的节点的主观方面的故障判断,存在误判的可能性,因此不能作为节点实际下线的判断依据。
- 客观下线:正如之前所说,主观下线存在误判的可能性,所有需要多个 sentinel 节点共同参与商讨对某个节点的下线判断,从而确定该节点是否真的处于不可达的状态。具体操作是:sentinel 节点会向其它 sentinel 节点发送命令 is-master-down-by-addr 询问其他节点对主节点的故障判断,只有当达到 quorum 个 sentinel 节点对主节点做出故障判断时,才会执行对主节点的客观下线。
- down-after-millisecondes:该参数可以在 redis sentinel 节点启动配置文件中设置。这个参数设置得越大,则误判节点故障的概率就越小,但这也意味着故障转移的操作延后了,降低了对故障节点的反应速度。反之,该参数设置得越小,虽然提高了对故障节点的反应速度,但是对故障判断的错误率也随之升高。
- quorum:该参数也是在 sentinel 节点启动配置文件中设置的,可用于客观下线的判断和 sentinel 领导者的选举。如果该参数设置得太小,则客观下线的条件就越宽松,反之越严格。
- is-master-down-by-addr:这个命令除了可以用于交换 sentinel 节点之间对主节点的主观下线判断,还可以用于 sentinel 领导者的选举,不同的行为由不同的参数决定。
# 三个定时任务
- 每隔 10s,sentinel 会向主节点和从节点发送 info 命令来获取最新的数据节点拓扑结构,用于及时感知节点之间的变化。就是因为有了这个定时任务,在配置 redis sentinel 节点的时候就只需要配置主节点的信息,从节点的信息可以由这个定时任务获取到。
- 每隔 2s,sentinel 节点会向__sentinel__:hello 频道发送自身的 sentinel 信息以及对主节点的故障判断,sentinel 节点集合中的所有节点都会订阅这个频道。当有新的 sentinel 节点加入进来时,通过订阅这个频道,各个 sentinel 节点可以及时的获取到新加入的 sentinel 节点信息。此外,通过这个频道 sentinel 节点之间可以相互交换对主节点的状态信息,也就是与其它 sentinel 节点交换对主节点的客观下线判断,作为后续主节点客观下线以及领导者选举的依据。
- 每隔 1s,每个 sentinel 节点会向主节点、从节点和其它的 sentinel 节点发送 ping 命令做心跳检测,来确认这些节点是否可达,如果节点不可达,则标记节点下线。这个定时任务是判断节点故障的重要依据。
# 领导者选举
当 sentienl 节点对主节点做出了客观下线的判断后,不会立即执行故障转移的操作,此时 sentinel 节点结合会选举出一个领导者,由该领导者 sentinel 节点来执行后续的故障转移。Redis 使用了 Raft 算法来选举领导者,这里见到介绍整个选举的流程。
- 只有没有标记为主观下线的 sentinel 节点才有资格成为候选人。sentinel 节点通过向其它 sentinel 节点发送 is-master-down-by-addr 命令寻求对方将选票投个自己。
- 收到命令的 sentinel 节点会判断自身在本轮 (epoch) 的投票中自己的选票是否还存在,如果选票还在,就同意对方成为领导者,反之拒绝。
- 此时如果 sentinel 节点发现自己获得的选票达到 max (quorum, num (sentinels)/2),那么它将成为领导,由于每轮选举每个 sentinel 节点都只有一张票,因此只有人拿到半数以上的票数,其它人就不可能拿到半数以上的票。
- 如果本轮投票没有选出领导者,开启下一轮选举。
暂时无法在飞书文档外展示此内容
# failover (故障转移)
- sentinel 会从过滤掉不健康的候选从节点,包括主观下线、5s 没有回复 sentinel 节点 ping 响应、与主节点失联超过 down-after-milliseconds*10 秒的节点
- 选择从节点优先级最高的从节点,如果存在直接返回
- 选择复制偏移量最大的从节点,复制偏移量代表了主从同步的完整度
- 选择 run id 最小的从节点
1 | vim sentinel.conf |
主机回来后,只能在新的 master 下当 slave
优点:
哨兵集群一般基于主从复制模式,所有主从配置的优点,他都有
主从可以切换,故障可以转移
哨兵模式是主从模式的升级
缺点:
redis 不好在线扩容,集群容量到达上限,扩容很麻烦
哨兵模式配置很麻烦

# redis 缓存穿透和雪崩
# redis 缓存

收益:数据存在内存中,加速读,同时降低了后端访问的负担。
成本:数据不一致,缓存层和数据层会存在数据不一致的问题,为了提高数据一致性需要付出一定的代价,并且代码逻辑也会变得更复杂
# 缓存更新策略
- 依赖淘汰策略剔除数据
当内存超过预先设定 maxmemroy 配置的值时,Redis 会使用自身的 LRU/LFU 算法来删除数据。无法控制数据什么时候删除。
- 依赖过期策略剔除数据
对每个数据都设置超时时间,到期后 Redis 会自动删除数据,虽然在一定的时间窗口内会存在数据不一致的问题,但可以保证最终一致性。如果业务不要求非常强的一致性,就可以采用这种策略。
-
主动更新
-
先写缓存,再写数据库(绝对不能使用这种方式)
- 写缓存成功,写数据库失败,客户端会读到脏数据。
-
先写数据库,再写缓存(也不能使用这种方式)
-
先删缓存,再写数据库
-
先写数据库,再删缓存
![image-20250222211253162]()
-
# 缓存穿透
缓存穿透的概念很简单,用户想要查询一个数据,发现 redisp 为存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命命中 (秒杀!), 于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
布隆过滤器
布隆过滤器是一种数据结构,对所有可能查询的参数以 hash 形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层有储系统的查询压力;
缓存空对象
但是这种方法会存在两个问题:
1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
# 缓存击穿
这里需要注意和缓存击穿的区别,缓存击穿,是指一个 key 非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个 key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞
当某个 key 在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。
设置热点数据永不过期
从缓存层面来看,没有设置过期时间,所以不会出现热点 key 过期后产生的问题。
加互斥锁
分布式锁:使用分布式锁,保证对于每个 key 同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大。
# 缓存雪崩
缓存雪崩,是指在某一个时间段,缓存集中过期失效。
redis 高可用
这个思想的含义是,既然 redis 有可能挂掉,那我多增设几台 redis, 这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的 key, 设置不同的过期时间,让缓存失效的时间点尽量均匀。
https://www.bilibili.com/video/BV1S54y1R7SB?spm_id_from=333.788.player.switch&vd_source=c2e917a6700cad349be9160e973c18a6&p=36
# redis 线程模型
- Redis 6.0 之前
6.0 之前的版本 Reids 使用的是单线程架构,这里的单线程是指处理用户发过来的命令是在单个线程内完成的,包括网络 I/O、命令解析和命令执行。
暂时无法在飞书文档外展示此内容
- Redis 6.0
在高并发情况下,Redis 的主线程很多时候都在进行 socket 的读写操作,这块儿其实消耗了很多的 CPU 等待时间。特别是写操作,如果客户端要求返回的数据量特别大,会严重阻塞主线。现代处理器一般都采用了多核架构,但 Redis 单线程模型只使用单一核心,这限制了其性能的释放。
所以在 6.0 版本之后,Redis 采用了多线程(1 个主线程 + IO 线程组)架构,网络 I/O 以及命令的解析都放在了多个线程当中,主线程则主要负责命令的执行。这种架构在高并发场景下极大提高网络 I/O 带来的性能。
暂时无法在飞书文档外展示此内容
# redis cluster
Redis Cluster 是 Redis 官方的分布式解决方案,在 redis3.0 之后生产环境正式可用。在实现了高可用的同时,分布式的架构极大扩展了 redis 的存储、读写能力。
每个节点在 cluster 中有一个唯一的名字。这个名字由 160bit 随机十六进制数字表示,并在节点启动时第一次获得 (通常通过 /dev/urandom)。节点在配置文件中保留它的 ID,并永远地使用这个 ID,直到被管理员使用 CLUSTER RESET HARD 命令 hard reset 这个节点。
节点 ID 被用来在整个 cluster 中标识每个节点。一个节点可以修改自己的 IP 地址而不需要修改自己的 ID。Cluster 可以检测到 IP /port 的改动并通过运行在 cluster bus 上的 gossip 协议重新配置该节点。
# 解决问题
- 单集群主节点的写能力受到限制,传统主从模式和 redis sentinel 无法提升写性能。
- 单集群主节点的存储能力受到限制。
# cluster 总线
每个 Redis Cluster 节点有一个额外的 TCP 端口用来接受其他节点的连接。这个端口与用来接收 client 命令的普通 TCP 端口有一个固定的 offset。该端口等于普通命令端口加上 10000. 例如,一个 Redis 在端口 6379 监听客户端连接,那么它的集群总线端口 16379 也会被打开。
节点到节点的通讯只使用集群总线,同时使用集群总线协议:有不同的类型和大小的帧组成的二进制协议
# Redis cluster 的拓扑结构
暂时无法在飞书文档外展示此内容
redis cluster 采用的是去中心化的分布式架构,它们之间通过不断的交换信息来获取集群整体的状态(图中集群的从节点也参与信息交换,为简化结构,忽略它们之间的连线)。其中 redis cluster 中的节点也区分主从,从节点负责处理当主节点出现故障时的自动故障转移,默认情况下从节点不支持任何的读写操作,仅作为主节点的一个 “热备” 存在。在 Redis cluster 模式下,成为从节点的 salveof 命令将会失效,应使用 cluster replication 命令
# 数据分区
分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集。
Redis cluster 采用哈希分区规则,这里介绍三种常用的哈希分区规则
- 哈希取余分区
- 一致性哈希算法
- 虚拟槽分区
一致性哈希算法也存在一些问题:
- 当使用少量节点时,若节点哈希在环上分布不均匀,节点变化有可能将大范围影响哈希环中数据映射,因此这种方式不适合少量数据节点的分布式方案。
- 普通的一致性哈希分区无法保证数据和负载的均衡,需要增加虚拟节点。
在此基础之上,redis 使用虚拟槽的分区规则。其主要思想就是每个节点负责一部分虚拟槽,而数据经过哈希计算后直接映射到对应的槽上,每个槽又维护着一定量的数据,如图 3-3 所示。
暂时无法在飞书文档外展示此内容
Redis cluster 使用的槽范围为 0~16383,共计 16384 个槽。数据到槽的映射可以通过 hash (key)&16383 的方式进行计算。使用虚拟槽的方式,可以很好的解决一致性哈希的问题:通过均匀分配给每个节点虚拟槽,当节点较少时,至少可以保证只有 16384/num (node) 个槽中的数据需要进行迁移。此外,均匀分配的槽可以比较好的保证负载均衡。
# 节点间通信
在分布式存储中需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据,是否出现故障等状态信息。常见的元数据维护方式分为:集中式和 P2P 方式。Redis 集群采用 P2P 的 Gossip 协议,Gossip 协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播。
Gossip 协议的主要职责就是信息交换。信息交换的载体就是节点彼此发送的 Gossip 消息。常用的 Gossip 消息可分为:ping 消息、pong 消息、meet 消息、fail 消息等。
- ping 消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送 ping 消息,用于检测节点是否在线和交换彼此状态信息,ping 消息中封装了自身节点和部分其他节点的状态数据。
- meet 消息:用于通知新节点加入。发送该消息的节点通知接收节点自己要加入到当前集群,meet 消息通信正常完成后,接收节点会加入到集群中并进行周期性的 ping、pong 消息交换,从而把自己的加入到集群的信息传播到整个集群。
- pong 消息:当接收到 ping、meet 消息时,作为响应消息回复给发送方确认消息正常通信。pong 消息内部也封装了自身状态数据。节点也可以向集群内广播自身的 pong 消息来通知整个集群对自身状态进行更新。
- fail 消息:当节点判定集群内另一个节点客观下线时,会向集群内广播一个 fail 消息,其他节点接收到 fail 消息之后把对应节点更新为客观下线状态,从节点收到 fail 消息会执行自动故障转移。
虽然 Gossip 协议的信息交换机制具有天然的分布式特性,但它是有成本的。由于内部需要频繁地进行节点信息交换,而 ping/pong 消息会携带当前节点和部分其他节点的状态数据,势必会加重带宽和计算的负担。Redis 集群内节点通信采用固定频率(定时任务每秒执行 10 次)。因此节点每次选择需要通信的节点列表变得非常重要。通信节点选择过多虽然可以做到信息及时交换但成本过高。节点选择过少会降低集群内所有节点彼此信息交换频率,从而影响故障判定、新节点发现等需求的速度。因此 Redis 集群的 Gossip 协议需要兼顾信息交换实时性和成本开销,通信节点选择的规则如图 3-5 所示。
暂时无法在飞书文档外展示此内容
每个节点每秒会向 1+10*num (最后通信时间> node_timeout/2) 个节点发送 ping 消息来告知自身和其它部分节点的信息。
# 集群扩缩容
Redis cluster 提供了灵活的集群扩缩容方案,主要得益于 redis 抽象了虚拟槽这一存储单元,集群的扩缩容的底层操作实际上就是虚拟槽在各个 redis 节点之间的移动
暂时无法在飞书文档外展示此内容
- 扩容集群
扩容集群分为一下三步:
-
准备新节点,为了高可用,至少准备两个节点,启动这两个 redis-server,新启动的节点作为孤儿节点运行,没有与任何节点通信。
-
加入集群,新节点向集群中任意节点发送 meet 消息,请求加入到节点,集群中节点收到消息后回复 pong 消息,并在与集群中其它节点正常的 ping/pong 通信时将新节点的信息传播到整个集群。
-
执行槽迁移,给其中一个节点分配槽,并开始执行节点与节点之间的槽中数据的迁移。
-
执行 cluster replication 命令,使新节点中的其中一个成为另一个具有槽的节点的从节点。
-
收缩集群
收缩集群的操作与扩容集群相反:
- 如果需要将主从节点一起下线,需要先下线从节点,否则主节点下线后从节点会被分配给从节点最少的主节点并执行复制操作,增加了额外的带宽压力。
- 下线主节点,将槽迁移至集群中的其它的节点。
- 对集群中其它的节点执行 cluster forget 命令来忘记下线节点,忘记节点后,节点就不会向下线节点发送 gossip 消息。
- 停止下线节点进程。
# 故障发现
Redis Cluster 通过 ping/pong 消息实现故障发现:不需要 sentinel
ping/pong 不仅能传递节点与槽的对应消息,也能传递其他状态,比如:节点主从状态,节点故障等
节点之间相互进行 PING - PONG 消息通信来检测对方状态。如果一个主节点发现某个节点在一定时间内没有响应 PING 消息,就会将其标记为疑似下线(PFAIL);当集群中超过半数的主节点都将某个节点标记为疑似下线时,该节点会被标记为已下线(FAIL)。
集群中的每个节点都会参与故障检测,节点之间相互监控,一旦发现某个节点故障,会共同协作完成故障转移。
# 故障恢复
故障节点被标志为客观下线后,为了保证系统的高可用,必须从从节点中选出一个成为新的主节点。从节点的内部定时任务发现其主节点被标志为客观下线后,将会触发故障恢复流程:
- 资格检查:每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障的主节点。如果短线时间过长,不会发起投票选举。
- 准备选举延迟时间:当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程。从节点根据自身复制偏移量设置延迟选举时间,保证复制延迟低的从节点优先发起选举。
- 发起选举:递增全局配置 epoch,并赋值给从节点本地 epoch,并广播选举消息。
- 选举投票:每个具有槽的主节点在每个 epoch 内有一张选票,在收到从节点的选举消息后,如果选票还在,就会投出这一票。当从节点收到的票数达到 N/2+1,从节点就可以执行替换主节点的操作,其中 N 为具有槽的主节点个数。为了能够达到选举票数,必须保证 N 大于等于 3。如果在一个 epoch 内从节点没有获取到足够的选票,会更新 epoch,并发起下一轮投票。
- 替换主节点:取消复制主节点,接管主节点的槽并广播 pong 消息通知集群当前节点以晋升为主节点并接管了原主节点的槽。
# 客户端路由
# moved 重定向
1. 每个节点通过通信都会共享 Redis Cluster 中槽和集群中对应节点的关系
2. 客户端向 Redis Cluster 的任意节点发送命令,接收命令的节点会根据 CRC16 规则进行 hash 运算与 16383 取余,计算自己的槽和对应节点
3. 如果保存数据的槽被分配给当前节点,则去槽中执行命令,并把命令执行结果返回给客户端
4. 如果保存数据的槽不在当前节点的管理范围内,则向客户端返回 moved 重定向异常
5. 客户端接收到节点返回的结果,如果是 moved 异常,则从 moved 异常中获取目标节点的信息
6. 客户端向目标节点发送命令,获取命令执行结果

槽命中:直接返回

1 | # 计算slot |
# ask 重定向
在对集群进行扩容和缩容时,需要对槽及槽中数据进行迁移
当客户端向某个节点发送命令,节点向客户端返回 moved 异常,告诉客户端数据对应的槽的节点信息
如果此时正在进行集群扩展或者缩空操作,当客户端向正确的节点发送命令时,槽及槽中数据已经被迁移到别的节点了,就会返回 ask,这就是 ask 重定向机制

步骤:
1. 客户端向目标节点发送命令,目标节点中的槽已经迁移支别的节点上了,此时目标节点会返回 ask 转向给客户端
2. 客户端向新的节点发送 Asking 命令给新的节点,然后再次向新节点发送命令
3. 新节点执行命令,把命令执行结果返回给客户端
moved 异常与 ask 异常的相同点和不同点
两者都是客户端重定向
moved 异常:槽已经确定迁移,即槽已经不在当前节点
ask 异常:槽还在迁移中
# smart 智能客户端
将 Cluster slots 的结果映射在本地,为每个节点创建 JedisPool,相当于为每个 redis 节点都设置一个 JedisPool,然后就可以进行数据读写操作
读写数据时的注意事项:
- 每个 JedisPool 中缓存了 slot 和节点 node 的关系
- key 和 slot 的关系:对 key 进行 CRC16 规则进行 hash 后与 16383 取余得到的结果就是槽
- JedisCluster 启动时,已经知道 key,slot 和 node 之间的关系,可以找到目标节点
- JedisCluster 对目标节点发送命令,目标节点直接响应给 JedisCluster
- 如果 JedisCluster 与目标节点连接出错,则 JedisCluster 会知道连接的节点是一个错误的节点
- 此时 JedisCluster 会随机节点发送命令,随机节点返回 moved 异常给 JedisCluster
- JedisCluster 会重新初始化 slot 与 node 节点的缓存关系,然后向新的目标节点发送命令,目标命令执行命令并向 JedisCluster 响应
- 如果命令发送次数超过 5 次,则抛出异常 "Too many cluster redirection!"

# 多节点命令实现
Redis Cluster 不支持使用 scan 命令扫描所有节点 多节点命令就是在在所有节点上都执行一条命令 批量操作优化
# 串行 mget
定义 for 循环,遍历所有的 key,分别去所有的 Redis 节点中获取值并进行汇总,简单,但是效率不高,需要 n 次网络时间

# 串行 IO
对串行 mget 进行优化,在客户端本地做内聚,对每个 key 进行 CRC16hash,然后与 16383 取余,就可以知道哪个 key 对应的是哪个槽
本地已经缓存了槽与节点的对应关系,然后对 key 按节点进行分组,成立子集,然后使用 pipeline 把命令发送到对应的 node,需要 nodes 次网络时间,大大减少了网络时间开销

# 并行 IO
并行 IO 是对串行 IO 的一个优化,把 key 分组之后,根据节点数量启动对应的线程数,根据多线程模式并行向 node 节点请求数据,只需要 1 次网络时间

# hash_tag
将 key 进行 hash_tag 的包装,然后把 tag 用大括号括起来,保证所有的 key 只向一个 node 请求数据,这样执行类似 mget 命令只需要去一个节点获取数据即可,效率更高

# 四种优化方案优缺点分析

https://cloud.tencent.com/developer/article/2226847
https://pdai.tech/md/db/nosql-redis/db-redis-x-cluster.html
1 | 1 wget http://mirrors.aliyun.com/repo/Centos-altarch-7.repo -O /etc/yum.repos.d/CentOS-Base.repo |

