跳到主要内容

Redis 部署与高可用

先分清三种方案​

单机 Redis 有两个绕不过去的问题:机器挂了服务就没了,内存和 QPS 顶到单机上限就上不去了。高可用方案就是围绕这两点做文章:

  • 主从复制:一主多从,数据实时同步。解决「读扩展」和「数据备份」,但主库挂了要人工切,不解决自动故障转移。
  • 哨兵(Sentinel):在主从基础上加一组监控进程,主库挂了自动选一个从库顶上。解决「自动切换」,但每个节点仍然要装得下全量数据,写还是单点。
  • Cluster:数据分片到多个主节点,每个主节点带从节点。同时解决「容量/写扩展」和「自动故障转移」,代价是客户端和运维复杂度上一个台阶。

选型没有绝对答案,先看数据量和写 QPS,再看能接受多长的不可用时间。

主从复制​

配置​

主库基本不用改,从库加几行即可。以 10.0.0.11 为主、10.0.0.12 为从:

# 从节点 redis.conf
replicaof 10.0.0.11 6379
# 主库设了 requirepass,从库必须带同样的密码,否则同步被拒
masterauth Your_Master_Password
# 从库默认只读,防止误写导致主从不一致
replica-read-only yes
# 故障转移时优先选 priority 小的,0 表示永不升主
replica-priority 100
# 复制积压缓冲区,越大越能扛住短暂断连后的增量同步
repl-backlog-size 64mb

# 以下两项配在主节点,可选:至少 1 个延迟不超过 10 秒的从库,否则拒绝写入
min-replicas-to-write 1
min-replicas-max-lag 10

也可以在线改,不用重启:

redis-cli -h 10.0.0.12 REPLICAOF 10.0.0.11 6379
redis-cli -h 10.0.0.12 CONFIG SET masterauth 'Your_Master_Password'
min-replicas-to-write 是把双刃剑

它能防止「从库全挂、主库还在裸奔写」导致的数据丢失,但配置不当会直接让业务写失败:从库网络抖动超过 10 秒,主库就返回 NOREPLICAS Not enough good replicas to write。上线前先确认监控能覆盖从库状态,别把它当成默认必选项。

全量同步与增量同步​

从库第一次连上主库,走的是全量同步:

  1. 从库发 PSYNC ? -1,主库回 +FULLRESYNC <replid> <offset>,把自己的复制 ID 和当前偏移量告诉从库。
  2. 主库 fork 一个子进程做 BGSAVE 生成 RDB,同时把这段时间的新写命令攒在复制缓冲区里。
  3. RDB 传给从库,从库清空自己的数据后加载。
  4. 主库把缓冲区里的命令补发给从库,追平后进入在线状态。

之后就是增量同步:主库每执行一条写命令,就异步发给所有从库;从库每秒回 REPLCONF ACK <offset> 报告自己同步到哪了。网络闪断重连时,从库带着 replid + offset 发 PSYNC,如果主库的积压缓冲区(backlog)里还有这段数据,就回 +CONTINUE 只补差额,不用再传一遍全量。

两个关键参数:

  • repl-backlog-size:环形缓冲区大小,默认 1MB。写入量大、断连时间长时,1MB 几秒就覆盖了,重连只能走全量。生产建议 64MB 起,按「写入速率 × 可容忍断连秒数」估算。
  • repl-diskless-sync:无盘复制,RDB 不落盘直接通过 socket 发给从库,省一次磁盘 IO。磁盘慢、从库多时收益明显,repl-diskless-sync-delay 控制等待多久再开始,好让多个从库一起接。

查看复制状态:

127.0.0.1:6379> INFO replication
role:master
connected_slaves:1
slave0:ip=10.0.0.12,port=6379,state=online,offset=1024,lag=0
master_replid:8a1f0c...
master_repl_offset:1024

lag 是从库最后一次 ACK 距今的秒数,正常应该是 0 或 1;持续增长说明复制链路有问题。master_repl_offset 和从库的 slave_repl_offset 之差就是复制延迟。

主从是异步复制

主库写完内存就返回成功,不等从库确认。所以主库宕机时,最后一段没发出去的数据会丢;从库升主后这段数据就永久消失了。这是所有基于 Redis 主从的方案(包括哨兵)的共同前提,对数据零丢失有要求的场景要么上 WAIT 命令做同步确认(性能代价大),要么别把 Redis 当唯一存储。

哨兵 Sentinel​

最小 3 节点​

哨兵本身也是 Redis 进程,只是跑的是 redis-sentinel 模式。必须至少 3 个、且为奇数,因为选主需要多数派投票,2 个节点挂一个就凑不齐多数。

