使用 Redis 集群需要 Redis 服务器版本在 3.0 或以上。

在使用 Redis 集群时不要依赖键空间事件,因为键空间事件不会在各个实例节点之间复制。Pub/Sub 订阅的是一个随机的集群节点,该节点仅接收来自单个节点的键空间事件。使用单节点 Redis 以避免键空间事件丢失。

一、RedisClusterConnection

Redis 集群的行为与单节点 Redis 或者由 Sentinel 监控的主从环境有所不同。这是因为自动分片将一个键映射到 16384 个槽位之一,并且这些槽位分布在不同的节点上。因此,涉及多个键的命令必须确保所有键都映射到完全相同的槽位,以避免跨槽错误。单个集群节点只服务于一组特定的键。针对特定服务器发出的命令只会返回该服务器所服务的那些键的结果。作为一个简单的例子,考虑 KEYS 命令。当在一个集群环境中向一个服务器发出此命令时,它只会返回请求发送到的节点所服务的那些键,并不一定返回集群内的所有键。因此,要在集群环境中获取所有键,你必须从所有已知的主节点读取键。

示例如下,集群中有多条键中包含 ”string“ 的数据,但是使用 KEYS 命令只能获取到其中一条数据:

1
2
$ keys *
1) "stringKey5"

虽然特定键到对应槽位服务节点的重定向由驱动库处理,但是更高层次的功能,如跨节点收集信息或将命令发送到集群中的所有节点,则由 RedisClusterConnection 负责。以前面提到的键的例子来说,这意味着 keys(pattern) 方法会选取集群中的每一个主节点,并同时在每个主节点上运行 KEYS 命令,同时收集结果并返回累积的键集。若只是请求单个节点的键,RedisClusterConnection 为这些方法提供了重载(例如,keys(node, pattern))。

RedisClusterNode 可以通过 RedisClusterConnection.clusterGetNodes 获得,也可以通过使用主机名和端口或节点 ID 来构造。

下面的示例中展示了一系列运行在集群之间的命令:

1
2
3
4
5
6
7
8
9
10
$ cluster nodes
ab805fa1f55fe0549e8d272e3110637c73b88515 192.168.0.103:6323@6333 slave df6bbda2507ce155372cd1caddede17d5582ef00 0 1729696654000 3 connected
2a65a45426b81942990d75421452c6c898ddc917 192.168.0.103:6328@6338 slave df6bbda2507ce155372cd1caddede17d5582ef00 0 1729696652000 3 connected
99d5f5b8aff69dfe76bb94d98892fcb9c3d8cb56 192.168.0.103:6326@6336 slave 2c06746a9232fdb7c511f9b0a69cb5bf36566340 0 1729696652000 1 connected
d026b2a89a3bc70b9f62a1baad14cb74329249da 192.168.0.103:6324@6334 master - 0 1729696652000 10 connected 5461-10922
df6bbda2507ce155372cd1caddede17d5582ef00 192.168.0.103:6322@6332 master - 0 1729696655529 3 connected 10923-16383
23398a39d93fa4577f69b9e878934de89cd74d0f 192.168.0.103:6327@6337 slave d026b2a89a3bc70b9f62a1baad14cb74329249da 0 1729696652414 10 connected
13bacc3258fff844c3b34d71b7d2849509ba8e33 192.168.0.103:6325@6335 slave 2c06746a9232fdb7c511f9b0a69cb5bf36566340 0 1729696654495 1 connected
2c06746a9232fdb7c511f9b0a69cb5bf36566340 192.168.0.103:6320@6330 myself,master - 0 0 1 connected 0-5460
7de54d48cf6815be86f99afd8e963b14a3a9fddb 192.168.0.103:6321@6331 slave d026b2a89a3bc70b9f62a1baad14cb74329249da 0 1729696653456 10 connected

