前几天朋友阿哲半夜给我发消息,说他做的小区团购小程序出事了:后台显示某款草莓库存是负的,但订单还在继续进来,客服只能一边给顾客道歉,一边手动改库存,我问他库存是怎么扣的,他说:“图省事,订单表一插入,就用 sqltrigger 自动扣库存表。”听完我大概就明白了,这不是触发器本身有错,而是他把“自动”当成了“省心”,最后把业务逻辑塞进了一个不容易被看见、也不容易被团队记住的地方。
先用人话解释一下,SQL 触发器,也就是 sqltrigger,可以理解成数据库里的“条件反射”:某张表发生了插入、更新或删除,数据库就自动执行一段预先写好的逻辑,比如新增订单后自动扣库存、修改客户资料后自动写一条审计日志、更新文章时自动刷新更新时间,它的优点很明显:离数据近、执行自动、不容易被应用层漏掉,对很多小项目来说,刚上手时会觉得它像一个默默干活的小帮手,写完之后业务代码都清爽了不少。
阿哲当时就是这么想的,团购小程序刚上线时,订单流程很简单:用户下单,订单表新增一条记录,触发器把商品库存减掉,那段时间确实省事,应用层不用到处写扣库存代码,但生活不会一直按最初的流程图走,后来他们加了退款、取消订单、部分退款、后台手动改单,还碰上大促并发,问题来了:有的入口会更新订单状态,有的入口会直接改库存,有的客服操作又会补一条记录,触发器监听的是订单表更新,可它并不知道这次更新是“支付成功”还是“备注修改”,也不知道取消订单该退几件,结果就是库存偶尔扣两次、退一次,热门商品还因为事务里锁库存行,导致下单变慢,最尴尬的是,排查时应用日志里看不出谁扣的,最后得翻数据库触发器和操作记录,像在黑暗里找一根掉在地上的针。
这件事之后,我对 sqltrigger 的态度变得很明确:它适合做“安全网”和“记录员”,不适合当“业务大脑”,这是我个人非常鲜明的观点,触发器不是不能用,而是不能因为写起来方便,就把复杂的跨表流程、状态流转、退款计算、积分发放全塞进去,为什么呢?因为触发器最大的特点是“隐式执行”,你在应用代码里调用一个方法,至少能搜到、能打断点、能写日志;触发器藏在数据库里,新同事接手时未必知道它存在,一次批量导入、一次后台修数据、一次迁移脚本,都可能顺手触发它,它安静的时候很可爱,出事的时候很要命。
我自己也遇到过一个正面例子,以前公司客户表经常有人误改手机号、误覆盖地址,后来我们加了一个 sqltrigger,专门把客户表修改前后的关键字段写进审计表,平时几乎感觉不到它存在,直到有个新人批量导入时把一批客户手机号覆盖了,我们靠审计表里的旧值,半小时内恢复了大部分数据,从那以后我承认,触发器在审计、留痕、强制记录这类场景里,确实很香,它像一个不抢功的档案管理员,不参与业务决策,只负责把发生过的事情记下来。
所以我现在给团队的建议是:第一,触发器命名要清楚,比如按“表名_事件_用途”来写,别叫 trg_1、trg_2;第二,必须有文档,告诉后来人它为什么存在、会影响哪些操作;第三,逻辑尽量短,只做简单校验、审计或兜底,不要在触发器里调用外部服务、发消息、跑复杂计算;第四,批量操作和迁移前要评估触发器影响,该临时禁用就禁用;第五,要有监控和测试,别等库存变负数才发现它出错,说得再直白一点,sqltrigger 可以是数据库里的隐形同事,但你得给它工牌、职责说明书和边界,不然它就会变成那个“不知道什么时候冒出来帮你一下”的同事,帮得好是惊喜,帮不好是事故。
阿哲后来把扣库存逻辑挪回了业务层,触发器只保留订单变更日志和少量数据校验,又加了一个每日库存对账任务,系统没有变得多高级,但至少出了问题能顺着日志找到人、找到时间、找到操作,我的观点还是那句话:sqltrigger 不是万能药,也不是洪水猛兽,把它放在合适的位置,它是可靠的安全网;把它当业务逻辑的垃圾桶,迟早要半夜起来修库存。
