Redis 从入门到精通:Spring Boot 实战三部曲(二)------ 进阶原理与高可用架构
专题导读:本系列共三篇,带你系统掌握 Redis 在 Spring Boot 项目中的实战应用。
- 第一篇 基础核心与快速上手
- 第二篇:进阶原理与高可用架构(本文)
- 第三篇 高级特性与性能优化
📖 前言
在上一篇文章中,我们学习了 Redis 的五大数据结构及其在 Spring Boot 中的应用。本文将深入探讨 Redis 的核心原理,包括持久化机制、主从复制、哨兵模式等高级特性,帮助你构建高可用的 Redis 架构。
学完本文你将掌握:
- ✅ Redis 持久化机制(RDB & AOF)的原理与配置
- ✅ 主从复制的工作流程与搭建方法
- ✅ 哨兵模式实现高可用
- ✅ 事务与 Lua 脚本的原子操作
- ✅ 分布式锁的高级实现
- ✅ 生产环境的最佳实践
一、Redis 持久化机制深度解析
1.1 为什么需要持久化?
Redis 是基于内存的数据库,虽然速度极快,但断电后数据会丢失。持久化机制可以将内存中的数据保存到磁盘,保证数据的可靠性。
持久化的两大场景:
- 数据备份:定期保存数据快照
- 故障恢复:服务重启后恢复数据
1.2 RDB(Redis Database)快照
工作原理
RDB 是 Redis 默认的持久化方式,通过创建内存快照来保存数据。
触发 RDB → Fork 子进程 → 子进程遍历内存生成 RDB 文件 →
替换旧文件 → 通知主进程完成
流程图:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 主进程 │ │ 子进程 │ │ 磁盘 │
│ │ │ │ │ │
│ 继续处理 │─────►│ 遍历内存 │─────►│ 写入RDB │
│ 客户端请求│ │ 生成快照 │ │ 文件 │
└──────────┘ └──────────┘ └──────────┘
触发方式
1. 自动触发(配置文件)
conf
# redis.conf
save 900 1 # 900秒内至少1个key变化
save 300 10 # 300秒内至少10个key变化
save 60 10000 # 60秒内至少10000个key变化
2. 手动触发
bash
# 阻塞式(不推荐生产环境使用)
SAVE
# 非阻塞式(推荐)
BGSAVE
配置详解
conf
# RDB 文件名
dbfilename dump.rdb
# RDB 文件存储目录
dir /var/lib/redis
# RDB 压缩
rdbcompression yes
# RDB 校验和
rdbchecksum yes
# 停止写入时是否接受写操作
stop-writes-on-bgsave-error yes
优缺点分析
| 优点 | 缺点 |
|---|---|
| ✅ 文件紧凑,适合备份 | ❌ 可能丢失最后一次快照的数据 |
| ✅ 恢复速度快 | ❌ Fork 子进程消耗内存和 CPU |
| ✅ 最大化 Redis 性能 | ❌ 大数据集时 Fork 耗时较长 |
| ✅ 适合灾难恢复 | ❌ 无法实现实时持久化 |
Spring Boot 监控 RDB 状态
java
@Service
@Slf4j
public class RedisMonitorService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 获取 RDB 持久化信息
*/
public Map<String, Object> getRdbInfo() {
Properties info = redisTemplate.getConnectionFactory()
.getConnection().info("persistence");
Map<String, Object> rdbInfo = new HashMap<>();
rdbInfo.put("rdb_bgsave_in_progress",
info.getProperty("rdb_bgsave_in_progress"));
rdbInfo.put("rdb_last_bgsave_status",
info.getProperty("rdb_last_bgsave_status"));
rdbInfo.put("rdb_last_bgsave_time_sec",
info.getProperty("rdb_last_bgsave_time_sec"));
rdbInfo.put("rdb_current_bgsave_time_sec",
info.getProperty("rdb_current_bgsave_time_sec"));
rdbInfo.put("rdb_last_save_timestamp",
info.getProperty("rdb_last_save_timestamp"));
return rdbInfo;
}
/**
* 手动触发 BGSAVE
*/
public void triggerBgSave() {
redisTemplate.execute((RedisCallback<Void>) connection -> {
connection.bgSave();
return null;
});
log.info("已触发 BGSAVE");
}
}
1.3 AOF(Append Only File)追加日志
工作原理
AOF 通过记录每个写命令来实现持久化,重启时重新执行这些命令恢复数据。
写命令 → 追加到 AOF 缓冲区 → 根据策略同步到磁盘
三种同步策略:
| 策略 | 说明 | 性能 | 安全性 |
|---|---|---|---|
always |
每条命令都同步 | 慢 | 最安全 |
everysec |
每秒同步一次 | 适中 | 推荐 |
no |
由 OS 决定 | 快 | 最不安全 |
配置详解
conf
# 启用 AOF
appendonly yes
# AOF 文件名
appendfilename "appendonly.aof"
# 同步策略(推荐 everysec)
appendfsync everysec
# AOF 重写配置
auto-aof-rewrite-percentage 100 # 增长100%时触发重写
auto-aof-rewrite-min-size 64mb # 最小64MB才重写
# AOF 加载时是否检查完整性
aof-load-truncated yes
# 混合持久化(Redis 4.0+)
aof-use-rdb-preamble yes
AOF 重写机制
为什么需要重写?
AOF 文件会随着写操作不断增长,重写可以压缩文件大小。
原始 AOF: 重写后 AOF:
SET key1 value1 SET key1 value3
SET key1 value2 → SET key2 value4
SET key1 value3
SET key2 value4
重写流程:
1. 主进程 fork 子进程
2. 子进程根据当前内存状态生成新 AOF
3. 主进程累积新写入命令到 AOF buffer
4. 子进程完成后,主进程将 buffer 内容追加到新 AOF
5. 原子替换旧 AOF 文件
优缺点分析
| 优点 | 缺点 |
|---|---|
| ✅ 数据更安全(最多丢失1秒) | ❌ 文件体积通常比 RDB 大 |
| ✅ 易于理解和维护 | ❌ 恢复速度比 RDB 慢 |
| ✅ 支持实时持久化 | ❌ 相同数据集下 AOF 比 RDB 大 |
| ✅ AOF 文件损坏可修复 | ❌ 重写时消耗资源 |
Spring Boot AOF 管理
java
@Service
@Slf4j
public class AofManagementService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 获取 AOF 信息
*/
public Map<String, Object> getAofInfo() {
Properties info = redisTemplate.getConnectionFactory()
.getConnection().info("persistence");
Map<String, Object> aofInfo = new HashMap<>();
aofInfo.put("aof_enabled", info.getProperty("aof_enabled"));
aofInfo.put("aof_rewrite_in_progress",
info.getProperty("aof_rewrite_in_progress"));
aofInfo.put("aof_last_rewrite_time_sec",
info.getProperty("aof_last_rewrite_time_sec"));
aofInfo.put("aof_current_rewrite_time_sec",
info.getProperty("aof_current_rewrite_time_sec"));
aofInfo.put("aof_last_bgrewrite_status",
info.getProperty("aof_last_bgrewrite_status"));
aofInfo.put("aof_current_size",
info.getProperty("aof_current_size"));
aofInfo.put("aof_base_size",
info.getProperty("aof_base_size"));
return aofInfo;
}
/**
* 手动触发 AOF 重写
*/
public void triggerAofRewrite() {
redisTemplate.execute((RedisCallback<Void>) connection -> {
connection.bgRewriteAof();
return null;
});
log.info("已触发 AOF 重写");
}
/**
* 切换 AOF 同步策略
*/
public void setAofSyncPolicy(String policy) {
// policy: always, everysec, no
redisTemplate.execute((RedisCallback<Void>) connection -> {
connection.setConfig("appendfsync", policy);
return null;
});
log.info("AOF 同步策略已设置为: {}", policy);
}
}
1.4 RDB vs AOF 如何选择?
对比总结
| 维度 | RDB | AOF |
|---|---|---|
| 数据安全性 | 较低(可能丢失较多) | 较高(最多丢失1秒) |
| 文件大小 | 小(压缩快照) | 大(记录所有命令) |
| 恢复速度 | 快 | 慢 |
| 性能影响 | Fork 时短暂停顿 | 持续写入开销 |
| 适用场景 | 备份、容忍部分数据丢失 | 对数据安全性要求高 |
推荐方案
生产环境最佳实践:同时启用 RDB + AOF
conf
# 同时启用 RDB 和 AOF
save 900 1
save 300 10
save 60 10000
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes # 混合持久化
优势:
- Redis 重启时优先使用 AOF 恢复数据(更完整)
- AOF 文件过大时可以通过 RDB 重写压缩
- 兼顾数据安全性和恢复速度
二、主从复制(Master-Slave)
2.1 主从复制架构
┌──────────────┐
│ Master │ ← 写操作
│ 192.168.1.1 │
└──────┬───────┘
│
├──────────┬──────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Slave 1 │ │ Slave 2 │ │ Slave 3 │ ← 读操作
│ .1.2 │ │ .1.3 │ │ .1.4 │
└─────────┘ └─────────┘ └─────────┘
特点:
- 一个 Master 可以有多个 Slave
- Master 负责写,Slave 负责读(读写分离)
- Slave 可以级联连接(Slave of Slave)
2.2 复制流程详解
完整复制过程
1️⃣ 建立连接
Slave 发送 SYNC 命令到 Master
2️⃣ 主库生成 RDB
Master 执行 BGSAVE 生成 RDB 文件
3️⃣ 传输 RDB
Master 将 RDB 文件发送给 Slave
4️⃣ 从库加载
Slave 清空旧数据,加载 RDB 文件
5️⃣ 增量同步
Master 持续发送写命令给 Slave
全量复制 vs 增量复制
全量复制触发条件:
- Slave 首次连接 Master
- Slave 断线时间过长,repl_backlog_buffer 被覆盖
- 手动执行
SLAVEOF命令
增量复制:
- 短时间断线重连
- 通过
repl_backlog_buffer同步缺失的命令
repl_backlog_buffer 配置:
conf
# 复制积压缓冲区大小
repl-backlog-size 1mb
# 缓冲区超时时间(秒)
repl-backlog-ttl 3600
2.3 搭建主从环境
方式一:配置文件
Master 配置(redis-master.conf):
conf
bind 0.0.0.0
port 6379
requirepass master_password
masterauth master_password
Slave 配置(redis-slave-1.conf):
conf
bind 0.0.0.0
port 6380
requirepass slave_password
masterauth master_password
slaveof 192.168.1.1 6379
slave-read-only yes
启动:
bash
# 启动 Master
redis-server redis-master.conf
# 启动 Slave
redis-server redis-slave-1.conf
redis-server redis-slave-2.conf
方式二:命令行动态配置
bash
# 连接到 Slave 节点
redis-cli -p 6380
# 设置 Master
127.0.0.1:6380> SLAVEOF 192.168.1.1 6379
OK
# 设置 Master 密码
127.0.0.1:6380> CONFIG SET masterauth master_password
OK
2.4 监控主从状态
bash
# 查看复制信息
redis-cli INFO replication
# 输出示例:
# role:master
# connected_slaves:2
# slave0:ip=192.168.1.2,port=6380,state=online,offset=12345,lag=1
# slave1:ip=192.168.1.3,port=6381,state=online,offset=12340,lag=1
Spring Boot 监控主从状态
java
@Service
@Slf4j
public class ReplicationMonitorService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 获取复制信息
*/
public Map<String, Object> getReplicationInfo() {
Properties info = redisTemplate.getConnectionFactory()
.getConnection().info("replication");
Map<String, Object> replInfo = new HashMap<>();
replInfo.put("role", info.getProperty("role"));
replInfo.put("connected_slaves", info.getProperty("connected_slaves"));
replInfo.put("master_host", info.getProperty("master_host"));
replInfo.put("master_port", info.getProperty("master_port"));
replInfo.put("master_link_status", info.getProperty("master_link_status"));
replInfo.put("slave_read_only", info.getProperty("slave_read_only"));
// 解析 Slave 列表
List<Map<String, String>> slaves = new ArrayList<>();
int i = 0;
while (info.containsKey("slave" + i)) {
String slaveInfo = info.getProperty("slave" + i);
Map<String, String> slaveMap = parseSlaveInfo(slaveInfo);
slaves.add(slaveMap);
i++;
}
replInfo.put("slaves", slaves);
return replInfo;
}
private Map<String, String> parseSlaveInfo(String slaveInfo) {
Map<String, String> map = new HashMap<>();
String[] pairs = slaveInfo.split(",");
for (String pair : pairs) {
String[] kv = pair.split("=");
if (kv.length == 2) {
map.put(kv[0], kv[1]);
}
}
return map;
}
/**
* 检查主从同步状态
*/
public boolean checkReplicationHealth() {
Properties info = redisTemplate.getConnectionFactory()
.getConnection().info("replication");
String role = info.getProperty("role");
if ("master".equals(role)) {
String linkStatus = info.getProperty("master_link_status");
return "up".equals(linkStatus);
}
return true;
}
}
2.5 读写分离实现
Spring Boot 配置多数据源
java
@Configuration
public class RedisReadWriteConfig {
@Bean
public RedisTemplate<String, Object> masterRedisTemplate(
RedisConnectionFactory masterFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(masterFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
@Bean
public RedisTemplate<String, Object> slaveRedisTemplate(
RedisConnectionFactory slaveFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(slaveFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
读写分离服务
java
@Service
public class ReadWriteSeparationService {
@Autowired
@Qualifier("masterRedisTemplate")
private RedisTemplate<String, Object> masterTemplate;
@Autowired
@Qualifier("slaveRedisTemplate")
private RedisTemplate<String, Object> slaveTemplate;
/**
* 写操作(使用 Master)
*/
public void writeData(String key, Object value) {
masterTemplate.opsForValue().set(key, value);
}
/**
* 读操作(使用 Slave)
*/
public Object readData(String key) {
return slaveTemplate.opsForValue().get(key);
}
/**
* 删除操作(使用 Master)
*/
public void deleteData(String key) {
masterTemplate.delete(key);
}
}
三、哨兵模式(Sentinel)
3.1 为什么需要哨兵?
主从模式的缺陷:
- Master 宕机后需要手动切换
- 无法自动故障转移
- 不适合高可用场景
哨兵的作用:
- 监控(Monitoring):定期检查主从节点是否存活
- 通知(Notification):故障时通过 API 通知管理员
- 自动故障转移(Automatic Failover):选举新的 Master
3.2 哨兵工作原理
故障检测流程
1️⃣ 主观下线(SDOWN)
单个哨兵认为 Master 不可达
2️⃣ 客观下线(ODOWN)
多数哨兵确认 Master 不可达
3️⃣ Leader 选举
Raft 算法选出负责故障转移的 Leader
4️⃣ 选择新 Master
优先级 → 复制偏移量 → 运行 ID
5️⃣ 切换从库
其他 Slave 指向新 Master
6️⃣ 通知客户端
更新 Master 地址
架构图
┌──────────┐ ┌──────────┐ ┌──────────┐
│Sentinel 1│ │Sentinel 2│ │Sentinel 3│
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└───────────────┼───────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Master │ │ Slave 1 │ │ Slave 2 │
│ .1.1 │ │ .1.2 │ │ .1.3 │
└─────────┘ └─────────┘ └─────────┘
3.3 搭建哨兵集群
哨兵配置(sentinel.conf)
conf
# 监听端口
port 26379
# 守护进程模式
daemonize yes
# 日志文件
logfile "/var/log/redis/sentinel.log"
# 工作目录
dir /tmp
# 监控 Master(最后参数为法定票数)
sentinel monitor mymaster 192.168.1.1 6379 2
# Master 密码
sentinel auth-pass mymaster master_password
# 判断 Master 下线的时间(毫秒)
sentinel down-after-milliseconds mymaster 30000
# 故障转移超时时间(毫秒)
sentinel failover-timeout mymaster 180000
# 并行同步的 Slave 数量
sentinel parallel-syncs mymaster 1
# 通知脚本(可选)
sentinel notification-script mymaster /var/redis/notify.sh
启动哨兵
bash
# 启动三个哨兵节点
redis-sentinel sentinel-1.conf
redis-sentinel sentinel-2.conf
redis-sentinel sentinel-3.conf
# 查看哨兵状态
redis-cli -p 26379
127.0.0.1:26379> INFO sentinel
3.4 Spring Boot 集成哨兵
application.yml 配置
yaml
spring:
redis:
password: your_password
sentinel:
master: mymaster
nodes:
- 192.168.1.1:26379
- 192.168.1.2:26379
- 192.168.1.3:26379
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
max-wait: 3000ms
配置类
java
@Configuration
public class SentinelRedisConfig {
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.sentinel.master}")
private String masterName;
@Value("${spring.redis.sentinel.nodes}")
private List<String> sentinelNodes;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
// 哨兵配置
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration();
sentinelConfig.setMaster(masterName);
// 添加哨兵节点
for (String node : sentinelNodes) {
String[] hostPort = node.split(":");
sentinelConfig.sentinel(hostPort[0], Integer.parseInt(hostPort[1]));
}
// 密码配置
if (StringUtils.hasText(password)) {
sentinelConfig.setPassword(RedisPassword.of(password));
}
// 连接池配置
GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();
poolConfig.setMaxTotal(20);
poolConfig.setMaxIdle(10);
poolConfig.setMinIdle(5);
poolConfig.setMaxWaitMillis(3000);
LettucePoolingClientConfiguration clientConfig = LettucePoolingClientConfiguration.builder()
.poolConfig(poolConfig)
.commandTimeout(Duration.ofSeconds(5))
.build();
return new LettuceConnectionFactory(sentinelConfig, clientConfig);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
测试故障转移
java
@SpringBootTest
@Slf4j
public class SentinelTest {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Test
public void testFailover() throws InterruptedException {
// 1. 写入数据
redisTemplate.opsForValue().set("test:key", "test:value");
log.info("数据写入成功");
// 2. 模拟 Master 宕机(手动停止 Master)
log.info("请手动停止 Master 节点...");
Thread.sleep(60000); // 等待故障转移
// 3. 读取数据(应该能正常读取)
Object value = redisTemplate.opsForValue().get("test:key");
log.info("故障转移后读取数据: {}", value);
// 4. 验证新 Master
Properties info = redisTemplate.getConnectionFactory()
.getConnection().info("replication");
log.info("当前角色: {}", info.getProperty("role"));
}
}
四、事务与 Lua 脚本
4.1 Redis 事务
基本命令
bash
MULTI # 开启事务
EXEC # 执行事务
DISCARD # 取消事务
WATCH key # 监视 key(乐观锁)
UNWATCH # 取消监视
事务示例
bash
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key1 value1
QUEUED
127.0.0.1:6379> SET key2 value2
QUEUED
127.0.0.1:6379> INCR counter
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK
3) (integer) 1
特点与限制
特点:
- ✅ 批量执行命令,减少网络往返
- ✅ 隔离性:事务执行期间不会被其他命令插入
- ❌ 不支持原子性:某个命令失败,其他命令仍会执行
- ❌ 不支持回滚:没有 ROLLBACK 命令
WATCH 实现乐观锁:
bash
127.0.0.1:6379> WATCH balance
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY balance 100
QUEUED
127.0.0.1:6379> INCRBY other_balance 100
QUEUED
# 如果 balance 在 WATCH 后被其他客户端修改,EXEC 会失败
127.0.0.1:6379> EXEC
(nil) # 返回 nil 表示事务失败
Spring Boot 事务示例
java
@Service
public class TransactionService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 使用 SessionCallback 执行事务
*/
public void executeTransaction() {
List<Object> results = redisTemplate.execute(new SessionCallback<List<Object>>() {
@Override
public List<Object> execute(RedisOperations operations) throws DataAccessException {
// 开启事务
operations.multi();
// 批量操作
operations.opsForValue().set("key1", "value1");
operations.opsForValue().set("key2", "value2");
operations.opsForValue().increment("counter");
// 执行事务
return operations.exec();
}
});
log.info("事务执行结果: {}", results);
}
/**
* 使用 WATCH 实现乐观锁
*/
public boolean transferBalance(String fromAccount, String toAccount, double amount) {
String fromKey = "balance:" + fromAccount;
String toKey = "balance:" + toAccount;
while (true) {
try {
// 监视账户余额
redisTemplate.watch(fromKey);
Double fromBalance = (Double) redisTemplate.opsForValue().get(fromKey);
if (fromBalance == null || fromBalance < amount) {
redisTemplate.unwatch();
return false;
}
// 开启事务
Boolean result = redisTemplate.execute(new SessionCallback<Boolean>() {
@Override
public Boolean execute(RedisOperations operations) throws DataAccessException {
operations.multi();
operations.opsForValue().decrement(fromKey, amount);
operations.opsForValue().increment(toKey, amount);
List<Object> execResult = operations.exec();
// 如果 execResult 为 null,说明事务失败(WATCH 检测到变化)
return execResult != null;
}
});
if (Boolean.TRUE.equals(result)) {
return true;
}
// 事务失败,重试
log.warn("事务冲突,重试...");
} catch (Exception e) {
log.error("转账失败", e);
return false;
}
}
}
}
4.2 Lua 脚本
为什么需要 Lua?
Redis 事务的局限性:
- 不支持条件判断
- 不支持循环
- 无法保证复杂操作的原子性
Lua 脚本的优势:
- ✅ 原子执行:整个脚本作为一个整体执行
- ✅ 减少网络往返:一次性发送多个命令
- ✅ 灵活编程:支持条件判断、循环等
基本命令
bash
# 执行脚本
EVAL script numkeys key [key ...] arg [arg ...]
# 缓存脚本(通过 SHA1 调用)
SCRIPT LOAD script
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
示例 1:原子性计数器
lua
-- limit_counter.lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = ARGV[2]
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then
return 0
else
redis.call('incr', key)
redis.call('expire', key, expire_time)
return 1
end
Java 调用:
java
@Service
public class LuaScriptService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 限流器(Lua 脚本实现)
*/
public boolean rateLimit(String key, int limit, int expireSeconds) {
String luaScript =
"local key = KEYS[1]\n" +
"local limit = tonumber(ARGV[1])\n" +
"local expire_time = ARGV[2]\n" +
"\n" +
"local current = tonumber(redis.call('get', key) or \"0\")\n" +
"\n" +
"if current + 1 > limit then\n" +
" return 0\n" +
"else\n" +
" redis.call('incr', key)\n" +
" redis.call('expire', key, expire_time)\n" +
" return 1\n" +
"end";
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(key),
String.valueOf(limit),
String.valueOf(expireSeconds)
);
return result != null && result == 1;
}
}
示例 2:分布式锁释放
lua
-- release_lock.lua
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
Java 调用:
java
@Component
public class DistributedLock {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String RELEASE_LOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
/**
* 尝试获取锁
*/
public boolean tryLock(String key, String requestId, long expireMs) {
return Boolean.TRUE.equals(
redisTemplate.opsForValue()
.setIfAbsent(key, requestId, expireMs, TimeUnit.MILLISECONDS)
);
}
/**
* 释放锁(Lua 脚本保证原子性)
*/
public boolean releaseLock(String key, String requestId) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>(RELEASE_LOCK_SCRIPT, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(key),
requestId
);
return result != null && result == 1;
}
}
示例 3:批量操作
lua
-- batch_delete.lua
local keys = KEYS
local count = 0
for i, key in ipairs(keys) do
redis.call('del', key)
count = count + 1
end
return count
Java 调用:
java
public Long batchDelete(List<String> keys) {
String luaScript =
"local keys = KEYS\n" +
"local count = 0\n" +
"\n" +
"for i, key in ipairs(keys) do\n" +
" redis.call('del', key)\n" +
" count = count + 1\n" +
"end\n" +
"\n" +
"return count";
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
return redisTemplate.execute(script, keys);
}
五、分布式锁高级实现
5.1 基础分布式锁
java
@Component
@Slf4j
public class SimpleDistributedLock {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String LOCK_PREFIX = "lock:";
/**
* 获取锁
*/
public boolean lock(String key, String requestId, long expireMs) {
String lockKey = LOCK_PREFIX + key;
return Boolean.TRUE.equals(
redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, expireMs, TimeUnit.MILLISECONDS)
);
}
/**
* 释放锁
*/
public boolean unlock(String key, String requestId) {
String lockKey = LOCK_PREFIX + key;
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(lockKey),
requestId
);
return result != null && result == 1;
}
}
5.2 可重入锁
java
@Component
public class ReentrantLock {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String LOCK_PREFIX = "reentrant:lock:";
/**
* 获取可重入锁
*/
public boolean lock(String key, String requestId, long expireMs) {
String lockKey = LOCK_PREFIX + key;
// 检查是否已持有锁
Object existingValue = redisTemplate.opsForValue().get(lockKey);
if (requestId.equals(existingValue)) {
// 重入,延长锁时间
redisTemplate.expire(lockKey, expireMs, TimeUnit.MILLISECONDS);
return true;
}
// 尝试获取新锁
return Boolean.TRUE.equals(
redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, expireMs, TimeUnit.MILLISECONDS)
);
}
/**
* 释放锁
*/
public boolean unlock(String key, String requestId) {
String lockKey = LOCK_PREFIX + key;
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(lockKey),
requestId
);
return result != null && result == 1;
}
}
5.3 看门狗机制(自动续期)
java
@Component
@Slf4j
public class WatchDogLock {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String LOCK_PREFIX = "watchdog:lock:";
private static final long DEFAULT_LEASE_TIME = 30000; // 30秒
private static final long RENEWAL_INTERVAL = 10000; // 10秒续期
private ConcurrentHashMap<String, ScheduledFuture<?>> renewalTasks = new ConcurrentHashMap<>();
/**
* 获取锁并启动看门狗
*/
public boolean lockWithWatchDog(String key, String requestId) {
String lockKey = LOCK_PREFIX + key;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, DEFAULT_LEASE_TIME, TimeUnit.MILLISECONDS);
if (Boolean.TRUE.equals(locked)) {
// 启动看门狗线程
startWatchDog(lockKey, requestId);
return true;
}
return false;
}
/**
* 启动看门狗
*/
private void startWatchDog(String lockKey, String requestId) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(() -> {
try {
// 检查锁是否仍然持有
Object value = redisTemplate.opsForValue().get(lockKey);
if (requestId.equals(value)) {
// 续期
redisTemplate.expire(lockKey, DEFAULT_LEASE_TIME, TimeUnit.MILLISECONDS);
log.debug("锁续期成功: {}", lockKey);
} else {
// 锁已释放,停止看门狗
stopWatchDog(lockKey);
}
} catch (Exception e) {
log.error("看门狗续期失败", e);
}
}, RENEWAL_INTERVAL, RENEWAL_INTERVAL, TimeUnit.MILLISECONDS);
renewalTasks.put(lockKey, future);
}
/**
* 停止看门狗
*/
private void stopWatchDog(String lockKey) {
ScheduledFuture<?> future = renewalTasks.remove(lockKey);
if (future != null) {
future.cancel(false);
}
}
/**
* 释放锁
*/
public boolean unlock(String key, String requestId) {
String lockKey = LOCK_PREFIX + key;
// 停止看门狗
stopWatchDog(lockKey);
// 释放锁
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
Long result = redisTemplate.execute(
script,
Collections.singletonList(lockKey),
requestId
);
return result != null && result == 1;
}
}
5.4 实战案例:防止超卖
java
@Service
@Slf4j
public class SeckillService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private WatchDogLock distributedLock;
@Autowired
private ProductMapper productMapper;
@Autowired
private OrderMapper orderMapper;
/**
* 秒杀下单
*/
@Transactional
public boolean seckill(Long productId, Long userId) {
String lockKey = "seckill:product:" + productId;
String requestId = UUID.randomUUID().toString();
try {
// 1. 获取分布式锁
if (!distributedLock.lockWithWatchDog(lockKey, requestId)) {
throw new RuntimeException("系统繁忙,请稍后重试");
}
// 2. 查询库存
Product product = productMapper.selectById(productId);
if (product == null || product.getStock() <= 0) {
log.warn("商品库存不足: productId={}", productId);
return false;
}
// 3. 扣减库存
product.setStock(product.getStock() - 1);
productMapper.updateById(product);
// 4. 创建订单
Order order = new Order();
order.setProductId(productId);
order.setUserId(userId);
order.setCreateTime(new Date());
orderMapper.insert(order);
log.info("秒杀成功: productId={}, userId={}", productId, userId);
return true;
} finally {
// 5. 释放锁
distributedLock.unlock(lockKey, requestId);
}
}
}
六、生产环境最佳实践
6.1 Key 设计规范
java
// ✅ 推荐:使用冒号分隔,语义清晰
String key = "user:info:1001";
String key = "order:2024:5678";
String key = "product:stock:999";
// ❌ 避免:过长或无意义的 key
String key = "u_i_1001";
String key = "this_is_a_very_long_key_name_that_wastes_memory:1001";
规范要点:
- 使用
业务模块:子模块:id的层级结构 - 控制 key 长度在 20-50 字符之间
- 避免特殊字符和空格
- 统一命名风格
6.2 过期时间策略
java
/**
* 设置随机过期时间,防止缓存雪崩
*/
public void setWithRandomTtl(String key, Object value, long baseTtlSeconds) {
// 基础时间 ± 20% 随机波动
long randomOffset = (long) (baseTtlSeconds * 0.2 * Math.random());
long ttl = baseTtlSeconds + (Math.random() > 0.5 ? randomOffset : -randomOffset);
redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
}
6.3 BigKey 处理
什么是 BigKey?
- String 类型:value 超过 10KB
- List/Hash/Set/ZSet:元素数量超过 5000
危害:
- 占用大量内存
- 网络拥塞
- 阻塞 Redis 主线程
解决方案:
java
/**
* 拆分大 Hash
*/
public void splitBigHash(String bigKey, Map<String, Object> data) {
int shardCount = 10;
for (Map.Entry<String, Object> entry : data.entrySet()) {
int shard = Math.abs(entry.getKey().hashCode() % shardCount);
String shardKey = bigKey + ":" + shard;
redisTemplate.opsForHash().put(shardKey, entry.getKey(), entry.getValue());
}
}
/**
* 分批获取大集合
*/
public Set<Object> scanLargeSet(String key, int count) {
Cursor<Object> cursor = redisTemplate.opsForSet()
.scan(key, ScanOptions.scanOptions().count(count).build());
Set<Object> result = new HashSet<>();
while (cursor.hasNext()) {
result.add(cursor.next());
}
return result;
}
6.4 监控与告警
java
@Component
@Slf4j
public class RedisHealthMonitor {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Scheduled(fixedRate = 60000) // 每分钟执行
public void monitorRedisHealth() {
try {
Properties info = redisTemplate.getConnectionFactory()
.getConnection().info();
// 内存使用
long usedMemory = Long.parseLong(info.getProperty("used_memory"));
long maxMemory = Long.parseLong(info.getProperty("maxmemory"));
double memoryUsage = (double) usedMemory / maxMemory * 100;
// 连接数
long connectedClients = Long.parseLong(info.getProperty("connected_clients"));
// QPS
long ops = Long.parseLong(info.getProperty("instantaneous_ops_per_sec"));
// 命中率
long keyspaceHits = Long.parseLong(info.getProperty("keyspace_hits"));
long keyspaceMisses = Long.parseLong(info.getProperty("keyspace_misses"));
double hitRate = (double) keyspaceHits / (keyspaceHits + keyspaceMisses) * 100;
log.info("Redis健康检查 - 内存: {:.2f}%, 连接: {}, QPS: {}, 命中率: {:.2f}%",
memoryUsage, connectedClients, ops, hitRate);
// 告警
if (memoryUsage > 80) {
sendAlert("Redis内存使用率过高: " + String.format("%.2f", memoryUsage) + "%");
}
if (hitRate < 60) {
sendAlert("Redis缓存命中率过低: " + String.format("%.2f", hitRate) + "%");
}
} catch (Exception e) {
log.error("Redis健康检查失败", e);
}
}
private void sendAlert(String message) {
// TODO: 发送钉钉/企业微信/邮件告警
log.warn("告警: {}", message);
}
}
七、总结与展望
7.1 本文要点回顾
✅ 持久化机制 :深入理解 RDB 和 AOF 的原理与配置
✅ 主从复制 :掌握读写分离架构的搭建与监控
✅ 哨兵模式 :实现高可用的自动故障转移
✅ 事务与 Lua :保证复杂操作的原子性
✅ 分布式锁 :从基础到高级的完整实现
✅ 最佳实践:生产环境的配置与优化建议
7.2 下篇预告
在下一篇文章《Redis 从入门到精通:Spring Boot 实战三部曲(三)------ 高级特性与性能优化》中,我们将探讨:
- 🔥 Redis Cluster 集群架构深度解析
- ⚡ 高性能原理与底层数据结构
- 🎯 性能优化技巧与调优实战
- 🛡️ 缓存穿透/击穿/雪崩的高级解决方案
- 📊 大规模 Redis 应用的架构设计
7.3 学习建议
- 理论结合实践:不仅要理解原理,更要动手搭建环境
- 关注官方文档:Redis 更新频繁,及时了解新特性
- 性能测试:使用 redis-benchmark 进行压力测试
- 监控先行:生产环境一定要做好监控和告警
📚 参考资料
- Redis 官方文档:https://redis.io/documentation
- Redis 持久化:https://redis.io/topics/persistence
- Redis 复制:https://redis.io/topics/replication
- Redis 哨兵:https://redis.io/topics/sentinel
- 《Redis 设计与实现》- 黄健宏
觉得有用?欢迎点赞、收藏、转发!
下一篇更精彩,敬请期待! 🚀
系列文章:
- 第一篇 Redis 从入门到精通:Spring Boot 实战三部曲(一)------ 基础核心与快速上手
- 第二篇 Redis 从入门到精通:Spring Boot 实战三部曲(二)------ 进阶原理与高可用架构
- 第三篇 Redis 从入门到精通:Spring Boot 实战三部曲(三)------ 高级特性与性能优化