触发器无法可靠实现状态锁防重——它既不原子也不隔离,高并发下必然失效;真正可用的状态锁必须由应用层发起,通过带条件的UPDATE+ROW_COUNT校验+事务包裹来保障原子性。

触发器无法可靠实现状态锁机制防重处理——它既不原子、也不隔离,高并发下必然失效。
为什么 BEFORE UPDATE 触发器查 status 再 SET 无效
常见写法是:在 UPDATE 前查当前 status 是否为 'processing',如果是就 SIGNAL 报错。但问题在于——这个“查+判+报错”不是原子操作。两个并发事务同时读到 status = 'pending',都通过校验,接着都执行 UPDATE,最终变成双写。
- MySQL 的
SELECT ... FOR UPDATE在触发器里调用会升级为间隙锁,极易引发死锁或长等待 - PostgreSQL 的
SELECT ... FOR UPDATE同样不可嵌套在触发器中安全使用,尤其当主事务已持锁时 - 触发器内无法控制事务边界,
SIGNAL只能中断当前语句,不能回滚上游已做的其他 DML
真正可用的状态锁必须由应用层发起
数据库只负责“执行”,不负责“决策”。状态变更的原子性必须靠显式加锁 + 事务包裹来保障:
- 应用先执行
UPDATE orders SET status = 'processing' WHERE id = ? AND status = 'pending' - 检查
ROW_COUNT()(MySQL)或GET DIAGNOSTICS(PG)是否为 1 - 为 0 表示已被抢占,直接退出;为 1 才继续后续业务逻辑(如扣库存、发消息)
- 整个流程必须在一个事务内完成,且该 UPDATE 是语句级乐观锁的唯一依据
触发器在状态流转中只能做副作用,不能做判断主体
如果你坚持要用触发器,它唯一安全的位置是 AFTER UPDATE,且仅限于记录日志、更新统计字段、或调用通知服务这类不改变主业务状态的操作:
- 禁止在触发器里再 UPDATE 同一张表的其他字段(比如自动填
processed_at),否则可能触发递归或锁等待 - 禁止读取其他业务表判断是否可处理(如查用户余额),外部依赖会拖慢事务、放大延迟、破坏一致性
- MySQL 中若用了
INSERT ... SELECT ... FROM t WHERE status = 'pending'类逻辑,务必确认status字段有索引,否则全表扫描会卡死写入
替代方案:用唯一索引 + 应用层幂等键更简单可靠
比起折腾状态锁,直接在业务唯一维度上建 UNIQUE INDEX 是最省心的选择:
- 例如对订单处理,加
UNIQUE KEY (order_id, event_type),同一事件类型只允许处理一次 - 应用层生成幂等键(如
idempotency_key = order_id + '_pay_v1'),插入前不查,直接INSERT IGNORE或捕获错误码1062 - SQL Server 需捕获
ERROR_NUMBER() IN (2601, 2627);PG 捕获SQLSTATE '23505';MySQL 判断errno = 1062
状态字段本身不该承担防重职责——它的作用是标记结果,不是守门员。真正拦住重复的,永远是唯一约束和应用层的明确意图表达。










