人人都会AI编程

15.1 事务控制语句:开启、提交、回滚、保存点

更新时间:2026-07-11

事务不是自动生效的魔法,而是通过几条简单的命令,你把一组 SQL 操作圈起来,告诉数据库“这堆活儿是一件事”。本小节就聚焦最基础也最重要的部分——如何用事务控制语句管好你自己的数据。

15.1.1 显式开启事务:START TRANSACTION 与 BEGIN

在 InnoDB 中,你需要在执行期望一起成功或失败的 SQL 之前,显式地发出“事务开始”的信号。

两种写法:

START TRANSACTION;
-- 或者
BEGIN;

两者效果几乎完全一样,但在某些高级特性上有细微差别:START TRANSACTION 支持后面紧跟修饰子句,例如 START TRANSACTION READ ONLY 开启只读事务,或者 START TRANSACTION WITH CONSISTENT SNAPSHOT 立刻获取一致性快照(常用于导出工具)。BEGIN 是标准 SQL 的简洁写法,纯基本开启就用它。

实际中我们这样写:

START TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;

COMMIT;

如果中间任何一条语句执行失败(或者你判断业务逻辑不允许继续),你可以跳转到 ROLLBACK,整个事务内所有已经执行的修改都会被撤销。

一个重要细节:autocommit 的影响

MySQL 默认在会话级开启了 autocommit = 1。这意味着每一条单独的 DML 语句(INSERT/UPDATE/DELETE)都会被自动包装成一个事务并立即提交。你单独跑一句 UPDATE 不写 BEGIN,数据库已经在执行后帮你提交了,无法再回滚。要想手动控制多个操作同生共死,必须显式用 START TRANSACTION(或 BEGIN)开启一个多语句事务,此时 autocommit 在当前事务结束前被临时禁用。

生产环境不建议全局关闭 autocommit(SET autocommit = 0),那样会导致所有操作都留在未提交状态,需要时刻记得手工 COMMIT,极易造成长事务锁表或连接断开丢数据,是一种已过时的使用方式。

15.1.2 提交事务:COMMIT

COMMIT 就是告诉数据库:我刚才这一通操作结果确定要留下,你可以把它持久化了。执行后:

  • 事务中所有修改正式生效,对其他事务可见(根据隔离级别决定可见时机)。
  • InnoDB 会把该事务产生的 Redo Log 刷盘(根据配置),确保即使宕机数据也不丢失。
  • 所有被该事务持有的行锁、间隙锁被释放,其他等待的语句可以继续执行。

使用姿势很简单:

COMMIT;
-- 也可以写成 COMMIT WORK; 两者几乎等价

一旦 COMMIT 成功返回,就代表“钱已到账”,不可再回滚。如果你在代码里调用完 commit 之后发现业务发短信通知失败了,已提交的事务不能通过数据库回滚来撤销,只能在应用层做补偿逻辑(发补偿消息、冲正等),这是实现最终一致性时要特别注意的点。

15.1.3 回滚事务:ROLLBACK

ROLLBACK 是事务的后悔药,它撤销自事务开始以来所有还没提交的修改,让数据恢复到事务开启前的状态。

典型场景:转账扣款成功但加款失败,必须回滚。

START TRANSACTION;

UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
-- 假设此处模拟加款失败,例如账户不存在
UPDATE accounts SET balance = balance + 100 WHERE user_id = 999; -- 可能影响0行

ROLLBACK;

执行 ROLLBACK 后,user_id = 1 扣掉的 100 块会回到账户,就好像什么都没发生过。

Rollback 有几个执行细节值得记住:

  • Rollback 本身会被记录到 Binlog,主从复制环境里从库同样会执行回滚动作,保证数据一致。
  • 如果事务内没有任何修改(或全是只读操作),ROLLBACK 也能正常执行,只是没有东西可回滚。
  • 事务中如果某条 SQL 执行出错,不一定自动回滚。你需要主动根据返回值或异常来调用 ROLLBACK。有些错误(如死锁被引擎回滚)会整个事务回滚,但锁等待超时等错误只回滚当前语句,事务仍处于开启状态,这时如果直接 COMMIT,前面成功的部分还是会提交。保险做法是处理异常时无条件 ROLLBACK。

编程规范:try-catch 封装事务

在 Java/Python/Go 等应用中,你几乎永远需要这样写:

try {
    beginTransaction();
    // 一系列数据库操作
    commit();
} catch (Exception e) {
    rollback();
    throw e;
}

有一个常见陷阱:在 Spring 框架的 @Transactional 注解下,默认只对 RuntimeException 和 Error 回滚,受检异常不会自动回滚。许多开发者因为不知道这条规则而导致数据异常,务必亲自验证自己框架的回滚行为。

15.1.4 事务保存点:SAVEPOINT 与 ROLLBACK TO

保存点让你可以在事务内设置“检查点”,然后选择性地回滚到某个检查点,而不是非要整个事务回滚,适合长事务中部分操作需要撤销的场景。

创建保存点:

SAVEPOINT sp1;

回滚到保存点:

ROLLBACK TO SAVEPOINT sp1;

释放保存点(不是回滚,只是删除标记):

RELEASE SAVEPOINT sp1;

一个实用场景:批量更新,容错继续。

假设你要迁移一批用户数据,中间某几条可能失败,但不想全盘回滚:

START TRANSACTION;

UPDATE users SET level = 5 WHERE id = 101;
SAVEPOINT sp_102;
UPDATE users SET level = 5 WHERE id = 102; -- 假设这条因为约束失败
ROLLBACK TO SAVEPOINT sp_102; -- 只撤销 id=102 的修改,id=101 的修改保留
RELEASE SAVEPOINT sp_102;

UPDATE users SET level = 5 WHERE id = 103;
COMMIT;

这样 id=101 和 103 被修改,id=102 失败但不影响整体提交。这个技巧在数据修复脚本中非常管用。

保存点使用注意事项:

  • 保存点在事务内是临时的,一旦事务提交或回滚,所有保存点自动释放,不可再使用。
  • 回滚到保存点并不会释放保存点本身,你需要再通过 RELEASE SAVEPOINT 删除,或者让它随着事务结束自然清理。
  • 同一个保存点名称被重复定义时,旧的会被覆盖,无法再回滚到旧的。
  • 过去版本的 MySQL 中,回滚到保存点后,保存点之后获取的锁会被释放,从保存点之后的操作被撤销,但保存点前已有的锁继续保持。这一点在处理显式锁定(如 SELECT ... FOR UPDATE)时要注意,不要以为回滚会解除所有锁。

15.1.5 常见踩坑与最佳实践

1. 事务不要忘记关闭

长事务是大忌,开启后如果没有及时 COMMIT/ROLLBACK,加上连接断开,可能导致锁长时间持有、Undo 膨胀、主从延迟加剧。代码里务必使用 finally 或 try-with-resources 确保事务关闭。

2. 混合使用 DDL 要注意

在 MySQL 早期版本中,部分 DDL(如 ALTER TABLE)会隐式提交当前事务,导致你之前未提交的修改被意外提交。MySQL 8.0 虽然引入了原子 DDL,但建议事务中还是避免混入 DDL 操作,无法完全保证所有 DDL 都不触发隐式提交。

3. 客户端工具中的自动提交

使用 Navicat、DBeaver 等图形客户端时,默认可能开启了自动提交且不显示事务。执行几句 UPDATE/DELETE 可能已经提交了,没法回滚。务必确认工具的“自动提交”开关状态,或养成手动写 START TRANSACTION 的习惯。

4. 保存点不要过度使用

虽然保存点看起来灵活,但在高并发下会增加额外的内部管理开销,大量保存点可能影响性能。更关键的,它容易掩盖业务逻辑上的混乱——如果一个事务内部的业务步骤需要频繁回滚部分,可能意味着事务粒度划分不合理,应当考虑拆分成更小的业务单元。

5. 结合框架的事务传播行为

Spring 等框架有事务传播机制,如 REQUIRES_NEW 会挂起当前事务开启新事务,保存点也会被用于嵌套事务的实现。如果你同时使用编程式事务和声明式事务,容易混淆边界。建议团队内约定统一的事务管理模式,减少意外回滚或提交不全。

掌握这些事务控制语句,你就拥有了安全操作数据的基本功。它们不只是几个命令,而是确保你每次对数据动手时都有一张保险单。后续章节我们将进一步讨论事务隔离、锁与长事务治理,那时你会更深刻地体会到:正确的开启与关闭事务,其实是高性能和强一致性的起点。