乐观锁更新问题:请求命令中Version字段未被使用的困惑
乐观锁更新用户数据:命令版本字段的作用与无竞态实现方案
我需要实现一个更新用户数据的方法,满足以下要求:
- 无竞态条件
- 请求提交更新时必须提供对象当前版本,数据库中版本自增1
- 提供错误版本时抛出
ObjectOptimisticLockingFailureException异常
我已经在Person实体类上使用了@Version注解,但要求请求必须携带包含version字段的UpdatePersonCommand命令,而当前代码里这个version字段并未被使用,我不清楚这个要求的意义何在。同时,手动校验实体与命令的版本号会引发竞态条件,用原生查询也不安全,求解决方案。
相关代码如下:
Person实体类
public class Person implements Serializable{ @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private Long id; private String firstName; private String lastName; @Version protected Long version; }
UpdatePersonCommand类
@Data public class UpdatePersonCommand{ private Long version; @NotNull private String firstName; @NotNull private String lastName; }
Controller代码
@PatchMapping("/{id}") public ResponseEntity<PersonDto> update(@PathVariable Long id, UpdatePersonCommand command) throws IOException { return ResponseEntity.status(HttpStatus.OK).body(personService.update(id, command)); }
Service代码
@Override @Transactional public PersonDto update(Long personId, UpdatePersonCommand command) throws IOException { try { Person person = personRepository.findById(personId) .orElseThrow(() -> new EntityNotFoundException(MessageFormat .format("Person with id={0} not found", personId))); return objectMapper.updateValue(person, command); } catch (OptimisticLockException e) { throw new ObjectOptimisticLockingFailureException(e.getMessage(),"The person has been modified by another transaction"); } }
一、命令中version字段的意义
请求携带version字段的核心目的有两个:
- 提前快速失败:如果客户端提交的版本与数据库当前版本不一致,无需执行后续更新操作,直接返回错误,避免浪费数据库资源。
- 明确操作意图:确保客户端是基于自己已知的最新数据版本进行更新,防止客户端使用过时数据(stale data)执行无效更新。
它是Hibernate乐观锁机制的前置补充,虽然Hibernate本身会在事务提交时校验版本,但提前校验能更早反馈错误给客户端。
二、当前代码的问题
- 错误覆盖version字段:使用
objectMapper.updateValue会把UpdatePersonCommand中的version覆盖到Person实体,破坏Hibernate乐观锁的校验逻辑——Hibernate依赖实体加载时的版本值来生成带版本条件的更新语句,手动修改version会导致校验逻辑混乱。 - 缺少前置版本校验:未对比客户端提交的version与实体当前version,导致客户端传错版本时,要等到事务提交阶段才会抛出异常,无法提前失败。
三、无竞态条件的解决方案
结合@Version注解的原生乐观锁机制,配合前置版本校验,即可满足所有要求:
修正后的Service代码
import org.springframework.orm.ObjectOptimisticLockingFailureException; import java.util.Objects; @Override @Transactional public PersonDto update(Long personId, UpdatePersonCommand command) { // 1. 加载实体 Person person = personRepository.findById(personId) .orElseThrow(() -> new EntityNotFoundException( String.format("Person with id=%d not found", personId))); // 2. 前置版本校验:快速失败 if (!Objects.equals(command.getVersion(), person.getVersion())) { throw new ObjectOptimisticLockingFailureException( "The person has been modified by another transaction", null); } // 3. 仅更新业务字段,不修改version person.setFirstName(command.getFirstName()); person.setLastName(command.getLastName()); // 4. 保存实体(Hibernate自动处理version自增与乐观锁校验) try { Person updatedPerson = personRepository.save(person); return convertToPersonDto(updatedPerson); } catch (OptimisticLockException e) { // 捕获并发更新导致的乐观锁异常,转换为指定异常 throw new ObjectOptimisticLockingFailureException( "The person has been modified by another transaction", e); } } // 补充DTO转换方法示例 private PersonDto convertToPersonDto(Person person) { PersonDto dto = new PersonDto(); dto.setId(person.getId()); dto.setFirstName(person.getFirstName()); dto.setLastName(person.getLastName()); dto.setVersion(person.getVersion()); return dto; }
关键逻辑说明
- 前置版本校验:对比客户端提交的version与实体当前version,不一致则直接抛出指定异常,实现快速失败。
- 避免修改version字段:仅更新业务字段(firstName、lastName),Hibernate会自动管理version的自增。
- Hibernate原生乐观锁保障:事务提交时,Hibernate会生成如下SQL:
其中UPDATE person SET first_name=?, last_name=?, version=? WHERE id=? AND version=?WHERE version=?是加载实体时的版本值,若此时数据库中version已被其他事务修改,该SQL会返回0行更新,Hibernate自动抛出OptimisticLockException,我们再将其转换为指定的异常。
为什么不会有竞态条件?
- 前置校验后,即使其他事务在当前事务提交前修改了version,Hibernate的update语句会通过
WHERE version=?条件原子性地校验版本,只有版本匹配的事务能成功更新,其他事务会触发乐观锁异常,完全避免了竞态条件。
内容的提问来源于stack exchange,提问作者IvaneP
相关产品推荐
相关产品推荐

