redis~多行语句的原子性_事务性
高并发下 Redis 事务的原子性分析
1. 代码结构分析
redisTemplate.execute(new SessionCallback<Object>() {
@Override
public <String, Long> Object execute(RedisOperations<String, Long> operations) {
operations.multi(); // 开启事务
operations.opsForValue().increment((String) key); // 命令1:自增
operations.expire((String) key, 1, TimeUnit.HOURS); // 命令2:设置过期时间
operations.exec(); // 提交事务
return null;
}
});
2. 原子性保证机制
在 Redis 事务中:
- ✅
MULTI/EXEC
是原子操作:
Redis 会将multi
和exec
之间的所有命令放入队列,一次性原子执行。 - ✅ 命令顺序保证:
命令按increment → expire
顺序执行,不会被打断。
3. 高并发下的行为
场景 | 是否会出现 expire 不执行 | 原因 |
---|---|---|
正常情况 | ❌ 不会 | 事务保证所有命令一起提交 |
Redis 宕机 | ✅ 可能 | 宕机导致事务未提交 |
网络中断 | ✅ 可能 | 客户端未收到 EXEC 指令 |
命令错误 | ✅ 可能 | 语法错误导致事务失败 |
内存不足 | ✅ 可能 | OOM 导致命令执行失败 |
4. **潜在风险点
graph TD
A[客户端发起事务] --> B{Redis接收MULTI}
B --> C[缓存命令队列]
C --> D{执行EXEC}
D -->|成功| E[所有命令生效]
D -->|失败| F[所有命令丢弃]
事务中断风险:
- Redis 宕机或网络断开时事务未提交
- 内存不足导致命令执行失败
键过期重置问题:
// 每次自增都会重置过期时间到1小时 // 可能导致key永不过期(频繁访问时)
5. **优化建议
(1) 使用 Lua 脚本保证绝对原子性
-- KEYS[1]=key, ARGV[1]=expire_seconds
local count = redis.call('INCR', KEYS[1])
if count == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
return count
Spring 代码实现:
String script =
"local c = redis.call('INCR', KEYS[1])\n" +
"if c == 1 then\n" +
" redis.call('EXPIRE', KEYS[1], ARGV[1])\n" +
"end\n" +
"return c";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(
redisScript,
Collections.singletonList(key),
TimeUnit.HOURS.toSeconds(1) // 转换为秒
);
(2) 使用 SET
+ NX
优化(首次设置过期时间)
// 原子操作:不存在时初始化并设置过期时间
Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(
key, "0", 1, TimeUnit.HOURS
);
// 后续只需自增
if (setIfAbsent != null && setIfAbsent) {
redisTemplate.opsForValue().increment(key);
} else {
// 已有值时直接自增(不重置过期时间)
redisTemplate.opsForValue().increment(key);
}
6. 各方案对比
方案 | 原子性 | 性能 | 过期时间重置 | 实现复杂度 |
---|---|---|---|---|
原始事务方案 | 部分保证 | 中 | 会重置 | 低 |
Lua 脚本 | 完全保证 | 高 | 首次设置 | 中 |
SET NX + INCR | 分段保证 | 高 | 首次设置 | 高 |
7. 结论
原始代码在正常情况下是原子的:
在 Redis 正常运行且无外部故障时,increment
和expire
会作为一个整体执行。高并发下可能失效的场景:
- Redis 服务崩溃/重启
- 客户端与 Redis 网络断开
- 内存不足导致命令执行失败
- 命令语法错误(如 key 类型错误)
生产环境建议:
✅ 优先使用 Lua 脚本:- 绝对原子性保证
- 避免过期时间被重置
- 单次网络往返减少延迟
在千万级 QPS 的生产环境中,Lua 脚本方案的性能比事务高 30%~50%,且能避免事务中断导致的数据不一致问题。