基于SpringBoot的乐观锁实现与并发测试问题排查
测试用例是否规范?
从核心维度排查测试用例的规范性:
- 并发触发时机是否可控:如果没借助
CountDownLatch这类同步工具,保证多个线程同时读取旧版本数据再执行更新,可能出现一个线程已提交更新、版本号递增后,另一个线程才开始读取,不会触发乐观锁冲突,导致本该失败的接口返回200。 - 事务边界是否合理:若测试方法加了
@Transactional,所有线程操作会处于同一事务内,JPA一级缓存会让线程读取相同旧版本,但事务提交时才触发版本校验,可能出现异常被事务管理器吞掉的情况。 - 断言逻辑是否准确:本该失败的接口,是否明确断言返回500?如果接口内部捕获乐观锁异常后返回了带错误提示的200响应,断言500就会出错;重试接口若因重试次数不足、或重试时未重新查询最新版本数据,最终仍会失败返回500。
乐观锁实现是否正确?
逐一核对实现细节:
- @Version注解使用是否正确:实体类的版本字段必须用
@Version标注,类型建议用Long或Integer,且不能手动修改,需由JPA自动维护。如果字段类型错误(比如用String)或未被JPA扫描到,乐观锁不会生效。 - 更新方法是否在事务内:JPA乐观锁校验仅在事务提交时触发,所以更新方法必须被
@Transactional修饰。无事务的话,版本字段不会自动递增,也不会触发版本冲突检查。 - @Retryable配置是否生效:
- 项目是否加了
@EnableRetry开启重试功能? @Retryable是否指定了正确的异常类型(比如value = OptimisticLockingFailureException.class)?未指定的话,不会捕获乐观锁异常进行重试。- 重试时是否重新查询最新数据?如果复用旧的实体对象(版本还是旧值),再次更新依然会失败,必须重新从数据库查询最新版本再执行更新。
- 项目是否加了
- 异常处理逻辑是否正确:直接提示用户的接口,是否正确捕获
OptimisticLockingFailureException并返回对应错误响应(比如自定义错误信息+200,或明确的500状态码)?如果异常未被捕获,会直接抛到Spring MVC返回500,导致结果不符合预期。
如何验证功能有效性?
按以下步骤逐步验证:
- 单线程基础验证:
- 插入一条测试数据,记录初始版本号
v0。 - 调用更新接口,检查数据库版本号是否递增到
v1。 - 手动把数据库版本号改回
v0,再次调用更新接口,确认是否触发OptimisticLockingFailureException,验证乐观锁生效。
- 插入一条测试数据,记录初始版本号
- 可控并发测试:
- 初始化一条数据,版本号
v0。 - 创建两个线程,每个线程逻辑:先查询数据(获取
v0),等待同步信号,再执行更新。 - 用
CountDownLatch让两个线程同时开始更新,观察结果:- 直接提示的接口:一个线程返回成功(版本变为
v1),另一个应返回乐观锁错误(比如500或自定义提示)。 - 带重试的接口:若重试次数≥1,失败线程会重试,重新查询到
v1后更新成功,最终两个线程都返回200。
- 直接提示的接口:一个线程返回成功(版本变为
- 初始化一条数据,版本号
- 日志与SQL排查:
- 开启JPA SQL日志(
spring.jpa.show-sql=true),观察更新语句是否包含版本条件:UPDATE table SET ... WHERE id=? AND version=?。 - 在更新方法、重试逻辑中加日志,打印线程ID、当前版本号、重试次数,追踪每次操作的版本变化和异常触发情况。
- 开启JPA SQL日志(
- 重试逻辑专项验证:
设置重试次数为1,执行并发测试,查看失败线程是否触发重试,且重试时是否重新查询最新版本数据,最终是否更新成功。
内容的提问来源于stack exchange,提问作者Roberto Guardascione
相关产品推荐
相关产品推荐

