Spring Data Redis 4.1.0 在 WATCH 后读取,EXEC 报结果数量不匹配
Jedis 的 WATCH 与 MULTI 之间读取键,可能被错误计入事务结果,出现 Expected: 3 Actual: 2。4.1.1 已修正;异常不代表服务端写入没有发生。
乐观锁流程先 WATCH,读取当前值,再 MULTI 排入两条写命令,最后执行 EXEC,却收到:
Incorrect number of transaction results; Expected: 3 Actual: 2
在 Spring Data Redis 4.1.0 与 Jedis 7.5.2 的组合中,这可能是客户端把 WATCH 与 MULTI 之间的读取也算进了事务结果。WATCH 后、MULTI 前的 GET 在服务端即时执行,不属于 EXEC 的结果;4.1.0 的 Spring 适配却可能把它登记成事务结果,并向调用方返回 null。服务端最终只有两条排队命令,EXEC 结果数量本来就应该是二。
先保存完整异常链、实际依赖版本和命令顺序。这里对应 Jedis 的 WATCH-only 状态,不能把同一报错文字直接套到所有 Lettuce、流水线或自定义转换器故障上。
WATCH 尚未开始排队
正常的乐观锁需要先观察键,再读取决定写入所依据的值。WATCH 在当前连接上登记要监视的键;直到 MULTI 才开始把后续命令排进事务。读取放在这两步之间,是有意保留的业务语义。
4.1.0 的相关 Jedis 适配路径把“已经存在 transaction 对象”当成“正在排队”。WATCH 已创建了这个对象,但 MULTI 还没有执行,于是即时读取被登记成待转换的事务结果。EXEC 的真实返回值没有那次读取,转换时才发现数量不符。
不能为了凑齐结果而给业务多加一条写命令,也不要把读取挪到 MULTI 后面。后者改变了读取时机,事务中的读取可能只返回排队占位值,无法继续支撑原来的比较判断。删掉 WATCH 虽可能绕过这条路径,却同时撤掉了并发修改检测。
4.1.1 将监视与排队分开
Spring Data Redis 4.1.1 已正式修正这一问题,4.1.2 的正式实现也保留了区分:WATCH-only 状态下的命令在被固定的连接上即时执行,不登记进 EXEC 结果;真正执行 MULTI 后才按事务排队处理。
由 Spring Boot 或其他平台管理依赖的项目,应在已有兼容的依赖管理入口更新,确认最终装入包含修正的 Spring Data Redis 版本。直接管理 4.1.x 依赖、且平台与 Jedis 版本已经核对兼容的 Gradle 项目,可声明当前 4.1.2:
implementation("org.springframework.data:spring-data-redis:4.1.2")
这行不是让旧主版本框架跨代混装的配置。保存原依赖文件和锁定结果,核对运行包里的实际版本,而不仅是构建文件中写出的版本号。
事务操作仍须在同一条连接中完成。使用 RedisTemplate 时,应通过 SessionCallback 等现有连接绑定入口组织整个 WATCH、读取、MULTI 和 EXEC 流程;几个独立模板调用并不天然保证共享连接。发生在这个前提之外的监视丢失,需要另行处理。
转换异常发生时,写入可能已经完成
这条异常出现在客户端检查 EXEC 返回结果的阶段,不能据此断言 Redis 没有执行那两条写入。暂停对同一批非幂等操作的无条件重发,按业务 ID、目标键和值、已有幂等记录核对实际状态。计数、扣减或消息确认尤其不能简单“再执行一次”。
版本切换也不会撤销已经提交的事务。升级前保存受影响请求及其业务核对结果,通过应用原有的连接和进程生命周期部署新依赖,避免把尚未确定结果的请求一并重投。
验收要保留原来的 WATCH 后读取。没有并发冲突时,读取应得到即时值,EXEC 结果只对应 MULTI 之后的两条命令;另一个连接修改被监视键时,应走事务取消路径,EXEC 的空结果与正常提交结果分开处理。再确认连接返回池后,后续普通读写没有残留事务状态。
异常消失而监视失效,也不算修好。应同时核对成功提交和冲突取消,确认没有为了避开客户端缺陷而改变原来的并发约束。


