6.3 Redis Cluster 的数据读写 Redis Cluster 数据读写详解与实践 (Redis 3.0+) Redis Cluster 架构回顾与数据分片 在深入数据读写之前,我们先简要回顾 Redis Cluster 的架构和数据分片机制,这对于理解后续的数据路由和操作至关重要。 1.1 Redis Cluster 架构 Redis Cluster 由多个 Redis 节点组成,这些节点分为主节点 (Master) 和从节点 (Replica)。 主节点 (Master): 负责接收客户端的读写请求,存储数据,并进行数据同步到从节点。 从节点 (Replica): 复制主节点的数据,当主节点发生故障时,可以被选举为新的主节点,提供高可用性。
在深入数据读写之前,我们先简要回顾 Redis Cluster 的架构和数据分片机制,这对于理解后续的数据路由和操作至关重要。
1.1 Redis Cluster 架构
Redis Cluster 由多个 Redis 节点组成,这些节点分为主节点 (Master) 和从节点 (Replica)。
主节点 (Master): 负责接收客户端的读写请求,存储数据,并进行数据同步到从节点。
从节点 (Replica): 复制主节点的数据,当主节点发生故障时,可以被选举为新的主节点,提供高可用性。
Redis Cluster 采用去中心化的架构,节点之间通过 Gossip 协议进行通信,维护集群的状态信息。客户端可以连接任意一个节点作为入口,集群内部会自动进行请求的路由。
1.2 数据分片 (Sharding)
Redis Cluster 的核心思想是将整个数据集划分为多个槽 (Slot),默认情况下有 16384 个槽 (0-16383)。每个主节点负责一部分槽,以及这些槽对应的数据。
数据分片的规则是基于键 (Key) 的哈希值。Redis Cluster 使用 CRC16 算法计算 Key 的哈希值,然后将哈希值对 16384 取模,得到 Key 所属的槽。
SLOT = CRC16(key) % 16384
通过这种方式,每个 Key 都会被映射到一个唯一的槽,并最终存储在负责该槽的主节点上。这种分片机制实现了数据的均匀分布,从而提高了整体的存储和性能容量。
数据写入是 Redis Cluster 最基本的操作之一。客户端发送写命令时,集群需要根据 Key 计算槽,并将请求路由到正确的节点。
2.1 写入流程概述
客户端连接任意节点: 客户端可以连接集群中的任意一个节点发起写请求。
节点接收请求并计算槽: 接收请求的节点(可能是任意节点)会根据 Key 计算其所属的槽。
槽定位与请求路由:
槽在本节点负责: 如果计算出的槽正好由当前节点负责,则直接在本节点执行写操作。
槽不在本节点负责: 如果槽不在当前节点负责,当前节点会返回 MOVED 重定向错误给客户端,告知客户端正确的节点地址和槽号。
客户端重定向请求: 客户端接收到 MOVED 错误后,需要根据错误信息中的新节点地址,重新连接到正确的节点,并再次发送写请求。
目标节点执行写操作: 正确的节点接收到请求后,执行写操作,并将数据同步到其从节点。
返回客户端响应: 写操作完成后,目标节点返回客户端操作结果。
2.2 MOVED 重定向错误详解
MOVED 错误是 Redis Cluster 中非常重要的重定向机制。当客户端请求的 Key 所在的槽不再由当前连接的节点负责时,Redis 会返回 MOVED 错误。
MOVED <slot> <ip:port>
<slot>: 请求 Key 所属的槽号。
<ip:port>: 负责该槽的新节点的 IP 地址和端口号。
客户端需要解析 MOVED 错误信息,并更新本地的路由表(槽与节点对应关系),然后重新连接到新的节点并发送请求。
2.3 代码实践:写入数据并处理 MOVED 重定向 (Python 示例)
我们使用 Python 的 redis-py-cluster 库来演示 Redis Cluster 的写入操作和 MOVED 重定向的处理。
首先,确保安装了 redis-py-cluster 库:
pip install redis-py-cluster
示例代码:写入字符串数据
from rediscluster import RedisCluster, ClusterError # 集群启动节点信息,可以只提供一个或几个 startup_nodes = [{"host": "127.0.0.1", "port": "7000"}, {"host": "127.0.0.1", "port": "7001"}, {"host": "127.0.0.1", "port": "7002"}] # 创建 RedisCluster 客户端 try: rc = RedisCluster(startup_nodes=startup_nodes, decode_responses=True) except ClusterError as e: print(f"连接集群失败: {e}") exit() key = "mykey" value = "Hello Redis Cluster" try: response = rc.set(key, value) print(f"写入结果: {response}") # 输出: True (表示写入成功) retrieved_value = rc.get(key) print(f"读取结果: {retrieved_value}") # 输出: Hello Redis Cluster except ClusterError as e: print(f"写入或读取数据失败: {e}")
代码解释:
startup_nodes: 定义集群的启动节点列表。客户端会从这些节点中发现整个集群的拓扑结构。
RedisCluster(...): 创建 RedisCluster 客户端实例。decode_responses=True 表示返回的字符串数据自动解码为 Python 字符串。
rc.set(key, value): 使用 set 命令写入数据。redis-py-cluster 库会自动处理槽计算和 MOVED 重定向,对用户透明。
rc.get(key): 使用 get 命令读取数据。同样,库会自动处理路由。
ClusterError 异常处理: 捕获 ClusterError 异常,处理集群连接或操作失败的情况。
redis-py-cluster 库的重定向处理:
redis-py-cluster 库在内部已经实现了 MOVED 重定向的处理逻辑。当客户端发送请求到错误的节点并收到 MOVED 错误时,库会自动:
解析 MOVED 错误信息,获取新的节点地址和槽号。
更新本地的槽与节点的映射关系缓存。
重新连接到新的节点。
再次发送原始请求到新的节点。
这个过程对用户是透明的,用户只需要像操作单机 Redis 一样使用 RedisCluster 客户端即可。
数据读取操作与写入操作类似,也需要经过槽计算和路由。但读取操作还涉及到从节点读取的策略,以提高读取性能和负载均衡。
3.1 读取流程概述
客户端连接任意节点: 客户端连接集群中的任意节点发起读请求。
节点接收请求并计算槽: 接收请求的节点根据 Key 计算槽。
槽定位与请求路由:
槽在本节点负责: 如果槽由当前节点负责,则在本节点执行读操作。
槽不在本节点负责: 返回 MOVED 重定向错误,流程与写入操作相同。
客户端重定向请求: 客户端根据 MOVED 错误信息重定向请求到正确的节点。
目标节点执行读操作: 正确的节点接收到读请求。
读取策略选择 (主节点或从节点):
默认情况: 默认情况下,读取操作通常在主节点上执行,以保证数据的一致性。
从节点读取 (Read Replica): 为了提高读取性能和分担主节点的读取压力,可以配置客户端从从节点读取数据。但需要注意,从节点读取可能存在数据延迟(最终一致性)。
返回客户端响应: 读取操作完成后,目标节点返回客户端数据。
3.2 从节点读取策略 (Read Replica)
Redis Cluster 允许客户端配置读取策略,可以从主节点读取,也可以从从节点读取。从从节点读取可以提高读取吞吐量,并减轻主节点的负载,但需要权衡数据一致性。
配置从节点读取:
不同的 Redis 客户端库提供了不同的方式来配置从节点读取。在 redis-py-cluster 中,可以通过 read_from_replicas 参数来启用从节点读取。
3.3 代码实践:读取数据并处理 MOVED 重定向 (Python 示例)
示例代码:读取字符串数据 (默认主节点读取)
前面写入数据的示例代码中,rc.get(key) 已经演示了基本的读取操作,默认情况下是从主节点读取。
示例代码:配置从节点读取 (Read Replica)
from rediscluster import RedisCluster, ClusterError # 集群启动节点信息 startup_nodes = [{"host": "127.0.0.1", "port": "7000"}, {"host": "127.0.0.1", "port": "7001"}, {"host": "127.0.0.1", "port": "7002"}] # 创建 RedisCluster 客户端,启用从节点读取 try: rc_replica = RedisCluster(startup_nodes=startup_nodes, decode_responses=True, read_from_replicas=True) except ClusterError as e: print(f"连接集群失败: {e}") exit() key = "mykey" try: retrieved_value_replica = rc_replica.get(key) print(f"从从节点读取结果: {retrieved_value_replica}") # 输出: Hello Redis Cluster (如果数据已同步到从节点) except ClusterError as e: print(f"从从节点读取数据失败: {e}")
代码解释:
read_from_replicas=True: 在创建 RedisCluster 客户端时,设置 read_from_replicas=True 参数,启用从节点读取。
rc_replica.get(key): 使用启用了从节点读取的客户端进行读取操作。
从节点读取的注意事项:
数据延迟: 从节点的数据复制是异步的,可能存在一定的延迟。因此,从节点读取的数据可能不是最新的,存在最终一致性的问题。
节点选择: 当启用从节点读取时,客户端库通常会从可用的从节点中随机选择一个进行读取。
适用场景: 从节点读取适用于对数据一致性要求不高,但对读取性能和吞吐量要求较高的场景,例如:
缓存场景
报表统计
非核心业务的读取操作
ASK 重定向错误详解与处理除了 MOVED 重定向错误,Redis Cluster 还有一种 ASK 重定向错误。ASK 错误发生在槽迁移 (Slot Migration) 过程中。
4.1 槽迁移 (Slot Migration)
当需要扩容或缩容 Redis Cluster 集群时,需要进行槽迁移。槽迁移是指将某些槽从一个节点迁移到另一个节点。在迁移过程中,槽的所有权会发生短暂的转移。
4.2 ASK 重定向错误
在槽迁移过程中,客户端请求的 Key 可能正好处在迁移的槽中。这时,源节点 (Source Node,迁移前的节点) 会返回 ASK 重定向错误给客户端。
ASK <slot> <ip:port>
<slot>: 请求 Key 所属的槽号。
<ip:port>: 目标节点 (Target Node,迁移后的节点) 的 IP 地址和端口号。
ASK 错误表示槽的迁移正在进行中,客户端应该临时将请求发送到 ASK 错误指定的节点,但不需要更新本地的路由表。
4.3 ASKING 命令
客户端在收到 ASK 错误后,需要先向目标节点发送 ASKING 命令,然后再发送原始的请求命令。ASKING 命令的作用是告知目标节点,客户端已经收到了 ASK 重定向,并准备接收属于迁移槽的请求。
4.4 代码实践:处理 ASK 重定向 (Python 示例 - 模拟 ASK 场景)
redis-py-cluster 库通常也会自动处理 ASK 重定向,但为了演示 ASK 的处理过程,我们可以手动模拟一个 ASK 场景 (实际生产环境中 ASK 错误相对较少出现)。
模拟 ASK 场景 (简化模拟,实际槽迁移过程更复杂):
假设我们手动将某个槽 (例如槽 1000) 从节点 A 迁移到节点 B。在迁移过程中,当客户端请求属于槽 1000 的 Key 时,节点 A 可能会返回 ASK 重定向到节点 B。
示例代码 (手动处理 ASK,redis-py-cluster 库已自动处理):
import redis # 假设节点 A 的连接信息 node_a_host = "127.0.0.1" node_a_port = 7000 rc_a = redis.Redis(host=node_a_host, port=node_a_port, decode_responses=True) # 假设节点 B 的连接信息 node_b_host = "127.0.0.1" node_b_port = 7001 rc_b = redis.Redis(host=node_b_host, port=node_b_port, decode_responses=True) key_in_slot_1000 = "key_slot_1000" # 假设这个 key 属于槽 1000 try: response_from_a = rc_a.get(key_in_slot_1000) print(f"从节点 A 读取结果: {response_from_a}") # 假设节点 A 返回 ASK 错误 except redis.exceptions.ResponseError as e: error_message = str(e) if "ASK" in error_message: parts = error_message.split(" ") ask_slot = parts[1] ask_node_address = parts[2] ask_node_host, ask_node_port = ask_node_address.split(":") print(f"收到 ASK 重定向错误,槽: {ask_slot}, 节点: {ask_node_address}") # 手动处理 ASK 重定向 rc_ask_node = redis.Redis(host=ask_node_host, port=ask_node_port, decode_responses=True) rc_ask_node.execute_command("ASKING") # 发送 ASKING 命令 value_from_ask_node = rc_ask_node.get(key_in_slot_1000) # 再次发送原始请求 print(f"从 ASK 节点读取结果: {value_from_ask_node}") else: print(f"从节点 A 读取数据失败: {e}")
代码解释 (手动处理 ASK 的逻辑):
捕获 ResponseError: 当节点 A 返回 ASK 错误时,redis-py 库会抛出 ResponseError 异常。
解析 ASK 错误信息: 解析错误信息,提取槽号和目标节点地址。
连接到 ASK 节点: 根据 ASK 错误信息,创建连接到目标节点的 Redis 客户端实例 (rc_ask_node).
发送 ASKING 命令: 使用 rc_ask_node.execute_command("ASKING") 发送 ASKING 命令到目标节点。
重新发送原始请求: 再次使用 rc_ask_node.get(key_in_slot_1000) 发送原始的 get 请求。
redis-py-cluster 库的 ASK 处理:
与 MOVED 类似,redis-py-cluster 库也已经内置了 ASK 重定向的处理逻辑。当收到 ASK 错误时,库会自动:
解析 ASK 错误信息,获取目标节点地址。
连接到目标节点。
发送 ASKING 命令。
重新发送原始请求。
用户无需手动处理 ASK 重定向,redis-py-cluster 会透明地完成这些步骤。
Redis Cluster 对跨槽操作 (Multi-Slot Operations) 有限制。如果一个命令需要操作多个 Key,而这些 Key 分布在不同的槽中,Redis Cluster 通常不支持直接执行该命令。
5.1 跨槽操作的限制
大多数 Redis 命令都是单 Key 操作,例如 GET, SET, HGET, HSET 等。这些命令在 Redis Cluster 中可以正常使用,因为它们只操作一个 Key,集群可以根据 Key 路由到正确的节点。
但是,一些命令需要操作多个 Key,例如 MGET, MSET, DEL, SUNION, SINTER 等。如果这些命令操作的 Key 分布在不同的槽中,Redis Cluster 默认情况下会返回错误 CROSSSLOT。
错误信息: CROSSSLOT Keys in request don't hash to the same slot
5.2 解决方案:Hash Tags
为了解决跨槽操作的限制,Redis Cluster 提供了 Hash Tags 功能。Hash Tags 允许将 Key 的一部分用 {} 包裹起来,Redis Cluster 在计算槽时,只会使用 {} 中的内容进行哈希计算。
Hash Tag 的规则:
如果 Key 中包含 {}, 则只对第一个 { 和第一个 } 之间的字符串进行哈希计算。
如果 Key 中不包含 {}, 则对整个 Key 进行哈希计算。
示例:使用 Hash Tags 将多个 Key 强制分配到同一个槽
假设我们需要批量操作以下几个 Key:
user:1000:name
user:1000:age
user:1000:address
我们可以使用 Hash Tags 将它们都分配到同一个槽,例如:
user:{1000}:name
user:{1000}:age
user:{1000}:address
这样,Redis Cluster 在计算槽时,只会对 {1000} 进行哈希计算,这三个 Key 就会被分配到同一个槽,从而可以使用 MGET, MSET 等多 Key 命令进行批量操作。
5.3 代码实践:使用 Hash Tags 进行跨槽操作 (Python 示例)
from rediscluster import RedisCluster, ClusterError # 集群启动节点信息 startup_nodes = [{"host": "127.0.0.1", "port": "7000"}, {"host": "127.0.0.1", "port": "7001"}, {"host": "127.0.0.1", "port": "7002"}] # 创建 RedisCluster 客户端 try: rc = RedisCluster(startup_nodes=startup_nodes, decode_responses=True) except ClusterError as e: print(f"连接集群失败: {e}") exit() # 使用 Hash Tags 的 Keys keys_with_tag = ["user:{1000}:name", "user:{1000}:age", "user:{1000}:address"] values = ["Alice", "30", "New York"] # MSET 操作 (使用 Hash Tags 确保 Keys 在同一槽) try: mapping = dict(zip(keys_with_tag, values)) response_mset = rc.mset(mapping) print(f"MSET 结果: {response_mset}") # 输出: True # MGET 操作 retrieved_values_mget = rc.mget(keys_with_tag) print(f"MGET 结果: {retrieved_values_mget}") # 输出: ['Alice', '30', 'New York'] except ClusterError as e: print(f"MSET 或 MGET 操作失败: {e}")
代码解释:
keys_with_tag: 定义使用了 Hash Tags 的 Key 列表。
rc.mset(mapping): 使用 mset 命令批量设置多个 Key-Value 对。由于使用了 Hash Tags,这些 Key 会被分配到同一个槽,mset 操作可以成功执行。
rc.mget(keys_with_tag): 使用 mget 命令批量获取多个 Key 的值。同样,由于 Hash Tags 的作用,mget 操作可以成功执行。
Hash Tags 的应用场景:
关联性数据操作: 当需要原子性地操作一组关联性较强的数据时,可以使用 Hash Tags 将这些数据分配到同一个槽,方便使用事务或批量操作。
性能优化: 避免跨槽操作可以提高性能,减少网络开销和重定向次数。
5.4 应用层聚合:跨槽操作的另一种方案
除了 Hash Tags,另一种处理跨槽操作的方法是在应用层进行聚合。
拆分操作: 将跨槽的多 Key 操作拆分成多个单 Key 操作。
应用层聚合: 在应用层分别从不同的节点获取数据,然后在应用层进行聚合处理。
示例:应用层聚合实现 MGET 的效果 (简化示例):
def mget_cross_slot(rc_cluster, keys): """ 应用层聚合实现跨槽 MGET 的效果 (简化示例,未处理所有异常情况) """ results = {} for key in keys: try: value = rc_cluster.get(key) results[key] = value except ClusterError as e: print(f"获取 Key {key} 失败: {e}") results[key] = None # 或者抛出异常,根据实际需求处理 return [results.get(key) for key in keys] # 返回与输入 keys 顺序一致的结果列表 # ... (创建 RedisCluster 客户端 rc) ... keys_cross_slot = ["key1", "key2", "key3"] # 假设这些 keys 分布在不同槽 retrieved_values_app_level = mget_cross_slot(rc, keys_cross_slot) print(f"应用层聚合 MGET 结果: {retrieved_values_app_level}")
应用层聚合的优缺点:
优点:
灵活性高,可以处理更复杂的跨槽场景。
不需要使用 Hash Tags,Key 的命名可以更自由。
缺点:
性能相对较低,需要多次网络请求。
需要在应用层处理更多的逻辑,包括错误处理和数据聚合。
选择 Hash Tags 还是应用层聚合,需要根据具体的应用场景和需求进行权衡。 如果关联性数据操作较多,且性能要求较高,Hash Tags 是一个更好的选择。如果跨槽操作场景复杂,或者希望保持 Key 命名的灵活性,应用层聚合可能更适合。
本文详细介绍了 Redis Cluster 的数据读写机制,包括写入流程、读取流程、MOVED 和 ASK 重定向错误的处理,以及跨槽操作的限制与解决方案。
总结关键点:
数据分片与槽: Redis Cluster 使用槽 (Slot) 进行数据分片,Key 通过哈希映射到槽,槽分配给不同的主节点。
MOVED 重定向: 当请求的槽不在当前节点负责时,返回 MOVED 错误,客户端需要更新路由表并重定向请求。
ASK 重定向: 槽迁移过程中,可能返回 ASK 错误,客户端需要临时将请求发送到 ASK 节点,并发送 ASKING 命令。
从节点读取: 可以配置客户端从从节点读取数据,提高读取性能,但需要注意数据延迟。
跨槽操作限制: Redis Cluster 默认不支持跨槽操作。
Hash Tags: 使用 Hash Tags 可以将多个 Key 强制分配到同一个槽,解决跨槽操作的限制。
应用层聚合: 另一种处理跨槽操作的方法是在应用层进行聚合。
最佳实践建议:
使用 Cluster-aware 客户端: 选择支持 Redis Cluster 的客户端库 (例如 redis-py-cluster),这些库已经内置了重定向处理、槽管理等功能,简化了开发工作。
合理使用 Hash Tags: 对于需要批量操作或事务操作的关联性数据,合理使用 Hash Tags 将它们分配到同一个槽。
权衡从节点读取: 在对数据一致性要求不高,但对读取性能要求较高的场景,可以考虑启用从节点读取,但要充分测试和监控数据延迟情况。
监控集群状态: 监控 Redis Cluster 的状态,包括节点健康状况、槽分配情况、重定向错误率等,及时发现和解决问题。
理解数据一致性: Redis Cluster 提供了最终一致性,在某些情况下 (例如网络分区、节点故障),可能会出现数据延迟或短暂的不一致性。在设计应用时,需要充分考虑数据一致性的影响。
通过深入理解 Redis Cluster 的数据读写机制,并结合代码实践,相信读者可以更好地应用 Redis Cluster 构建高可用、高性能的分布式 Redis 服务。希望本文能够对您有所帮助!