Redis 虽然是单线程处理命令的(主线程负责网络 I/O 和命令处理),但它依然具备 百万级 QPS 的吞吐能力。这个看似矛盾的现象,其实是 Redis 高性能架构设计和 底层实现精妙配合的结果。
下面我们从架构、内核原理、操作系统机制、与其他系统对比等多维度深入剖析,为何 Redis 单线程却读写性能极高。
模块 | 是否多线程 | 说明 |
---|---|---|
主线程 | ✅ 单线程 | 网络请求 + 命令处理 |
AOF 写盘 | ✅ 单独线程 | 异步写磁盘 |
RDB 子进程 | ✅ 多进程 | fork 子进程进行快照 |
集群复制 | ✅ 多线程 | 主从同步、传输增量数据 |
I/O 解压压缩(6.0+) | ✅ 多线程 | io-threads 支持并行读写处理 |
结论:“命令执行是单线程”,但 Redis 本质是一个多组件协同的高性能系统。
Redis 所有数据都存在内存中,命令执行直接操作数据结构,无需 I/O。
内存随机访问速度是磁盘的百万倍(ns vs ms):
内存:100 ns
SSD:100 µs
HDD:10 ms
Redis 使用 Reactor 模型 + epoll
实现网络事件处理:
单线程事件循环:
while (true) {
epoll_wait(...) → 返回就绪事件集合
遍历处理每个客户端连接的请求
}
非阻塞 I/O,连接不会阻塞线程
没有线程切换上下文开销(节省 CPU)
Redis 用的是高度优化的数据结构(C 语言实现):
类型 | 底层结构 | 性能特性 |
---|---|---|
String | SDS | 动态数组,避免频繁 realloc |
Hash | ziplist / dict | 紧凑结构 + 哈希冲突最小化 |
ZSet | 跳表 + dict | log(N) 级别插入与范围查询 |
List | quicklist | ziplist + 双向链表 |
Set | intset / dict | 整数集合内存节省,多数 O(1) |
每条命令执行路径都在 100 行以内,执行耗时极短,CPU 缓存命中率高
相比多线程系统,Redis 单线程:
无需加锁(没有竞争) → 没有锁等待、死锁、上下文切换
保证串行语义一致性 → 实现原子性和事务机制简单(MULTI)
在高并发场景下,锁开销和线程切换代价比 Redis 单线程要大得多
Redis 单线程可以实现:
10 万级 QPS:普通业务场景
100 万级 QPS:使用流水线批处理 + 简单命令(如 INCR)
实测 Redis 单实例可处理 100k~150k ops/s,在 1ms 内响应
redis-benchmark -t set,get -n 1000000 -c 100 -P 10
输出结果(示例):
SET: 120000 requests/sec
GET: 130000 requests/sec
说明 Redis 能在单线程下稳定支撑高并发
多线程引入锁 → 数据结构加锁 → 性能下降
多线程之间竞争资源 → 需要线程协调机制(复杂)
多线程命令顺序不可控 → 难以实现事务和原子操作(MULTI、Lua)
从 Redis 6.0 开始,加入 io-threads
支持:
网络读写拆分到多线程中(解包 + 编码阶段)
命令执行依然是主线程串行处理
配置方式:
io-threads-do-reads yes
io-threads 4
效果:
降低主线程 CPU 压力
提高网络密集场景性能(比如 pipeline 请求、TLS)
原因 | 描述 |
---|---|
① 内存操作极快 | 全在内存,跳过磁盘 I/O |
② 无锁单线程处理 | 避免线程切换与锁开销 |
③ 高效 I/O 机制 | epoll + Reactor,异步处理连接 |
④ 数据结构精简 | C 实现的结构,执行逻辑极短 |
⑤ I/O 多线程辅助 | Redis6+ 解放部分网络线程 |
IO 多路复用是一种操作系统提供的机制,允许单个线程同时监听多个文件描述符(socket fd),并在任一 fd 准备好时通知应用程序进行读写操作。
这解决了传统阻塞 IO 每个连接都需要一个线程的问题,大幅提升了并发连接处理能力。
举个经典例子:
假设你要监听 100 个客户端 socket,如果用传统模型:
每个 socket 一个线程,开销大、切换频繁。
而 IO 多路复用只需一个线程就能“监听所有连接”!
模型 | 系统调用 | 是否跨平台 | 特点 |
---|---|---|---|
select | select() |
✅ | 最老旧,有 FD 数量限制(1024) |
poll | poll() |
✅ | 无数量限制,但效率仍低 |
epoll | epoll_*() |
❌(Linux 专有) | 性能最佳,Redis 默认使用 |
kqueue | kqueue() |
❌(BSD/OSX) | 类似 epoll |
IOCP | Windows 系统 | ❌ | Windows 专属 IO 模型 |
✅ Redis 默认使用的是 Linux 下的 epoll
模型。
epoll
是 Linux 2.6 之后提供的高性能 IO 事件通知机制,具备如下优势:
不再轮询每个连接,而是让内核“通知”应用层哪些 socket 有事件。
与监听的 fd 数量无关,事件到来才触发回调处理。
Redis 使用 水平触发(Level Triggered),更稳妥。
int epfd = epoll_create();
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); // 注册事件
epoll_wait(epfd, events, MAX_EVENTS, timeout); // 阻塞等待事件发生
Redis 中的核心事件循环基于一个通用的 IO 多路复用抽象层 ae
,底层实现根据平台选择:
Linux:ae_epoll.c
macOS:ae_kqueue.c
BSD:ae_select.c
while (1) {
// 1. 等待 socket 就绪(读/写)
fired = aeApiPoll(...);
// 2. 处理可读事件(客户端命令请求)
processInputBuffer();
// 3. 执行命令逻辑(SET/GET 等)
// 4. 可写事件响应结果(发送给客户端)
sendReplyToClient();
}
ae.c
框架核心模块:模块 | 功能 |
---|---|
aeCreateEventLoop |
创建事件循环 |
aeCreateFileEvent |
注册文件事件(读写) |
aeProcessEvents |
主循环处理事件 |
aeMain |
Redis 主线程主循环所在 |
传统多线程 | IO 多路复用 |
---|---|
每连接一个线程,线程切换频繁 | 单线程异步监听所有连接 |
上下文切换消耗大 | 没有线程切换 |
需加锁,存在竞争 | 无锁逻辑,效率高 |
并发连接量受限 | 支持百万连接并发 |
操作阶段 | 是否多线程 | IO 多路复用角色 |
---|---|---|
网络读取请求 | ✅ Redis6 可多线程 | epoll 通知可读事件 |
命令解析与执行 | ❌ 主线程串行处理 | 由主线程处理 buffer |
网络返回响应 | ✅ Redis6 可多线程 | epoll 通知可写事件 |
单线程监听 + 处理十万并发连接是常态。
每个 socket 都是非阻塞处理,避免任何阻塞操作。
Redis 对外响应延迟常常在 亚毫秒级别。
场景 | 建议 |
---|---|
高并发短连接 | 使用 pipeline 减少 RTT |
高连接数 | 优化 ulimit -n ,避免 fd 被耗尽 |
网络负载高 | 开启 io-threads 多线程读写 |
超大 key 导致事件阻塞 | 拆分数据结构,限制 key 大小 |
特性 | 描述 |
---|---|
模型 | IO 多路复用(epoll) |
优点 | 高并发、低延迟、无锁、无阻塞 |
结合点 | 单线程模型完美结合 epoll |
Redis 效果 | 百万级连接吞吐,高速低延迟响应 |
Redis Cluster 由多个节点组成,每个节点负责一部分 16384 个槽(hash slot)。集群中每个主节点(master)可以有 1 个或多个从节点(slave/replica)组成复制关系。
例如:
M1 负责 slot 0~5460 ←—— R1 (M1 的 replica)
M2 负责 slot 5461~10922 ←—— R2 (M2 的 replica)
M3 负责 slot 10923~16383←—— R3 (M3 的 replica)
目标:自动将 M1 的副本节点 R1 提升为新的主节点
阶段 | 细节 |
---|---|
1️⃣ 故障发现 | 节点之间通过 gossip 协议定期发送 PING/PONG |
2️⃣ 主观下线(PFAIL) | 某个节点收到 M1 的超时响应后,标记其为“主观下线” |
3️⃣ 客观下线(FAIL) | 如果半数以上主节点也检测到 M1 超时 → 宣布 M1 为“客观下线” |
4️⃣ 选主 | R1、R1' 等副本竞争成为新的 master |
5️⃣ 故障转移 | 由胜出的 R1 发起 failover,接管 M1 的 slot 并广播更新 |
6️⃣ 更新拓扑 | 所有节点更新 cluster 配置,slot → R1,新 master 上线继续服务 |
每个 Redis 节点定时随机探测其他节点(PING/PONG)
如果 N 秒未响应,就将其标记为 PFAIL(主观下线)
默认超时时间 cluster-node-timeout
,如 15 秒
若大多数主节点也认为某节点为 PFAIL → 客观下线(FAIL)
节点在 cluster bus 中广播 FAIL 消息,告诉其他节点该节点已宕机
不依赖中心节点,全是分布式一致性投票
当主节点 FAIL,副本会发起竞选,选出一个副本进行主从切换:
步骤 | 描述 |
---|---|
Step1 | 副本等待随机时间,发送 FAILOVER AUTH REQUEST 给其他主节点 |
Step2 | 多数主节点响应同意(给票) |
Step3 | 获得多数票后,副本执行 slaveof no one 成为新的 master |
Step4 | 通过 cluster bus 广播新的 slot 分配关系(slot → 新主节点) |
Redis Cluster 中,优先选:
副本延迟最小(replica-priority
高)
数据最全(复制偏移量最大)
Redis Cluster 使用 MOVED 重定向 + 客户端缓存节点槽信息 实现透明重连:
如果客户端访问了已经挂掉的主节点的 slot
集群中其他节点响应 MOVED slot ip:port
客户端更新槽位映射并重发请求 → 自动路由到新主节点(R1)
高级客户端(如 Jedis、Lettuce)默认支持这一机制。
主从复制机制:
从节点异步复制主节点数据
一般复制延迟在毫秒级,丢失极少
复制偏移量对比选主:
偏移量大的副本优先(说明数据更全)
AOF + RDB 持久化(如果开启):
提高宕机节点恢复可能性
问题 | 说明 |
---|---|
数据延迟 | 异步复制有可能导致极端情况丢失最后几条数据 |
所有副本不可用 | 如果所有副本都宕机,slot 将不可用,必须人工修复 |
分区脑裂 | 网络分区时,主从同时可写可能导致数据不一致(Redis 禁止这种场景自动切主) |
配置不当 | replica-priority=0 的副本不会参与主选举;务必正确设置 |
每个主节点至少配置 1 个副本
副本部署在不同机器/机房,避免单点故障
开启 AOF(append-only)提高数据恢复能力
客户端使用支持 MOVED 重定向的 SDK
合理配置 cluster-node-timeout
(推荐 15~30s)
+----------+ +----------+
| M1:主 | <—— Replication ——> | R1:从 |
+----------+ +----------+
↓宕机
其他主节点 PING 超时
↓
多数节点确认 FAIL
↓
R1 请求投票
↓
获得多数选票
↓
R1 → master,广播新的 slot 映射
↓
客户端收到 MOVED,重试请求到 R1
Redis Cluster 将键的空间划分为 0 ~ 16383
共 16384 个槽(slot)
每个节点持有部分槽,比如:
Node1: 0-5460
Node2: 5461-10922
Node3: 10923-16383
当你新增一个节点时,需要将部分槽(slot)从已有节点迁移到新节点,同时将这些 slot 对应的数据也迁移过去。
不会自动迁移,Redis Cluster 不具备自动重分布功能。
你需要手动进行以下步骤:
向集群中添加新节点
指定要迁移的槽位范围
将这些槽从旧节点迁移到新节点(槽迁移 + 数据迁移)
Redis 官方推荐使用 redis-cli
提供的以下命令:
# 添加节点
redis-cli --cluster add-node NEW_HOST:PORT EXISTING_HOST:PORT
# 重分片槽并迁移数据
redis-cli --cluster reshard EXISTING_HOST:PORT
Redis Cluster 使用 槽状态 和 MIGRATE 命令 实现数据从旧节点到新节点的迁移。
每个迁移中的槽位,涉及两种节点:
节点 | 状态 |
---|---|
源节点 | MIGRATING |
目标节点 | IMPORTING |
Redis 使用 MIGRATE
命令从源节点复制 key 到目标节点
源节点逐个扫描属于该 slot 的 key,并将其迁移
每迁移一个 key,源节点删除该 key
# 设置源节点为 MIGRATING 状态
CLUSTER SETSLOT 5460 MIGRATING node4_id
# 设置目标节点为 IMPORTING 状态
CLUSTER SETSLOT 5460 IMPORTING node1_id
# 使用 MIGRATE 命令迁移 key
MIGRATE node4_ip port key 0 timeout
通常这些由 redis-cli --cluster reshard
自动处理。
迁移过程 不会阻塞客户端读写,Redis Cluster 采用了:
状况 | 响应类型 | 客户端行为 |
---|---|---|
槽迁移已完成 | MOVED slot new_ip:port |
客户端更新槽映射,重发请求 |
正在迁移过程中 | ASK slot new_ip:port |
客户端先向新节点发送 ASKING 再发命令 |
这保证了 数据一致性 和 读写不中断
高级客户端(如 Jedis、Lettuce)自动识别 ASK/MOVED
并自动重试
客户端会缓存 slot → 节点的映射表,定期或出错时刷新
使用 redis-cli --cluster reshard
将要下线节点的 slot 迁移到其他节点
确保该节点不再持有 slot 后,执行:
redis-cli --cluster del-node EXISTING_HOST:PORT NODE_ID
强制下线未迁移完 slot 的节点,会导致数据丢失!
问题 | 说明 |
---|---|
并发迁移压力 | 数据量大时建议分批迁移 slot,避免带宽/内存压力过大 |
读写冲突 | Redis 的 slot 状态机制 + ASK 保证请求正确重定向 |
高可用保障 | 避免同时迁移多个 master 的 slot,容易产生重负载节点 |
客户端异常 | 使用支持自动重定向的客户端,非标准客户端会出错 |
# 1. 启动新节点
redis-server --port 7004 --cluster-enabled yes --cluster-config-file nodes.conf --appendonly yes
# 2. 加入集群
redis-cli --cluster add-node 127.0.0.1:7004 127.0.0.1:7000
# 3. 重分片 slot 到新节点(如分 4000 个 slot)
redis-cli --cluster reshard 127.0.0.1:7000
# 输入 4000,选择源节点,目标节点,确认执行
# 4. 完成后集群结构更新
redis-cli --cluster info 127.0.0.1:7000
问题 | 答案 |
---|---|
新增/删除节点是否自动迁移数据? | ❌ 不自动,需手动 reshard |
数据迁移期间客户端是否可用? | ✅ 可用,依赖 ASK /MOVED 重定向 |
客户端如何正确处理? | 使用支持 Cluster 的 SDK(Jedis、Lettuce 等) |
Redis 如何实现无缝迁移? | 使用 MIGRATING /IMPORTING + MIGRATE 命令逐 key 搬迁 |
在 Redis Cluster 中进行 数据迁移(如槽位 reshard
或节点 扩容/缩容
)时,涉及多个关键流程,包括:
槽位(slot)的迁移
键值数据(key-value)的迁移
客户端请求处理(ASK/MOVED 重定向)
假设:我们要把 slot 1000
从节点 A
迁移到节点 B
。
阶段 | 操作 | 描述 |
---|---|---|
1️⃣ 槽位迁移准备 | 设置槽状态 | 节点 A 标记为 MIGRATING , B 标记为 IMPORTING |
2️⃣ 数据迁移执行 | 迁移 key | A 使用 MIGRATE 命令将 slot=1000 的 key 搬到 B |
# 在源节点 A 上执行:
CLUSTER SETSLOT 1000 MIGRATING
# 在目标节点 B 上执行:
CLUSTER SETSLOT 1000 IMPORTING
作用:
告诉集群:此 slot 正在被从 A 迁移到 B
迁移状态使得集群中的其他节点也能感知槽状态变化
源节点(A)逐个将属于 slot 1000
的 key 搬到目标节点(B):
# 对每个 key 执行
MIGRATE B_HOST B_PORT key 0 timeout [COPY] [REPLACE] KEYS key
内部流程:
源节点 A 打开与目标节点 B 的连接
源节点将 key 的数据序列化(RDB 编码)
通过 socket 传输给目标节点
目标节点将 key 写入本地内存
源节点删除本地 key(除非加 COPY
)
这个过程是逐个 key 迁移的,所以数据量大时需要分批迁移避免阻塞。
# 所有 key 搬迁完后,在集群内广播 slot 所属更新
CLUSTER SETSLOT 1000 NODE
这样所有节点都会知道:slot 1000 现在属于节点 B。
客户端缓存有:slot → 节点
映射,比如:
slot 1000 → A
槽 1000
被标记为 MIGRATING
Redis 返回错误:
-ASK 1000
客户端执行以下流程:
# Step 1: 发送 ASKING 命令(告知 B 临时允许访问此 slot)
ASKING
# Step 2: 重新发送原始命令(GET、SET 等)给 B
GET user:1234
ASKING
是 Redis 的临时许可机制,让目标节点 B 接收未完成迁移 slot 的请求
一旦迁移完成,客户端将收到 MOVED
指令并更新槽映射
如果迁移已经结束,Redis 返回:
-MOVED 1000
客户端收到 MOVED 后,刷新本地槽位映射表。
客户端 --- GET user:1234 ---> 源节点 A (slot 1000 MIGRATING)
|
<---- -ASK 1000 B_ip:port
|
客户端 ---> ASKING + GET user:1234 ---> 目标节点 B (IMPORTING)
|
<---- key result
# 1. 查询 key 的槽位(确认是 slot 1000)
redis-cli -c cluster keyslot user:1234
# 2. 在 A 上标记为 MIGRATING
redis-cli -c -h A_IP -p A_PORT cluster setslot 1000 migrating B_NODE_ID
# 3. 在 B 上标记为 IMPORTING
redis-cli -c -h B_IP -p B_PORT cluster setslot 1000 importing A_NODE_ID
# 4. 找出 A 中 slot=1000 的所有 key(可用 scan + keyslot)
redis-cli -c -h A_IP -p A_PORT --scan | while read key; do
SLOT=$(redis-cli -c cluster keyslot "$key")
if [[ "$SLOT" -eq 1000 ]]; then
redis-cli -c -h A_IP -p A_PORT migrate B_IP B_PORT "$key" 0 5000
fi
done
# 5. 设置 slot 归属权
redis-cli -c -h A_IP -p A_PORT cluster setslot 1000 node B_NODE_ID
redis-cli --cluster reshard
工具其实是自动化做了上面所有事情。
维度 | 描述 |
---|---|
是否自动迁移 | ❌ 不自动,需要手动或工具迁移 |
迁移粒度 | 按 slot,slot 包含多个 key |
客户端请求是否中断 | ❌ 不会中断,Redis 使用 ASK / MOVED 重定向处理 |
如何避免丢失数据 | Redis 使用 MIGRATING + MIGRATE + ASKING 流程精确控制迁移 |
客户端支持 | 建议使用 Jedis、Lettuce 等自动支持 ASK/MOVED 的客户端 |
在 Redis Cluster 中,哈希槽(slot)总共 16384 个,这些槽决定了 key 的分布。假设当前有 4 个主节点,各自持有的槽平均为:
原始槽分布(共 16384 槽):
Node A:0 - 4095
Node B:4096 - 8191
Node C:8192 - 12287
Node D:12288 - 16383
现在新增了一个节点 Node E,我们希望将集群进行重新均衡(reshard),让 5 个节点均分槽位,每个节点应该持有大约:
16384 / 5 = 3276.8 ≈ 3276 ~ 3277 个槽
我们希望最终槽位分布为:
节点 | 目标槽位范围(近似) | 数量 |
---|---|---|
Node A | 0 - 3275 | 3276 |
Node B | 3276 - 6551 | 3276 |
Node C | 6552 - 9827 | 3276 |
Node D | 9828 - 13103 | 3276 |
Node E | 13104 - 16383 | 3280 |
注意:最后一个节点可以稍多几个槽以补全总数。
我们现在知道 Node E 没有任何槽,它需要接管约 3280
个槽。我们需要从原有的节点 A~D 中按比例迁出一些槽位,例如:
迁出源节点 | 原持有槽数 | 迁出槽数(近似) |
---|---|---|
Node A | 4096 | 820 |
Node B | 4096 | 820 |
Node C | 4096 | 820 |
Node D | 4096 | 820 |
合计:820 * 4 = 3280(正好够给 Node E)
我们可以按以下方式进行:
Node A 迁出:槽 3276 - 4095
(820 个)
Node B 迁出:槽 7372 - 8191
(820 个)
Node C 迁出:槽 11468 - 12287
(820 个)
Node D 迁出:槽 15564 - 16383
(820 个)
Node E 将接管这些槽:
迁入槽总范围:
[3276-4095] + [7372-8191] + [11468-12287] + [15564-16383] = 共 3280 个槽
redis-cli --cluster add-node : :
redis-cli --cluster reshard :
交互界面示例:
How many slots do you want to move (from existing nodes)? 3280
What is the receiving node ID?
Please enter all source node IDs separated by space:
Do you want to proceed with the proposed reshard plan (yes/no)? yes
工具会自动从每个 source node 迁出约等量的槽,并使用 MIGRATE 将 key 搬至目标节点。
Redis 会对迁移中的槽设置状态:
源节点:MIGRATING
目标节点:IMPORTING
如果客户端访问迁移中的 key,会收到:
-ASK slot new_ip:port
客户端会先发 ASKING
,再发原始请求至新节点
客户端自动更新槽表(Jedis、Lettuce 支持)
节点 | 最终槽位(示例) | 数量 |
---|---|---|
Node A | 0 - 3275 | 3276 |
Node B | 3276 - 7371 | 3276 |
Node C | 7372 - 11467 | 3276 |
Node D | 11468 - 15563 | 3276 |
Node E | 15564 - 16383 + ... | 3280 |
由于每次 slot 分布不能做到完全精确划分,可能最后部分节点多几个槽,不影响功能。
大数据量时,使用 --cluster use-empty-masters yes
避免主从冲突
迁移过程中注意磁盘和网络压力,建议 按 slot 分批迁移
如果需要自动脚本迁移,可以用 redis-trib.rb
或封装版的 Python 工具
在 Redis Cluster 中,数据倾斜是指某些节点上的槽虽然数量看起来一致,但实际承载的数据量明显高于其他节点,造成这些节点成为瓶颈。防止数据倾斜的关键在于:
不仅要平均分配槽位(slots),还要确保 key 的哈希分布尽量均匀;
避免“热点 key”或“同类 key 前缀”集中落到某一个槽。
虽然 Redis Cluster 的 key 是通过 CRC16 算法取模映射到 16384 个槽(slot):
slot = CRC16(key) % 16384
但如果 key 的分布不均衡,即使槽均分了,某些节点上的 key 数量或 key 大小也可能暴增。
场景 | 描述 |
---|---|
热点 key | 某些 key 的访问频率极高,造成某节点 CPU/内存负载高 |
key 前缀重复 | 比如 user:1 , user:2 ... 这些 key 落入同一槽 |
哈希标签不当 | 使用了 {} 包裹部分 key,导致所有 key 落入相同槽(聚簇) |
大 key | 某些 key(如 zset/list/hash)数据量非常大,导致单节点内存暴涨 |
确保每个节点分配大致相同数量的槽(约 16384 ÷ N 个主节点):
redis-cli --cluster reshard --cluster-use-empty-masters yes
但注意:槽均分 ≠ 数据均分,还要关注 key 分布!
user:{1000}:profile
user:{1000}:settings
user:{1000}:tokens
这些 key 都被哈希到同一个 slot,造成集中。
user:1000:profile
user:1001:settings
user:1002:tokens
默认采用全 key 参与 CRC16,不使用 {}
,这样 key 会自然分散。
你可以做简单的 hash 前缀打散:
slot_prefix = CRC16(userId) % 16384
key = "prefix:" + slot_prefix + ":user:" + userId
这样即便 userId 连续,槽也会打散。
Redis 官方命令或脚本工具:
# 按 slot 采样 key 分布情况
redis-cli --scan | while read key; do
slot=$(redis-cli cluster keyslot "$key")
echo "$slot" >> slot_dist.txt
done
sort slot_dist.txt | uniq -c | sort -nr | head
可视化输出哪些槽过于拥挤,可以进行迁移调整。
有些槽虽然 key 数不多,但 key 太大或太频繁访问。你可以使用如下方式探查:
使用 MEMORY USAGE key
检查 key 大小
使用 MONITOR
、SLOWLOG
或代理层(如 Codis/Twemproxy)做热点分析
对热点 key 分布的槽做调整,把它们拆分出去
工具方案如:
工具 | 说明 |
---|---|
redis-trib.rb / redis-cli --cluster | 提供 slot 的迁移,但不分析热点 |
Redis Shake、KeyHub、Codis | 支持 key 扫描和热点统计分析 |
自研脚本 | 基于 --scan + cluster keyslot + MEMORY USAGE 做 key 分布和大小采样 |
部署前: 预生成 key 示例,使用脚本 hash 计算槽位分布评估是否均匀
部署后: 定期运行 key 采样分析,观察槽位中 key 总量是否平衡
运行中: 若出现访问慢、内存飙升、CPU 局部高,结合 MONITOR
+ key 分布分析
迁移方案:
如果槽位数量均衡,但数据不均 → 分析热点槽,执行局部槽位迁移
如果槽位数量不均 → redis-cli --cluster reshard
手动或脚本重新均衡
# 遍历集群 key,计算每个 slot key 数
redis-cli --scan | while read key; do
slot=$(redis-cli cluster keyslot "$key")
echo "$slot" >> slots.txt
done
sort slots.txt | uniq -c | sort -nr | head -20
输出示例:
800 12536
780 8732
779 8756
...
说明:槽位 12536 中有 800 个 key,可能需要迁出部分 key 给空闲槽位。
策略 | 说明 |
---|---|
均分槽位 | 保证基础分布一致性(每节点约 3276 个槽) |
Key 命名优化 | 避免使用 hash tag 聚簇 key;避免热点前缀 |
热点检测 | 通过 key 扫描、访问频率分析检测热点槽 |
数据大小平衡 | 使用 MEMORY USAGE 对 key 大小做统计 |
热点迁移 | 对热点槽使用 CLUSTER SETSLOT + MIGRATE 做局部缓解 |
在 Redis Cluster 中,槽位(slot)总数固定为 16384(编号为 0 ~ 16383),这是 Redis Cluster 的核心机制之一。所有的 key 都通过 CRC16 哈希映射到这些槽位中:
slot = CRC16(key) % 16384
你不能“创建”或“使用”第 16384 以上的槽位——超出范围的槽位是非法的,Redis 会直接报错。
你不能直接指定槽位编号来存 key,但你可以通过特定 key 设计来确保 key 落入你期望的槽位。有两种方式:
{}
来固定槽位Redis Cluster 中,只有 {}
中的内容会参与哈希计算。
SET user:{1000}:name "Alice"
SET user:{1000}:email "[email protected]"
这两个 key 都会被映射到:
slot = CRC16("1000") % 16384
所以,它们会被强制路由到同一个槽位(Cluster 的同一节点)。
⚠️ 这是 Redis Cluster 支持“跨 key 操作”的唯一机制,比如
MGET key1 key2
仅在 key1、key2 落在相同槽位时才可执行。
你可以先计算你想要的 slot,然后构造一个 key 让它哈希落到你指定的槽位。
例如你想让一个 key 落到 slot 9999,你可以使用工具来生成这样的 key:
import crcmod
crc16 = crcmod.predefined.mkCrcFun('crc-16')
for i in range(1000000):
key = f"key{i}"
slot = crc16(key.encode()) % 16384
if slot == 9999:
print(f"Key: {key} => Slot: {slot}")
break
Redis 明确限制槽位范围:
slot ∈ [0, 16383]
如果你通过 CLUSTER SETSLOT
、CLUSTER GETKEYSINSLOT
、CLUSTER ADDSLOTS
等命令尝试使用非法槽位,比如 20000,会报错:
127.0.0.1:7000> CLUSTER SETSLOT 20000 NODE
(error) ERR Invalid slot
或:
127.0.0.1:7000> CLUSTER GETKEYSINSLOT 20000 10
(error) ERR Invalid slot
这是 Redis Cluster 源码中硬编码的上限,无法突破。
redis-cli -c cluster keyslot yourkey
示例:
> cluster keyslot "user:{1000}:email"
(integer) 5792
redis-cli -c cluster slots
Redis Cluster 不直接存 key 的映射,而是通过槽位来间接映射;
槽的数量要足够大,以支持灵活迁移、负载均衡;
槽数设为 2 的幂(16384 = 2^14)有利于位运算优化。
问题 | 结论 |
---|---|
如何让 key 落在特定槽位? | 使用 {} 包裹部分 key,或手动计算 CRC16 |
槽位最大是多少? | 固定为 0 ~ 16383,共 16384 个 |
使用超出槽位会怎样? | Redis 返回 ERR Invalid slot ,操作失败 |
如何避免冲突和错位? | 统一规范 key 的 hash tag 使用;槽位映射合理 |
在Redis集群环境中实现Redlock(分布式锁算法),需要遵循Redlock算法的核心思想:在多个独立的Redis节点上获取大多数节点的锁,以确保高可用和正确性。Redis Cluster 自身并不天然支持 Redlock 的所有机制,因此通常是将多个独立的 Redis 实例部署为 Redlock 节点,而不是使用 Redis Cluster 的分片架构。
Redlock 是由 Redis 作者 antirez(Salvatore Sanfilippo)提出的一个 分布式锁算法,用于确保在分布式系统中安全可靠地加锁。其核心流程如下:
TTL
T
毫秒内拿到多数(N/2+1)个锁,则认为加锁成功Redis Cluster(集群)是基于分片的集群系统,它的节点之间不是独立的,因此不能直接用来实现 Redlock 的“多个独立 Redis 实例”的要求。
要求 | Redis Cluster 支持情况 |
---|---|
多个完全独立 Redis 实例 | 不支持(Redis Cluster 节点间通信) |
跨节点一致性加锁 | 不支持(单 key 属于单个节点) |
多节点同时写入锁 | 需要额外逻辑或客户端支持 |
因此,Redlock 更适合部署在多个独立 Redis 实例上,而不是 Redis Cluster 中。
部署 5 个完全独立的 Redis 实例(不在同一个物理机,网络独立性较好),然后在应用层通过 Redlock 算法加锁。例如:
Redis1: 10.0.0.1:6379
Redis2: 10.0.0.2:6379
Redis3: 10.0.0.3:6379
Redis4: 10.0.0.4:6379
Redis5: 10.0.0.5:6379
使用 Redlock 客户端库,如:
Java: Redisson
Python: redis-py + redis.lock or redlock-py
Node.js: node-redlock
虽然不能直接实现 Redlock,但如果你只使用 Redis Cluster(没有独立 Redis 实例),可使用:
单 key 加锁:Redis Cluster 会将 key 映射到对应节点,只保证该节点的一致性,适合非关键锁。
通过哈希标签强制同 slot key:如 {lock_key}
,让多个 key 保持在一个节点,简化锁操作。
搭配 ZooKeeper / etcd 等实现更高级别的分布式锁机制。
场景 | 是否适合 Redlock |
---|---|
多个独立 Redis 实例 | 是,推荐 |
Redis Sentinel 模式 | 是,可做 Redlock 节点 |
Redis Cluster(分片) | 否,不能直接用作 Redlock |
如使用 Redis Cluster,建议采用本地锁+幂等性+补偿机制的混合方案,而不是强行实现 Redlock。
Redisson 提供了 RedissonRedLock
类来封装这一机制,使用非常简单。
// 连接5个独立的Redis节点(必须是独立的实例,不是同一个Redis的多个db)
RedissonClient redisson1 = Redisson.create(config1);
RedissonClient redisson2 = Redisson.create(config2);
RedissonClient redisson3 = Redisson.create(config3);
RedissonClient redisson4 = Redisson.create(config4);
RedissonClient redisson5 = Redisson.create(config5);
// 获取每个节点的锁
RLock lock1 = redisson1.getLock("my-lock");
RLock lock2 = redisson2.getLock("my-lock");
RLock lock3 = redisson3.getLock("my-lock");
RLock lock4 = redisson4.getLock("my-lock");
RLock lock5 = redisson5.getLock("my-lock");
// 构造 RedLock(至少需要 3 个锁成功)
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3, lock4, lock5);
// 尝试加锁:最多等待 2 秒,锁自动释放时间 10 秒
boolean locked = redLock.tryLock(2, 10, TimeUnit.SECONDS);
if (locked) {
try {
// 执行业务逻辑
} finally {
redLock.unlock(); // 自动释放全部节点锁
}
}
RedissonRedLock
public class RedissonRedLock extends RedissonMultiLock {
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
// 所有子锁同时尝试获取
// 获取超过半数则返回成功
}
@Override
public void unlock() {
// 释放所有子锁
}
}
默认 5 个 Redis 实例时,至少 3 个成功加锁(过半)
每个子锁都使用 tryLock(waitTime, leaseTime)
,其中 waitTime
是加锁等待总时间
若任何一个子锁返回失败,会立即放弃加锁,释放已获得的锁
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
或连接多个 Redis:
Config config = new Config();
config.useClusterServers()
.addNodeAddress("redis://127.0.0.1:7001", "redis://127.0.0.1:7002");
⚠️ 注意:
RedLock 必须使用多个 Redis 实例(建议部署在不同机房/节点)
多 Redis 节点 不能是单机多个 DB,那不符合分布式锁容错性设计
注意点 | 说明 |
---|---|
Redis 实例独立性 | 要求多个 Redis 物理隔离,部署在不同主机 |
网络延迟处理 | Redisson 内部处理了 tryLock 的时间预算与租约计算 |
少数节点失败容忍 | 支持小部分节点宕机,保证超过半数成功即可 |
多实例 RedissonClient | 每个 Redis 实例都需创建独立 RedissonClient |
特性 | 单节点 Redis 锁 | RedLock(多节点) |
---|---|---|
可用性 | Redis 挂掉锁失效 | 容忍部分节点失败 |
安全性 | 主从切换可能导致锁丢失 | 多节点一致性加锁 |
复杂性 | 实现简单 | 配置和资源较复杂 |
推荐场景 | 单机开发或容忍少量锁失效 | 关键业务锁、分布式系统中高一致性要求 |
Redisson 的
RedLock
是对 Redis 官方分布式锁算法的完整实现,适合跨多 Redis 实例、高可用、高一致性的分布式系统中使用,需正确配置多个独立的 Redis 节点才能发挥其真正价值。