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

Spring Boot中如何确保Repository保存同步,避免重复编号?

解决JPA同步块中重复编号的事务提交问题

你遇到的重复编号问题,核心原因确实和事务提交时机密切相关——saveAndFlush()仅会将数据刷入数据库的持久化上下文,但如果当前事务尚未提交,其他线程可能在事务完成前就读取到旧的编号值。另外,如果你的方法整体被@Transactional注解修饰,那整个方法的事务会在方法结束时才提交,synchronized(this)的作用范围如果只覆盖了编号获取和保存,同步块内的操作其实还是处于同一个未提交的事务中,这就导致其他线程仍可能读到未更新的编号。

下面给你几个可行的解决方案:

1. 缩小事务范围,用独立事务强制提交

把编号生成和保存的逻辑单独抽出来,放在一个带有REQUIRES_NEW事务传播属性的方法里,强制开启新事务并在方法执行完毕后立即提交,保证编号的修改立刻写入数据库:

// 原方法,移除或调整事务注解
public Protocolo processProtocolo(Protocolo protocolo) {
    // 调用独立事务方法处理编号
    protocolo = generateAndSaveProtocoloNumber(protocolo);
    
    // 其他保存文件等逻辑
    saveFile(protocolo.getFile());
    
    return protocolo;
}

// 独立事务方法,强制开启新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
private synchronized Protocolo generateAndSaveProtocoloNumber(Protocolo protocolo) {
    Long number = geradorProtocoloService.getNumero(protocolo.getTipo());
    protocolo.setNumber(number);
    return protocoloRepository.saveAndFlush(protocolo);
}

这里REQUIRES_NEW会让该方法启动一个全新的事务,方法结束时自动提交,确保其他线程再获取编号时能拿到最新值,同时synchronized保证同一时间只有一个线程进入这个方法。

2. 同步块内手动提交事务(应急方案,不推荐)

如果不想重构方法,可以在同步块内手动获取事务管理器强制提交,但这种方式侵入性强,容易破坏事务的原子性:

@Autowired
private PlatformTransactionManager transactionManager;

public Protocolo processProtocolo(Protocolo protocolo) {
    synchronized (this) {
        Long number = geradorProtocoloService.getNumero(protocolo.getTipo());
        protocolo.setNumber(number);
        protocolo = protocoloRepository.saveAndFlush(protocolo);
        
        // 手动提交当前事务
        TransactionStatus status = TransactionAspectSupport.currentTransactionStatus();
        transactionManager.commit(status);
    }
    
    // 注意:后续文件保存逻辑会在新事务中执行,若需要和编号操作同事务则不适用
    saveFile(protocolo.getFile());
    
    return protocolo;
}

3. 数据库层面锁保证唯一性(最可靠)

Java层面的synchronized在分布式场景下会失效,即使单实例也可能受事务隔离级别影响。最稳妥的方式是让数据库来控制编号唯一性:
比如在geradorProtocoloService.getNumero()中,用SELECT ... FOR UPDATE锁定对应类型的记录,或者直接用更新返回的方式获取最新编号:

-- 假设编号表为protocolo_sequence,包含tipo和last_number字段
UPDATE protocolo_sequence 
SET last_number = last_number + 1 
WHERE tipo = ? 
RETURNING last_number;

这种方式下数据库会锁定目标记录,其他线程必须等待当前事务提交后才能修改,从根源上避免重复编号。

另外提醒下:如果你的类是多实例(比如Spring配置了多实例),synchronized(this)的锁会失效,这时候可以改用静态锁对象,比如private static final Object LOCK = new Object();,然后synchronized(LOCK)。

内容的提问来源于stack exchange,提问作者Fabio Ebner

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 07:01:04