触发器里不能用SELECT ... FOR UPDATE,因其会报错或引发死锁;正确做法是用OLD/NEW字段做前置计算校验,如IF NEW.balance + NEW.amount
触发器里不能用 SELECT ... FOR UPDATE
想在
UPDATE账户余额前查当前值做校验?别在触发器里写SELECT ... FOR UPDATE—— 大多数数据库(如 MySQL 5.7/8.0 默认事务隔离级别下)会直接报错ER_LOCK_WAIT_TIMEOUT或Can't update table 'account' in stored function/trigger。触发器运行时表锁/行锁状态复杂,显式加锁极易死锁或违反 SQL 标准限制。正确做法是:只依赖
OLD.balance和NEW.amount做前置计算校验,不查表。
- 假设业务规则是「变动后余额不得为负」,校验逻辑应写成:
IF NEW.balance + NEW.amount- 若需校验「单笔变动不得超过账户余额 20%」,则用:
ABS(NEW.amount) > 0.2 * OLD.balance(注意OLD.balance是变更前值)- MySQL 触发器中
NEW表示即将写入的行,OLD表示原行;PostgreSQL 用NEW/OLD同理,但语法略有不同(需声明BEFORE UPDATE并用RETURN NULL中断)INSERT/UPDATE/DELETE 必须分触发器处理
一个触发器只能绑定一种操作类型。想对
transaction表的插入、更新、删除都做余额校验?不能合在一个触发器里——MySQL 不支持BEFORE INSERT OR UPDATE这种写法。必须拆成三个独立触发器:
BEFORE INSERT ON transaction:校验插入的amount是否导致目标账户balance超限(需先查该账户当前余额,此处允许SELECT,因为是 INSERT 场景,无行锁冲突)BEFORE UPDATE ON transaction:重点校验amount修改是否引发余额违规(此时不能查账户表,要用OLD.account_id关联查,但更推荐把校验逻辑移到应用层或用存储过程封装)BEFORE DELETE ON transaction:通常无需余额校验,但若业务要求「已清算交易不可删」,可在此加IF OLD.status != 'cleared' THEN ...触发器无法捕获并发更新的中间态
两个事务同时更新同一账户:T1 读到余额 100,T2 也读到 100;T1 扣 80 → 写入 20;T2 扣 50 → 写入 50。最终余额变成 50,但两次扣款共 130,实际只剩 20 —— 触发器没报错,因为每次校验时看到的都是“当时有效的旧值”。
这是典型的“丢失更新”问题,触发器本身不提供串行化保障:
- 唯一可靠解法是应用层加
SELECT ... FOR UPDATE(在事务开始后、触发器执行前锁定账户行)- 或者改用带 CAS(Compare-And-Swap)语义的
UPDATE account SET balance = balance - ? WHERE id = ? AND balance >= ?,检查ROW_COUNT()是否为 1- MySQL 的
READ COMMITTED隔离级别下,触发器内SELECT看不到未提交变更,但无法避免上述竞态;只有SERIALIZABLE可缓解,但性能代价大PostgreSQL 的 AFTER ROW 触发器不适合强校验
PostgreSQL 支持
AFTER触发器,但此时数据已写入,SIGNAL抛异常会导致事务回滚——听起来可行?实际不行。因为AFTER触发器无法修改NEW行内容,也不能阻止写入发生(只是回滚),而你真正需要的是在写入前就否决非法操作。务必用
BEFORE ROW触发器,并在其中完成全部校验和修正(比如自动截断超额扣款):
- 错误写法:
AFTER UPDATE ON account→ 检查完再报错 → 数据已脏写一次- 正确写法:
BEFORE UPDATE ON account→ 计算NEW.balance := OLD.balance + NEW.delta→ 校验NEW.balance >= 0→ 不通过则RAISE EXCEPTION- 注意 PostgreSQL 中触发器函数需返回
NEW(允许修改)或NULL(取消操作),不能只RAISE就完事触发器能拦住明显违规,但拦不住并发下的逻辑漏洞。真要强一致性,得靠应用层加锁或数据库原生命令(如带
WHERE条件的原子更新),而不是指望触发器替你兜底。











