redis事务在spring boot中“不生效”的根本原因是误用sessioncallback且未正确配合watch与multi/exec;必须手动通过redisconnection严格按watch→multi→操作→exec顺序执行,或改用lua脚本实现原子性。

Redis事务在Spring Boot中“不生效”,根本不是没调用exec(),而是你误用了SessionCallback——它根本不支持WATCH语义,也不保证MULTI/EXEC原子连贯执行。
setEnableTransactionSupport(true)只对RedisTemplate高层API起作用
这个配置仅影响opsForValue().set()、boundHashOps().put()等模板方法:开启后,它们发出的命令会进入事务队列,直到exec()才真正提交。但它不改变连接行为,也不绑定WATCH生命周期。
- 如果你在
setEnableTransactionSupport(true)后直接调用redisTemplate.opsForValue().get("key"),返回null——因为事务未提交,读不到中间态 - 它不能和
WATCH混用:WATCH必须紧接MULTI,而opsForValue()这类调用会绕过事务上下文,直接发命令,导致WATCH失效 - 该模式下无法做条件判断:比如“先GET再决定是否SET”,因为GET本身不在事务内,且不触发WATCH保护
SessionCallback不是事务封装器,而是命令批处理器
SessionCallback的设计目标是复用RedisConnection对象批量执行命令,它不隐含MULTI/EXEC,也不感知WATCH。你在里面写connection.watch(key),不代表后续操作就自动进事务。
- 常见错误:在
SessionCallback里先watch(),再调用redisTemplate.opsForValue().set()——后者会走另一条路径,脱离当前连接上下文,WATCH立即作废 - 更隐蔽的坑:Lettuce连接池可能复用连接,导致WATCH在A请求中设置,却被B请求的
multi()意外触发,或被连接回收清空 - 正确做法:所有操作必须用同一个
RedisConnection实例,且顺序为watch() → multi() → set()/get() → exec(),中间不能穿插任何高层API调用
WATCH+EXEC在Spring中真正生效的唯一方式
放弃SessionCallback封装幻想,手动控制连接与命令流。这是目前最稳定、可验证的路径:
- 用
redisTemplate.execute(new SessionCallback<object>() { ... })</object>获取RedisConnection - 在回调内严格按顺序调用:
connection.watch(key.getBytes())→connection.multi()→connection.set(...)→connection.exec() - 所有键/值必须转
byte[],避免序列化干扰;不要用StringRedisTemplate的字符串方法 - 如果
exec()返回null,说明WATCH失败(key被改写),此时需重试或降级,不能当作成功
Lua脚本才是多数场景下的务实替代方案
如果你只是想“检查某key存在再更新”,或者“计数器自增并限制上限”,别硬刚WATCH事务。Redis原生命令+Lua天然原子,且不受连接复用、线程调度影响:
- 用
redisTemplate.execute(new DefaultRedisScript<long>("return redis.call('incr', KEYS[1])", Long.class))</long>替代手动事务 - Lua中可自由组合
exists、get、set、del,无竞态、无回滚逻辑、无连接绑定负担 - 注意:Lua脚本长度建议控制在10KB内,避免阻塞主线程;KEYS和ARGV必须显式传入,不能动态拼接
WATCH真正的复杂点不在语法,而在于它把一致性保障从Redis推给了应用层:你得自己处理exec()返回null的重试逻辑、超时退避、业务幂等。很多所谓“事务不生效”,其实是没意识到——Redis的事务从来就不是ACID里的那个“事务”。











