高并发下 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%,且能避免事务中断导致的数据不一致问题。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |