在MySQL触发器开发过程中,一个常被忽略的隐患是全局用户变量@var的滥用。许多开发者习惯用它暂存中间结果,但绝大多数情况下,这种写法会引发难以排查的并发问题。本文将从原理到实践,详细剖析这一陷阱,并提供可靠的替代方案。

全局变量 @var 在触发器中极度不可靠
在触发器里使用@var存储临时值,看似方便快捷,实则埋下了“上线即崩”的隐患。@var并非局部变量,而是会话级用户变量,其生命周期贯穿整个会话——只要同一数据库连接未断开,@var的值就会一直保留。这意味着什么?在高并发插入场景下,A用户的@counter可能被B用户的同名赋值覆盖,最终A读到的是B的值,导致数据错乱。
具体来看,NEW和OLD是只读的行别名,无法直接赋值给@var。虽然SET @id = NEW.id;语法能够通过,但后续用@id参与计算或条件判断时,它已经成为一个不可靠的会话级变量。更棘手的是,触发器的执行路径并非唯一——IF分支未执行、LEAVE提前退出、或同时有多条INSERT触发,@var的值可能来自上一次调用,而非当前行。调试时尤其容易误判:SELECT @var返回某个值,你以为“生效了”,实则只是恰巧未被其他操作修改而已。
替代 @var 的正确方案:必须声明局部变量
要存储NEW.status、OLD.amount这类中间值,唯一可靠的方式是使用DECLARE声明的局部变量,且必须在BEGIN后第一行进行声明。正确写法如下:BEGIN DECLARE l_status VARCHAR(20); SET l_status = NEW.status;。命名时建议添加前缀(如l_),避免与参数或字段名冲突;切勿与@var同名,否则SELECT l_total和SELECT @total极易混淆。此外,嵌套的BEGIN END块也需要各自DECLARE,外层声明的变量在内层不可见,不会自动继承——这一点很多人容易忽略。
为什么 PREPARE 动态 SQL 中也不能直接使用局部变量?
PREPARE语句不识别DECLARE变量,传参只能依赖@用户变量,这是MySQL的硬性限制。错误写法示例:PREPARE stmt FROM 'UPDATE t SET x = ? WHERE id = ?'; EXECUTE stmt USING v_id, v_val;——这里v_id是局部变量,EXECUTE时会被当作NULL处理,结果必然出错。可行的方案是:先SET @v_id = v_id;,再EXECUTE stmt USING @v_id;。注意:@变量在PREPARE场景下是必要的妥协,但仅限此处;其他逻辑一律回归DECLARE加SET。
BEFORE 与 AFTER 触发器对变量使用的根本差异
变量本身在BEFORE和AFTER触发器中并无区别,但使用场景受Timing严格限制。只有BEFORE触发器才能通过SET NEW.field = ...修改即将写入的值;AFTER触发器中NEW是只读的,更无法通过@var间接修改。若试图在AFTER触发器中写SET NEW.status = 'done',会直接报错:ERROR 1362 (HY000)。如果逻辑需要“先查旧值、再算新值、最后更新”,必须拆分为两步:BEFORE中用DECLARE存储OLD/NEW字段,AFTER中用这些值去更新其他表。切勿为了省事,在BEFORE中塞入一堆@var赋值,再在AFTER中读取——两个触发器无法共享局部变量,@var又不可靠,纯属自找麻烦。
真正容易被忽略的核心要点是:局部变量作用域和声明位置是硬性语法要求,而非风格建议。哪怕只差一行(比如BEGIN后先写了SELECT再DECLARE),整个触发器就会创建失败;而@var表面能跑通,实则已在生产环境埋下并发脏数据隐患。因此,编写触发器时,养成使用DECLARE局部变量的习惯,比什么都重要。
