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

jOOQ事务未按预期执行:并发调用下重复创建数据库记录问题

问题原因

你当前的“先查询再插入”是两步非原子操作,即便包裹在事务中,由于PostgreSQL默认的Read Committed隔离级别,并行的两个事务互相看不到对方未提交的插入记录,最终都会走到插入逻辑,导致重复创建。

解决方案

1. 先给数据库加唯一约束(必做)

不管用哪种方案,先在数据库层面保证LINK_ID的唯一性,从根源杜绝脏数据:

ALTER TABLE table1 ADD CONSTRAINT unique_table1_link_id UNIQUE (link_id);

2. 方案一:使用jOOQ封装的UPSERT原子操作(推荐)

利用PostgreSQL的INSERT ... ON CONFLICT语法,把“查+插”合并为数据库层面的原子操作,彻底消除竞态:

public static RecordDto getOrCreateData(int id, DSLContext dslContext) {
    // 尝试插入,若LINK_ID冲突则不做操作,返回插入成功的记录(如果有的话)
    var inserted = dslContext.insertInto(TABLE1)
            .set(TABLE1.LINK_ID, id)
            // 补充你需要设置的其他字段
            .onConflict(TABLE1.LINK_ID)
            .doNothing()
            .returning()
            .fetchOptionalInto(RecordDto.class);

    // 插入成功则返回,否则直接查询已存在的记录
    return inserted.orElseGet(() -> 
        dslContext.selectFrom(TABLE1)
                  .where(TABLE1.LINK_ID.eq(id))
                  .fetchSingleInto(RecordDto.class)
    );
}

3. 方案二:悲观锁+异常捕获(兼容原有逻辑)

如果必须保留“查+插”的流程,可以给查询加行锁,同时捕获唯一约束冲突异常,回退到查询逻辑:

public static RecordDto getOrCreateData(int id, DSLContext dslContext) {
    try {
        return dslContext.transactionResult(config -> {
            var dsl = DSL.using(config);
            // 查询时加行锁,锁定已存在的记录
            var existingData = dsl.selectFrom(TABLE1)
                                  .where(TABLE1.LINK_ID.eq(id))
                                  .forUpdate()
                                  .fetchOptionalInto(RecordDto.class);

            if (existingData.isPresent()) {
                return existingData.get();
            }

            var record = dsl.newRecord(TABLE1);
            record.setLinkId(id);
            // 设置其他字段
            record.store();
            return record.into(RecordDto.class);
        });
    } catch (DataAccessException e) {
        // 捕获PostgreSQL唯一约束冲突异常(SQLState: 23505)
        if (e.getCause() instanceof PSQLException && ((PSQLException) e.getCause()).getSQLState().equals("23505")) {
            return dslContext.selectFrom(TABLE1)
                             .where(TABLE1.LINK_ID.eq(id))
                             .fetchSingleInto(RecordDto.class);
        }
        // 其他异常正常抛出
        throw e;
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 06:03:12