人人都会AI编程

15.2 自动提交机制与手动事务使用

更新时间:2026-07-10

事务是保证数据一致性的基本单元,但在日常开发中,很多开发者并不清楚自己的 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 都不会自动提交,直到你显式执行 COMMITROLLBACK。这种方式的问题在于,一旦忘记开启或提交,会话会长时间持有未提交的事务,容易造成锁等待和长事务。

方式二:使用显式事务控制语句(推荐)

无论当前 autocommit 设置为何,都可以通过 START TRANSACTIONBEGIN 显式开启一个事务,事务结束时必须显式提交或回滚。这是最清晰、最推荐的方式:

-- 开启事务
START TRANSACTION;

-- 一组操作
UPDATE account SET balance = balance - 100 WHERE user_id = 1;
UPDATE account SET balance = balance + 100 WHERE user_id = 2;

-- 如果一切正常,提交
COMMIT;
-- 如果发生错误,回滚
ROLLBACK;

BEGINSTART TRANSACTION 作用相同,但 START TRANSACTION 可以附加修饰符,例如 START TRANSACTION READ ONLY 声明只读事务,InnoDB 可以对其做一定优化。在日常业务中,BEGIN 更简洁常用。

特别注意:执行 COMMITROLLBACK 后,事务结束,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)还是其他语言,控制事务的原理是一致的:

  1. 从连接池获取一个数据库连接。
  2. 将该连接的自动提交设为 false(conn.setAutoCommit(false))。
  3. 执行多条 SQL。
  4. 如果一切正常,调用 conn.commit()
  5. 如果发生异常,调用 conn.rollback()
  6. 在 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 TABLECREATE 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 以及保存点的用法,理解自动提交和隐式提交的影响,是开发者写出可靠业务逻辑的基础。