Redis事务与Lua脚本
Spring 事务用起来简单,但失效场景非常多。很多人遇到过 @Transactional 不生效的情况,却不知道原因。本文把日常开发中常见的坑和对应的排查思路整理出来,帮你避免踩坑。
一、Redis事务
#
1.1 事务基础
Redis事务通过MULTI、EXEC、WATCH等命令实现:
MULTI # 开启事务 |
#
1.2 事务命令
| 命令 | 作用 |
|---|---|
| MULTI | 开启事务,后续命令进入队列 |
| EXEC | 执行事务队列中的所有命令 |
| DISCARD | 取消事务,清空队列 |
| WATCH | 监视一个或多个key,如果被修改则事务失败 |
| UNWATCH | 取消对所有key的监视 |
#
1.3 事务示例
# 无WATCH的简单事务 |
#
1.4 Java中使用事务
|
#
1.5 Redis事务的特点
优点:
- 命令批量执行,减少网络往返
- WATCH提供乐观锁机制
局限性:
- 不支持回滚:如果事务中某条命令执行失败,其他命令仍会执行
- 没有隔离级别概念:事务中的命令不会阻塞其他客户端
- 不支持条件判断:不能根据查询结果决定后续操作
# 事务中命令失败的例子 |
二、Lua脚本
#
2.1 为什么需要Lua
Redis事务的局限性促使我们使用Lua脚本:
- Lua脚本可以包含逻辑判断
- Lua脚本在Redis中原子执行
- 可以减少网络往返
#
2.2 执行Lua脚本
# 方式一:直接执行 |
#
2.3 Lua脚本基础语法
-- 调用Redis命令 |
#
2.4 Lua脚本实现原子操作
# 安全扣减库存 |
#
2.5 Java中使用Lua脚本
|
#
2.6 复杂Lua脚本示例
实现可重入分布式锁:
-- 加锁脚本 |
|
三、事务 vs Lua脚本对比
| 特性 | 事务 | Lua脚本 |
|---|---|---|
| 原子性 | 命令批量执行 | 脚本整体原子执行 |
| 逻辑判断 | 不支持 | 支持 |
| 回滚 | 不支持 | 不支持(但可通过逻辑避免) |
| 网络往返 | 减少(批量) | 极少(一次发送) |
| 复杂度 | 低 | 中高 |
| 可维护性 | 好 | 较差(脚本管理) |
| 适用场景 | 简单批量操作 | 需要逻辑判断的原子操作 |
四、实际应用场景
#
4.1 秒杀系统
-- 秒杀扣减脚本 |
#
4.2 滑动窗口限流
-- 滑动窗口限流脚本 |
#
4.3 排行榜更新
-- 原子更新排行榜并获取排名 |
五、Lua脚本最佳实践
#
5.1 脚本管理
|
#
5.2 脚本调试
# 使用redis-cli调试Lua脚本 |
#
5.3 性能注意
Lua脚本执行时: |
六、总结
| 场景 | 推荐方案 |
|---|---|
| 简单批量写入 | 事务(MULTI/EXEC) |
| 需要条件判断 | Lua脚本 |
| 库存扣减 | Lua脚本 |
| 分布式锁 | Lua脚本 |
| 限流计数 | Lua脚本 |
| 简单转账 | 事务+WATCH |
选择建议:
- 简单批量操作:使用事务
- 需要逻辑判断:使用Lua脚本
- 性能敏感:使用Lua脚本(减少网络往返)
- 复杂业务:考虑在应用层实现,Redis只做存储
核心要点
事务失效的常见原因:非 public 方法、自调用、异常被吞掉、错误的传播级别
事务传播级别决定了方法之间的事务关系,REQUIRED 是默认值
使用 @Transactional(rollbackFor = Exception.class) 确保异常回滚
编程式事务在某些场景下比声明式事务更灵活
总结
事务管理是保证数据一致性的关键。理解事务的工作机制和常见陷阱,能帮你写出更健壮的代码。在实际项目中,合理配置事务边界非常重要。