Redis集群架构:语法知识点、使用方法与综合案例
本章将深入探讨Redis的三种主要架构模式:主从复制(Master-Slave) 、哨兵机制(Sentinel)以及分片集群(Cluster) 。我们将重点讲解如何使用 Lettuce (Spring Data Redis的默认客户端)和 Spring Data Redis 进行整合,并涵盖节点的动态扩缩容及代理层(Predixy)的配置。
1. Redis 主从架构 (Master-Slave)
1.1 核心概念
- 原理:一个主节点(Master)负责写操作,多个从节点(Slave)负责读操作和数据备份。数据单向同步(Master -> Slave)。
- 特点:配置简单,但主节点故障需要人工干预切换,不具备自动高可用。
1.2 配置文件 (redis.conf)
Master 节点 : 无需特殊配置(默认即为Master)。
Slave 节点:
conf
# 指定主节点地址
replicaof 192.168.1.100 6379
# 或者旧版本写法
# slaveof 192.168.1.100 6379
# 设置只读(默认就是只读,防止误写)
replica-read-only yes
1.3 Lettuce 整合主从架构
Lettuce 原生支持读写分离,通过 RedisClient 连接主节点,利用 StatefulRedisConnection 的 readOnly() 方法切换到从节点读取。
案例代码:Lettuce 手动读写分离
java
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.protocol.ReadOnlyCommands;
public class LettuceMasterSlaveExample {
public static void main(String[] args) {
// 1. 创建客户端,通常连接主节点地址(也可以连接任意节点,Lettuce会自动发现拓扑,但在纯主从模式下建议连主)
RedisClient client = RedisClient.create("redis://192.168.1.100:6379");
// 2. 获取有状态连接
StatefulRedisConnection<String, String> connection = client.connect();
// --- 写操作 (必须在主节点) ---
RedisCommands<String, String> syncCommands = connection.sync();
syncCommands.set("user:1001", "ZhangSan");
System.out.println("写入成功 (Master)");
// --- 读操作 (切换到从节点) ---
// 开启只读模式,后续命令会路由到从节点(如果客户端配置了多节点,Lettuce会自动负载均衡)
connection.readOnly();
String value = syncCommands.get("user:1001");
System.out.println("读取结果 (Slave): " + value);
// 关闭连接
connection.close();
client.shutdown();
}
}
1.4 Spring Data Redis 整合主从架构
在Spring Boot中,主要通过配置 application.yml 指定主节点,并在代码中使用 @Readonly 注解或编程式设置实现读写分离。
配置文件 (application.yml)
yaml
spring:
data:
redis:
host: 192.168.1.100 # 主节点地址
port: 6379
password: your_password
# 注意:Spring Data Redis 默认连接主节点。
# 若要自动读写分离,通常需要配置多个节点或使用自定义配置类定义 LoadBalancer
lettuce:
pool:
max-active: 8
案例代码:Spring Data Redis 读写分离
java
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.ReadFrom; // 关键类
// 自定义配置类示例(启用读写分离策略)
@Configuration
public class RedisConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName("192.168.1.100");
config.setPort(6379);
LettuceConnectionFactory factory = new LettuceConnectionFactory(config);
// 【关键点】设置读取策略:从从节点读取
// ReadFrom.REPLICA_PREFERRED: 优先从从节点读,如果没有则从主节点读
factory.setReadFrom(ReadFrom.REPLICA_PREFERRED);
return factory;
}
// 使用模板
@Autowired
private StringRedisTemplate redisTemplate;
public void testReadWrite() {
// 写操作 (自动路由到 Master)
redisTemplate.opsForValue().set("order:1001", "Pending");
// 读操作 (根据策略自动路由到 Slave)
String status = redisTemplate.opsForValue().get("order:1001");
System.out.println("订单状态: " + status);
}
}
2. Redis 哨兵机制 (Sentinel)
2.1 核心概念
- 原理:哨兵进程监控主从节点状态。当主节点宕机,哨兵集群投票选举新的主节点,并通知从节点切换。
- 特点 :提供高可用(HA),自动故障转移。客户端连接的是哨兵地址,由哨兵告知当前主节点地址。
2.2 配置 Redis 哨兵集群 (sentinel.conf)
需要启动至少3个哨兵实例以达成多数派共识。
conf
# 监控的主节点名称、IP、端口、法定人数(最少几个哨兵同意才切换)
sentinel monitor mymaster 192.168.1.100 6379 2
# 主节点密码
sentinel auth-pass mymaster your_password
# 判定主观下线时间 (毫秒)
sentinel down-after-milliseconds mymaster 5000
# 故障转移超时时间
sentinel failover-timeout mymaster 10000
# 并行同步的从节点数量
sentinel parallel-syncs mymaster 1
2.3 Lettuce 整合哨兵
Lettuce 通过 RedisSentinelClient 连接哨兵,自动感知主节点变化。
案例代码:Lettuce 连接哨兵
java
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.sentinel.api.StatefulRedisSentinelConnection;
import io.lettuce.core.sentinel.api.sync.RedisSentinelCommands;
import java.util.Arrays;
public class LettuceSentinelExample {
public static void main(String[] args) {
// 1. 构建哨兵地址列表
RedisURI sentinelUri = RedisURI.builder()
.withSentinel("192.168.1.201", 26379) // 哨兵1
.withSentinel("192.168.1.202", 26379) // 哨兵2
.withSentinel("192.168.1.203", 26379) // 哨兵3
.withSentinelMasterId("mymaster") // 监控的主节点名称
.withPassword("your_password")
.build();
// 2. 创建客户端 (连接到哨兵)
RedisClient client = RedisClient.create(sentinelUri);
// 3. 获取连接 (客户端会自动从哨兵获取当前Master地址并连接)
StatefulRedisConnection<String, String> connection = client.connect();
RedisCommands<String, String> commands = connection.sync();
// 4. 执行操作 (无需关心当前谁是Master,自动处理)
commands.set("session:id", "abc-123");
System.out.println("写入成功,当前主节点由哨兵自动定位");
String val = commands.get("session:id");
System.out.println("读取值: " + val);
connection.close();
client.shutdown();
}
}
2.4 Spring Data Redis 整合哨兵
这是生产环境最常用的配置方式。
配置文件 (application.yml)
yaml
spring:
data:
redis:
sentinel:
master: mymaster # 必须与 sentinel.conf 中的 monitor 名称一致
nodes:
- 192.168.1.201:26379
- 192.168.1.202:26379
- 192.168.1.203:26379
password: your_password
lettuce:
pool:
max-active: 10
# 可选:读取策略
read-from: replica_preferred
案例代码:Spring Boot 自动重连测试
java
@Service
public class SentinelService {
@Autowired
private StringRedisTemplate redisTemplate;
public void saveData(String key, String value) {
// 即使发生主从切换,这里也不需要修改代码
// Lettuce 会收到哨兵通知并重新连接新的主节点
try {
redisTemplate.opsForValue().set(key, value);
System.out.println("数据保存成功: " + key);
} catch (Exception e) {
System.err.println("写入失败,可能正在故障转移中: " + e.getMessage());
// 实际生产中应增加重试机制
}
}
public String getData(String key) {
return redisTemplate.opsForValue().get(key);
}
}
3. Redis Cluster (分片集群)
3.1 核心概念
- 原理:去中心化,数据分片存储在16384个槽(Slots)中。每个节点负责一部分槽。
- 特点:线性扩展,高可用(每个分片也是主从结构)。
- 通信:节点间通过 Gossip 协议通信。
3.2 配置 Redis Cluster 服务
每个节点 redis.conf 需开启集群模式:
conf
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 5000
初始化集群命令 (使用 redis-cli):
bash
# 创建3主3从集群
redis-cli --cluster create \
192.168.1.101:6379 192.168.1.102:6379 192.168.1.103:6379 \
192.168.1.104:6379 192.168.1.105:6379 192.168.1.106:6379 \
--cluster-replicas 1 -a your_password --cluster-yes
3.3 Lettuce 整合 Redis Cluster
Lettuce 对 Cluster 支持非常好,会自动处理 MOVED 和 ASK 重定向。
案例代码:Lettuce Cluster 操作
java
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.cluster.RedisClusterClient;
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
import io.lettuce.core.cluster.api.sync.RedisClusterCommands;
import java.util.Arrays;
public class LettuceClusterExample {
public static void main(String[] args) {
// 1. 只需提供一个种子节点地址,客户端会自动发现整个集群拓扑
RedisURI uri = RedisURI.create("192.168.1.101", 6379);
// 如果有密码
// uri.setPassword("your_password");
// 2. 创建 Cluster 客户端
RedisClusterClient clusterClient = RedisClusterClient.create(uri);
// 3. 获取连接
StatefulRedisClusterConnection<String, String> connection = clusterClient.connect();
RedisClusterCommands<String, String> commands = connection.sync();
// 4. 操作数据
// Lettuce 会自动计算 key 的 hash slot 并路由到正确的节点
commands.set("cluster:key1", "value1");
commands.set("cluster:key2", "value2");
// 批量操作 (注意:MGET/MSET 在集群模式下要求所有 key 必须在同一个 slot,否则报错)
// 解决方案:使用 {} 包裹哈希标签,强制路由到同一节点
commands.mset(Map.of("{user}:1001:name", "Alice", "{user}:1001:age", "25"));
System.out.println("Cluster 操作完成");
connection.close();
clusterClient.shutdown();
}
}
3.4 Spring Data Redis 整合 Redis Cluster
配置文件 (application.yml)
yaml
spring:
data:
redis:
cluster:
nodes:
- 192.168.1.101:6379
- 192.168.1.102:6379
- 192.168.1.103:6379
- 192.168.1.104:6379
- 192.168.1.105:6379
- 192.168.1.106:6379
max-redirects: 3 # 最大重定向次数
password: your_password
lettuce:
pool:
enabled: true
cluster:
refresh:
adaptive: true # 开启自适应拓扑刷新(重要!节点变动时自动更新路由)
period: 60000 # 定期刷新间隔
案例代码:Spring Data Redis Cluster 注意事项
java
@Service
public class ClusterService {
@Autowired
private StringRedisTemplate redisTemplate;
public void saveUserData(String userId, String name, String age) {
// 【关键点】在集群模式下,涉及多个Key的操作(如 mset, transaction, lua脚本)
// 必须保证这些Key落在同一个 Slot 上。
// 使用 Hash Tag: {userId}
String nameKey = "{" + userId + "}:name";
String ageKey = "{" + userId + "}:age";
Map<String, String> map = new HashMap<>();
map.put(nameKey, name);
map.put(ageKey, age);
// 这样就能成功执行 mset,因为它们被路由到了同一个节点
redisTemplate.opsForValue().multiSet(map);
}
public void nonCompliantOperation() {
// 错误示范:以下代码在集群模式下会抛出 CrossSlotException
// redisTemplate.opsForValue().multiSet(Map.of("key1", "v1", "key2", "v2"));
// 因为 key1 和 key2 很可能不在同一个槽
}
}
3.5 动态追加/删除 Redis Cluster 数据节点
此操作通常通过命令行 redis-cli 完成,但也可以通过 Lettuce 编程实现(较少见,通常由运维脚本执行)。
场景 A:动态追加节点 (扩容)
- 启动新节点(配置好
cluster-enabled yes)。 - 将新节点加入集群。
- 分配槽(Slots)给新节点。
- 数据迁移。
命令行操作案例:
bash
# 1. 将新节点 192.168.1.200:6379 加入现有集群 (接触任意现有节点)
redis-cli --cluster add-node 192.168.1.200:6379 192.168.1.101:6379 -a your_password
# 2. 检查集群状态,确认新节点已加入但无槽
redis-cli --cluster check 192.168.1.101:6379 -a your_password
# 3. 重新分片 (Resharding) - 将部分槽从旧节点移动到新节点
# 系统会交互式询问:移动多少个槽?目标节点ID?源节点ID?
redis-cli --cluster reshard 192.168.1.101:6379 -a your_password
场景 B:动态删除节点 (缩容)
- 迁移该节点上的所有槽到其他节点。
- 从集群中移除该节点。
命令行操作案例:
bash
# 1. 先迁移槽 (假设要移除节点 ID 为 abc...def)
# 将该节点的所有槽移动到 192.168.1.101
redis-cli --cluster reshard 192.168.1.101:6379 --cluster-from <待删除节点ID> --cluster-to <接收节点ID> --cluster-slots <总槽数> -a your_password
# 2. 确认该节点槽数为 0 后,移除节点
redis-cli --cluster del-node 192.168.1.101:6379 <待删除节点ID> -a your_password
Java (Lettuce) 触发刷新拓扑 :
当节点变更后,客户端需要感知。如果开启了 adaptive: true (Spring配置),它会自动感知。否则可手动触发:
java
// 获取集群拓扑刷新
clusterClient.getResources().getPartitions().reload();
4. Predixy 集群代理
4.1 核心概念
- 作用:Predixy 是一个高性能的 Redis 集群代理。它对客户端屏蔽了集群的复杂性(如重定向、分片逻辑)。客户端只需连接 Predixy,就像连接单机 Redis 一样。
- 优势:简化客户端代码,支持更复杂的路由策略,统一管理连接池。
4.2 Predixy 配置 (predixy.conf)
conf
# 监听地址
bind 0.0.0.0:7100
# 后端 Redis Cluster 配置
cluster {
name "my-redis-cluster"
# 可以配置多个种子节点
node 192.168.1.101:6379
node 192.168.1.102:6379
# 密码配置
auth "your_password"
}
# 日志等其它配置...
4.3 客户端整合 (通用)
一旦部署了 Predixy,客户端(Lettuce 或 Spring Data Redis)的配置将退化为单机模式,指向 Predixy 的地址。
案例代码:通过 Predixy 访问集群
java
// 此时完全不需要知道后端是集群还是哨兵
// 配置指向 Predixy 的端口 (例如 7100)
RedisClient client = RedisClient.create("redis://192.168.1.50:7100");
// 如果有密码
// RedisClient.create(RedisURI.create("redis://:your_password@192.168.1.50:7100"));
StatefulRedisConnection<String, String> conn = client.connect();
// 直接使用,无需处理 MOVED 异常,无需配置多个节点
conn.sync().set("proxy:key", "value_via_predixy");
Spring Boot 配置 (application.yml):
yaml
spring:
data:
redis:
host: 192.168.1.50 # Predixy IP
port: 7100 # Predixy Port
password: your_password
# 不需要配置 cluster 或 sentinel 段落,当作单机使用
5. 综合性案例:高可用电商库存系统
场景描述 :
构建一个电商库存扣减服务。
- 架构:采用 Redis Cluster (3主3从) 保证数据量和性能。
- 高可用:结合哨兵思想(Cluster自带高可用),并使用 Spring Data Redis。
- 需求 :
- 扣减库存必须是原子操作(使用 Lua 脚本)。
- 处理集群下的多键问题(商品库存 + 用户购买记录需在同一槽)。
- 模拟节点故障时的自动恢复。
5.1 依赖配置 (pom.xml)
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lombok for brevity -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
5.2 核心代码实现
A. Lua 脚本定义 (原子扣减)
java
@Component
public class InventoryScript {
// KEYS[1]: 商品库存 Key (e.g., {prod:1001}:stock)
// KEYS[2]: 用户购买记录 Key (e.g., {prod:1001}:user:bought:u55)
// ARGV[1]: 购买数量
// 返回: 1 成功, 0 库存不足, -1 重复购买
public static final String SCRIPT =
"local stock = tonumber(redis.call('GET', KEYS[1]) or '0') " +
"local count = tonumber(ARGV[1]) " +
"if stock < count then return 0 end " +
"if redis.call('EXISTS', KEYS[2]) == 1 then return -1 end " +
"redis.call('DECRBY', KEYS[1], count) " +
"redis.call('SET', KEYS[2], '1', 'EX', '86400') " +
"return 1";
}
B. 服务层实现
java
@Service
@Slf4j
public class InventoryService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private RedisScript<Long> redisScript; // 需要配置 Bean
/**
* 扣减库存
* @param productId 商品ID
* @param userId 用户ID
* @param count 数量
* @return 0:失败(库存不足), 1:成功, -1:重复购买
*/
public int deductStock(String productId, String userId, int count) {
// 【关键】使用 Hash Tag 确保两个 Key 落在同一个 Slot,避免 CrossSlotException
String hashTag = "{" + productId + "}";
String stockKey = hashTag + ":stock";
String userRecordKey = hashTag + ":user:bought:" + userId;
try {
// 执行 Lua 脚本
Long result = redisTemplate.execute(
DefaultRedisScript.of(InventoryScript.SCRIPT, Long.class),
Arrays.asList(stockKey, userRecordKey),
String.valueOf(count)
);
if (result == 1) {
log.info("扣减成功: 商品{}, 用户{}", productId, userId);
return 1;
} else if (result == 0) {
log.warn("库存不足: 商品{}", productId);
return 0;
} else {
log.warn("重复购买: 商品{}, 用户{}", productId, userId);
return -1;
}
} catch (Exception e) {
log.error("Redis 执行异常,可能正在发生主从切换", e);
// 在实际系统中,这里应触发重试机制 (Retry Template)
throw new RuntimeException("库存服务暂时不可用", e);
}
}
// 配置 RedisScript Bean
@Bean
public RedisScript<Long> redisScript() {
return DefaultRedisScript.of(InventoryScript.SCRIPT, Long.class);
}
}
C. 测试控制器 (模拟高并发与故障)
java
@RestController
@RequestMapping("/api/inventory")
public class InventoryController {
@Autowired
private InventoryService inventoryService;
@PostMapping("/buy")
public ResponseEntity<String> buy(@RequestParam String prodId,
@RequestParam String userId,
@RequestParam int count) {
int result = inventoryService.deductStock(prodId, userId, count);
switch (result) {
case 1: return ResponseEntity.ok("购买成功");
case 0: return ResponseEntity.status(400).body("库存不足");
case -1: return ResponseEntity.status(400).body("请勿重复购买");
default: return ResponseEntity.status(500).body("系统繁忙");
}
}
}
5.3 综合案例总结
- 架构选择 :使用了 Redis Cluster 应对海量商品数据。
- 客户端配置 :Spring Boot 配置了
lettuce.cluster.refresh.adaptive=true,确保当某个分片的主节点宕机,哨兵机制完成切换后,应用能自动感知新拓扑,无需重启。 - 数据一致性 :利用 Lua 脚本 保证了"查库存"和"扣库存"的原子性。
- 集群规范 :巧妙使用
{}哈希标签 解决了集群模式下多键操作的限制。 - 扩展性 :如果未来流量激增,可通过
redis-cli --cluster reshard动态添加节点,代码无需任何修改。