如何避免PostgreSQL并发插入时的唯一索引冲突问题?
为什么带
NOT EXISTS的INSERT仍会触发唯一索引冲突? PostgreSQL默认事务隔离级别是READ COMMITTED,在这个级别下,事务只能读取已提交的数据。当双击按钮触发两个并发请求时:
- 事务A执行
NOT EXISTS检查,此时数据库无对应(event_id, person_id)记录,通过检查后准备插入但未提交; - 同时事务B执行相同的
NOT EXISTS检查,由于事务A的插入未提交,事务B看不到这条数据,同样通过检查并准备插入; - 事务A先提交后,事务B再提交时就会触发唯一索引冲突——因为此时数据库已存在该记录。
本质是NOT EXISTS检查与插入操作并非原子执行,并发场景下会出现“检查-插入”的竞态条件。
架构层面的解决方案
1. 用PostgreSQL 9.5原生INSERT ... ON CONFLICT语法(最推荐)
Postgres 9.5刚好支持该语法,让数据库原子性处理冲突,彻底规避竞态:
INSERT INTO production.event_attendee(event_id, person_id) VALUES (?, ?) ON CONFLICT (event_id, person_id) DO NOTHING;
逻辑是:尝试插入记录,若触发(event_id, person_id)唯一约束冲突则不执行任何操作,整个过程是原子的。
2. 调整事务隔离级别(备选)
将事务隔离级别提升至REPEATABLE READ:
- 在Spring Boot中可通过
@Transactional(isolation = Isolation.REPEATABLE_READ)配置; - 该级别下事务执行
NOT EXISTS后,会保持一致的数据快照,后续其他事务的插入不会干扰当前事务判断。但需注意,并发冲突时会抛出SerializationFailureException,代码中需捕获并重试事务。
3. 实现接口幂等性
从接口层面拦截重复请求:
- 前端发起请求时生成唯一幂等键(如UUID),放在请求头或参数中;
- 后端收到请求后,先检查幂等键是否已处理(可存储在Redis或数据库幂等日志表);
- 若已处理直接返回成功,未处理则执行插入并标记幂等键为已处理。
4. 后端接口防重复提交
通过短时间锁拦截重复请求:
- 以用户ID、event_id、person_id为维度,用Redis设置一个短过期时间的锁(如2秒);
- 第一个请求进来时加锁并处理,第二个请求发现锁存在时,直接返回“操作处理中”或成功响应,避免重复执行。
内容的提问来源于stack exchange,提问作者Walker Farrow
相关产品推荐
相关产品推荐

