You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring服务并发请求下existsBy方法结果不一致问题求助

问题原因分析
  1. 事务隔离与并发可见性:默认数据库事务隔离级别(如MySQL的REPEATABLE READ)下,并发事务无法看到彼此未提交的修改。当多个请求同时处理同一个sourceId时,每个事务的existsBySourceIdAndStatusIsNot查询都会读取到相同的初始状态(无未处理记录),导致多个事务都返回PROCESSING状态。
  2. 事务传播行为冲突:getRecordStatus方法的@Transactional默认使用REQUIRED传播行为,会复用外层processRecords的事务,但不同请求的事务相互独立,无法阻止其他事务的并发查询。
  3. 无并发控制的查询逻辑:仅通过普通查询判断状态,未对目标数据加锁,无法避免并发场景下的"读-写"竞态条件。
解决方案

方案1:数据库悲观锁(单实例场景优先)

通过在查询时添加悲观写锁,强制并发事务排队执行,确保查询结果的准确性。

  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);
}
  1. 重构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。

  1. 创建数据库部分唯一索引(以MySQL为例):
CREATE UNIQUE INDEX idx_sourceid_non_processed ON records(source_id) WHERE status != 'PROCESSED';
  1. 重构业务逻辑,去掉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)保证全局并发控制。

  1. 引入Redisson依赖(以Maven为例):
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.3</version>
</dependency>
  1. 重构业务逻辑:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 00:27:11