@Transactional与@Lock问题排查:为何触发DataIntegrityViolationException?
问题分析与解决方案
异常原因分析
- 悲观锁范围错误:
findOneForUpdate方法虽加了@Lock(LockModeType.PESSIMISTIC_WRITE)悲观写锁,但查询仅按clientId过滤,未匹配完整的ComplexKey(包含clientId、documentType、resetDate)。多线程场景下,同一clientId下不同ComplexKey的请求会干扰彼此,甚至同一ComplexKey的请求也因锁范围不精准,无法阻止并发插入操作。 - 无锁查询引发并发冲突:当
updateInCache=false时,get方法直接调用无锁的findById,多个线程同时查询同一ComplexKey会同时得到空结果,随后都执行put里的save操作,尝试插入同主键的实体,触发数据库唯一约束,抛出DataIntegrityViolationException。 - 事务与锁协同失效:尽管
@Transactional(REQUIRES_NEW)保证每个调用开启独立事务,但锁的作用范围错误导致锁无法覆盖目标实体,事务的隔离性未发挥应有的作用。
注解层面调整方案
1. 修正悲观锁的查询范围
调整findOneForUpdate的查询条件,匹配完整ComplexKey,确保锁精准锁住目标实体:
@Lock(LockModeType.PESSIMISTIC_WRITE) @Query("select e from ENumber e where e.clientId = :clientId and e.documentType = :documentType and e.resetDate = :resetDate") Optional<ENumber> findOneForUpdate( @Param("clientId") Long clientId, @Param("documentType") String documentType, // 类型根据实际业务调整 @Param("resetDate") LocalDate resetDate // 类型根据实际业务调整 );
2. 给get方法添加悲观锁
让get方法复用加锁的查询逻辑,避免无锁场景下的并发插入:
default Optional<ENumber> get(ComplexKey key) { return findOneForUpdate(key.getClientId(), key.getDocumentType(), key.getResetDate()); }
或者直接给findById添加锁注解(基于Spring Data JPA派生查询):
@Lock(LockModeType.PESSIMISTIC_WRITE) Optional<ENumber> findById(ComplexKey key);
3. 优化事务与锁的配合
@Transactional(REQUIRES_NEW)的配置无需改动,确保每个getNumber调用都在独立事务中执行,配合精准的悲观锁,就能让同一ComplexKey的并发请求串行化处理,从根源避免重复插入。
可选:强化插入/更新的原子性
作为补充优化,调整put方法的逻辑,确保在锁保护下完成实体的查询-更新/插入操作:
default void put(ComplexKey key, ComplexNumber complexNumber) { ENumber number = numberRepository.findById(key) .map(existing -> { existing.setPrefix(complexNumber.getPrefix()); existing.setNumber(complexNumber.getNumber()); return existing; }) .orElseGet(() -> new ENumber() .setKey(key) .setPrefix(complexNumber.getPrefix()) .setNumber(complexNumber.getNumber()) ); numberRepository.save(number); }
内容的提问来源于stack exchange,提问作者Evgenia Rubanova
相关产品推荐
相关产品推荐

