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