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

Java多线程下SELECT+INSERT校验唯一性触发唯一键约束冲突问题

解决并发场景下PostgreSQL唯一键冲突问题

问题根源

你的代码中「查询已有签名-不存在则创建」的逻辑是非原子操作,在多线程+Spring事务的场景下:

  1. 默认事务隔离级别为READ_COMMITTED,线程A插入签名后未提交时,线程B无法读取到该未提交数据,会重复执行创建逻辑;
  2. 若之前尝试用synchronized修饰updateSignature方法,因该方法是private且属于类内部调用,Spring事务代理无法拦截,导致锁无法覆盖整个事务周期,锁失效。

可行解决方案

方案1:数据库原子操作(推荐)

利用PostgreSQL的INSERT ... ON CONFLICT语法,将「查询-插入」合并为数据库层面的原子操作,从根源避免冲突。

实现步骤:

  1. 在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();
}
  1. 修改updateSignature方法,直接调用该原子方法:
private void updateSignature(HoldingEntity holding, String signature) {
    SignatureEntity signatureEntity = signatureDAO.getOrCreate(signature);
    holding.setSignature(signatureEntity);
}

优点:

  • 数据库层面保证原子性,无需应用层锁,性能高且可靠;
  • 支持多实例部署场景。

方案2:应用层全局锁(单实例场景)

若仅单实例部署,可通过全局锁+编程式事务,确保「查询-插入」操作在同一锁和事务周期内执行。

实现步骤:

  1. 注入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);
        }
    }
}
  1. 在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 21:51:17