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,它就开始复制。第一次要把整个数据集拷过去(全量同步),之后主节点每执行一条写命令,都实时转发过来(命令传播)。

1从节点请求同步发 PSYNC ? -1:我是新来的,什么都没有
2主节点回 FULLRESYNC附上自己的复制 ID 和当前偏移量
3主节点 fork 生成 RDBRedis 7 默认直接通过网络发出去,不落盘;期间的新写入先攒在缓冲区
4从节点清空、加载把旧数据删掉,加载收到的 RDB
5补发缓冲 + 持续传播先补上第 3 步攒下的命令,之后每条写命令实时转发
断线重连:能补就补,补不上就全量

主节点维护一个环形的复制积压缓冲区(repl-backlog-size,默认 1 MB),里面是最近转发过的命令。从节点断线重连时报上“我复制到第几个字节了”(PSYNC 复制ID 偏移量)。缺的那段还在缓冲区里,就只补这一段(部分同步);已经被覆盖了,只能重新全量同步。

复制是异步的

主节点执行完写命令就回复客户端,不等从节点。所以从节点的数据总是慢一点,主节点在这之间挂了,这部分写入就丢了。

WAIT 1 1000 可以等至少 1 个从节点确认收到(最多等 1 秒),能减少丢失,但不能保证不丢。

积压缓冲区够不够:断线重连计算器

复制流速度:
从节点断线多久:
repl-backlog-size:

经验:repl-backlog-size ≥ 每秒复制流量 × 可能断线的最长秒数。主节点数据 8 GB 时,一次全量同步要 fork、生成并传 8 GB 的 RDB,从节点还要清空重新加载。

从节点配置redis.conf
replicaof 10.0.0.1 6379          # Redis 5 起叫 replicaof,老名字 slaveof 仍可用
masterauth ******                # 主节点有密码时
replica-read-only yes             # 默认只读
repl-backlog-size 64mb            # 默认 1mb,写多的实例要调大

哨兵:主节点挂了,谁来换、换谁

哨兵是单独的进程(默认端口 26379),通常部署 3 个,放在不同的机器上。它们每秒给主从节点发 PING,也互相交流看法。客户端不直接写死主节点地址,而是先问哨兵“现在的主节点是谁”。

主观下线 SDOWN某一个哨兵,超过 down-after-milliseconds(默认 30 秒)没收到主节点的正常回复,就认为它挂了。可能只是这个哨兵自己网络不好
客观下线 ODOWN认为它挂了的哨兵数达到 quorum(比如 2),才算真挂了,可以开始故障转移。sentinel monitor mymaster 10.0.0.1 6379 2
选领头哨兵真正动手的只能有一个。哨兵们投票,拿到过半票数(3 个里要 2 票)的当领头。quorum 决定能不能判死,过半决定能不能动手

故障转移,一步一步

    示意:down-after-milliseconds 设成 5000,所以第 5 秒判定下线;用默认的 30000,整个过程要再往后推 25 秒。日志是哨兵真实的事件名,地址做了简化。

    新主节点怎么挑
    1. 先排除断线太久、数据太旧的从节点
    2. replica-priority 小的优先(设成 0 表示永远不当主)
    3. 复制偏移量大的优先:数据最新
    4. 还一样,就选运行 ID 字典序小的
    脑裂:两个主节点同时在写

    旧主节点只是和哨兵断了网,自己还活着,旁边的客户端还在往里写。哨兵这边已经选出新主。网络恢复后,旧主变成从节点,清空数据跟新主同步,那段时间写进去的全丢了。

    防法:min-replicas-to-write 1 + min-replicas-max-lag 10。连着的健康从节点不够,主节点就拒绝写入,报 NOREPLICAS Not enough good replicas to write.

    Spring Boot 连哨兵Boot 3 前缀;Boot 2 是 spring.redis
    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 取余,得到它的槽号。三个主节点各管一段槽。

    主节点 A槽 0 – 5460
    主节点 B槽 5461 – 10922
    主节点 C槽 10923 – 16383

    算一算 key 落在哪

    花括号叫 hash tag:key 里有 {…} 时,只用括号里的内容算槽。想让几个 key 一起用 MGET、事务或 Lua 脚本,就给它们相同的 tag。

    找错节点:MOVED 和 ASK

    客户端缓存着“哪个槽在哪个节点”。发错了,节点回 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 个槽

    节点之间心跳时,要带上一张“我负责哪些槽”的位图。16384 位正好 2 KB,心跳包不会太大;而集群一般不会超过 1000 个主节点,槽也够分。

    集群下有哪些限制

    多 key 命令(MGET、事务、Lua)的所有 key 必须在同一个槽;只能用 0 号库,不能 SELECT;KEYS、SCAN 只扫当前连的那个节点。

    Spring Boot 连集群Lettuce 默认不会自动刷新拓扑,最好打开
    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.*
    没变的:异步复制PSYNC 部分同步SDOWN → ODOWN → 选领头 → 换主16384 个槽MOVED / ASK

    常踩的坑

    多数坑都来自两点:复制是异步的,客户端看到的拓扑可能是旧的。

    刚写进去,从节点上读不到

    复制有延迟。读写分离时,“写完马上要读”的请求走主节点。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 秒。可以调到几秒,但别小于网络正常抖动的时间,不然会频繁误切。

    CROSSSLOT Keys in request don't hash to the same slot

    集群下 MGET、事务、Lua 用到了不同槽的 key,同一个节点也不行。用 hash tag 把它们放到同一个槽,但别把大量 key 塞进同一个 tag,会让一个节点特别热。

    CLUSTERDOWN The cluster is down

    某一段槽的主从全挂了,默认 cluster-require-full-coverage yes 会让整个集群停止服务。能接受部分可用,就改成 no。

    集群切换后,应用一直报错,重启应用才好

    Lettuce 默认不会主动刷新集群拓扑,一直往旧节点发。打开 lettuce.cluster.refresh.adaptive 和 period。

    集群里 SCAN 只扫出一部分 key

    SCAN、KEYS 只作用于当前连接的节点。要遍历全部,得对每个主节点各扫一遍;KEYS 会阻塞,生产环境别用。