三台机器各放一份 /etc/redis/sentinel.conf,内容相同:

port 26379
daemonize yes
dir /var/lib/redis-sentinel
logfile /var/log/redis/sentinel.log

# 监控名为 mymaster 的主库,2 表示至少 2 个哨兵认为它挂了才判定客观下线
sentinel monitor mymaster 10.0.0.11 6379 2
sentinel auth-pass mymaster Your_Master_Password
# 30 秒联系不上就主观下线
sentinel down-after-milliseconds mymaster 30000
# 故障转移的总超时
sentinel failover-timeout mymaster 180000
# 一次只让 1 个从库去同步新主,避免新主被压垮
sentinel parallel-syncs mymaster 1

启动:

redis-server /etc/redis/sentinel.conf --sentinel
# 或者用 redis-sentinel 软链
redis-sentinel /etc/redis/sentinel.conf
sentinel.conf 必须可写

哨兵会在运行中自动改写配置文件:发现新从库、切换主库后都会把结果写回 sentinel.conf。如果文件属主不对或目录只读,哨兵日志里会报配置保存失败,重启后就丢失了最新的拓扑。用 redis 用户启动并确保目录可写。

故障转移流程​

一次完整的切换分五步:

  1. 主观下线(SDOWN):某个哨兵在 down-after-milliseconds 内 ping 不通主库,先把它标记为「我认为它挂了」。
  2. 客观下线(ODOWN):这个哨兵通过 SENTINEL is-master-down-by-addr 询问其他哨兵,达到配置的 quorum(示例是 2)后,判定主库客观下线。
  3. 选举领头哨兵:通过类似 Raft 的投票,选出一个哨兵负责执行这次故障转移。要拿到多数派的票,这也是为什么哨兵要奇数且至少 3 个。
  4. 挑新主:在从库里按优先级筛——replica-priority 小的优先,相同则比复制偏移量(数据更新的优先),再相同比 runid。选中的从库执行 REPLICAOF NO ONE 升主。
  5. 改拓扑:其余从库指向新主,旧主恢复后也被降级为从库。整个过程写回 sentinel.conf。

切换期间(通常几秒到几十秒)写请求会失败,客户端要能捕获异常并重试。哨兵只负责「选出新主」,不负责转发流量,客户端必须自己知道新主是谁。

客户端怎么配合​

三种常见做法:

  • 客户端直连哨兵(推荐):应用配置哨兵地址列表和 master 名称,客户端启动时问哨兵「mymaster 现在是谁」,之后持续订阅切换事件。Jedis 的 JedisSentinelPool、Lettuce 的 RedisURI(配置 masterId 和哨兵地址)、Spring Boot 的 spring.data.redis.sentinel.master/nodes 都是这个模式。
  • VIP 漂移:用 keepalived 之类把虚拟 IP 绑到当前主库,应用连 VIP 不变。好处是应用零改造,坏处是 VIP 本身可能成为单点,切换时 ARP 收敛也有延迟。
  • 代理层:HAProxy、Twemproxy、Codis 之类挡在中间,由代理感知后端变化。多一层就多一个故障点和一份延迟,非必要不加。

普通直连(只写一个 IP)的客户端不感知哨兵,主库挂了它只会一直报连接失败,必须改造成上面三种之一。

# 连上哨兵查当前主库
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
redis-cli -p 26379 SENTINEL replicas mymaster
redis-cli -p 26379 SENTINEL sentinels mymaster

Redis Cluster​

槽位与数据分布​

Cluster 把整个 key 空间切成 16384 个槽(slot),每个主节点负责其中一部分。key 落在哪个槽由这个公式决定:

slot = CRC16(key) mod 16384

16384 这个数字不是随便定的:心跳包要携带槽位 bitmap,16384 个 bit 是 2KB,在节点数上限和包体积之间取了个平衡;槽位数再翻倍,心跳包就太肥了。

每个主节点可以带若干从节点,主挂了从自动升主。至少 3 主 3 从才能构成一个能容忍单点故障的最小集群。

节点配置与建集群​

六台机器(或六个端口)各放一份配置,只有端口和文件路径不同:

port 6379
cluster-enabled yes
# 集群拓扑文件,由 Redis 自己维护,不要手改
cluster-config-file nodes-6379.conf
# 节点失联多久判定下线,默认 15 秒
cluster-node-timeout 15000
appendonly yes
dir /var/lib/redis
总线端口也要放行

