SQL Server中SAVE TRANSACTION不是事务边界,仅是回滚锚点;整个执行链仍属最外层事务,ROLLBACK会全部回滚,无法局部保留。

SQL Server 里 SAVE TRANSACTION 不是事务边界
很多人以为 SAVE TRANSACTION savepoint_a 创建了一个可独立回滚的“子事务”,实际它只是个回滚锚点,不改变事务本质。整个执行链仍属于最外层 BEGIN TRANSACTION,@@TRANCOUNT 只增不嵌套。一旦最终触发 ROLLBACK(哪怕只写一次),所有改动全丢——包括你本想保留的那部分。
常见错误是:在 TRY/CATCH 里对保存点做 ROLLBACK TRANSACTION savepoint_a,看似局部回滚成功;但后续异常没捕获住,外层又执行了一次 ROLLBACK,结果之前所有操作都消失了。
-
SAVE TRANSACTION后不能靠它隔离逻辑块,它不提供原子性保障 - 不要在
TRY外裸写BEGIN TRANSACTION,容易导致@@TRANCOUNT失控 - 检查
XACT_STATE()而非只看@@TRANCOUNT > 0:值为-1时事务已不可提交,必须ROLLBACK,COMMIT必报错
MySQL 中 START TRANSACTION 会隐式提交前一个事务
在 MySQL 存储过程里写两次 START TRANSACTION,第二次执行时,系统会自动提交第一次开启但尚未 COMMIT 的事务——不是挂起,是立刻落库。这意味着中间插入/更新的数据已经不可逆写入磁盘,后续 ROLLBACK 只能撤回第二次事务里的操作。
典型现象是:执行完存储过程后查表,发现部分数据还在,以为回滚失败;其实是前一段早就被隐式提交了。
- MySQL 不支持嵌套事务,
START TRANSACTION= 隐式COMMIT+ 新事务启动 - 唯一安全的分段控制手段是
SAVEPOINT,它只打标记、不启新事务 -
SAVEPOINT sp_name名字必须在同一事务内唯一;重复定义会覆盖,不是报错 - 异常处理器里要用
ROLLBACK TO SAVEPOINT sp_name,而非全局ROLLBACK
事务上下文断裂:autocommit=1 是最大隐形杀手
无论 SQL Server 还是 MySQL,只要连接处于 autocommit=1 模式,存储过程里的每条语句都是独立事务,ROLLBACK 根本无事可滚。这是回滚失效最普遍却最容易被忽略的原因。
Go 驱动(如 go-mssqldb)默认 autocommit=true;MySQL Workbench 连接初始化也会重置该值。你写了 BEGIN 和 ROLLBACK,但底层连接根本不认。
- 查当前状态:
SELECT @@autocommit;(MySQL)或SELECT DATABASEPROPERTYEX(DB_NAME(), 'IsAutoCommit')(SQL Server) - 显式关闭:
SET autocommit = 0;(MySQL)或连接字符串加autocommit=false(SQL Server) - DDL 操作(
ALTER TABLE、CREATE INDEX)会强制隐式提交,事务中应避免混用 - ORM(如 Django、SQLAlchemy)可能接管事务控制,需确认其配置是否绕过了你的手动事务
嵌套调用放大锁持有时间与死锁风险
表面是“一个事务里调多个过程”,实际锁范围和持有时间被不可控拉长。比如事务 A 先 UPDATE orders,再调用含 UPDATE customers 的过程(复用同一事务),而事务 B 恰好反序访问——死锁闭环就形成了。
更隐蔽的是隐式锁升级:内层过程执行 SELECT * FROM order_items WHERE order_id = @id,但 order_id 没索引,触发全表扫描+页锁,瞬间锁住上千行,远超业务真正需要的几行。
- 检查执行计划中是否出现
Clustered Index Scan或Index Scan,这是锁失控信号 - 所有被嵌套调用过程的
WHERE条件字段必须建索引,且类型严格匹配(INT字段别传字符串) - 禁止在事务中调用含外部依赖的过程(如
OPENQUERY、EXEC xp_cmdshell),它们不释放锁但卡住几秒 - 扁平化拆分 + 显式锁序比模拟嵌套更可靠:应用层统一控制事务边界,并按固定顺序访问表(如
accounts → transactions → audit_log)
事务嵌套问题的本质,不是语法写错,而是对“事务上下文”“锁生命周期”“引擎行为差异”的误判。每个数据库对“嵌套”的实现完全不同,强行统一抽象只会踩坑。真正关键的,是看清当前连接的状态、当前语句触发的实际行为、以及调用链中每一层是否真的共享同一个事务ID。

















