REDIS 7 · REPLICATION · PSYNC · SENTINEL · FAILOVER · CLUSTER · 16384 SLOTS
一台不够用,先复制、再派人盯着、最后分家
单台 Redis 有三个问题:挂了就没了,挂了没人管,一台装不下。这三个问题各有一个答案,一个比一个复杂,按需要往上加就行。
主从复制Replication
解决:数据多存几份,读可以分出去从节点不停地复制主节点的数据。主节点挂了,数据还在,但要人工把从节点改成主。
哨兵Sentinel
解决:主节点挂了,自动换人几个哨兵进程一直盯着主节点,发现它挂了,投票选一个从节点升为主,再通知客户端。
集群Cluster
解决:一台装不下、写不过来数据按 key 分到 16384 个槽,几个主节点各管一段,每个主节点再带自己的从节点。
master唯一能写的那台,写进来的命令都会转发给从节点。
一个分片只有一个主replica主节点的副本,默认只读,可以分担读请求。
数据总比主节点慢一点redis-sentinel :26379不存数据,只负责盯着、投票、换主、告诉客户端。
至少 3 个,分开部署16384 slots集群里每个 key 都算出一个槽号,槽归哪个主节点,key 就在哪。
CRC16(key) mod 16384主从复制:先抄一整本,再一条条跟
在从节点上执行 REPLICAOF 10.0.0.1 6379,它就开始复制。第一次要把整个数据集拷过去(全量同步),之后主节点每执行一条写命令,都实时转发过来(命令传播)。
PSYNC ? -1:我是新来的,什么都没有主节点维护一个环形的复制积压缓冲区(repl-backlog-size,默认 1 MB),里面是最近转发过的命令。从节点断线重连时报上“我复制到第几个字节了”(PSYNC 复制ID 偏移量)。缺的那段还在缓冲区里,就只补这一段(部分同步);已经被覆盖了,只能重新全量同步。
主节点执行完写命令就回复客户端,不等从节点。所以从节点的数据总是慢一点,主节点在这之间挂了,这部分写入就丢了。
WAIT 1 1000 可以等至少 1 个从节点确认收到(最多等 1 秒),能减少丢失,但不能保证不丢。
积压缓冲区够不够:断线重连计算器
经验:repl-backlog-size ≥ 每秒复制流量 × 可能断线的最长秒数。主节点数据 8 GB 时,一次全量同步要 fork、生成并传 8 GB 的 RDB,从节点还要清空重新加载。
replicaof 10.0.0.1 6379 # Redis 5 起叫 replicaof,老名字 slaveof 仍可用
masterauth ****** # 主节点有密码时
replica-read-only yes # 默认只读
repl-backlog-size 64mb # 默认 1mb,写多的实例要调大
哨兵:主节点挂了,谁来换、换谁
哨兵是单独的进程(默认端口 26379),通常部署 3 个,放在不同的机器上。它们每秒给主从节点发 PING,也互相交流看法。客户端不直接写死主节点地址,而是先问哨兵“现在的主节点是谁”。
down-after-milliseconds(默认 30 秒)没收到主节点的正常回复,就认为它挂了。可能只是这个哨兵自己网络不好quorum(比如 2),才算真挂了,可以开始故障转移。sentinel monitor mymaster 10.0.0.1 6379 2quorum 决定能不能判死,过半决定能不能动手故障转移,一步一步
示意:down-after-milliseconds 设成 5000,所以第 5 秒判定下线;用默认的 30000,整个过程要再往后推 25 秒。日志是哨兵真实的事件名,地址做了简化。
- 先排除断线太久、数据太旧的从节点
replica-priority小的优先(设成 0 表示永远不当主)- 复制偏移量大的优先:数据最新
- 还一样,就选运行 ID 字典序小的
旧主节点只是和哨兵断了网,自己还活着,旁边的客户端还在往里写。哨兵这边已经选出新主。网络恢复后,旧主变成从节点,清空数据跟新主同步,那段时间写进去的全丢了。
防法:min-replicas-to-write 1 + min-replicas-max-lag 10。连着的健康从节点不够,主节点就拒绝写入,报 NOREPLICAS Not enough good replicas to write.
spring:
data:
redis:
sentinel:
master: mymaster # 和 sentinel monitor 里的名字一致
nodes: 10.0.0.11:26379,10.0.0.12:26379,10.0.0.13:26379 # 写哨兵的地址,不写主节点
password: ******
集群:把 key 分到 16384 个槽里
哨兵只解决了“挂了换人”,所有数据还是在一个主节点上。数据量或写入量一台扛不住时,就用集群:每个 key 用 CRC16 算一个数,再对 16384 取余,得到它的槽号。三个主节点各管一段槽。
算一算 key 落在哪
花括号叫 hash tag:key 里有 {…} 时,只用括号里的内容算槽。想让几个 key 一起用 MGET、事务或 Lua 脚本,就给它们相同的 tag。
客户端缓存着“哪个槽在哪个节点”。发错了,节点回 MOVED 5712 10.0.0.2:6379:这个槽在别处,以后都去那儿,客户端更新缓存。
扩容迁移槽的过程中,一部分 key 已经搬走,节点回 ASK 5712 10.0.0.4:6379:这一次去那儿问一下(先发 ASKING),不用更新缓存。
节点之间通过集群总线(客户端端口 + 10000,比如 16379)不停地互相 PING。一个主节点超过 cluster-node-timeout(默认 15 秒)没回应,被多数主节点标记为 FAIL 后,它的从节点发起选举,拿到多数主节点的票就升为主,接管这段槽。
节点之间心跳时,要带上一张“我负责哪些槽”的位图。16384 位正好 2 KB,心跳包不会太大;而集群一般不会超过 1000 个主节点,槽也够分。
多 key 命令(MGET、事务、Lua)的所有 key 必须在同一个槽;只能用 0 号库,不能 SELECT;KEYS、SCAN 只扫当前连的那个节点。
spring:
data:
redis:
cluster:
nodes: 10.0.0.1:6379,10.0.0.2:6379,10.0.0.3:6379 # 写几个就行,其余的会自动发现
max-redirects: 3
lettuce:
cluster:
refresh:
adaptive: true # 收到 MOVED、连接断开时马上刷新
period: 30s # 另外每 30 秒刷新一次
三种方案怎么选
一句话:数据一台装得下,用哨兵;装不下或写不过来,用集群。主从复制是两者的基础。
| 主从复制 | 哨兵 | 集群 | |
|---|---|---|---|
| 解决什么 | 数据多存几份,读可以分到从节点 | 主节点挂了自动切换 | 数据和写入分摊到多个主节点 |
| 自动故障转移 | 没有,要人工 | 有,哨兵投票 | 有,节点之间投票 |
| 数据分片 | 没有,每台全量 | 没有 | 16384 个槽 |
| 写能力 | 一个主节点 | 一个主节点 | 多个主节点相加 |
| 建议的最小部署 | 1 主 1 从 | 1 主 2 从 + 3 个哨兵 | 3 主 3 从 |
| 多 key 命令 | 随便用 | 随便用 | 只能在同一个槽 |
| 客户端要做的 | 连主节点 | 先问哨兵谁是主 | 记住槽分布,处理 MOVED / ASK |
| 适合 | 读多、要备份 | 一台装得下,要高可用 | 一台装不下,或写入扛不住 |
Redis 5~6 和 Redis 7,Boot 2 和 Boot 3
JDK 8 还是 17 不影响这些。变化在 Redis 的默认值和命令,以及 Spring 的配置前缀。
Redis 5~6 · Boot 2
JDK 8+- Redis 5 起命令改叫
REPLICAOF,建集群从 redis-trib.rb 换成redis-cli --cluster - 全量同步默认先把 RDB 写到磁盘再发(
repl-diskless-sync no) - 查槽分布用
CLUSTER SLOTS - 配置前缀
spring.redis.sentinel.*、spring.redis.cluster.*;Lettuce 拓扑刷新的配置项 Boot 2.3 起才有
Redis 7 · Boot 3
JDK 17+repl-diskless-sync默认改成 yes:RDB 直接通过网络发给从节点- 新增
CLUSTER SHARDS,取代 CLUSTER SLOTS - 分片发布订阅
SSUBSCRIBE/SPUBLISH:消息只在槽所在的分片里传,不再广播全集群 - 配置前缀改成
spring.data.redis.*
常踩的坑
多数坑都来自两点:复制是异步的,客户端看到的拓扑可能是旧的。
复制有延迟。读写分离时,“写完马上要读”的请求走主节点。INFO replication 里比较主从的 offset 可以看延迟。
异步复制,主节点挂之前还没传给从节点的写入就丢了。关键写入用 WAIT 降低风险;一条都不能丢的数据,别只放在 Redis。
脑裂:被隔离的旧主还在接受写入,回来后变成从节点被清空。配 min-replicas-to-write 和 min-replicas-max-lag。
积压缓冲区太小,或者全量同步期间攒的命令超过了 client-output-buffer-limit replica 256mb 64mb 60,连接被断开,重连又是全量。日志里有 overcoming of output buffer limits。两个都调大。
只部署了 2 个哨兵,或者哨兵和主节点放在同一台机器,挂一台就凑不够过半票数。至少 3 个,放在 3 台不同的机器上。
down-after-milliseconds 默认 30 秒。可以调到几秒,但别小于网络正常抖动的时间,不然会频繁误切。
集群下 MGET、事务、Lua 用到了不同槽的 key,同一个节点也不行。用 hash tag 把它们放到同一个槽,但别把大量 key 塞进同一个 tag,会让一个节点特别热。
某一段槽的主从全挂了,默认 cluster-require-full-coverage yes 会让整个集群停止服务。能接受部分可用,就改成 no。
Lettuce 默认不会主动刷新集群拓扑,一直往旧节点发。打开 lettuce.cluster.refresh.adaptive 和 period。
SCAN、KEYS 只作用于当前连接的节点。要遍历全部,得对每个主节点各扫一遍;KEYS 会阻塞,生产环境别用。