sql server触发器不适用于双向增量同步,易导致循环触发、死锁和数据不一致;应采用“单向捕获+应用层协调”模式,触发器仅本地记录变更并写入带sourcesystem标识的日志表,消费者按sourcesystem过滤自身变更。

SQL Server 触发器无法可靠支撑双向增量同步 —— 它天生不是为这个场景设计的。 一旦两个系统都通过触发器写对方的日志表,就会陷入循环触发、死锁、事务嵌套失败或数据状态不一致。真正可行的方案是「单向捕获 + 应用层协调」,触发器只负责把本库变更「单向记录」,同步逻辑由外部服务(如 Spring Boot 消费者)按业务规则判断是否反向写回。
为什么 trg_after_insert_update_delete 会引发双向冲突
常见错误是给 obj_Company 表建一个触发器,同时监听 INSERT/UPDATE/DELETE,再让下游系统也对同一张表写入 —— 这会导致:
- 下游写入触发本库触发器,本库又写日志表,日志表变更再被下游读取,形成无限循环
-
INSERTED和DELETED在 UPDATE 场景下同时非空,但若下游同步延迟,可能旧值还没写完,新值已覆盖,oldData字段存的是中间态而非真实前镜像 - 事务跨库时,SQL Server 本地事务无法控制下游 MySQL 或 MongoDB 的提交/回滚,一旦下游失败,本库日志已落盘,无法原子回退
trg_log_only_with_context 是唯一安全的写法
触发器只做一件事:把当前数据库的变更,以不可篡改、带上下文的方式写进本地日志表。关键约束必须硬编码:
- 日志表字段必须包含
sourceSystem(如'ERP'或'CRM'),由触发器固定写死,不能靠应用传参 - 禁止在触发器里调用链接服务器(
OPENQUERY)、远程存储过程或EXEC外部命令 - 所有 JSON 序列化必须用
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER,避免 NULL 字段导致解析失败 - 对大字段(如
text、xml)做显式CAST或CONVERT,否则FOR JSON会报错
示例片段:
CREATE TRIGGER trg_objCompany_log ON obj_Company AFTER INSERT, UPDATE, DELETE AS BEGIN SET NOCOUNT ON; DECLARE @action NVARCHAR(10), @src SYSNAME = 'ERP'; <p>IF EXISTS(SELECT <em> FROM inserted) AND NOT EXISTS(SELECT </em> FROM deleted) SET @action = 'INSERT'; ELSE IF EXISTS(SELECT <em> FROM deleted) AND NOT EXISTS(SELECT </em> FROM inserted) SET @action = 'DELETE'; ELSE SET @action = 'UPDATE';</p><p>INSERT INTO monitorLog (actionType, oldData, newData, tableName, sourceSystem, gmtCreate) SELECT @action, CASE WHEN @action = 'UPDATE' THEN (SELECT <em> FROM deleted d WHERE d.id = i.id FOR JSON PATH, WITHOUT_ARRAY_WRAPPER) ELSE NULL END, (SELECT </em> FROM inserted i FOR JSON PATH, WITHOUT_ARRAY_WRAPPER), 'obj_Company', @src, GETDATE() FROM inserted i; END</p>
sync_consumer_must_ignore_self_changes 是消费者端铁律
Spring Boot 消费者从 RabbitMQ 拉到消息后,必须检查 sourceSystem 字段:
- 如果消息里
sourceSystem = 'ERP',且当前服务代表 CRM 系统,则执行同步; - 如果
sourceSystem = 'CRM',则直接丢弃 —— 这说明该变更就是本系统刚发出的,防止自产自销; - 绝对不要依赖消息里的
tableName或id做去重,因为不同系统主键策略可能冲突(比如 ERP 用 bigint 自增,CRM 用 UUID); - 消费者处理完后,应往 MQ 发送一条确认消息,含
ackId和targetSystem='ERP',供对端校验闭环。
最易被忽略的一点:触发器日志表本身不能加任何业务索引以外的约束。比如在 monitorLog 上加唯一索引限制 (tableName, id, actionType),看似防重,实则会让批量 UPDATE 失败 —— 因为同一批操作中,id 相同但 actionType 可能从 UPDATE 变成 DELETE(比如先改再删),索引冲突直接中断整个事务。











