Java多线程下SELECT+INSERT校验唯一性触发唯一键约束冲突问题
解决并发场景下PostgreSQL唯一键冲突问题
问题根源
你的代码中「查询已有签名-不存在则创建」的逻辑是非原子操作,在多线程+Spring事务的场景下:
- 默认事务隔离级别为
READ_COMMITTED,线程A插入签名后未提交时,线程B无法读取到该未提交数据,会重复执行创建逻辑; - 若之前尝试用
synchronized修饰updateSignature方法,因该方法是private且属于类内部调用,Spring事务代理无法拦截,导致锁无法覆盖整个事务周期,锁失效。
可行解决方案
方案1:数据库原子操作(推荐)
利用PostgreSQL的INSERT ... ON CONFLICT语法,将「查询-插入」合并为数据库层面的原子操作,从根源避免冲突。
实现步骤:
- 在
SignatureDAO中新增方法,使用原生SQL执行原子操作:
public SignatureEntity getOrCreate(String signature) { String sql = """ INSERT INTO signature (text) VALUES (:signature) ON CONFLICT (text) DO UPDATE SET text = EXCLUDED.text RETURNING id, text """; return entityManager.createNativeQuery(sql, SignatureEntity.class) .setParameter("signature", signature) .getSingleResult(); }
- 修改
updateSignature方法,直接调用该原子方法:
private void updateSignature(HoldingEntity holding, String signature) { SignatureEntity signatureEntity = signatureDAO.getOrCreate(signature); holding.setSignature(signatureEntity); }
优点:
- 数据库层面保证原子性,无需应用层锁,性能高且可靠;
- 支持多实例部署场景。
方案2:应用层全局锁(单实例场景)
若仅单实例部署,可通过全局锁+编程式事务,确保「查询-插入」操作在同一锁和事务周期内执行。
实现步骤:
- 注入Spring事务管理器,改写为编程式事务并添加全局锁:
@Autowired private PlatformTransactionManager transactionManager; public void safeUpdateSignature(HoldingEntity holding, String signature) { // 用SignatureEntity类对象作为全局锁,确保所有线程共享同一锁 synchronized (SignatureEntity.class) { TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); try { SignatureEntity signatureInDb = signatureDAO.getByText(signature); SignatureEntity newSignature = signatureInDb != null ? signatureInDb : new SignatureEntity(); if (signatureInDb == null) { newSignature.setText(signature); signatureDAO.create(newSignature); } holding.setSignature(newSignature); transactionManager.commit(status); } catch (Exception e) { transactionManager.rollback(status); throw new HoldingServiceException("处理签名失败", e); } } }
- 在
create方法中调用该safeUpdateSignature(需改为public,确保锁生效)。
优点:
- 单实例场景下逻辑直观;
- 避免数据库层面的复杂SQL。
缺点:
- 仅支持单实例部署,多实例场景需改用Redis等分布式锁;
- 锁粒度较大,高并发下可能影响性能。
方案3:乐观冲突处理(低冲突场景)
先尝试创建签名,捕获唯一键冲突异常后再查询已有记录,适合冲突概率较低的场景。
实现步骤:
private void updateSignature(HoldingEntity holding, String signature) { try { SignatureEntity newSignature = new SignatureEntity(); newSignature.setText(signature); signatureDAO.create(newSignature); holding.setSignature(newSignature); } catch (PersistenceException e) { // 判断是否为PostgreSQL唯一键冲突(SQL状态码23505) if (e.getCause() instanceof PSQLException psqlEx && "23505".equals(psqlEx.getSQLState())) { // 冲突后查询已有记录 SignatureEntity existingSignature = signatureDAO.getByText(signature); holding.setSignature(existingSignature); } else { // 非冲突异常,重新抛出 throw new HoldingServiceException("处理签名失败", e); } } }
优点:
- 无需锁,性能最高;
- 实现简单。
缺点:
- 冲突频繁时会产生大量异常,影响性能和日志可读性。
内容的提问来源于stack exchange,提问作者Nux
相关产品推荐
相关产品推荐

