事务是保证数据一致性的基本单元,但在日常开发中,很多开发者并不清楚自己的 SQL 到底运行在事务中还是在自动提交模式下。理解自动提交与手动事务的区别,是写出可靠业务代码的第一步。
15.2.1 自动提交:MySQL 的默认工作模式
MySQL 默认开启自动提交(autocommit)。这意味着每一条单独的 SQL 语句(INSERT、UPDATE、DELETE 等)在执行成功后都会立即被当作一个事务提交,不需要显式执行 COMMIT。换句话说,在自动提交模式下,一条 SQL 就是一个事务。
查看当前会话的自动提交状态:
SELECT @@autocommit;
或:
SHOW VARIABLES LIKE 'autocommit';
返回值通常为 1(ON),表示自动提交已开启。
在自动提交模式下:
UPDATE account SET balance = balance - 100 WHERE user_id = 1;
UPDATE account SET balance = balance + 100 WHERE user_id = 2;
这两条语句是两个独立的事务,每条执行完就立即提交。如果第一条成功、第二条失败(比如 user_id=2 不存在),第一条的扣款已经生效,100 元就凭空消失了。这显然不符合转账的原子性要求。
因此,对于需要多条操作“同生共死”的场景,自动提交模式是不可接受的。
15.2.2 关闭自动提交,开启手动事务
MySQL 提供了两种方式进入手动事务模式:
方式一:临时关闭自动提交(不推荐生产环境使用)
SET autocommit = 0;
之后执行的每条 SQL 都不会自动提交,直到你显式执行 COMMIT 或 ROLLBACK。这种方式的问题在于,一旦忘记开启或提交,会话会长时间持有未提交的事务,容易造成锁等待和长事务。
方式二:使用显式事务控制语句(推荐)
无论当前 autocommit 设置为何,都可以通过 START TRANSACTION 或 BEGIN 显式开启一个事务,事务结束时必须显式提交或回滚。这是最清晰、最推荐的方式:
-- 开启事务
START TRANSACTION;
-- 一组操作
UPDATE account SET balance = balance - 100 WHERE user_id = 1;
UPDATE account SET balance = balance + 100 WHERE user_id = 2;
-- 如果一切正常,提交
COMMIT;
-- 如果发生错误,回滚
ROLLBACK;
BEGIN 和 START TRANSACTION 作用相同,但 START TRANSACTION 可以附加修饰符,例如 START TRANSACTION READ ONLY 声明只读事务,InnoDB 可以对其做一定优化。在日常业务中,BEGIN 更简洁常用。
特别注意:执行 COMMIT 或 ROLLBACK 后,事务结束,MySQL 会恢复到自动提交模式(若之前 autocommit 为 1)或继续非自动提交状态(若之前 autocommit 为 0)。如果你是通过 START TRANSACTION 开启的事务(未修改 autocommit),事务结束后就会回到自动提交模式,这是最干净的行为。
15.2.3 保存点:事务内部的回滚标记
有时候事务比较大,希望在某些步骤出错时只撤销一部分操作,而不是回滚整个事务。可以使用保存点(SAVEPOINT):
START TRANSACTION;
UPDATE inventory SET count = count - 1 WHERE product_id = 100;
-- 设置一个保存点
SAVEPOINT after_inventory;
INSERT INTO orders (user_id, product_id, amount) VALUES (1, 100, 1);
-- 如果插入订单失败,可以回滚到保存点,保留库存更新的结果
ROLLBACK TO SAVEPOINT after_inventory;
-- 其他操作...
COMMIT;
也可以释放保存点:RELEASE SAVEPOINT savepoint_name;,释放后无法再回滚到该点。
保存点让长事务内部的错误处理更加灵活,但它只在当前事务内有效,事务提交或回滚后,所有保存点都会消失。
15.2.4 事务嵌套问题与 MySQL 的处理
SQL 标准中有“嵌套事务”的概念,但 MySQL 的 InnoDB 引擎并不支持真正的嵌套事务——一个事务内部再开启一个事务,会把前一个事务隐式提交。
例如:
START TRANSACTION;
INSERT INTO t1 VALUES (1);
START TRANSACTION; -- 这里会隐式提交前一个事务!
INSERT INTO t1 VALUES (2);
ROLLBACK; -- 只回滚第二个事务,插入的 (1) 已经提交了
这种行为很容易导致数据不一致,需要格外注意。在应用代码中,应注意避免在已有事务中再次调用开启事务的逻辑。如果确实需要在复杂流程中实现“部分回滚”,可以使用保存点来模拟嵌套事务的效果,但这需要手动管理保存点名称,无法自动嵌套。
一些应用框架(如 Spring)可以通过 @Transactional 的传播机制管理事务嵌套,底层通常也是通过保存点来实现“内层回滚不影响外层”,但需要清楚配置,否则也可能出现隐式提交。
15.2.5 在应用程序中控制事务
无论是 Java(JDBC/MyBatis/JPA)、Python(pymysql/SQLAlchemy)、Go(database/sql)还是其他语言,控制事务的原理是一致的:
- 从连接池获取一个数据库连接。
- 将该连接的自动提交设为 false(
conn.setAutoCommit(false))。 - 执行多条 SQL。
- 如果一切正常,调用
conn.commit()。 - 如果发生异常,调用
conn.rollback()。 - 在 finally 块中将连接归还连接池,并恢复自动提交状态(或由连接池组件自动处理)。
以 Java 的 JDBC 为例:
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
// 执行多条 SQL
stmt1.executeUpdate("UPDATE account SET balance = balance - 100 WHERE user_id = 1");
stmt2.executeUpdate("UPDATE account SET balance = balance + 100 WHERE user_id = 2");
conn.commit();
} catch (SQLException e) {
if (conn != null) {
conn.rollback();
}
} finally {
if (conn != null) {
conn.setAutoCommit(true);
conn.close();
}
}
现代框架通常会封装这些样板代码。例如在 Spring 中,只需加上 @Transactional 注解,框架就会自动在方法开始前关闭自动提交,方法正常结束时提交,抛出指定异常时回滚。但作为开发者,了解底层的自动提交与手动事务机制,依然有助于你排查奇怪的事务行为(比如“为什么这条数据明明没提交却已经写入?”——可能是因为自动提交被意外开启了)。
15.2.6 手动事务的注意事项与避坑指南
1. 长事务的危害
手动事务的最大风险是忘记提交或回滚,导致事务长时间处于活跃状态。长事务的危害包括:
- 持有锁不释放,阻塞其他写操作,严重时导致整个库的写入积压。
- Undo Log 无法清理,导致 Undo 表空间膨胀,占用磁盘空间。
- 历史版本链变长,影响 MVCC 读的性能。
所以在编写事务时,务必保证事务执行时间足够短,不要在事务中执行外部服务调用、大批量数据计算、人工等待等耗时操作。如果业务实在需要,可以考虑拆分为多个短小事务,并通过补偿逻辑保持最终一致性。
2. 事务与自动提交的混用陷阱
一些开发者可能遇到这样的情况:明明手动开启了事务,但数据却被立即提交了,排查后发现是在事务中间调用了 DDL 语句(如 ALTER TABLE、CREATE INDEX)或隐式提交的操作。MySQL 中,下列语句会隐式提交当前事务:
- DDL 语句(CREATE、ALTER、DROP 等)。
- 事务控制语句(START TRANSACTION、BEGIN、COMMIT、ROLLBACK)。
- 锁定语句(LOCK TABLES、UNLOCK TABLES)。
- 导入导出语句(LOAD DATA INFILE)。
- 主从复制相关语句(START SLAVE、STOP SLAVE 等)。
在编写复杂流程时,一定要避免把 DDL 或上述语句放在一个期望保持原子性的手动事务中间,否则你的数据会在你不经意间被部分提交。
3. 读操作也需注意事务一致性
在手动事务中,如果你需要多次读取数据并基于读取结果做决策(例如先查库存数量,满足条件才下单),必须保证读取到的数据在事务期间不变。在可重复读隔离级别下,通过手动事务的 SELECT 会自动建立快照,多次读取的结果一致。但如果你使用了读已提交(Read Committed)隔离级别,每次 SELECT 都会读取最新已提交的数据,可能导致数据前后不同。在选择隔离级别时需要结合业务需求明确这一点。
4. 回滚后连接状态
当使用 ROLLBACK 回滚一个事务后,连接是否保持可用?是的,回滚只是撤销了当前事务中的修改,连接依然存活,你可以继续执行新的事务。但要注意,如果回滚是由于死锁导致的(MySQL 会自动回滚某个事务来解决死锁),应用需要捕获对应的异常,并重试整个事务。
总之,自动提交是 MySQL 的默认行为,适合单条 SQL 的场景,但任何需要原子性的多步操作都必须使用手动事务。掌握 BEGIN/COMMIT/ROLLBACK 以及保存点的用法,理解自动提交和隐式提交的影响,是开发者写出可靠业务逻辑的基础。