IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。
通过主从复制和 Sentinel 哨兵,我们解决了数据冗余、读写分离和自动故障转移。但所有这些架构中,写入操作始终只能由一台主节点承担。当数据量超过单机内存,或写入 QPS 突破单机瓶颈时,纵向扩展(升级更大内存、更强 CPU)终有尽头。
Redis Cluster就是突破这一物理限制的终极方案。它通过哈希槽将数据分散到多个主节点上,让写入能力可以随着节点增加而线性扩展。本文将从原理出发,用 Docker 搭建一个 6 节点集群,深入 MOVED/ASK 重定向机制,并让 Python 客户端像操作单机一样操作集群。
1. Redis Cluster 是什么?为什么需要它?
Redis Cluster 是 Redis 官方提供的去中心化分片方案,每个节点只存储一部分数据,所有节点共同组成一个逻辑上的“大数据池”。它具有三个核心能力:
数据分片:通过哈希槽将键自动分布到不同主节点,突破单机内存和写入瓶颈。
高可用:每个主节点可配置从节点,主节点故障时自动进行故障转移。
去中心化:节点之间通过 Gossip 协议交换状态信息,无需第三方协调者(如 Sentinel)。
┌─────────────┬─────────────┬─────────────┐ │ Master1│ Master2│ Master3│ │ 槽0-5460 │ 槽5461-10922│ 槽10923-16383│ ├─────────────┼─────────────┼─────────────┤ │ Slave1│ Slave2│ Slave3│ └─────────────┴─────────────┴─────────────┘💡 官方推荐至少3 主 3 从,这是生产集群的最小配置。
2. 核心原理:哈希槽(Hash Slot)
Redis Cluster 没有使用一致性哈希,而是采用了哈希槽。总计16384个槽,每个主节点负责一部分槽。
键到槽的映射:
HASH_SLOT=CRC16(key)%16384CRC16会对键名计算一个 16 位校验和,然后对 16384 取模,结果在 0~16383 之间。集群中的每个主节点管理一段连续的哈希槽。
为什么是 16384?
16384 个槽足够在最多 1000 个节点间均匀分配(每个节点 16 个槽)。
槽信息通过 Gossip 协议在节点间传播,16384 个槽的位图只需要 2KB,开销可控。
心跳包中可以轻松携带完整的槽分配信息。
Hash Tag:
有时我们希望多个键(如user:1001:profile和user:1001:score)落在同一个槽。可以通过{...}指定参与哈希计算的部分:
HASH_SLOT=CRC16("1001")%16384# 只对 {} 内的部分计算例如:user:{1001}:profile和user:{1001}:score一定会落在同一节点,方便批量操作和事务。
3. 实战:Docker 搭建 6 节点集群
我们用 Docker 在一台机器上搭建 3 主 3 从的集群(仅演示,生产应分布在不同机器)。
3.1 创建网络和 6 个节点
dockernetwork create cluster-net# 创建 6 个节点(端口 6371~6376)forportin$(seq63716376);dodockerrun-d\--nameredis-cluster-${port}\--networkcluster-net\-p${port}:6379\redis:7.2 redis-server\--cluster-enabledyes\--cluster-config-file nodes.conf\--cluster-node-timeout5000\--appendonlyyes\--appendfsynceverysecdone参数说明:
cluster-enabled yes:开启集群模式。cluster-config-file nodes.conf:节点自动保存集群拓扑的文件。cluster-node-timeout 5000:节点超时判定时间(毫秒)。
3.2 初始化集群
Redis 5.0+ 提供了redis-cli --cluster create命令,一行完成初始化:
# 获取容器 IP(在同一 Docker 网络内使用容器名即可)# 或者进入任意容器用 redis-cli 操作dockerexec-itredis-cluster-6371bash# 在容器内执行(容器名就是 hostname)redis-cli--clustercreate\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-replicas1--cluster-replicas 1表示为每个主节点分配 1 个从节点。
交互过程输出:
>>>Performinghashslots allocation on6nodes... Master[0]->Slots0-5460Master[1]->Slots5461-10922Master[2]->Slots10923-16383Adding 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 allocationforanti-affinity[OK]All16384slots covered.输入yes确认后,集群搭建完成。
3.3 验证集群状态
dockerexec-itredis-cluster-6371 redis-cli-ccluster nodes输出示例:
a1b2c3...172.18.0.2:6379@16379 myself,master -017181000000001connected0-5460 d4e5f6...172.18.0.3:6379@16379 master -017181000010002connected5461-10922 g7h8i9...172.18.0.4:6379@16379 master -017181000020003connected10923-16383 j0k1l2...172.18.0.5:6379@16379 slave a1b2c3...017181000030001connected m3n4o5...172.18.0.6:6379@16379 slave d4e5f6...017181000040002connected p6q7r8...172.18.0.7:6379@16379 slave g7h8i9...017181000050003connected可以看到 3 个master各自负责一段槽,每个 master 有一个slave。
dockerexec-itredis-cluster-6371 redis-cli-ccluster info# cluster_state:ok# cluster_slots_assigned:16384# cluster_slots_ok:16384# cluster_known_nodes:6# cluster_size:34. 重定向:MOVED 与 ASK
客户端向任意节点发送命令,如果键不在该节点,会发生什么?
4.1 MOVED 重定向(永久)
# 连接到节点 6371dockerexec-itredis-cluster-6371 redis-cli-c# 设置一个键,可能不在本节点127.0.0.1:6379>SET test:1"hello"如果test:1不在当前节点,-c模式会自动跟随重定向,输出类似:
->Redirected to slot[13643]located at172.18.0.4:6379 OK不带-c的客户端会收到错误:
127.0.0.1:6379>SET test:1"hello"(error)MOVED13643172.18.0.4:6379MOVED错误包含了正确节点的 IP 和端口,客户端应更新本地槽映射并重新向正确节点发送命令。
4.2 ASK 重定向(临时)
当集群进行槽迁移时(例如扩容缩容期间),部分键可能暂时存在于两个节点。此时如果访问迁移中的槽,客户端可能收到ASK重定向:
127.0.0.1:6379>GET migrating_key(error)ASK13643172.18.0.5:6379ASK和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 安装与连接
from redis.clusterimportRedisCluster# 连接集群(只需提供部分节点,客户端会自动发现全部)cluster=RedisCluster(host='localhost',port=6371,decode_responses=True,# 或者直接传入多个启动节点# startup_nodes=[# {'host': 'localhost', 'port': 6371},# {'host': 'localhost', 'port': 6372},# ])# Pingprint(cluster.ping())# True5.2 基础读写
# 写入cluster.set('user:1001','Alice')cluster.set('user:2002','Bob')cluster.set('product:5001','iPhone')# 读取print(cluster.get('user:1001'))# Aliceprint(cluster.get('user:2002'))# Bobprint(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']# 键不存在返回 Noneprint(cluster.get('nonexistent'))# None5.3 使用 Hash Tag 实现跨键原子操作
由于集群中事务和 Lua 脚本只能操作同一槽中的键,我们需要使用 Hash Tag:
# 错误的用法:两个键不在同一槽,Lua 脚本会报错# cluster.eval("...", 2, 'user:1001', 'order:1001')# 正确的用法:用 Hash Tag 强制同一槽script="""localuser=redis.call('GET', KEYS[1])localorder=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支持自动分组:
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 封装集群操作工具类
from redis.clusterimportRedisClusterimportjson 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)returnself.client.set(key, data,ex=ex)def cache_get(self, key):"""获取缓存""" data=self.client.get(key)ifdata:returnjson.loads(data)returnNone def atomic_incr_with_limit(self, key, limit,delta=1):"""原子递增并检查上限(使用 Hash Tag 确保落在同一槽)""" script="""localcurrent=redis.call('GET', KEYS[1])ifcurrent==falsethencurrent=0elsecurrent=tonumber(current)endifcurrent + tonumber(ARGV[1])>tonumber(ARGV[2])thenreturn-1endreturnredis.call('INCRBY', KEYS[1], ARGV[1])"""returnself.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))# 1print(cluster_client.atomic_incr_with_limit('rate:{user123}',5))# 25.6 异步 Redis Cluster
importasyncio from redis.asyncio.clusterimportRedisCluster 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 Clusterawait 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 思维 !