不能用触发器防止同一订单并发重复支付,因其查判抛错非原子操作,高并发下必然失效;真正可行的是应用层用带条件的update+事务+行数校验实现原子性防重。

不能用触发器防止同一订单并发重复支付——它既不原子也不隔离,高并发下必然失效;真正能落地的方案必须由应用层发起带条件的 UPDATE + 事务包裹 + 行数校验。
为什么 BEFORE/AFTER 触发器查 status 再 SIGNAL 会失效
典型错误写法是:在 BEFORE INSERT 或 AFTER INSERT 中查 payments 表是否存在同 order_id 的已支付记录,存在就 THROW。问题在于——这个“查+判+抛错”不是原子操作。
- 两个并发请求同时执行插入前查询,都看到
status = 'pending',都通过校验 - 接着都进入插入流程,最终生成两条支付记录
- SQL Server 的
SELECT ... WITH (UPDLOCK, HOLDLOCK)在触发器里调用风险极高:可能引发死锁、锁升级成表级、或被主事务锁阻塞而超时 - 触发器运行在语句级上下文中,
THROW只能中止当前INSERT,但无法撤回上游已做的其他 DML(比如应用层先UPDATE orders SET payment_status = 'processing')
真正可用的防重支付流程(应用层主导)
数据库只负责执行,不参与决策。原子性必须靠一次带条件的 UPDATE 完成:
- 应用执行:
UPDATE payments SET status = 'paid' WHERE order_id = @order_id AND status = 'pending' - 立即检查
@@ROWCOUNT:为0表示已被抢占,直接返回“支付已存在”;为1才继续后续动作(如更新订单主表、发消息) - 整个流程必须包裹在
BEGIN TRAN→UPDATE→IF @@ROWCOUNT = 1 ...→COMMIT中 -
payments(order_id)字段必须有索引,否则WHERE条件全表扫描会拖垮并发性能
如果坚持要用触发器,它只能放在 AFTER UPDATE 且仅做副作用
例如记录日志、调用外部通知服务、更新统计字段——但绝不能反向修改 orders 表或读取用户余额等外部状态:
- 禁止在触发器里再
UPDATE同一张payments表(比如自动填processed_at),可能触发递归或锁等待 - 禁止
SELECT其他业务表判断是否可支付(如查账户余额),这会拉长事务时间、放大延迟、破坏一致性 - 若业务需要“24 小时内同一订单只允许支付一次”,这种复杂规则应由应用层控制,而不是塞进触发器里查时间窗口
最常被忽略的一点:哪怕用了上述正确流程,也必须确保 payments(order_id, status) 有联合索引;否则 WHERE order_id = ? AND status = 'pending' 仍会走全表扫描,锁住整张表,把并发能力压到个位数。










