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

Java中setField方法是否用synchronized?生产环境主键重复问题排查

重复主键问题的根源分析与解决办法

首先得明确:你怀疑的两步操作拆分导致ID复用方向是对的,但核心问题不是setId()没加锁,而是多个线程在共享同一个Bill实例!

问题发生的完整流程

虽然IdGenerator.generateId()是线程安全的(每次调用都能生成唯一ID),但如果多个线程同时操作同一个Bill对象,就会出现致命的并发覆盖:

  1. 线程A调用IdGenerator.generateId(),拿到唯一ID-A
  2. 线程B同时调用IdGenerator.generateId(),拿到唯一ID-B
  3. 线程B先执行bill.setId(ID-B),此时Bill对象的ID是ID-B
  4. 线程A紧接着执行bill.setId(ID-A),直接把Bill的ID覆盖成了ID-A
  5. 线程B去执行数据库保存时,用的是已经被覆盖的ID-A
  6. 线程A也用ID-A执行保存,最终触发数据库重复主键错误

简单说就是:两个线程生成的是不同ID,但因为共享同一个Bill对象,后执行setId的线程会覆盖先执行的,导致两个线程最终用同一个ID去保存。


针对性解决方案

方案1:每个线程使用独立的Bill实例(优先推荐)

这是从根源上解决并发问题的办法——不要在多线程间共享Bill对象。每个需要保存账单的线程,都创建自己的Bill实例:

public long saveBill() throws Exception {
    // 每个线程都新建独立的Bill对象
    Bill bill = new Bill();
    bill.setId(IdGenerator.generateId());
    // 设置其他账单属性
    return billDao.save(bill);
}

如果业务场景中必须传递Bill对象,那要确保进入多线程逻辑前,每个线程都拿到对象的深拷贝(比如通过拷贝构造方法、BeanUtils工具类实现),避免共享实例。

方案2:同步ID生成+设置的原子操作

如果确实无法避免共享Bill对象,那就要把「生成ID」和「设置ID」这两步合并成一个不可打断的原子操作,用synchronized锁住当前Bill实例:

public long saveBill(Bill bill) throws Exception {
    // 锁住共享的Bill对象,确保生成ID和设置ID的操作不会被打断
    synchronized(bill) {
        long uniqueId = IdGenerator.generateId();
        bill.setId(uniqueId);
    }
    // 执行后续保存逻辑
    return billDao.save(bill);
}

这样每个线程对同一个Bill对象,都会完整执行「生成唯一ID→设置到对象」的流程,不会出现ID被覆盖的情况。


额外检查点

建议排查saveBill方法的调用场景:是不是在异步任务、线程池任务中,不小心把同一个Bill实例传递给了多个线程?这种情况下除了ID问题,Bill的其他属性也可能出现并发修改的异常,所以优先选择方案1解决。

内容的提问来源于stack exchange,提问作者leo yang

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 07:59:04