raiserror不会自动回滚事务,必须显式执行rollback transaction;推荐改用throw配合set xact_abort on以确保事务可靠终止。

SQL Server 中 RAISERROR 不自动回滚事务
RAISERROR 只是抛出错误消息,不会终止事务执行流。你看到错误日志,但 INSERT/UPDATE 已经落库,是因为没显式写 ROLLBACK TRANSACTION。常见误写:RAISERROR('库存不足', 16, 1) 后直接 RETURN,没回滚——事务仍处于“已修改未提交”状态,后续语句可能继续执行,甚至被外层 COMMIT。
必须确保:RAISERROR 所在分支里紧跟着 ROLLBACK TRANSACTION,且该语句一定会被执行(比如放在 IF 条件末尾,或 TRY...CATCH 的 CATCH 块内)。更稳妥的做法是改用 THROW(SQL Server 2012+),它默认中断批处理,但需配合 SET XACT_ABORT ON 才能可靠回滚整个事务。
-
RAISERROR严重级 -
@@TRANCOUNT在嵌套触发器中会累加,但一次ROLLBACK TRANSACTION就能回到最外层起点 - 不要在
TRY块里只RAISERROR而不ROLLBACK,否则事务挂起,可能阻塞其他会话
MySQL 触发器 SIGNAL 失效的隐性原因
SIGNAL SQLSTATE '45000' 理论上会让主语句失败并回滚,但实际不生效,往往是因为连接处于自动提交模式(autocommit=1)。此时单条 INSERT 就是独立事务,SIGNAL 只让它自己失败,没有“回滚”可言——因为根本没开启事务上下文。
另一个常见陷阱是 sql_mode 缺失 STRICT_TRANS_TABLES。例如字段类型不匹配、插入 NULL 到 NOT NULL 字段时,MySQL 会静默截断或设默认值,跳过触发器校验逻辑,导致 SIGNAL 根本没机会执行。
- 执行
SELECT @@autocommit, @@sql_mode确认连接状态 - BEFORE 触发器里
SIGNAL有效;AFTER 触发器里再SIGNAL无法撤回主语句已做的变更 - SHOW WARNINGS 必须紧跟 DML 语句执行,否则触发器内部错误被吞掉,看不到真实报错
触发器报错被应用层静默吞掉
触发器抛出异常(如 THROW 或约束冲突)后,如果应用代码没捕获 SQLException 或对应驱动异常(如 Go 的 sql.ErrNoRows),错误会透传到线程顶层,造成 HTTP 500、连接中断甚至进程崩溃。更隐蔽的是:某些 ORM 或中间件把异常转成空响应或超时,让你误以为“没报错”,其实数据根本没写入。
尤其要注意 Spring 的 UnexpectedRollbackException:这不是你主动回滚的,而是触发器内 ROLLBACK 后,Spring 在 commit 阶段发现事务已被数据库硬终结,才包装抛出的。日志里看不到原始触发器错误,只看到 “Transaction rolled back because it has been marked as rollback-only”。
- .NET 应用需在中间件里捕获
SqlException,检查Number是否 ≥ 50000(自定义错误号) - Java JDBC 需确保
executeUpdate()调用外有 try/catch,不能依赖 Spring 默认的@Transactional回滚机制 - Go 使用
go-mssqldb时,连接字符串必须含autocommit=false,否则tx.Rollback()形同虚设
DDL 或隐式提交语句让事务提前终结
在事务里混用 DDL(如 ALTER TABLE)、用户权限操作(如 GRANT)或管理语句(如 OPTIMIZE TABLE),会导致 MySQL 或 SQL Server 在执行到该语句时**隐式提交当前事务**。后面再抛异常,只能回滚该语句之后的操作,前面的 DML 已落地。
典型场景:存储过程中先 UPDATE,再 CREATE TEMPORARY TABLE,最后触发器报错——结果是临时表创建成功,UPDATE 也已提交,只有触发器后的逻辑被回滚。
- MySQL 中
TRUNCATE TABLE、DROP TABLE都会隐式提交 - SQL Server 中
CREATE INDEX、DBCC类命令同样不可回滚 - 用
SHOW ENGINE INNODB STATUS(MySQL)或DBCC OPENTRAN(SQL Server)确认事务是否真正在运行,而非被悄悄提交











