Spring服务并发请求下existsBy方法结果不一致问题求助
问题原因分析
- 事务隔离与并发可见性:默认数据库事务隔离级别(如MySQL的
REPEATABLE READ)下,并发事务无法看到彼此未提交的修改。当多个请求同时处理同一个sourceId时,每个事务的existsBySourceIdAndStatusIsNot查询都会读取到相同的初始状态(无未处理记录),导致多个事务都返回PROCESSING状态。 - 事务传播行为冲突:
getRecordStatus方法的@Transactional默认使用REQUIRED传播行为,会复用外层processRecords的事务,但不同请求的事务相互独立,无法阻止其他事务的并发查询。 - 无并发控制的查询逻辑:仅通过普通查询判断状态,未对目标数据加锁,无法避免并发场景下的"读-写"竞态条件。
解决方案
方案1:数据库悲观锁(单实例场景优先)
通过在查询时添加悲观写锁,强制并发事务排队执行,确保查询结果的准确性。
- 修改Spring Data Repository方法,添加悲观锁注解:
import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.LockModeType; public interface RecordRepository extends JpaRepository<Record, Long> { // 加悲观写锁,查询时锁定符合条件的行 @Lock(LockModeType.PESSIMISTIC_WRITE) Optional<Record> findBySourceIdAndStatusIsNot(String sourceId, RecordStatus status); }
- 重构
getRecordStatus方法:
@Transactional(isolation = Isolation.REPEATABLE_READ) public RecordStatus getRecordStatus(String sourceId) { var existingUnprocessedRecord = recordRepository.findBySourceIdAndStatusIsNot(sourceId, PROCESSED); return existingUnprocessedRecord.isPresent() ? WAITING : PROCESSING; }
原理:悲观锁会锁定查询到的行,其他事务必须等待当前事务提交/回滚后才能访问该行,彻底避免并发查询的不一致问题。
方案2:数据库唯一约束(高效且可靠)
通过数据库层面的部分唯一索引,强制同一sourceId只能存在一条非PROCESSED状态的记录,冲突时降级为WAITING。
- 创建数据库部分唯一索引(以MySQL为例):
CREATE UNIQUE INDEX idx_sourceid_non_processed ON records(source_id) WHERE status != 'PROCESSED';
- 重构业务逻辑,去掉
getRecordStatus方法:
@Transactional public void processRecords(List<IncomingRecordDto> inputRecords, SomeRequest someRequest) { for (IncomingRecordDto inputRecord : inputRecords) { var sourceId = getSourceId(inputRecord); internalRecord.setSourceId(sourceId); try { internalRecord.setStatus(PROCESSING); internalRecord = recordRepository.save(internalRecord); } catch (DataIntegrityViolationException e) { // 唯一约束冲突,说明已有未处理记录,改为WAITING状态 internalRecord.setStatus(WAITING); internalRecord = recordRepository.save(internalRecord); } } }
原理:数据库层面的约束是最可靠的并发控制手段,避免了应用层查询的竞态条件,性能也优于悲观锁。
方案3:分布式锁(多实例部署场景)
如果服务是多实例集群部署,数据库锁无法跨实例生效,需要使用分布式锁(如Redis Redisson)保证全局并发控制。
- 引入Redisson依赖(以Maven为例):
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.23.3</version> </dependency>
- 重构业务逻辑:
import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import java.util.concurrent.TimeUnit; @Autowired private RedissonClient redissonClient; @Transactional public void processRecords(List<IncomingRecordDto> inputRecords, SomeRequest someRequest) { for (IncomingRecordDto inputRecord : inputRecords) { var sourceId = getSourceId(inputRecord); RLock lock = redissonClient.getLock("record:processing:" + sourceId); try { // 尝试获取锁,5秒超时 if (lock.tryLock(5, TimeUnit.SECONDS)) { // 加锁后再次查询,确保状态准确 boolean existsUnprocessed = recordRepository.existsBySourceIdAndStatusIsNot(sourceId, PROCESSED); internalRecord.setStatus(existsUnprocessed ? WAITING : PROCESSING); internalRecord = recordRepository.save(internalRecord); } else { // 未获取到锁,直接设为WAITING internalRecord.setStatus(WAITING); internalRecord = recordRepository.save(internalRecord); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Failed to acquire lock for sourceId: " + sourceId, e); } finally { // 确保锁释放 if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } }
原理:分布式锁保证同一sourceId在集群范围内只能被一个实例的事务处理,避免跨实例的并发冲突。
内容的提问来源于stack exchange,提问作者Mykhailo Yablon
相关产品推荐
相关产品推荐

