2023 年我们公司业务量翻倍 单 Redis 实例 32GB 内存撑不住 决定上 Redis Cluster 6 主 6 从 18 个 hash slot 我们以为只是配置一下集群就行 三个节点跑 redis-cli --cluster create 自动分槽 结果上生产第一周连续出 5 次 P1 故障第一种最让我傻眼的是有个开发用 MULTI/EXEC 事务 跨 slot 操作报错 CROSSSLOT Keys in request don't hash to the same slot 整个下单流程崩了我们才意识到 Cluster 模式下事务必须保证所有 key 在同一 slot 必须用 hashtag {user:123}:cart 这种写法;第二种最难缠是 fail over 切主后客户端疯狂报 MOVED 错误日志爆炸 排查发现 Jedis 客户端版本太老 cluster topology refresh 阈值 默认是 5 次错误才刷新 切主期间几千个请求全报错;第三种最离谱是我们做 SCAN 操作以为像单机一样一次扫完 结果发现 Cluster 的 SCAN 是 per-node 必须遍历所有主节点 而且分页 cursor 是按节点维护的 漏一个节点就漏一批 key;第四种最致命是 Sentinel 集群我们只部署了 3 个 哨兵节点之一所在的机房网络抖动 quorum 从 3 个 majority 2 个变成 2 个 majority 2 个 触发脑裂 同时有 2 个 master 数据写两边 恢复后差异手工对账;第五种最莫名其妙是大 key 迁移 我们有一个 hash 200 万字段 cluster slot 迁移走 MIGRATE 命令 单次 timeout 30 秒整个迁移失败 集群进入 fail 状态业务报 CLUSTERDOWN 半小时;第六种最坑是我们用 KEYS pattern 调试 直接把主节点扫死 集群心跳超时被踢出 fail over 又来一遍。真正能稳定撑住生产负载的 Redis 高可用是一个 Cluster 数据分片设计 + Sentinel 哨兵部署 + 客户端配置精调 + 大 key 治理 + 慢命令防御 + 持久化策略 + 监控告警 + 故障演练的完整方法论,任何一环失守都可能让你的 Redis 从"缓存层"变成"业务雪崩源"。本文从头梳理 Redis 高可用与 Cluster 部署的要点,Cluster 怎么设计 Sentinel 怎么部署 客户端怎么配 大 key 怎么治 慢命令怎么防 持久化怎么选 监控怎么搭,以及一些把 Redis 撑过亿级 QPS 要避开的工程坑。
问题背景:为什么"redis-cli cluster create"远远不够
很多团队上 Redis Cluster 跑通三节点就以为完事,但生产化 Cluster 远比想象的复杂:
- Slot 设计与 hashtag:不懂 16384 slot 与 hashtag 的关系,事务/pipeline 直接崩。
- Sentinel quorum 与脑裂:哨兵数量与 quorum 配置错,跨机房网络抖动直接脑裂。
- 客户端 cluster 感知:Jedis/Lettuce/redis-py 不同客户端对 MOVED/ASK 的处理差异巨大。
- 大 key 与热 key:单 key 几 MB 阻塞主线程,迁移 timeout,热 key 把单 shard 打爆。
- 持久化选型:RDB / AOF / 混合的内存与恢复时间 trade-off 选错恢复几小时。
- 慢命令防御:KEYS / SMEMBERS / LRANGE 大列表都是隐藏炸弹。
一 Cluster 数据分片与 Hashtag 设计
Redis Cluster 用 CRC16 把 key 映射到 16384 个 slot 槽 然后分配给各个 master。同一 key 必须在同一 slot 才能事务/pipeline 操作。生产中靠 hashtag 控制相关 key 共槽。
# 1 部署 6 节点 cluster(3 主 3 从)
# 各节点 redis.conf
"""
port 6379
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000 # 15s 节点超时
cluster-require-full-coverage no # 关键 部分 slot 不可用时仍可服务
cluster-allow-reads-when-down no # fail 时拒绝读 防脏数据
cluster-replica-no-failover no # 允许从节点 failover
appendonly yes
appendfsync everysec
maxmemory 16gb
maxmemory-policy allkeys-lru
"""
# 2 创建集群
redis-cli --cluster create \
10.0.1.10:6379 10.0.1.11:6379 10.0.1.12:6379 \
10.0.1.13:6379 10.0.1.14:6379 10.0.1.15:6379 \
--cluster-replicas 1 \
--cluster-yes
# 3 检查 slot 分布
redis-cli --cluster check 10.0.1.10:6379
# [OK] 16384 slots covered
# 4 扩容(加新主从)
redis-cli --cluster add-node 10.0.1.16:6379 10.0.1.10:6379
redis-cli --cluster add-node 10.0.1.17:6379 10.0.1.10:6379 \
--cluster-slave --cluster-master-id <new_master_id>
# reshard 把部分 slot 迁到新节点
redis-cli --cluster reshard 10.0.1.10:6379 \
--cluster-from all \
--cluster-to <new_master_id> \
--cluster-slots 2048 \
--cluster-yes
# 5 在线 reshard 注意事项
# - reshard 期间业务正常 但延迟略升
# - 大 key 迁移可能 timeout 必须提前清理
# - 跨机房 reshard 网络带宽吃紧 限速 --pipeline 10
# 6 Hashtag 让相关 key 同 slot
# 错误:user:123:cart 与 user:123:order 不同 slot 不能事务
r.mset({"user:123:cart": "...", "user:123:order": "..."}) # CROSSSLOT 错误
# 正确:用 {} 强制 hash 同一部分
r.mset({"{user:123}:cart": "...", "{user:123}:order": "..."}) # 同 slot OK
# 配合 pipeline / 事务
pipe = r.pipeline()
pipe.hset("{user:123}:cart", "item1", "...")
pipe.hset("{user:123}:order", "ord1", "...")
pipe.execute()
# 7 hashtag 设计原则
# - 业务粒度 必须事务一致的放一组 (用户级 / 订单级 / 会话级)
# - 不能放太大粒度 例如 {tenant:1} 会让一个租户所有 key 在一 slot 单节点崩
# - 不能放太细 失去事务能力
# 8 检查 hashtag 是否正确
def hashtag(key):
"""提取 hashtag 部分 用于 CRC16"""
if "{" in key and "}" in key:
start = key.index("{")
end = key.index("}", start)
if end > start + 1:
return key[start+1:end]
return key
print(hashtag("{user:123}:cart")) # user:123
print(hashtag("user:123:cart")) # user:123:cart (整个 key 参与 hash)
# 9 keyslot 命令验证
print(r.execute_command("CLUSTER KEYSLOT", "{user:123}:cart")) # 5694
print(r.execute_command("CLUSTER KEYSLOT", "{user:123}:order")) # 5694 同 slot
实战经验:cluster-require-full-coverage 必须设 no 否则一个 master 挂全集群拒服务;hashtag 设计是 Cluster 模式核心 必须按业务事务粒度规划;reshard 前必须清大 key 否则 MIGRATE timeout 集群进 fail 状态;扩容用 cluster add-node + reshard 不要直接改 nodes.conf;hashtag 太大会让单 slot 数据倾斜 太细失去事务能力 需平衡。
二 Sentinel 哨兵部署与脑裂防御
Sentinel 是 Redis 高可用的另一套方案 (非 Cluster) 适合中小规模主从+故障切换。错误配置最容易脑裂 多主同时写数据。
# 1 Sentinel 配置
# sentinel.conf
"""
port 26379
sentinel monitor mymaster 10.0.1.10 6379 2 # quorum=2 至少 2 个哨兵认同才切
sentinel down-after-milliseconds mymaster 5000 # 5s 无响应认定主观下线
sentinel failover-timeout mymaster 30000 # 30s failover 超时
sentinel parallel-syncs mymaster 1 # 一次只让 1 个从同步新主 防新主压力
sentinel auth-pass mymaster yourpassword
# 关键 防脑裂参数(在 master redis.conf)
min-replicas-to-write 1 # 至少 1 个从同步成功才写
min-replicas-max-lag 10 # 从滞后超 10s 不算
"""
# 2 哨兵数量与 quorum 设计原则
# - 至少 3 个哨兵 否则一个挂 quorum 不足
# - 推荐 5 个哨兵跨 3 机房 任一机房挂仍能 majority
# - quorum 设 N/2+1 (3 个哨兵设 2 5 个哨兵设 3)
#
# 反模式:
# 3 哨兵 quorum=2 但 2 个在同一机房 → 该机房挂 quorum 不足无法 failover
# 4 哨兵 quorum=2 → 网络分区 2v2 可能脑裂
# 3 跨机房 5 哨兵部署
"""
机房 A: master + sentinel-1 + sentinel-2
机房 B: replica-1 + sentinel-3 + sentinel-4
机房 C: replica-2 + sentinel-5
quorum=3 (5/2+1)
机房 A 全挂: B+C 还有 3 个哨兵 quorum 满足 failover 到 replica-1
机房 B 全挂: A+C 还有 3 个哨兵 quorum 满足
任一机房挂都能恢复
"""
# 4 防脑裂:min-replicas-to-write
# 老主与新主同时存在时 老主必须至少有 1 从同步成功才允许写
# 网络分区时老主孤立无从同步 拒绝写 防数据分裂
# redis-cli -h 10.0.1.10 -p 6379
CONFIG SET min-replicas-to-write 1
CONFIG SET min-replicas-max-lag 10
# 5 测试 failover
# 模拟主挂
redis-cli -h 10.0.1.10 -p 6379 DEBUG SLEEP 30
# 哨兵会发现主无响应 5s 后主观下线 quorum 达成后客观下线 选举新主
# 业务侧客户端 (Jedis SentinelPool) 自动重连新主
# 6 哨兵集群状态查看
redis-cli -p 26379 SENTINEL masters
redis-cli -p 26379 SENTINEL replicas mymaster
redis-cli -p 26379 SENTINEL sentinels mymaster
redis-cli -p 26379 SENTINEL ckquorum mymaster
实战经验:哨兵数量必须奇数 至少 3 个 推荐 5 个跨 3 机房;quorum 必须 N/2+1 偶数哨兵容易脑裂;min-replicas-to-write 是防脑裂神器 必配 + min-replicas-max-lag;parallel-syncs=1 避免新主被 N 个从同步压垮;每季度 chaos test 模拟机房挂 验证 failover 真能跑通。我们曾因 3 哨兵全部署同一机房 该机房光纤挖断 Redis 直接不可用 后改 5 哨兵跨机房再无此问题。
三 客户端配置:MOVED/ASK 与连接池
Cluster 模式下客户端必须懂 MOVED/ASK 协议自动重定向 否则 fail over 期间疯狂报错。客户端配置错是事故重灾区。
// 1 Java Jedis Cluster 配置
import redis.clients.jedis.*;
import java.util.HashSet;
import java.util.Set;
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("10.0.1.10", 6379));
nodes.add(new HostAndPort("10.0.1.11", 6379));
nodes.add(new HostAndPort("10.0.1.12", 6379));
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(200);
poolConfig.setMaxIdle(50);
poolConfig.setMinIdle(20);
poolConfig.setMaxWaitMillis(1000);
poolConfig.setTestOnBorrow(false); // 关键 别打满连接 borrow 时测试
poolConfig.setTestWhileIdle(true);
poolConfig.setTimeBetweenEvictionRunsMillis(30000);
JedisCluster cluster = new JedisCluster(
nodes,
5000, // connectionTimeout
5000, // soTimeout
10, // maxAttempts 关键 fail over 期间重试
"password",
"redisCluster",
poolConfig
);
// 2 用法
cluster.set("{user:123}:cart", "value");
cluster.hset("{user:123}:order", "ord1", "data");
// 3 pipeline 必须 hashtag 同一 slot
// JedisCluster 不直接支持 pipeline 跨 slot
// 改用 ClusterPipeline (Lettuce 也支持)
// 4 Lettuce(Spring Boot 默认)
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import java.time.Duration;
ClusterTopologyRefreshOptions refreshOpts = ClusterTopologyRefreshOptions.builder()
.enablePeriodicRefresh(Duration.ofSeconds(30)) // 定期刷拓扑
.enableAllAdaptiveRefreshTriggers() // failover 自动触发刷
.adaptiveRefreshTriggersTimeout(Duration.ofSeconds(15))
.build();
ClusterClientOptions clientOpts = ClusterClientOptions.builder()
.topologyRefreshOptions(refreshOpts)
.maxRedirects(5) // MOVED 重定向次数
.autoReconnect(true)
.build();
RedisClusterClient client = RedisClusterClient.create("redis://password@10.0.1.10:6379");
client.setOptions(clientOpts);
# 5 Python redis-py cluster
from redis.cluster import RedisCluster, ClusterNode
startup_nodes = [
ClusterNode("10.0.1.10", 6379),
ClusterNode("10.0.1.11", 6379),
ClusterNode("10.0.1.12", 6379),
]
rc = RedisCluster(
startup_nodes=startup_nodes,
password="password",
decode_responses=True,
socket_timeout=5,
socket_connect_timeout=5,
max_connections_per_node=50,
retry_on_timeout=True,
retry_on_error=[ConnectionError, TimeoutError],
cluster_error_retry_attempts=5, # 关键 fail over 期间重试
reinitialize_steps=5, # 5 次错误后刷新 topology
read_from_replicas=True, # 读走从 提升吞吐
)
# 6 跨 slot 操作必须 hashtag
pipe = rc.pipeline(transaction=False)
pipe.hset("{user:123}:cart", "item1", "phone")
pipe.expire("{user:123}:cart", 3600)
pipe.zincrby("{user:123}:order", 1, "ord_a")
pipe.execute()
# 7 SCAN 全集群遍历
def scan_all_keys(rc, pattern, count=1000):
"""SCAN 必须遍历所有 master node"""
all_keys = []
for node in rc.get_primaries():
node_client = node.redis_connection
cursor = 0
while True:
cursor, keys = node_client.scan(cursor, match=pattern, count=count)
all_keys.extend(keys)
if cursor == 0:
break
return all_keys
# 8 监控连接池状态
def pool_stats(rc):
stats = {}
for node in rc.get_nodes():
pool = node.redis_connection.connection_pool
stats[f"{node.host}:{node.port}"] = {
"created": pool._created_connections,
"available": len(pool._available_connections),
"in_use": len(pool._in_use_connections),
}
return stats
实战经验:cluster_error_retry_attempts / maxAttempts 必须设 5+ failover 期间几秒重试期不能直接报错;enablePeriodicRefresh + adaptiveRefresh 让 Lettuce 自动感知拓扑变化 不刷新会持续打挂的节点;read_from_replicas 在 Cluster 模式下让读走从 主节点压力立降;SCAN 必须 per-node 遍历 不能像单机一次扫完;testOnBorrow=false 否则高 QPS 下 borrow 时测试连接成为瓶颈。
四 大 key 与热 key 治理
大 key (单 key 几 MB) 与热 key (单 key QPS 几万) 是 Redis 最致命的两类问题 阻塞主线程 + 单 shard 打爆。生产必须有治理手段。
# 1 扫描大 key
redis-cli --bigkeys -h 10.0.1.10 -p 6379
# 输出 top 大 key 按类型分组
# - String: 大 value
# - List: 长 list
# - Hash: 多 field
# - Set/Zset: 多 member
# 2 精细扫描 用 redis-rdb-tools 离线分析
# pip install rdbtools python-lzf
rdb -c memory /var/lib/redis/dump.rdb --bytes 10240 > bigkeys.csv
# 找出 > 10KB 的所有 key
# 3 用 MEMORY USAGE 实时查
redis-cli -h 10.0.1.10 -p 6379 MEMORY USAGE mykey SAMPLES 0
# samples=0 精确计算 (慢) > 0 采样估算 (快)
# 4 大 key 治理
# 拆分:hash 200 万字段 → 按 hash bucket 拆 1000 份
def write_split_hash(rc, base_key, field, value, bucket_count=1000):
bucket = hash(field) % bucket_count
rc.hset(f"{{{base_key}}}:b{bucket}", field, value)
def read_split_hash(rc, base_key, field, bucket_count=1000):
bucket = hash(field) % bucket_count
return rc.hget(f"{{{base_key}}}:b{bucket}", field)
# 删除大 key 用 UNLINK(异步)不要用 DEL(阻塞)
redis-cli UNLINK mykey
# 5 热 key 识别
# 4.0+ 用 redis-cli --hotkeys (前提 maxmemory-policy = allkeys-lfu/volatile-lfu)
"""
redis-cli --hotkeys -h 10.0.1.10 -p 6379
"""
# 6 在线热 key 探测 用 monitor 抽样
def detect_hotkeys(rc, sample_seconds=10):
"""抽样 monitor 几秒钟统计 key 频率"""
from collections import Counter
counter = Counter()
start = time.time()
pubsub = rc.pubsub()
# 用 client.monitor 在低 QPS 时段抽样
with rc.monitor() as m:
for cmd in m.listen():
if time.time() - start > sample_seconds:
break
# 提取 key
parts = cmd["command"].split()
if len(parts) >= 2:
key = parts[1]
counter[key] += 1
return counter.most_common(20)
# 7 热 key 治理
# a) 多副本读 read_from_replicas + 增加从节点
# b) 本地 cache Caffeine / guava cache 一级缓存 Redis 二级
# c) 拆分热 key hot_key_xxx → hot_key_xxx_0 hot_key_xxx_1 ... 随机后缀
class HotKeyClient:
def __init__(self, rc, hot_keys, shard_count=10):
self.rc = rc
self.hot_keys = set(hot_keys)
self.shard_count = shard_count
def get(self, key):
if key in self.hot_keys:
# 随机选一个 shard
shard = random.randint(0, self.shard_count - 1)
value = self.rc.get(f"{key}:shard:{shard}")
return value
return self.rc.get(key)
def set(self, key, value, ex=None):
if key in self.hot_keys:
# 写所有 shard
pipe = self.rc.pipeline()
for s in range(self.shard_count):
pipe.set(f"{key}:shard:{s}", value, ex=ex)
pipe.execute()
else:
self.rc.set(key, value, ex=ex)
# 8 本地缓存兜底
from cachetools import TTLCache
local_cache = TTLCache(maxsize=10000, ttl=10)
def get_with_local_cache(rc, key):
if key in local_cache:
return local_cache[key]
value = rc.get(key)
local_cache[key] = value
return value
# 高频读热 key 90% 在本地命中 Redis 压力降 10 倍
实战经验:bigkeys / hotkeys 命令必须定期跑(每天一次) 异常立即告警;大 key 删除必须用 UNLINK 不要 DEL DEL 阻塞主线程几秒;大 hash 必须拆 bucket(1000 份左右)避免单 key 几 MB;热 key 多副本 + 本地缓存双管齐下 单 shard 才不会被打爆;monitor 不能长期开 抽样几秒即可 monitor 自身就是性能杀手。我们曾因一个 200 万字段的 hash 删 DEL 时主线程阻塞 8 秒 整个集群 fail over 灾难性。
[mermaid]
flowchart TD
A[业务请求] --> B[客户端连接池]
B --> C{key 是否热点}
C -->|是| D[本地缓存 L1]
D -->|命中| E[直接返回]
D -->|未命中| F[多 shard 随机读]
C -->|否| G[CRC16 计算 slot]
F --> H[Redis Cluster]
G --> H
H --> I{slot 在哪个 master}
I -->|本节点| J[直接执行]
I -->|其他节点| K[返回 MOVED]
K --> L[客户端刷新拓扑]
L --> H
J --> M[结果返回]
H --> N[Sentinel 监控]
N --> O{主节点存活}
O -->|否| P[quorum 投票]
P --> Q[选举新主]
Q --> R[拓扑变更通知]
R --> L
五 慢命令防御与持久化策略
KEYS / SMEMBERS / LRANGE 大列表都是隐藏炸弹 一条慢命令能把整个 Redis 阻塞几秒。持久化 RDB / AOF / 混合 选错恢复几小时。生产必须有防御与选型。
# 1 慢日志配置
CONFIG SET slowlog-log-slower-than 10000 # 10ms 算慢
CONFIG SET slowlog-max-len 1000
# 查慢日志
SLOWLOG GET 100
SLOWLOG RESET
# 2 ACL 禁用危险命令
# Redis 6+
ACL SETUSER appuser on >password ~* +@all -KEYS -FLUSHALL -FLUSHDB -DEBUG -CONFIG -SHUTDOWN
# 业务账号禁用 KEYS / FLUSH 防误操作
# Redis 5 用 rename-command
"""
rename-command KEYS ""
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command DEBUG ""
"""
# 3 慢命令替代
# KEYS pattern → SCAN cursor MATCH pattern COUNT 100
def scan_keys(rc, pattern, count=100):
cursor = 0
keys = []
while True:
cursor, batch = rc.scan(cursor, match=pattern, count=count)
keys.extend(batch)
if cursor == 0:
break
return keys
# SMEMBERS big_set → SSCAN
def scan_set(rc, set_key, count=100):
cursor = 0
members = []
while True:
cursor, batch = rc.sscan(set_key, cursor, count=count)
members.extend(batch)
if cursor == 0:
break
return members
# LRANGE long_list 0 -1 → 分页 LRANGE
def page_list(rc, list_key, page_size=100):
length = rc.llen(list_key)
for offset in range(0, length, page_size):
yield rc.lrange(list_key, offset, offset + page_size - 1)
# 4 持久化配置 RDB + AOF 混合
"""
# RDB 快照
save 900 1
save 300 10
save 60 10000
rdbcompression yes
rdbchecksum yes
# AOF 增量
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec # 每秒 fsync 平衡性能与安全
no-appendfsync-on-rewrite yes
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 7.0+ 混合持久化(RDB + AOF 增量)
aof-use-rdb-preamble yes
"""
# 5 持久化策略选型
# 仅缓存场景:可关闭持久化 (appendonly no save "")
# 数据可丢失 1s:appendfsync everysec(性能好)
# 强一致:appendfsync always(性能差 50%)
# 大数据量:混合持久化 RDB 加载快 + AOF 增量补丁
# 6 子线程做 BGSAVE 防主线程阻塞
# bgsave 用 fork() 子进程做 不阻塞主
# 但 fork 本身可能慢(大内存实例 fork 几秒)
# 解决:Linux 关 THP (Transparent Huge Pages)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
# 7 监控关键指标
redis-cli -h 10.0.1.10 -p 6379 INFO stats
# instantaneous_ops_per_sec 当前 QPS
# rejected_connections 连接被拒次数 应为 0
# expired_keys 过期 key 数
# evicted_keys 淘汰 key 数 持续增长说明 maxmemory 不够
# keyspace_hits/misses 命中率
redis-cli INFO replication # 主从状态
redis-cli INFO persistence # 持久化状态
redis-cli INFO memory # 内存碎片率 mem_fragmentation_ratio > 1.5 需 MEMORY PURGE
实战经验:slowlog-log-slower-than 设 10ms 收集慢命令 每天 review 发现新隐患;ACL 禁用 KEYS/FLUSH 防误操作 这是生产铁律 多少事故都是手抖 KEYS *;SCAN/SSCAN/HSCAN 替代所有"全量"命令 cursor 分批不阻塞;持久化 everysec 是大多数场景最佳平衡 always 性能差 50% 大多场景过度;混合持久化(7.0+)是最佳选择 启动快 + 增量精确;THP 必须关 否则 fork 阻塞秒级。我们曾因没关 THP 一个 32GB 实例 BGSAVE fork 阻塞 4 秒 客户端 timeout 雪崩。
六 监控告警与故障演练
不监控等于裸奔 不演练等于纸上谈兵。生产 Redis 必须 Prometheus + Grafana + chaos engineering 定期演练 fail over。
# 1 redis_exporter 部署
# docker-compose.yml
services:
redis-exporter:
image: oliver006/redis_exporter:latest
environment:
REDIS_ADDR: "redis://10.0.1.10:6379,redis://10.0.1.11:6379,redis://10.0.1.12:6379"
REDIS_PASSWORD: "password"
ports:
- "9121:9121"
# 2 Prometheus scrape
scrape_configs:
- job_name: "redis"
static_configs:
- targets: ["redis-exporter:9121"]
# 3 关键告警规则
groups:
- name: redis_alerts
rules:
- alert: RedisDown
expr: redis_up == 0
for: 1m
annotations:
summary: "Redis 实例 {{ $labels.instance }} 不可达"
- alert: RedisMemoryHigh
expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.85
for: 5m
annotations:
summary: "Redis {{ $labels.instance }} 内存使用 > 85%"
- alert: RedisRejectedConnections
expr: increase(redis_rejected_connections_total[5m]) > 0
annotations:
summary: "Redis {{ $labels.instance }} 连接被拒"
- alert: RedisReplicationLag
expr: redis_master_repl_offset - on(instance) redis_slave_repl_offset > 1000000
for: 2m
annotations:
summary: "Redis 主从复制延迟 > 1MB"
- alert: RedisHighEvictionRate
expr: rate(redis_evicted_keys_total[5m]) > 1000
annotations:
summary: "Redis 高频淘汰 maxmemory 可能不够"
- alert: RedisHighLatency
expr: redis_commands_duration_seconds{quantile="0.99"} > 0.1
annotations:
summary: "Redis P99 延迟 > 100ms"
- alert: RedisClusterDown
expr: redis_cluster_state != 1
annotations:
summary: "Redis Cluster 状态异常"
# 4 Chaos Engineering 故障演练
import subprocess
import time
class RedisChaosTest:
def __init__(self, cluster_nodes):
self.nodes = cluster_nodes
def test_master_kill(self, master_node):
"""杀掉一个主节点 验证 fail over 时间与业务影响"""
# 记录开始
start = time.time()
# SSH 上去 kill
subprocess.run(["ssh", master_node, "pkill -9 redis-server"])
# 持续 ping 业务 观察恢复时间
recovered = False
while time.time() - start < 60:
try:
rc.set("chaos_test", "ok", ex=10)
if not recovered:
print(f"业务恢复 耗时 {time.time() - start:.1f}s")
recovered = True
break
except Exception as e:
pass
time.sleep(0.5)
return time.time() - start
def test_network_partition(self, node, duration=30):
"""模拟网络分区 验证脑裂防御"""
subprocess.run(["ssh", node, f"iptables -A INPUT -p tcp --dport 6379 -j DROP"])
time.sleep(duration)
subprocess.run(["ssh", node, f"iptables -D INPUT -p tcp --dport 6379 -j DROP"])
def test_slow_query(self, node):
"""注入慢命令 验证慢日志与告警"""
subprocess.run(["ssh", node, "redis-cli DEBUG SLEEP 5"])
# 5 月度演练计划
"""
每月第一周:
- 周一: 杀主节点 验证 fail over 时间 < 30s
- 周三: 网络分区 验证防脑裂(数据无分裂)
- 周五: 注入大 key 验证迁移超时与告警
"""
实战经验:redis_exporter + Prometheus + Grafana 是生产标配 关键指标必告警 内存/连接/延迟/复制/淘汰五件套;每月 chaos test 杀主节点验证 fail over 真能用 不演练就等于没有;脑裂演练特别重要 一次脑裂数据对账几天;慢命令注入告警链路是否真能 page on-call 别等真出事才发现告警漏配。我们建立月度演练后 真实事故响应时间从 30 分钟降到 3 分钟 因为团队对 fail over 流程已经形成肌肉记忆。
关键概念速查
| 概念 | 关键参数/命令 | 推荐 | 备注 |
|---|---|---|---|
| Cluster 槽 | 16384 slot + CRC16 | 必懂 | 分片基础 |
| Hashtag | {tag}:key 强制同槽 | 必用 | 事务 pipeline 必需 |
| Sentinel 数量 | 5 个跨 3 机房 | 推荐 | quorum=3 防脑裂 |
| 防脑裂 | min-replicas-to-write 1 | 必配 | + max-lag 10 |
| 客户端重试 | cluster_error_retry_attempts 5+ | 必配 | fail over 期间不报错 |
| SCAN 替代 | SCAN/SSCAN/HSCAN cursor | 必用 | 禁 KEYS / SMEMBERS |
| 大 key 删除 | UNLINK 异步 | 必用 | 禁 DEL 阻塞主线程 |
| 热 key 拆分 | + 本地 cache + 多 shard | 必做 | 防单节点打爆 |
| 持久化 | RDB + AOF 混合 | 推荐 | everysec 平衡 |
| THP 关闭 | echo never > ...thp | 必做 | 防 fork 阻塞 |
避坑清单
- 不要忘 hashtag 跨 slot 操作直接报 CROSSSLOT 业务崩。
- 不要 3 哨兵全部署同机房 跨 3 机房 5 个起 quorum 3。
- 不要省 min-replicas-to-write 网络分区脑裂数据分裂对账噩梦。
- 不要用老版本客户端 fail over 期间不会刷拓扑 疯狂报错。
- 不要 KEYS pattern 调试 直接把主节点扫死。
- 不要 DEL 大 key 必须 UNLINK 否则主线程阻塞秒级。
- 不要 reshard 前不清大 key MIGRATE timeout 集群进 fail。
- 不要忽视 THP 大内存实例 fork 阻塞秒级 业务雪崩。
- 不要不开慢日志 + ACL 禁危险命令 防误操作铁律。
- 不要不演练 fail over 真出事手忙脚乱响应时间 30 分钟以上。
总结
把 Redis 高可用与 Cluster 部署这套从我们踩过的所有坑里反过来看 你会发现真正影响 Redis 稳定性的不是机器配置 而是工程化的全栈高可用能力。同样一个 Redis Cluster 直接 redis-cli cluster create 跑通 上生产一周连 5 次 P1 故障;hashtag 设计 + 5 哨兵跨机房 + min-replicas-to-write + 客户端 retry 配齐 + 大 key 治理 + chaos 月度演练 同样的硬件半年零事故 P99 延迟稳定 5ms。Redis 高可用不是"装个 Cluster"的活儿 它是一个分片设计 + 哨兵部署 + 客户端精调 + 大热 key 治理 + 持久化选型 + 监控告警 + 故障演练的完整系统工程。
另一个常见的认知误区是把 Redis 当万能缓存 觉得加 Redis 就能解决所有性能问题。但事实是 Redis 是单线程命令处理 一条慢命令阻塞全部 一个大 key 操作影响所有客户端 一次脑裂数据可能分裂对账几天。Redis 工程化的核心是 假设每条命令都可能慢 每个 key 都可能成大 key 每个节点都可能挂 用多层防御 + 持续演练保证整体可用性。
打个比方 Redis Cluster 像一个大型物流分拣中心。Slot 槽是固定的传送带编号 Hashtag 是包裹分组标签(同一个客户的件必须走同一传送带)Sentinel 是值班调度员(发现某条传送带停了立即切到备用)客户端 retry 是司机的应变(临时路线变化能跟上)大 key 治理是超大件单独处理(不能塞进普通传送带)慢命令防御是禁运清单(危险品不许进)持久化是货品记录(出意外能追溯重建)监控告警是中控室大屏(异常立即响)chaos 演练是消防演习(真出事不慌)。哪一环没做 这个分拣中心可能短期跑得动 但长期一定出大事故 要么传送带瘫痪 要么货品丢失 要么调度混乱整个系统停摆。
所以下一次再有人跟你说"Redis 加个 Cluster 就高可用了"你可以反问他 hashtag 设计了吗 哨兵 quorum 算清了吗 min-replicas-to-write 配了吗 客户端 retry 设了吗 大 key 监控了吗 慢日志开了吗 ACL 禁危险命令了吗 chaos 演练做了吗 这些工作没做完 Redis Cluster 只是一个能跑通 demo 的玩具 不是一个能扛真实流量的生产基础设施。从踩坑到稳如老狗 中间隔着一整套高可用工程方法论 这条路没有捷径 但走完之后 你的 Redis 会从间歇 P1 变成 99.99% 可用 从每月几次脑裂对账变成半年零事故 从客户端疯狂报错变成 fail over 业务无感。
—— 别看了 · 2026