savepoint 是事务内可回滚标记点,非子事务;须在 autocommit=0 的显式事务中使用,ddl 或连接池复用易致失效;rollback to 不释放锁,同名保存点会静默覆盖,需命名规范并主动 release。

Savepoint 不是局部事务,只是当前事务内的一个可回滚标记点;它不能隔离锁、不释放行锁、也不改变事务边界——用错就以为“子事务”成功了,其实只是数据没全丢而已。
SAVEPOINT 必须在显式事务中使用,autocommit=1 下完全无效
MySQL 默认开启 autocommit=1,此时每条语句自成事务,SAVEPOINT 会报错或静默失效(比如提示 Query OK 但后续 ROLLBACK TO SAVEPOINT 直接抛 SAVEPOINT sp1 does not exist)。
- 先确认会话模式:
SELECT @@autocommit;,必须为0 - 显式开启事务:
BEGIN或START TRANSACTION,仅SET autocommit = 0不够 - DDL(如
ALTER TABLE)会隐式触发COMMIT,导致前面所有SAVEPOINT立即失效 - 连接池复用时,若上个请求没
COMMIT或ROLLBACK,当前请求一执行SAVEPOINT就可能报错:事务已处于非活跃状态
ROLLBACK TO SAVEPOINT 后数据回退但锁不释放
ROLLBACK TO SAVEPOINT sp1 会撤销该点之后的 DML 修改(比如 INSERT、UPDATE),但 InnoDB **不会释放这些操作持有的行锁**——尤其是对新插入行的锁,靠 undo 回滚时才真正释放。
- 这意味着:回滚到保存点后,其他事务仍可能被阻塞(比如你插入又回滚了一行,另一事务想更新同一行,会等)
- 如果你在保存点后做了
SELECT ... FOR UPDATE,锁依然持有,直到整个事务COMMIT或ROLLBACK - 验证是否真回退?别信返回信息,直接
SELECT查表数据;锁状态可查information_schema.INNODB_TRX和INNODB_LOCK_WAITS
同名 SAVEPOINT 覆盖无声,RELEASE 是手动清理唯一方式
执行 SAVEPOINT sp1 两次,第二次会覆盖第一次——不报错、不警告,但旧的 sp1 对应的 undo 位置和 binlog offset 已丢失,再 ROLLBACK TO SAVEPOINT sp1 只能回到第二次设点的位置。
- 建议命名带上下文,比如
sp_after_header_insert、sp_before_batch_update,避免复用 -
RELEASE SAVEPOINT sp1是唯一显式清理方式;不释放也不会报错,但下次同名SAVEPOINT仍会覆盖,容易误判逻辑断点 - 事务结束(
COMMIT或全量ROLLBACK)时,所有保存点自动清除,无需手动RELEASE,但中间过程建议主动释放以明确意图 - 存储过程中调用函数或触发器时,它们内部新建的保存点与外层隔离,退出即销毁,不可跨作用域引用
Python + PyMySQL 中用 SAVEPOINT 容易踩的三个坑
ORM(如 SQLAlchemy)默认屏蔽 SAVEPOINT,原生驱动可控但需严守连接约束:所有操作必须走同一个 Connection 实例,且不能被其他线程/协程干扰。
- 务必关闭自动提交:
conn.autocommit(False),并在事务开始前显式conn.begin() - 不要在不同
cursor上混用SAVEPOINT和ROLLBACK TO,虽属同连接,但某些驱动版本存在状态不同步 - 异常捕获后若执行
cursor.execute("ROLLBACK TO SAVEPOINT sp1"),必须确保sp1确实由当前连接创建过;跨线程共享连接时,sp1可能属于另一个线程的事务上下文,直接报错 - 批量导入场景下,建议每条记录前重设保存点(
cursor.execute("SAVEPOINT sp_per_row")),失败则回滚并跳过,避免嵌套过深拖慢事务状态管理
最常被忽略的一点:SAVEPOINT 本身不解决并发冲突,它只提供回退能力。如果两条并发事务都设了同名保存点、又都执行 ROLLBACK TO,它们互不影响——但各自锁的状态、binlog 位点、undo 链都独立维护。设计时得把“回退后继续做什么”写清楚,而不是默认“回退=恢复到安全状态”。