上面的集群中有 3 个主节点,一个副本节点,3 个主节点分别获取 16384 个槽位中的 0-54605461-1092210923-16383 个槽。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Disabled  
@Slf4j
@SpringBootTest(classes = SpringDataRedisSpringBootApplication.class)
public class RedisClusterConnectionTest {

@Autowired
private RedisConnectionFactory redisConnectionFactory;

@Test
public void test() {
RedisClusterConnection connection = redisConnectionFactory.getClusterConnection();

connection.set("thing1".getBytes(StandardCharsets.UTF_8), "value1".getBytes(StandardCharsets.UTF_8));
connection.set("thing2".getBytes(StandardCharsets.UTF_8), "value2".getBytes(StandardCharsets.UTF_8));

log.info("直接调用 connection.keys(pattern) 方法");
Set<byte[]> keys = connection.keys("*".getBytes(StandardCharsets.UTF_8));
log.info("keys 数量 = {}", keys.size());
for (byte[] key : keys) {
log.info("key: {}", new String(key));
}

log.info("调用各个节点的 connection.keys(node, pattern) 方法");
for (RedisClusterNode clusterGetNode : connection.clusterGetNodes()) {
int[] slotsArray = clusterGetNode.getSlotRange().getSlotsArray();
log.info("获取节点 {} 的 keys,该节点分配的槽的最大槽为 {}", clusterGetNode, slotsArray.length > 0 ? slotsArray[slotsArray.length - 1] : 0);

Set<byte[]> keys2 = connection.keys(clusterGetNode, "*".getBytes(StandardCharsets.UTF_8));
log.info("keys 数量 = {}", keys2.size());
for (byte[] key : keys2) {
log.info("key: {}", new String(key));
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
2024-10-23T23:42:41.640+08:00  INFO 28497 --- : 直接调用 connection.keys(pattern) 方法
2024-10-23T23:42:41.642+08:00 INFO 28497 --- : key: thing1
2024-10-23T23:42:41.642+08:00 INFO 28497 --- : key: EOSZPMiHuAMeoZCO
2024-10-23T23:42:41.642+08:00 INFO 28497 --- : key: stringKey4
2024-10-23T23:42:41.642+08:00 INFO 28497 --- : key: stringKey3
2024-10-23T23:42:41.642+08:00 INFO 28497 --- : key: stringKey6
2024-10-23T23:42:41.642+08:00 INFO 28497 --- : key: thing2
2024-10-23T23:42:41.642+08:00 INFO 28497 --- : key: umdCsrIDuuXxqCjT
2024-10-23T23:42:41.642+08:00 INFO 28497 --- : key: stringKey2
2024-10-23T23:42:41.642+08:00 INFO 28497 --- : key: stringKey
2024-10-23T23:42:41.642+08:00 INFO 28497 --- : key: stringKey5
2024-10-23T23:42:41.642+08:00 INFO 28497 --- : 调用各个节点的 connection.keys(node, pattern) 方法
2024-10-23T23:42:41.649+08:00 INFO 28497 --- : 获取节点 192.168.0.103:6320 的 keys,该节点分配的槽的最大槽为 5460
2024-10-23T23:42:41.650+08:00 INFO 28497 --- : key: thing1
2024-10-23T23:42:41.651+08:00 INFO 28497 --- : key: stringKey5
2024-10-23T23:42:41.651+08:00 INFO 28497 --- : 获取节点 192.168.0.103:6323 的 keys,该节点分配的槽的最大槽为 0
2024-10-23T23:42:41.651+08:00 INFO 28497 --- : key: thing2
2024-10-23T23:42:41.651+08:00 INFO 28497 --- : key: stringKey3
2024-10-23T23:42:41.651+08:00 INFO 28497 --- : key: stringKey2
2024-10-23T23:42:41.651+08:00 INFO 28497 --- : key: stringKey6
2024-10-23T23:42:41.651+08:00 INFO 28497 --- : 获取节点 192.168.0.103:6327 的 keys,该节点分配的槽的最大槽为 0
2024-10-23T23:42:41.652+08:00 INFO 28497 --- : key: stringKey
2024-10-23T23:42:41.652+08:00 INFO 28497 --- : key: stringKey4
2024-10-23T23:42:41.652+08:00 INFO 28497 --- : key: EOSZPMiHuAMeoZCO
2024-10-23T23:42:41.652+08:00 INFO 28497 --- : key: umdCsrIDuuXxqCjT
2024-10-23T23:42:41.652+08:00 INFO 28497 --- : 获取节点 192.168.0.103:6328 的 keys,该节点分配的槽的最大槽为 0
2024-10-23T23:42:41.653+08:00 INFO 28497 --- : key: stringKey3
2024-10-23T23:42:41.653+08:00 INFO 28497 --- : key: stringKey2
2024-10-23T23:42:41.653+08:00 INFO 28497 --- : key: thing2
2024-10-23T23:42:41.653+08:00 INFO 28497 --- : key: stringKey6
2024-10-23T23:42:41.653+08:00 INFO 28497 --- : 获取节点 192.168.0.103:6326 的 keys,该节点分配的槽的最大槽为 0
2024-10-23T23:42:41.653+08:00 INFO 28497 --- : key: thing1
2024-10-23T23:42:41.653+08:00 INFO 28497 --- : key: stringKey5
2024-10-23T23:42:41.653+08:00 INFO 28497 --- : 获取节点 192.168.0.103:6321 的 keys,该节点分配的槽的最大槽为 0
2024-10-23T23:42:41.654+08:00 INFO 28497 --- : key: umdCsrIDuuXxqCjT
2024-10-23T23:42:41.654+08:00 INFO 28497 --- : key: EOSZPMiHuAMeoZCO
2024-10-23T23:42:41.654+08:00 INFO 28497 --- : key: stringKey4
2024-10-23T23:42:41.654+08:00 INFO 28497 --- : key: stringKey
2024-10-23T23:42:41.654+08:00 INFO 28497 --- : 获取节点 192.168.0.103:6324 的 keys,该节点分配的槽的最大槽为 10922
2024-10-23T23:42:41.655+08:00 INFO 28497 --- : key: stringKey4
2024-10-23T23:42:41.655+08:00 INFO 28497 --- : key: stringKey
2024-10-23T23:42:41.655+08:00 INFO 28497 --- : key: EOSZPMiHuAMeoZCO
2024-10-23T23:42:41.655+08:00 INFO 28497 --- : key: umdCsrIDuuXxqCjT
2024-10-23T23:42:41.655+08:00 INFO 28497 --- : 获取节点 192.168.0.103:6325 的 keys,该节点分配的槽的最大槽为 0
2024-10-23T23:42:41.655+08:00 INFO 28497 --- : key: thing1
2024-10-23T23:42:41.655+08:00 INFO 28497 --- : key: stringKey5
2024-10-23T23:42:41.656+08:00 INFO 28497 --- : 获取节点 192.168.0.103:6322 的 keys,该节点分配的槽的最大槽为 16383
2024-10-23T23:42:41.656+08:00 INFO 28497 --- : key: stringKey3
2024-10-23T23:42:41.656+08:00 INFO 28497 --- : key: stringKey6
2024-10-23T23:42:41.656+08:00 INFO 28497 --- : key: thing2
2024-10-23T23:42:41.656+08:00 INFO 28497 --- : key: stringKey2

当所有键都映射到同一个槽位时,原生驱动库会自动处理跨槽请求,例如 MGET。然而,一旦这种情况不再成立,RedisClusterConnection 会并行地在多个槽位服务节点上执行多个 GET 命令,并再次返回累积的结果。这种方法的性能低于单槽位方法,因此应谨慎使用。如果有疑问,可以考虑通过在花括号中提供前缀来将键固定到同一个槽位,例如 {my-prefix}.thing1{my-prefix}.thing2,它们都会映射到相同的槽位编号。下面的示例显示了跨槽请求处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Disabled  
@Slf4j
@SpringBootTest(classes = SpringDataRedisSpringBootApplication.class)
public class RedisClusterConnectionTest2 {

@Autowired
private RedisConnectionFactory redisConnectionFactory;

@Test
public void test() {
RedisClusterConnection connection = redisConnectionFactory.getClusterConnection();

connection.set("thing1".getBytes(StandardCharsets.UTF_8), "value1".getBytes(StandardCharsets.UTF_8));
connection.set("{thing1}.thing2".getBytes(StandardCharsets.UTF_8), "value12".getBytes(StandardCharsets.UTF_8));
connection.set("thing2".getBytes(StandardCharsets.UTF_8), "value2".getBytes(StandardCharsets.UTF_8));

List<byte[]> thingResult = connection.mGet("thing1".getBytes(StandardCharsets.UTF_8), "{thing1}.thing2".getBytes(StandardCharsets.UTF_8));
System.out.println(thingResult.stream().map(String::new).toList());

List<byte[]> thingResult2 = connection.mGet("thing1".getBytes(StandardCharsets.UTF_8), "thing2".getBytes(StandardCharsets.UTF_8));
System.out.println(thingResult2.stream().map(String::new).toList());
}
}
1
2
[value1, value12]
[value1, value2]

前面的示例展示了 Spring Data Redis 遵循的一般策略。请注意,某些操作可能需要将大量数据加载到内存中以计算所需的命令。此外,并非所有跨槽请求都可以安全地转换为多个单槽请求,如果误用(例如 PFCOUNT),这些请求会报错。

二、集群中使用 RedisTemplate 和 ClusterOperations

在使用任何 JSON Redis 序列化器设置 RedisTemplate#keySerializer 时要小心,因为改变 JSON 结构会立即影响哈希槽位的计算。

RedisTemplate 通过 ClusterOperations 接口提供对集群特定操作的访问,该接口可以通过 RedisTemplate.opsForCluster() 获得。这允许你在集群中的单个节点上显式运行命令,同时保留模板配置的序列化和反序列化功能。它还提供了管理命令(如 CLUSTER MEET)或更高层次的操作(例如,重新分片)。

下面的示例显示了如何使用 RedisTemplate 访问 RedisClusterConnection

1
2
ClusterOperations clusterOps = redisTemplate.opsForCluster();
clusterOps.shutdown(NODE_7379);

目前,Redis 集群管道(pipelining)仅通过 Lettuce 驱动支持,但以下命令在使用跨槽键时除外:rename, renameNX, sort, bLPop, bRPop, rPopLPush, bRPopLPush, info, sMove, sInter, sInterStore, sUnion, sUnionStore, sDiff, sDiffStore。同槽键则完全支持。

相关链接

Scale with Redis Cluster | redis.io

[[KEYS]]

OB tags

#Redis #Spring