Redis 从入门到精通:分片之道 —— Redis Cluster

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:profileuser:1001:score)落在同一个槽。可以通过 {...} 指定参与哈希计算的部分:

bash 复制代码
HASH_SLOT = CRC16("1001") % 16384  # 只对 {} 内的部分计算

例如:user:{1001}:profileuser:{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

ASKMOVED 的区别:

  • MOVED:槽的所有权已经转移,客户端应永久更新槽映射。

  • ASK:只是临时重定向,下一次请求可能还在原节点,客户端不应更新 槽映射。处理时需先发送 ASKING 命令。

4.3 Smart Client 如何工作

手动处理 MOVED/ASK 太麻烦,所以有了 Smart Client (智能客户端)。redis-pyRedisCluster 类会:

  1. 启动时通过 CLUSTER SLOTS 命令获取槽分布。

  2. 本地缓存槽到节点的映射。

  3. 收到 MOVED 后自动更新映射并重试。

  4. 收到 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. 集群限制与注意事项

使用集群前必须了解以下约束:

  • 多键操作必须同槽SINTERSUNIONMGETRENAME 等操作涉及的所有键必须落在同一槽。使用 Hash Tag 可解决。

  • Lua 脚本同槽限制:脚本访问的键也必须同槽。

  • 事务同槽限制MULTI/EXEC 操作的键必须同槽。

  • 跨槽订阅:Pub/Sub 在集群中消息会广播到所有节点,可以在任意节点订阅和发布。

  • Slot 迁移时性能下降:迁移过程中对应槽的请求会有短暂延迟。

7. 动手试试

  1. 搭建集群并测试重定向 :用不带 -credis-cli 向任意节点写入,观察 MOVED 错误。

  2. Hash Tag 实践 :尝试用 MSETMGET 操作 user:{1001}:nameuser:{1001}:age,验证成功。再试一个不带 Hash Tag 的 MGET,观察报错。

  3. 故障转移演练:停掉一个主节点(如 6371),观察其从节点自动提升为主,并用 Python 客户端验证读写不受影响。

  4. 扩缩容模拟 :用 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 思维 !

相关推荐
189228048611 小时前
NV114固态MT29F16T08EWLEHD6-MES:E
人工智能·算法·缓存·性能优化
AOwhisky2 小时前
学习自测与解析:Redis系列第一期与第二期核心知识点详解
运维·数据库·redis·学习·云计算
kishu_iOS&AI2 小时前
LLM —— Milvmus向量数据库
数据库·人工智能·milvus
名不经传的养虾人2 小时前
从0到1:企业级AI项目迭代日记 Vol.46|三个检索源、缓存限流、深度整合——联网检索一日冲刺
数据库·人工智能·agent·ai编程·ai工作流·企业ai
BugShare2 小时前
Mac 上原生开发的开源免费、尽享丝滑数据库工具
数据库·macos·开源
Java爱好狂.2 小时前
阿里1658页2026最新Java面试题总结(含答案)
数据库·redis·程序员·java面试·java面试题·java编程·java八股文
jieyucx2 小时前
《Go 数据库编程开篇:彻底打通 database/sql 与 MySQL 驱动的连接池调优密码》
数据库·sql·golang
白露与泡影2 小时前
深入理解MySQL事务隔离级别:MVCC机制与Next-Key Lock如何解决幻读问题?
数据库·mysql
Gong-Yu2 小时前
MySQL数据库运维——性能优化进阶2️⃣
运维·数据库·mysql·性能优化