触发器中禁止执行 select ... for update,因数据库为防死锁限制显式加锁;应改用唯一索引+lock_status字段实现逻辑锁,触发器仅作校验而非加锁。

触发器里不能执行 SELECT ... FOR UPDATE
想在 BEFORE UPDATE 或 BEFORE INSERT 触发器里直接对关联单据加行锁(比如 SELECT * FROM order_header WHERE id = NEW.order_id FOR UPDATE),多数数据库会报错。MySQL 8.0+ 明确禁止在触发器中使用显式锁定读,PostgreSQL 则在函数中调用 SELECT ... FOR UPDATE 会触发「cannot execute SELECT FOR UPDATE in a function called from trigger」错误。
根本原因:触发器运行在语句事务上下文中,而加锁操作需明确归属事务生命周期,数据库为避免锁范围不可控、死锁风险升高,主动限制该行为。
用唯一约束 + 业务字段模拟“逻辑锁”
真正可落地的做法,是把“加锁”转化为“写入互斥标识”。例如在单据主表增加 lock_status 字段(TINYINT 或 BOOLEAN),默认 0,锁定时设为 1,并配合唯一索引强制排他:
ALTER TABLE order_header ADD COLUMN lock_status TINYINT DEFAULT 0; CREATE UNIQUE INDEX uk_order_locked ON order_header (id) WHERE lock_status = 1;
然后在触发器中检查并尝试置锁:
以 PostgreSQL 为例,在 BEFORE UPDATE 触发器函数中:
IF NEW.lock_status = 1 THEN
PERFORM 1 FROM order_header
WHERE id = NEW.id AND lock_status = 1
FOR UPDATE NOWAIT; -- 防止并发抢锁时卡住
IF NOT FOUND THEN
RAISE EXCEPTION 'order % is already locked', NEW.id;
END IF;
END IF;
关键点:
-
FOR UPDATE NOWAIT必须加,否则可能无限等待,拖垮整个事务 - 唯一索引必须带
WHERE lock_status = 1条件,否则全表唯一会误拦正常单据 - 应用层更新前需先
UPDATE order_header SET lock_status = 1 WHERE id = ? AND lock_status = 0,靠影响行数判断是否抢锁成功
触发器只做校验,不负责加锁动作
触发器的合理角色是“守门员”,不是“执行者”。它适合做以下事情:
- 检查当前单据是否已被标记为锁定(查
lock_status或独立锁表) - 验证操作人是否有权修改该状态(比对
NEW.operator_id和已记录的locked_by) - 拒绝非法状态跃迁(如从「已审核」直接跳到「草稿」)
- 记录变更前快照到审计表(
INSERT INTO order_audit ... SELECT OLD.*)
但不要让它去调用 pg_advisory_lock() 或尝试更新锁表——这类操作容易因事务嵌套、锁超时或异常退出导致锁残留。
高并发下仍需应用层配合重试与超时
即使加了唯一索引和触发器校验,两个请求几乎同时执行 UPDATE ... SET lock_status = 1,仍会有一个拿到唯一键冲突(MySQL 报 1062 Duplicate entry,PostgreSQL 报 23505 unique_violation)。这时候必须由应用捕获该错误,并主动重试(带指数退避)或返回「单据正被他人编辑」提示。
容易被忽略的是:锁标识字段本身没加索引,会导致校验变慢甚至全表扫描。务必确保 lock_status 或组合条件(如 (id, lock_status))有高效索引支撑。










