Spring Boot中如何确保Repository保存同步,避免重复编号?
你遇到的重复编号问题,核心原因确实和事务提交时机密切相关——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

