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
相关产品推荐
相关产品推荐

