在业务系统里,用户重复点击、网络重试或分布式调用超时补偿,都会让同一笔请求被多次发往数据库。如果只在应用层做判断,并发瞬间仍可能穿透校验。借助数据库触发器,可以把幂等性控制下沉到存储层,从根源上阻止重复SQL写入。

为什么需要触发器做幂等性校验
应用层幂等方案通常依赖分布式锁或先查后写,但在高并发场景下,两个线程可能同时查到“无记录”然后都执行插入。数据库触发器运行在事务的最前沿,能够利用事务隔离级别和唯一约束,在写操作真正落盘前做同步拦截,避免竞态条件带来的重复数据。
另外,当系统由多个服务共用同一数据库时,并非所有写入都经过统一的应用网关。触发器作为数据库自身的规则,无论请求来自哪个客户端,只要执行对应的DML语句就会生效,这让幂等策略具备更强的通用性和强制力。
核心设计思路
实现触发器幂等校验的关键是定义“什么是一次重复操作”。通常我们会抽取业务维度上的唯一标识,例如订单号、用户ID加操作类型的组合,并在表中建立唯一索引。触发器在BEFORE INSERT阶段查询是否已存在相同标识的记录,若存在则抛出错误或忽略当前操作。
下面以MySQL为例,先建立一张订单表,并将订单号设为业务唯一键。注意这里用普通索引配合触发器判断,而不是单纯依赖唯一约束,是为了能在触发器中灵活返回自定义错误信息或做静默丢弃。
CREATE TABLE order_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(64) NOT NULL, user_id BIGINT NOT NULL, amount DECIMAL(10,2), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_order_no (order_no) ) ENGINE=InnoDB;
编写幂等校验触发器
在MySQL中,我们可以使用BEFORE INSERT触发器,在插入前用SELECT ... FOR UPDATE锁定可能冲突的行,防止并发插入。如果查到已有记录,则通过SIGNAL语句抛出异常,使整个事务回滚。
以下触发器逻辑先声明一个变量用于计数,然后在同一事务内查询订单号是否存在。若存在则主动报错,应用层捕获该异常后即可认为“重复提交已被拦截”,从而安全返回友好提示。
DELIMITER $$
CREATE TRIGGER trg_order_idempotent
BEFORE INSERT ON order_record
FOR EACH ROW
BEGIN
DECLARE v_count INT DEFAULT 0;
SELECT COUNT(1) INTO v_count
FROM order_record
WHERE order_no = NEW.order_no
FOR UPDATE;
IF v_count > 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '重复订单提交,已拦截';
END IF;
END$$
DELIMITER ;
对于希望静默忽略而非报错的业务,也可以将IF分支改为SET NEW.order_no = NULL并配合非空约束失败来丢弃,但更推荐显式报错,方便链路追踪。
触发器方案与应用层方案对比
我们将两种常见做法放在一张表中对照,便于团队根据场景取舍。
| 维度 | 应用层校验 | 触发器校验 |
|---|---|---|
| 防并发穿透 | 弱,依赖锁实现 | 强,事务内同步拦截 |
| 跨服务一致性 | 需统一网关 | 数据库级强制生效 |
| 维护成本 | 低,代码可见 | 中,需懂数据库对象 |
| 错误排查 | 日志清晰 | 需查数据库错误码 |
从表中可以看出,触发器更适合作为兜底防线,而不是完全替代应用层幂等。实际生产中,常采用“应用层防重令牌加数据库触发器兜底”的双重策略。
注意事项与避坑点
首先要避免触发器中出现长时间运行的逻辑,因为触发器在DML事务内执行,会拖慢主流程并增加锁等待。上面的COUNT查询配合索引是最轻量的做法,切勿在触发器里调用外部存储过程或发HTTP请求。
其次,如果业务表存在软删除,仅按order_no判断可能误拦历史已作废订单。此时应将删除标记纳入WHERE条件,例如AND deleted = 0,确保只有有效记录才参与幂等比对。最后,迁移环境时要记得连同触发器一起备份,很多团队只导表结构而丢了触发器,导致幂等防护悄悄失效。
完整调用示例
下面模拟两次相同订单插入,第二次会被触发器拦截。我们在MySQL客户端依次执行即可观察效果。
-- 第一次插入,成功
INSERT INTO order_record (order_no, user_id, amount)
VALUES ('NO202405001', 1001, 99.00);
-- 第二次插入,触发器报错:重复订单提交,已拦截
INSERT INTO order_record (order_no, user_id, amount)
VALUES ('NO202405001', 1001, 99.00);
应用层使用JDBC或ORM框架时,捕获SQL异常中的消息文本,就能向用户返回“请勿重复提交”的提示,而不必担心底层是否已经落库。
总结
利用数据库触发器实现幂等性校验,是把重复操作拦截在存储层的高效手段。它通过事务内的存在性判断,弥补了应用层并发漏洞,尤其适合多入口写入和强一致要求的场景。只要控制好触发器逻辑复杂度并配合唯一索引,就能用很小的改造成本换来计算可靠的防重能力。