集群节点之间除了业务端口 6379,还会用 端口 + 10000(这里是 16379)做集群总线通信。防火墙只开 6379 会出现「节点互相看不见、集群一直处于 fail 状态」的经典问题。云上安全组要同时放行两个端口。

启动所有节点后,用 redis-cli --cluster create 分配槽位并建立主从关系:

redis-cli --cluster create \
10.0.0.11:6379 10.0.0.12:6379 10.0.0.13:6379 \
10.0.0.14:6379 10.0.0.15:6379 10.0.0.16:6379 \
--cluster-replicas 1

--cluster-replicas 1 表示每个主节点配 1 个从节点。命令会给出槽位分配方案,输入 yes 确认。执行完检查状态:

redis-cli --cluster check 10.0.0.11:6379
redis-cli -h 10.0.0.11 -p 6379 CLUSTER INFO
redis-cli -h 10.0.0.11 -p 6379 CLUSTER NODES

CLUSTER INFO 里 cluster_state:ok 且 cluster_slots_assigned:16384 才算建好。日常运维命令:

# 在线迁移槽位(交互式,按提示输入源/目标节点 ID 和槽位范围)
redis-cli --cluster reshard 10.0.0.11:6379

# 在从节点上执行,把它的主库切走(演练、升级时用)
redis-cli -h 10.0.0.16 -p 6379 CLUSTER FAILOVER

# 加新节点
redis-cli --cluster add-node 10.0.0.17:6379 10.0.0.11:6379

客户端要支持集群协议,用 redis-cli -c 可以体验自动跟随重定向:

redis-cli -c -h 10.0.0.11 -p 6379

访问到不属于当前节点的槽时,会收到 MOVED <slot> <ip:port>,客户端要更新本地槽位表并重发;迁移过程中的临时 key 会收到 ASK,只对这一次请求重定向。成熟的客户端(Jedis Cluster、Lettuce、redis-py-cluster)都帮你处理了,自己写协议的话这两个响应必须实现。

集群的限制​

  • 只有 db0:SELECT 不可用,别指望像单机那样分 16 个库。
  • 多 key 操作必须同槽:MGET、SUNIONSTORE、事务、Lua 里涉及的 key 都要落在同一个槽。用 hash tag 强制同槽,例如 cart:{1001}:items 和 cart:{1001}:total。
  • 批量命令要客户端拆分:跨槽的 MGET 会直接报 CROSSSLOT,按槽分组后分批发。
  • 运维复杂度高:扩缩容要迁槽、要保证迁移期间业务可用;节点数多,监控和故障定位都更麻烦。
  • Pub/Sub 是集群广播:普通 PUBLISH 会把消息发到所有节点,频道流量大时带宽要单独评估;7.0 起可以用 SSUBSCRIBE 做分片订阅。

三种方案选型​

对比项主从哨兵Cluster
数据容量单机内存上限单机内存上限多主分片,可水平扩展
写扩展不支持,写仍在主库不支持支持,按槽分散到多个主
读扩展支持,从库可读支持,从库可读支持,从库可读
自动故障转移不支持,需人工支持支持
客户端改造无需支持哨兵协议需支持集群协议
运维成本低中高
典型场景读多写少、做备份、冷备数据量不大但要求自动切换大数据量、高并发写入

选择路径可以简化成三个问题:

  1. 数据量能不能装进一台机器?能,就别上 Cluster。
  2. 能不能接受主库挂掉后人工切换的几分钟?能,主从就够;不能,上哨兵。
  3. 写 QPS 或内存已经顶到单机上限?是,才考虑 Cluster,并做好客户端改造和运维投入的准备。
别为了「高可用」三个字盲目上 Cluster

Cluster 解决的是扩展性问题,不是可用性问题。3 主 3 从的 Cluster 在运维不当(总线端口没开、节点超时太短、客户端不支持重定向)时,比哨兵更容易出事故。数据量没到单机上限,哨兵是性价比最高的选择。

小结​

  • 主从解决读扩展和备份,全量同步靠 RDB、增量同步靠 backlog,异步复制意味着可能丢最后一段数据。
  • 哨兵至少 3 个奇数节点,quorum 决定何时判客观下线,多数派选出领头哨兵执行切换;客户端必须支持哨兵协议或用 VIP/代理。
  • Cluster 用 16384 个槽分片,至少 3 主 3 从,建集群用 redis-cli --cluster create,多 key 操作要靠 hash tag 同槽。
  • 选型顺序:先看容量能否单机装下,再看能否接受人工切换,最后才考虑分片。

集群搭起来只是开始,日常怎么盯指标、怎么查慢查询、怎么治理内存,见《运维与监控》。