SQL中如何实现SELECT判断后执行INSERT 规避并发重复插入问题
PostgreSQL 原子化实现「不存在则插入」规避并发重复写入
针对多并发下的竞态重复插入问题,不要在应用层拆分SELECT判断+INSERT写入两步操作,直接用PostgreSQL提供的原生原子语法即可,配合唯一约束可以100%拦住重复数据。
核心前置要求
必须先给判重维度加数据库级别的唯一约束,这是所有方案生效的基础,任何代码层判断都替代不了数据库约束的兜底作用。
你的判重逻辑是「同个product_id当日只允许一条payment记录」,因为created_at一般是带时分秒的时间戳类型,需要建表达式唯一索引:
CREATE UNIQUE INDEX idx_payment_product_daily ON payment (product_id, (created_at::date));
推荐写法:INSERT ... ON CONFLICT(PG 9.5+支持)
这是性能最好、并发安全性最高的方案,整条语句是原子执行的,不会出现两个请求同时通过判断、重复插入的问题。
INSERT INTO payment (product_id, created_at, amount /* 其余需要插入的字段 */) VALUES ('1', CURRENT_TIMESTAMP, 99.9 /* 对应字段的值 */) -- 冲突判定字段和上面建的唯一索引保持一致 ON CONFLICT (product_id, (created_at::date)) DO NOTHING -- 加RETURNING可以直接拿到插入结果:返回行代表插入成功,无返回代表当日该产品已存在记录 RETURNING id, product_id, created_at;
不推荐但可用的写法:INSERT ... SELECT ... WHERE NOT EXISTS
如果暂时没法加唯一约束(非常不建议长期这么用),可以用下面的写法,注意高并发场景下这个写法依然有小概率出现重复,需要把事务隔离级别提升到可串行化才安全,性能损耗更大:
INSERT INTO payment (product_id, created_at, amount /* 其余字段 */) SELECT '1', CURRENT_TIMESTAMP, 99.9 /* 对应字段值 */ WHERE NOT EXISTS ( SELECT 1 FROM payment WHERE product_id = '1' AND created_at::date = CURRENT_DATE ) RETURNING id, product_id, created_at;
Django ORM 对应实现
不要自己手写filter().exists()判断后再调用create(),这是典型的应用层非原子判断,并发下必然会出现重复插入。
- 先在模型里定义唯一约束,执行
makemigrations和migrate生效:
from django.db import models from django.db.models.functions import TruncDate class Payment(models.Model): product = models.ForeignKey('Product', on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) amount = models.DecimalField(max_digits=10, decimal_places=2) # 其余字段定义 class Meta: constraints = [ models.UniqueConstraint( 'product', TruncDate('created_at'), name='uniq_payment_product_per_day' ) ]
- 插入数据直接用Django内置的
get_or_create即可,ORM会自动适配数据库的原子冲突处理逻辑:
from datetime import date payment, is_created = Payment.objects.get_or_create( product_id='1', created_at__date=date.today(), defaults={ 'amount': 99.9 # 其余非判重字段的默认插入值放在这里 } ) # is_created为布尔值:True代表本次新插入成功,False代表当日该产品已有记录
如果需要更灵活的控制,也可以直接用数据库游标执行上面写的原生ON CONFLICT语句,拿到返回结果判断状态。
常见误区说明
- 不要以为把SELECT和INSERT放在同一个事务里就安全,默认的读已提交隔离级别下,两个并发事务完全可能同时执行SELECT查到记录不存在,再依次执行INSERT产生重复数据
- 加表级排他锁虽然能解决重复问题,但会把写入操作串行化,吞吐量会大幅下降,远不如数据库原生的ON CONFLICT效率高
- 唯一约束是最后一道兜底,哪怕代码逻辑写漏了分支,数据库层面也不会允许重复数据写入,不会产生脏数据
内容的提问来源于stack exchange,提问作者devnext
相关产品推荐
相关产品推荐

