Java中setField方法是否用synchronized?生产环境主键重复问题排查
重复主键问题的根源分析与解决办法
首先得明确:你怀疑的两步操作拆分导致ID复用方向是对的,但核心问题不是setId()没加锁,而是多个线程在共享同一个Bill实例!
问题发生的完整流程
虽然IdGenerator.generateId()是线程安全的(每次调用都能生成唯一ID),但如果多个线程同时操作同一个Bill对象,就会出现致命的并发覆盖:
- 线程A调用
IdGenerator.generateId(),拿到唯一ID-A - 线程B同时调用
IdGenerator.generateId(),拿到唯一ID-B - 线程B先执行
bill.setId(ID-B),此时Bill对象的ID是ID-B - 线程A紧接着执行
bill.setId(ID-A),直接把Bill的ID覆盖成了ID-A - 线程B去执行数据库保存时,用的是已经被覆盖的ID-A
- 线程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
相关产品推荐
相关产品推荐

