IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。
通过主从复制和 Sentinel 哨兵,我们解决了数据冗余、读写分离和自动故障转移。但所有这些架构中,写入操作始终只能由一台主节点承担。当数据量超过单机内存,或写入 QPS 突破单机瓶颈时,纵向扩展(升级更大内存、更强 CPU)终有尽头。
Redis Cluster 就是突破这一物理限制的终极方案。它通过哈希槽将数据分散到多个主节点上,让写入能力可以随着节点增加而线性扩展。本文将从原理出发,用 Docker 搭建一个 6 节点集群,深入 MOVED/ASK 重定向机制,并让 Python 客户端像操作单机一样操作集群。
1. Redis Cluster 是什么?为什么需要它?
Redis Cluster 是 Redis 官方提供的去中心化分片方案,每个节点只存储一部分数据,所有节点共同组成一个逻辑上的"大数据池"。它具有三个核心能力:
-
数据分片:通过哈希槽将键自动分布到不同主节点,突破单机内存和写入瓶颈。
-
高可用:每个主节点可配置从节点,主节点故障时自动进行故障转移。
-
去中心化:节点之间通过 Gossip 协议交换状态信息,无需第三方协调者(如 Sentinel)。
bash
┌─────────────┬─────────────┬─────────────┐
│ Master 1 │ Master 2 │ Master 3 │
│ 槽 0-5460 │ 槽 5461-10922│ 槽 10923-16383│
├─────────────┼─────────────┼─────────────┤
│ Slave 1 │ Slave 2 │ Slave 3 │
└─────────────┴─────────────┴─────────────┘
💡 官方推荐至少 3 主 3 从,这是生产集群的最小配置。
2. 核心原理:哈希槽(Hash Slot)
Redis Cluster 没有使用一致性哈希,而是采用了哈希槽 。总计 16384 个槽,每个主节点负责一部分槽。
键到槽的映射:
bash
HASH_SLOT = CRC16(key) % 16384
CRC16 会对键名计算一个 16 位校验和,然后对 16384 取模,结果在 0~16383 之间。集群中的每个主节点管理一段连续的哈希槽。
为什么是 16384?
-
16384 个槽足够在最多 1000 个节点间均匀分配(每个节点 16 个槽)。
-
槽信息通过 Gossip 协议在节点间传播,16384 个槽的位图只需要 2KB,开销可控。
-
心跳包中可以轻松携带完整的槽分配信息。
Hash Tag :
有时我们希望多个键(如 user:1001:profile 和 user:1001:score)落在同一个槽。可以通过 {...} 指定参与哈希计算的部分:
bash
HASH_SLOT = CRC16("1001") % 16384 # 只对 {} 内的部分计算
例如:user:{1001}:profile 和 user:{1001}:score 一定会落在同一节点,方便批量操作和事务。
3. 实战:Docker 搭建 6 节点集群
我们用 Docker 在一台机器上搭建 3 主 3 从的集群(仅演示,生产应分布在不同机器)。
3.1 创建网络和 6 个节点
bash
docker network create cluster-net
# 创建 6 个节点(端口 6371~6376)
for port in $(seq 6371 6376); do
docker run -d \
--name redis-cluster-${port} \
--network cluster-net \
-p ${port}:6379 \
redis:7.2 redis-server \
--cluster-enabled yes \
--cluster-config-file nodes.conf \
--cluster-node-timeout 5000 \
--appendonly yes \
--appendfsync everysec
done
参数说明:
-
cluster-enabled yes:开启集群模式。 -
cluster-config-file nodes.conf:节点自动保存集群拓扑的文件。 -
cluster-node-timeout 5000:节点超时判定时间(毫秒)。
3.2 初始化集群
Redis 5.0+ 提供了 redis-cli --cluster create 命令,一行完成初始化:
bash
# 获取容器 IP(在同一 Docker 网络内使用容器名即可)
# 或者进入任意容器用 redis-cli 操作
docker exec -it redis-cluster-6371 bash
# 在容器内执行(容器名就是 hostname)
redis-cli --cluster create \
redis-cluster-6371:6379 \
redis-cluster-6372:6379 \
redis-cluster-6373:6379 \
redis-cluster-6374:6379 \
redis-cluster-6375:6379 \
redis-cluster-6376:6379 \
--cluster-replicas 1
--cluster-replicas 1 表示为每个主节点分配 1 个从节点。
交互过程输出:
bash
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica redis-cluster-6375:6379 to redis-cluster-6371:6379
Adding replica redis-cluster-6376:6379 to redis-cluster-6372:6379
Adding replica redis-cluster-6374:6379 to redis-cluster-6373:6379
>>> Trying to optimize slaves allocation for anti-affinity
[OK] All 16384 slots covered.
输入 yes 确认后,集群搭建完成。
3.3 验证集群状态
bash
docker exec -it redis-cluster-6371 redis-cli -c cluster nodes
输出示例:
bash
a1b2c3... 172.18.0.2:6379@16379 myself,master - 0 1718100000000 1 connected 0-5460
d4e5f6... 172.18.0.3:6379@16379 master - 0 1718100001000 2 connected 5461-10922
g7h8i9... 172.18.0.4:6379@16379 master - 0 1718100002000 3 connected 10923-16383
j0k1l2... 172.18.0.5:6379@16379 slave a1b2c3... 0 1718100003000 1 connected
m3n4o5... 172.18.0.6:6379@16379 slave d4e5f6... 0 1718100004000 2 connected
p6q7r8... 172.18.0.7:6379@16379 slave g7h8i9... 0 1718100005000 3 connected
可以看到 3 个 master 各自负责一段槽,每个 master 有一个 slave。
bash
docker exec -it redis-cluster-6371 redis-cli -c cluster info
# cluster_state:ok
# cluster_slots_assigned:16384
# cluster_slots_ok:16384
# cluster_known_nodes:6
# cluster_size:3
4. 重定向:MOVED 与 ASK
客户端向任意节点发送命令,如果键不在该节点,会发生什么?
4.1 MOVED 重定向(永久)
bash
# 连接到节点 6371
docker exec -it redis-cluster-6371 redis-cli -c
# 设置一个键,可能不在本节点
127.0.0.1:6379> SET test:1 "hello"
如果 test:1 不在当前节点,-c 模式会自动跟随重定向,输出类似:
bash
-> Redirected to slot [13643] located at 172.18.0.4:6379
OK
不带 -c 的客户端会收到错误:
bash
127.0.0.1:6379> SET test:1 "hello"
(error) MOVED 13643 172.18.0.4:6379
MOVED 错误包含了正确节点的 IP 和端口,客户端应更新本地槽映射并重新向正确节点发送命令。
4.2 ASK 重定向(临时)
当集群进行槽迁移 时(例如扩容缩容期间),部分键可能暂时存在于两个节点。此时如果访问迁移中的槽,客户端可能收到 ASK 重定向:
bash
127.0.0.1:6379> GET migrating_key
(error) ASK 13643 172.18.0.5:6379
ASK 和 MOVED 的区别:
-
MOVED:槽的所有权已经转移,客户端应永久更新槽映射。 -
ASK:只是临时重定向,下一次请求可能还在原节点,客户端不应更新 槽映射。处理时需先发送ASKING命令。
4.3 Smart Client 如何工作
手动处理 MOVED/ASK 太麻烦,所以有了 Smart Client (智能客户端)。redis-py 的 RedisCluster 类会:
-
启动时通过
CLUSTER SLOTS命令获取槽分布。 -
本地缓存槽到节点的映射。
-
收到
MOVED后自动更新映射并重试。 -
收到
ASK后自动发送ASKING并重试。
对开发者来说,几乎感觉不到集群的存在。
5. Python 访问 Redis Cluster
5.1 安装与连接
bash
from redis.cluster import RedisCluster
# 连接集群(只需提供部分节点,客户端会自动发现全部)
cluster = RedisCluster(
host='localhost',
port=6371,
decode_responses=True,
# 或者直接传入多个启动节点
# startup_nodes=[
# {'host': 'localhost', 'port': 6371},
# {'host': 'localhost', 'port': 6372},
# ]
)
# Ping
print(cluster.ping()) # True
5.2 基础读写
bash
# 写入
cluster.set('user:1001', 'Alice')
cluster.set('user:2002', 'Bob')
cluster.set('product:5001', 'iPhone')
# 读取
print(cluster.get('user:1001')) # Alice
print(cluster.get('user:2002')) # Bob
print(cluster.get('product:5001')) # iPhone
# 批量操作(需要 hash tag 保证同一槽)
cluster.mset({'user:{1001}:name': 'Alice', 'user:{1001}:age': '30'})
print(cluster.mget(['user:{1001}:name', 'user:{1001}:age']))
# ['Alice', '30']
# 键不存在返回 None
print(cluster.get('nonexistent')) # None
5.3 使用 Hash Tag 实现跨键原子操作
由于集群中事务和 Lua 脚本只能操作同一槽中的键,我们需要使用 Hash Tag:
bash
# 错误的用法:两个键不在同一槽,Lua 脚本会报错
# cluster.eval("...", 2, 'user:1001', 'order:1001')
# 正确的用法:用 Hash Tag 强制同一槽
script = """
local user = redis.call('GET', KEYS[1])
local order = redis.call('GET', KEYS[2])
return {user, order}
"""
result = cluster.eval(script, 2,
'user:{1001}', # 都包含 {1001}
'order:{1001}'
)
print(result) # ['Alice', 'some_order']
5.4 Pipeline 在集群中的使用
集群模式下 Pipeline 默认只能操作同一节点的键。redis-py 支持自动分组:
bash
pipe = cluster.pipeline()
# 可以写不同槽的键,但 execute 时会自动按节点分组发送
pipe.set('key1', 'value1')
pipe.set('key2', 'value2')
pipe.set('key3', 'value3')
pipe.get('key1')
pipe.get('key2')
pipe.get('key3')
results = pipe.execute()
print(results)
# [True, True, True, 'value1', 'value2', 'value3']
⚠️ 跨节点的 Pipeline 性能增益有限(仍然需要向多个节点分别发送),但其原子性也不保证。
5.5 封装集群操作工具类
bash
from redis.cluster import RedisCluster
import json
class RedisClusterClient:
"""Redis Cluster 客户端封装"""
def __init__(self, startup_nodes, decode_responses=True):
self.client = RedisCluster(
startup_nodes=startup_nodes,
decode_responses=decode_responses,
max_connections=50,
retry_on_timeout=True,
)
def cache_set(self, key, value, ex=3600):
"""设置缓存"""
data = json.dumps(value, ensure_ascii=False)
return self.client.set(key, data, ex=ex)
def cache_get(self, key):
"""获取缓存"""
data = self.client.get(key)
if data:
return json.loads(data)
return None
def atomic_incr_with_limit(self, key, limit, delta=1):
"""原子递增并检查上限(使用 Hash Tag 确保落在同一槽)"""
script = """
local current = redis.call('GET', KEYS[1])
if current == false then
current = 0
else
current = tonumber(current)
end
if current + tonumber(ARGV[1]) > tonumber(ARGV[2]) then
return -1
end
return redis.call('INCRBY', KEYS[1], ARGV[1])
"""
return self.client.eval(script, 1, key, delta, limit)
def close(self):
self.client.close()
# 使用
cluster_client = RedisClusterClient(
startup_nodes=[
{'host': 'localhost', 'port': 6371},
]
)
cluster_client.cache_set('user:1001', {'name': 'IT策士', 'score': 100})
print(cluster_client.cache_get('user:1001'))
# {'name': 'IT策士', 'score': 100}
# 原子限流
print(cluster_client.atomic_incr_with_limit('rate:{user123}', 5)) # 1
print(cluster_client.atomic_incr_with_limit('rate:{user123}', 5)) # 2
5.6 异步 Redis Cluster
bash
import asyncio
from redis.asyncio.cluster import RedisCluster as AsyncRedisCluster
async def async_cluster_demo():
client = AsyncRedisCluster(
host='localhost',
port=6371,
decode_responses=True,
)
await client.set('async_key', 'Hello Cluster')
value = await client.get('async_key')
print(value) # Hello Cluster
await client.close()
asyncio.run(async_cluster_demo())
6. 集群限制与注意事项
使用集群前必须了解以下约束:
-
多键操作必须同槽 :
SINTER、SUNION、MGET、RENAME等操作涉及的所有键必须落在同一槽。使用 Hash Tag 可解决。 -
Lua 脚本同槽限制:脚本访问的键也必须同槽。
-
事务同槽限制 :
MULTI/EXEC操作的键必须同槽。 -
跨槽订阅:Pub/Sub 在集群中消息会广播到所有节点,可以在任意节点订阅和发布。
-
Slot 迁移时性能下降:迁移过程中对应槽的请求会有短暂延迟。
7. 动手试试
-
搭建集群并测试重定向 :用不带
-c的redis-cli向任意节点写入,观察MOVED错误。 -
Hash Tag 实践 :尝试用
MSET和MGET操作user:{1001}:name和user:{1001}:age,验证成功。再试一个不带 Hash Tag 的MGET,观察报错。 -
故障转移演练:停掉一个主节点(如 6371),观察其从节点自动提升为主,并用 Python 客户端验证读写不受影响。
-
扩缩容模拟 :用
redis-cli --cluster add-node添加新节点,--cluster reshard迁移槽,观察ASK重定向。
预期效果:MOVED 重定向报错正确;Hash Tag 让批量操作成功;主节点宕机后约 5 秒客户端恢复;扩容迁移时产生 ASK 重定向。
8. 总结
本文我们吃透了 Redis 终极形态------Cluster:
-
哈希槽 :16384 个槽,
CRC16(key) % 16384决定键落在哪个主节点。 -
搭建 :
redis-cli --cluster create一行命令创建集群。 -
重定向 :
MOVED永久、ASK临时,Smart Client 自动处理。 -
Python 操作 :
RedisCluster客户端,支持 Pipeline、Lua(同槽)、异步。 -
限制:多键操作和脚本需配合 Hash Tag。
至此,Redis 的高可用架构三部曲(主从复制 → Sentinel 哨兵 → Cluster 集群)全部完成。从下一篇开始,我们将切入实战性最强的主题------缓存穿透、击穿、雪崩,用 Redis 解决生产中最头疼的缓存难题。
想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维 !