事务不是自动生效的魔法,而是通过几条简单的命令,你把一组 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 会挂起当前事务开启新事务,保存点也会被用于嵌套事务的实现。如果你同时使用编程式事务和声明式事务,容易混淆边界。建议团队内约定统一的事务管理模式,减少意外回滚或提交不全。
掌握这些事务控制语句,你就拥有了安全操作数据的基本功。它们不只是几个命令,而是确保你每次对数据动手时都有一张保险单。后续章节我们将进一步讨论事务隔离、锁与长事务治理,那时你会更深刻地体会到:正确的开启与关闭事务,其实是高性能和强一致性的起点。