Redis集群详解
一、知识概述
在生产环境中,单机 Redis 无法满足高可用和大规模数据处理的需求。Redis 提供了三种集群方案:主从复制、哨兵模式和 Redis Cluster,每种方案适用于不同的场景。
本文将详细介绍这三种集群方案的原理、配置和实战应用。
二、主从复制
2.1 原理介绍
主从复制是最简单的集群方案,通过将主节点的数据复制到从节点,实现读写分离和数据备份。
核心特点:
- 一个主节点,多个从节点
- 数据单向复制:主 → 从
- 主节点负责写,从节点负责读
- 从节点只能读,不能写
复制流程:
- 从节点连接主节点,发送 SYNC 命令
- 主节点执行 BGSAVE,生成 RDB 快照
- 主节点将 RDB 发送给从节点
- 从节点加载 RDB
- 主节点将期间的写命令发送给从节点(增量复制)
2.2 配置方式
方式一:配置文件
# 从节点配置文件 redis.conf
# 指定主节点
replicaof 192.168.1.100 6379
# 主节点密码
masterauth "your_password"
# 从节点只读
replica-read-only yes
# 复制超时时间
repl-timeout 60
方式二:命令行
# 连接 Redis
redis-cli
# 设置主节点
REPLICAOF 192.168.1.100 6379
# 取消主从关系
REPLICAOF NO ONE
Java 客户端配置
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class MasterSlaveDemo {
public static void main(String[] args) {
// 主节点连接池
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(100);
config.setMaxIdle(50);
JedisPool masterPool = new JedisPool(config, "192.168.1.100", 6379);
JedisPool slavePool = new JedisPool(config, "192.168.1.101", 6379);
// 写操作 → 主节点
try (Jedis master = masterPool.getResource()) {
master.set("key1", "value1");
}
// 读操作 → 从节点
try (Jedis slave = slavePool.getResource()) {
String value = slave.get("key1");
System.out.println(value);
}
masterPool.close();
slavePool.close();
}
}
2.3 复制原理详解
全量复制
// 全量复制流程示例
public class FullSyncDemo {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
// 查看复制信息
String info = jedis.info("replication");
System.out.println(info);
// 关键指标
// role:slave # 角色:从节点
// master_host:192.168.1.100 # 主节点地址
// master_port:6379 # 主节点端口
// master_link_status:up # 连接状态
// master_sync_in_progress:0 # 是否正在同步
// slave_repl_offset:12345 # 复制偏移量
jedis.close();
}
}
部分复制(增量复制)
从 Redis 2.8 开始支持部分复制,减少全量复制的开销。
核心概念:
- 复制偏移量:主从节点各自维护
- 复制积压缓冲区:主节点维护的固定大小队列
- 运行 ID:主节点的唯一标识
# redis.conf 配置
# 复制积压缓冲区大小
repl-backlog-size 1mb
# 缓冲区生存时间
repl-backlog-ttl 3600
部分复制条件:
- 主节点运行 ID 不变
- 从节点偏移量在积压缓冲区范围内
2.4 优缺点分析
优点:
- 配置简单,易于部署
- 实现读写分离,提高读性能
- 数据冗余,提供数据备份
缺点:
- 主节点故障需要手动切换
- 主节点写入能力受限
- 主节点内存受限
三、哨兵模式
3.1 原理介绍
哨兵模式在主从复制的基础上,增加了自动故障转移功能。
核心功能:
- 监控:检查主从节点是否正常运行
- 通知:当节点出现问题时通知管理员
- 自动故障转移:主节点故障时自动选举新主节点
3.2 架构设计
+-------------------+
| Sentinel 集群 |
| (至少3个节点) |
+--------+----------+
|
+---------------+---------------+
| | |
+----+----+ +-----+-----+ +----+----+
| Master | | Slave1 | | Slave2 |
+---------+ +-----------+ +---------+
3.3 配置方式
哨兵配置文件
# sentinel.conf
# 监控主节点(名称 IP 端口 仲裁人数)
sentinel monitor mymaster 192.168.1.100 6379 2
# 主节点密码
sentinel auth-pass mymaster your_password
# 判断主节点下线的超时时间
sentinel down-after-milliseconds mymaster 30000
# 故障转移超时时间
sentinel failover-timeout mymaster 180000
# 同时可以有多少个从节点对新主节点进行同步
sentinel parallel-syncs mymaster 1
# 哨兵端口
port 26379
# 工作目录
dir /tmp
启动哨兵
# 方式一:使用配置文件启动
redis-sentinel /path/to/sentinel.conf
# 方式二:使用 redis-server 启动
redis-server /path/to/sentinel.conf --sentinel
Java 客户端连接
import redis.clients.jedis.JedisSentinelPool;
import redis.clients.jedis.Jedis;
import java.util.HashSet;
import java.util.Set;
public class SentinelDemo {
public static void main(String[] args) {
// 哨兵节点集合
Set<String> sentinels = new HashSet<>();
sentinels.add("192.168.1.100:26379");
sentinels.add("192.168.1.101:26379");
sentinels.add("192.168.1.102:26379");
// 创建哨兵连接池
JedisSentinelPool pool = new JedisSentinelPool(
"mymaster", // 主节点名称
sentinels, // 哨兵节点
"your_password" // 密码
);
// 获取连接(自动路由到当前主节点)
try (Jedis jedis = pool.getResource()) {
jedis.set("key1", "value1");
String value = jedis.get("key1");
System.out.println(value);
}
pool.close();
}
}
3.4 故障转移流程
// 故障转移流程示例说明
public class FailoverProcess {
/*
* 1. 主观下线(SDOWN)
* - 单个哨兵认为主节点下线(ping 超时)
*
* 2. 客观下线(ODOWN)
* - 足够数量(quorum)的哨兵认为主节点下线
*
* 3. 选举领导者哨兵
* - 使用 Raft 算法选举
*
* 4. 领导者执行故障转移
* a. 从从节点中选出一个新的主节点
* - 排除不健康的从节点
* - 优先选择复制偏移量大的
* - 优先选择运行 ID 小的
* b. 让其他从节点复制新主节点
* c. 将旧主节点设置为从节点
* d. 通知客户端新主节点地址
*/
}
3.5 哨兵命令
// 哨兵管理命令
public class SentinelCommands {
public static void main(String[] args) {
Jedis sentinel = new Jedis("192.168.1.100", 26379);
// 查看主节点信息
Map<String, String> masterInfo = sentinel.sentinelMaster("mymaster");
// 查看从节点列表
List<Map<String, String>> slaves =
sentinel.sentinelSlaves("mymaster");
// 查看哨兵列表
List<Map<String, String>> sentinels =
sentinel.sentinelSentinels("mymaster");
// 获取当前主节点地址
List<String> master = sentinel.sentinelGetMasterAddrByName("mymaster");
String masterHost = master.get(0);
int masterPort = Integer.parseInt(master.get(1));
// 重置主节点(用于测试)
sentinel.sentinelReset("mymaster");
// 强制故障转移
sentinel.sentinelFailover("mymaster");
sentinel.close();
}
}
3.6 优缺点分析
优点:
- 自动故障转移
- 配置相对简单
- 客户端自动发现新主节点
缺点:
- 无法横向扩展写能力
- 配置和管理相对复杂
- 需要足够数量的哨兵节点
四、Redis Cluster
4.1 原理介绍
Redis Cluster 是 Redis 官方提供的分布式解决方案,支持数据分片和自动故障转移。
核心特点:
- 无中心架构,所有节点互联
- 数据分片存储,支持横向扩展
- 自动分片和重新分片
- 自动故障转移
4.2 数据分片原理
哈希槽(Hash Slot)
Redis Cluster 将数据划分为 16384 个哈希槽,每个节点负责一部分槽。
Key → CRC16(key) % 16384 → Slot → Node
// 哈希槽计算示例
public class HashSlotDemo {
public static void main(String[] args) {
String key = "user:1001";
// 计算 CRC16
int crc16 = CRC16.crc16(key.getBytes());
// 计算槽位
int slot = crc16 % 16384;
System.out.println("Key: " + key);
System.out.println("Slot: " + slot);
// Jedis 计算
Jedis jedis = new Jedis("localhost", 6379);
int jedisSlot = jedis.clusterKeySlot(key).intValue();
System.out.println("Jedis Slot: " + jedisSlot);
jedis.close();
}
}
// CRC16 实现(简化版)
class CRC16 {
private static final int[] CRC16_TABLE = new int[256];
static {
for (int i = 0; i < 256; i++) {
int crc = i;
for (int j = 0; j < 8; j++) {
if ((crc & 1) == 1) {
crc = (crc >>> 1) ^ 0xA001;
} else {
crc >>>= 1;
}
}
CRC16_TABLE[i] = crc;
}
}
public static int crc16(byte[] bytes) {
int crc = 0;
for (byte b : bytes) {
crc = (crc >>> 8) ^ CRC16_TABLE[(crc ^ b) & 0xFF];
}
return crc & 0xFFFF;
}
}
槽位分配示例
节点 A:0 - 5460
节点 B:5461 - 10922
节点 C:10923 - 16383
4.3 集群搭建
配置文件
# redis-7000.conf(每个节点配置文件)
# 端口
port 7000
# 开启集群模式
cluster-enabled yes
# 集群配置文件(自动生成)
cluster-config-file nodes-7000.conf
# 节点超时时间
cluster-node-timeout 15000
# 开启 AOF
appendonly yes
# 后台运行
daemonize yes
# 数据目录
dir /var/redis/7000
创建集群
# 创建集群(3主3从)
redis-cli --cluster create \
192.168.1.100:7000 \
192.168.1.100:7001 \
192.168.1.100:7002 \
192.168.1.101:7000 \
192.168.1.101:7001 \
192.168.1.101:7002 \
--cluster-replicas 1
# 参数说明:
# --cluster-replicas 1 表示每个主节点有1个从节点
Java 客户端连接
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.HostAndPort;
import java.util.Set;
import java.util.HashSet;
public class ClusterDemo {
public static void main(String[] args) {
// 集群节点集合
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("192.168.1.100", 7000));
nodes.add(new HostAndPort("192.168.1.100", 7001));
nodes.add(new HostAndPort("192.168.1.100", 7002));
nodes.add(new HostAndPort("192.168.1.101", 7000));
nodes.add(new HostAndPort("192.168.1.101", 7001));
nodes.add(new HostAndPort("192.168.1.101", 7002));
// 创建集群连接
JedisCluster cluster = new JedisCluster(nodes);
// 基本操作
cluster.set("key1", "value1");
String value = cluster.get("key1");
System.out.println(value);
// 批量操作(需要使用 Pipeline 或 Lua 脚本)
// 注意:不同槽位的 key 不能在同一个事务中
cluster.close();
}
}
4.4 集群操作命令
// 集群管理命令
public class ClusterCommands {
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.1.100", 7000);
// 查看集群信息
String clusterInfo = jedis.clusterInfo();
System.out.println(clusterInfo);
// 查看集群节点
String nodes = jedis.clusterNodes();
System.out.println(nodes);
// 查看槽位分配
// cluster slots 命令返回槽位范围和节点信息
List<Object> slots = jedis.clusterSlots();
// 查看指定 key 的槽位
int slot = jedis.clusterKeySlot("user:1001").intValue();
System.out.println("Slot: " + slot);
// 查看槽位所在的节点
String node = jedis.clusterNodeForKey(slot);
System.out.println("Node: " + node);
jedis.close();
}
}
4.5 故障转移
// 集群故障转移示例
public class ClusterFailover {
/*
* 故障检测:
* 1. 主观下线(PFAIL):单个节点认为某节点下线
* 2. 客观下线(FAIL):多数主节点认为某主节点下线
*
* 故障转移流程:
* 1. 从节点取消复制状态
* 2. 从节点竞选成为新主节点
* 3. 其他从节点开始复制新主节点
* 4. 集群更新槽位映射
*/
public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.1.100", 7001);
// 手动触发故障转移(在从节点上执行)
jedis.clusterFailover();
// 强制故障转移(不需要主节点同意)
jedis.clusterFailoverForce();
// 接管故障转移(用于特定场景)
jedis.clusterFailoverTakeover();
jedis.close();
}
}
4.6 集群扩缩容
添加节点
# 添加主节点
redis-cli --cluster add-node \
新节点IP:端口 \
集群中任一节点IP:端口
# 添加从节点
redis-cli --cluster add-node \
新节点IP:端口 \
集群中任一节点IP:端口 \
--cluster-slave \
--cluster-master-id 主节点ID
# 重新分配槽位
redis-cli --cluster reshard \
集群中任一节点IP:端口
// Java 客户端扩容后自动感知
public class ClusterResizeDemo {
public static void main(String[] args) {
Set<HostAndPort> nodes = new HashSet<>();
nodes.add(new HostAndPort("192.168.1.100", 7000));
// ... 添加新节点
JedisCluster cluster = new JedisCluster(nodes);
// 客户端会自动获取最新的槽位映射
cluster.close();
}
}
删除节点
# 删除节点(需要先迁移槽位)
redis-cli --cluster del-node \
集群中任一节点IP:端口 \
要删除的节点ID
4.7 优缺点分析
优点:
- 横向扩展能力强
- 自动数据分片
- 自动故障转移
- 无中心架构
缺点:
- 配置和维护复杂
- 批量操作受限
- 事务支持有限(同一槽位)
- 客户端实现复杂
五、实战应用场景
5.1 高可用缓存方案
// 完整的高可用缓存方案
public class HighAvailabilityCache {
private JedisCluster cluster;
public HighAvailabilityCache(Set<HostAndPort> nodes) {
GenericObjectPoolConfig<Jedis> poolConfig = new GenericObjectPoolConfig<>();
poolConfig.setMaxTotal(100);
poolConfig.setMaxIdle(50);
poolConfig.setMinIdle(10);
poolConfig.setMaxWaitMillis(3000);
poolConfig.setTestOnBorrow(true);
poolConfig.setTestOnReturn(false);
poolConfig.setTestWhileIdle(true);
this.cluster = new JedisCluster(nodes, 2000, 2000, 5,
"password", poolConfig);
}
// 带重试的缓存读取
public String getWithRetry(String key, int maxRetries) {
for (int i = 0; i < maxRetries; i++) {
try {
return cluster.get(key);
} catch (Exception e) {
if (i == maxRetries - 1) {
throw new RuntimeException("Failed after " + maxRetries + " retries", e);
}
try {
Thread.sleep(100 * (i + 1));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted", ie);
}
}
}
return null;
}
// 分布式锁(集群版)
public boolean tryLock(String key, String value, int expireSeconds) {
try {
String result = cluster.set(key, value, "NX", "EX", expireSeconds);
return "OK".equals(result);
} catch (Exception e) {
return false;
}
}
public void unlock(String key, String value) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
cluster.eval(script, 1, key, value);
}
public void close() {
try {
cluster.close();
} catch (Exception e) {
// ignore
}
}
}
5.2 多Key操作优化
// 使用 Hash Tag 确保多个 key 在同一槽位
public class MultiKeyDemo {
private JedisCluster cluster;
public MultiKeyDemo(JedisCluster cluster) {
this.cluster = cluster;
}
// 使用 Hash Tag
// {user:1001}:profile 和 {user:1001}:settings 会在同一槽位
public void saveUserWithHashTag(long userId) {
String tag = "{user:" + userId + "}";
// 同一用户的不同数据使用相同的 Hash Tag
cluster.set(tag + ":profile", "profile data");
cluster.set(tag + ":settings", "settings data");
cluster.set(tag + ":history", "history data");
// 可以使用事务
Transaction tx = null;
try {
// 注意:JedisCluster 的事务支持有限
// 实际生产中建议使用 Lua 脚本
cluster.eval(
"redis.call('set', KEYS[1], ARGV[1]); " +
"redis.call('set', KEYS[2], ARGV[2]); " +
"redis.call('set', KEYS[3], ARGV[3]);",
3,
tag + ":profile", tag + ":settings", tag + ":history",
"profile data", "settings data", "history data"
);
} catch (Exception e) {
e.printStackTrace();
}
}
// 批量获取(Pipeline)
public List<String> multiGet(List<String> keys) {
// 对于集群,需要按节点分组
Map<JedisPool, List<String>> nodeKeys = groupKeysByNode(keys);
List<String> results = new ArrayList<>();
for (Map.Entry<JedisPool, List<String>> entry : nodeKeys.entrySet()) {
try (Jedis jedis = entry.getKey().getResource()) {
Pipeline pipeline = jedis.pipelined();
for (String key : entry.getValue()) {
pipeline.get(key);
}
List<Object> objects = pipeline.syncAndReturnAll();
for (Object obj : objects) {
results.add((String) obj);
}
}
}
return results;
}
private Map<JedisPool, List<String>> groupKeysByNode(List<String> keys) {
// 实现按节点分组的逻辑
// 这里简化处理,实际需要根据槽位映射
return new HashMap<>();
}
}
六、集群方案对比
| 特性 | 主从复制 | 哨兵模式 | Redis Cluster |
|---|---|---|---|
| 数据分片 | ❌ | ❌ | ✅ |
| 自动故障转移 | ❌ | ✅ | ✅ |
| 横向扩展写能力 | ❌ | ❌ | ✅ |
| 配置复杂度 | 低 | 中 | 高 |
| 客户端复杂度 | 低 | 低 | 中 |
| 运维复杂度 | 低 | 中 | 高 |
| 适用场景 | 数据备份、读写分离 | 高可用、自动故障转移 | 大规模数据、高并发 |
七、总结与最佳实践
7.1 方案选择建议
| 场景 | 推荐方案 |
|---|---|
| 数据量小,允许短暂不可用 | 主从复制 |
| 数据量小,需要高可用 | 哨兵模式 |
| 数据量大,需要横向扩展 | Redis Cluster |
| 数据量大 + 高可用 | Redis Cluster |
7.2 最佳实践
-
哨兵模式
- 至少部署 3 个哨兵节点
- 哨兵节点可以和 Redis 节点混部
- 配置合理的超时时间
-
Redis Cluster
- 至少 3 个主节点
- 每个主节点至少 1 个从节点
- 合理规划槽位分配
- 使用 Hash Tag 处理多 Key 操作
-
客户端配置
- 配置合理的连接池参数
- 实现重试机制
- 处理重定向(MOVED、ASK)
-
监控告警
- 监控节点状态
- 监控内存使用率
- 监控集群健康度
- 设置合理的告警阈值
7.3 常见问题
- 脑裂问题:配置
min-replicas-to-write和min-replicas-max-lag - 数据丢失:合理配置持久化和复制参数
- 性能问题:避免大 Key、热点 Key,合理使用 Pipeline
八、思考与练习
思考题
-
基础题:主从复制、哨兵模式、Redis Cluster 三种方案各自的核心特点是什么?它们分别解决了什么问题?
-
进阶题:Redis Cluster 使用 16384 个哈希槽进行数据分片,请分析为什么选择这个数字?哈希槽相比一致性哈希有什么优势?
-
实战题:设计一个方案,当 Redis Cluster 某个主节点宕机时,如何保证数据不丢失且服务快速恢复?
编程练习
练习:基于 Redis Cluster 实现一个分布式会话管理器,要求:
- 支持会话创建、获取、删除、过期
- 使用 Hash Tag 确保同一用户的所有数据在同一槽位
- 实现会话的自动续期功能
提示:使用 {session:sessionId} 作为 Hash Tag 前缀。
章节关联
- 前置章节:《Redis基础详解》- 掌握 Redis 数据结构和持久化
- 后续章节:《缓存设计详解》- 学习缓存架构设计最佳实践
- 扩展阅读:《Redis 设计与实现》第 16-18 章
📝 下一章预告
下一章将探讨缓存系统设计的核心问题,包括缓存穿透、缓存击穿、缓存雪崩的防护策略,以及缓存预热、缓存更新等最佳实践。
本章完