TiDB 如何用保存点(SAVEPOINT)回滚事务中的部分修改
【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidb
一批对账任务在长事务里写到一半时校验失败,整条ROLLBACK会把前面已核对通过的几千行写入全部作废。TiDB 保存点提供标准保存点语法:在事务内打一个检查点,用ROLLBACK TO SAVEPOINT只撤销其后的修改,事务本身保持可用,可以继续写入并提交。乐观、悲观两种事务模式都支持,但有三个前置约束:必须处于显式事务中、实例不能开启 binlog、悲观事务要求 in-place constraint check 开启。
快速开始:最小可运行示例
这步在悲观事务中演示保存点撤销部分写入:
DROP TABLE IF EXISTS t_batch; CREATE TABLE t_batch(id int, val int, UNIQUE INDEX idx(id)); BEGIN PESSIMISTIC; INSERT INTO t_batch VALUES (1, 1); SAVEPOINT s1; INSERT INTO t_batch VALUES (2, 2); -- 假设这行数据有问题 ROLLBACK TO s1; -- 只撤销 (2,2) INSERT INTO t_batch VALUES (2, 20); -- 事务仍可用 COMMIT; SELECT * FROM t_batch;预期输出(示例结果):
id val 1 1 2 20使用前的检查项
| 检查项 | 不满足时的表现 | 仓库内出处 |
|---|---|---|
| binlog 必须未启用 | SAVEPOINT is not supported when binlog is enabled | pkg/executor/simple.go#L659-L660、设计文档 2022-07-22-transaction-savepoint.md 的 Warning 节 |
必须处于显式事务(BEGIN之后) | 无报错,保存点被静默忽略,后续ROLLBACK TO报[executor:1305]SAVEPOINT s1 does not exist | pkg/executor/simple.go#L665-L667 |
悲观事务需tidb_constraint_check_in_place_pessimistic开启 | savepoint is not supported in pessimistic transactions when in-place constraint check is disabled | pkg/executor/simple.go#L668-L670 |
同名重复执行SAVEPOINT | 不报错:旧记录先删除,再在新位置压入检查点 | pkg/sessionctx/variable/session.go#L504-L515 |
语法与用法要点
保存点名统一转小写后存储,s1与S1指向同一记录,不存在 MySQL 中大小写敏感的坑(pkg/sessionctx/variable/session.go#L506):
SAVEPOINT s1; ROLLBACK TO SAVEPOINT s1; -- 与 ROLLBACK TO s1 等价 RELEASE SAVEPOINT s1;ROLLBACK TO找到保存点后执行 MemDB 检查点回滚,事务不终止(pkg/executor/simple.go#L802-L810)。
完整流程拆解
步骤一:建表并开启悲观事务
目的:准备数据并进入显式事务,使保存点真正生效。
DROP TABLE IF EXISTS t_batch; CREATE TABLE t_batch(id int, val int, UNIQUE INDEX idx(id)); BEGIN PESSIMISTIC; INSERT INTO t_batch VALUES (1, 1), (3, 3);该步验证的边界:BEGIN之前的SAVEPOINT是空操作,必须在事务内执行。
步骤二:打保存点后写入"问题数据"
目的:把检查点插在坏数据之前,为回滚留出分界。
SAVEPOINT s1; INSERT INTO t_batch VALUES (4, 4); SELECT * FROM t_batch ORDER BY id;预期输出(示例结果):
id val 1 1 3 3 4 4该步验证的边界:事务内查询能看到未提交写入,说明检查点记录在 MemDB 上而非落盘数据。
步骤三:回滚到保存点
目的:撤销 s1 之后的 (4,4),保留 (1,1)、(3,3)。
ROLLBACK TO s1; SELECT * FROM t_batch ORDER BY id;预期输出(示例结果):
id val 1 1 3 3该步验证的边界:回滚只撤销检查点之后的修改,事务仍处于活跃状态。
步骤四:继续写入并提交
目的:证明回滚后事务可继续写入并正常提交。
INSERT INTO t_batch VALUES (4, 40); COMMIT; SELECT * FROM t_batch ORDER BY id;预期输出(示例结果):
id val 1 1 3 3 4 40该步验证的边界:落盘数据只包含"最后一次回滚点之前加其后新写入"的行,(4,4) 不出现。
容易误判的语义
- 栈式删除。
ROLLBACK TO s1会丢弃 s1 之后的全部保存点,且RELEASE SAVEPOINT同样删除目标之后的所有保存点(pkg/sessionctx/variable/session.go#L529-L548)。后果:依次建 s1、s2、s3 后回退到 s2,再执行ROLLBACK TO s3直接报[executor:1305]SAVEPOINT s3 does not exist,脚本若按"栈还能恢复"的假设编写会意外中断。 - 非事务中静默忽略。autocommit 开启且不在事务中时,
SAVEPOINT直接返回成功,不建任何记录(pkg/executor/simple.go#L665-L667)。后果:后续ROLLBACK TO报保存点不存在,排障时容易误判成"保存点丢失"。 - 同名覆盖是位置替换。重复执行
SAVEPOINT s1会先删除旧的 s1 再压入新记录,新检查点位于栈顶(pkg/sessionctx/variable/session.go#L504-L515)。后果:旧检查点连同它之后建立的保存点一并消失,回滚范围比预想更大。 - 可重复回退。同一个保存点可以被多次
ROLLBACK TO,每次只恢复到同一个 MemDB 检查点(pkg/executor/test/txn/txn_test.go#L381-L386)。后果:保存点可当作"重置到某个干净状态"的开关反复使用。 - 名称大小写归一。
SAVEPOINT S1与SAVEPOINT s1是同一记录(pkg/executor/test/txn/txn_test.go#L428-L431)。后果:与 MySQL 行为相反,依赖大小写区分两个保存点的写法在 TiDB 里会互相覆盖。
与 MySQL 的差异
- 锁的释放时机。MySQL 在
ROLLBACK TO SAVEPOINT时释放保存点之后的行锁;TiDB 悲观事务不释放,统一在事务COMMIT或整体ROLLBACK时才释放(2022-07-22-transaction-savepoint.md MySQL Compatibility 节)。实际后果:回退保存点后,其他会话对同一批行的FOR UPDATE仍会阻塞到本事务结束,并发批处理任务可能长时间等待。 - 自增值不回收。
ROLLBACK TO SAVEPOINT不回收已分配的 AUTO_INCREMENT / SEQUENCE 值。实际后果:频繁部分回滚的表在提交后键值序列出现空洞,依赖"键值连续"做分片或对账的逻辑会读到空档。
如何验证操作生效
- 事务内执行
SELECT对比回滚前后的行数,确认目标行已被撤销(参考 tests/integrationtest/r/executor/executor_txn.result)。 COMMIT后再次SELECT,确认落盘数据只含保存点之前与最后一次回滚后新写入的行。- 执行
RELEASE SAVEPOINT试探:报[executor:1305]SAVEPOINT ... does not exist说明该保存点已不存在。
保存点栈本身无法用 SQL 查询,information_schema中没有对应视图,只能靠上述回退/释放是否报错来间接判断。
限制速查
| 限制 | 表现 | 来源 |
|---|---|---|
| 开启 binlog 时不可用 | SAVEPOINT is not supported when binlog is enabled | pkg/executor/simple.go#L659-L660、2022-07-22-transaction-savepoint.md Savepoint Implementation 节 |
| 悲观事务需 in-place constraint check | 关闭时报savepoint is not supported in pessimistic transactions when in-place constraint check is disabled | pkg/executor/simple.go#L668-L670 |
| 非事务 + autocommit 为空操作 | 静默忽略,后续ROLLBACK TO报[executor:1305] | pkg/executor/simple.go#L665-L667、pkg/executor/test/txn/txn_test.go#L273-L274 |
| 不回滚 AUTO_INCREMENT / SEQUENCE | 键值出现空洞 | 2022-07-22-transaction-savepoint.md ROLLBACK TO SAVEPOINT Implementation 节 |
| 悲观锁不随回退释放 | 其他会话FOR UPDATE等待至事务结束 | 2022-07-22-transaction-savepoint.md MySQL Compatibility 节 |
【免费下载链接】tidbTiDB is built for agentic workloads that grow unpredictably, with ACID guarantees and native support for transactions, analytics, and vector search. No data silos. No noisy neighbors. No infrastructure ceiling.项目地址: https://gitcode.com/GitHub_Trending/ti/tidb
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考