【MySQL篇|第七篇】MySQL事务

一、MySQL 简介

MySQL 是目前使用最广泛的开源关系型数据库之一,由 Oracle 公司维护,采用客户端/服务端架构,支持标准 SQL,具备跨平台、性能稳定、生态完善等特点,被大量应用于 Web 应用、电商、金融、日志分析等场景。

从整体结构上看,MySQL 可以分为以下几个层次:

  • 连接层:负责客户端连接管理、身份认证、权限校验和线程复用。
  • 服务层:包含 SQL 解析器、查询优化器、执行器以及内置函数,是 SQL 语句真正被理解和调度的位置。
  • 存储引擎层:负责数据的实际存取,采用插件式设计,常见引擎有 InnoDB、MyISAM、Memory 等。
  • 文件系统层:负责把数据、日志、索引落到磁盘文件上。

需要特别强调的是,事务能力由存储引擎提供。MySQL 默认使用 InnoDB 引擎,它支持事务、行级锁、外键和崩溃恢复;而 MyISAM 不支持事务,因此在实际项目中,涉及事务的业务表几乎都会选择 InnoDB。

二、什么是数据库事务

事务(Transaction)是一组逻辑上不可分割的数据库操作序列。这组操作要么全部成功,要么全部失败回滚,不允许出现"只做了一半"的中间状态。

最典型的例子是转账:A 账户扣款 100 元,B 账户增加 100 元。这两步必须作为一个整体执行,如果扣款成功而加款失败,就会造成资金凭空消失。事务正是用来保证这类业务一致性的机制。

一句话理解:事务就是把多条 SQL 打包成一个"原子操作",让数据库从一种一致状态安全地过渡到另一种一致状态。

三、事务的四大特性(ACID)

特性 含义 InnoDB 的实现方式
原子性(Atomicity) 事务中的操作要么全部提交,要么全部回滚 通过 undo log 记录反向操作,回滚时撤销已执行的修改
一致性(Consistency) 事务执行前后,数据都必须满足业务约束和完整性规则 由原子性、隔离性、持久性共同保障,并依赖约束、触发器等机制
隔离性(Isolation) 并发事务之间互不干扰,一个事务的中间状态对其他事务不可见 通过锁机制和 MVCC(多版本并发控制)实现
持久性(Durability) 事务一旦提交,修改就永久保存,即使数据库崩溃也不丢失 通过 redo log 与刷盘策略(如 innodb_flush_log_at_trx_commit)保证

其中,一致性是目的,原子性、隔离性、持久性是手段。理解这一点,有助于在排查问题时判断应该关注日志、锁还是隔离级别。

四、事务的隔离级别

并发事务会带来三类典型问题:

  • 脏读:读到了其他事务尚未提交的数据。
  • 不可重复读:同一事务内两次读取同一行,结果不一致(其他事务提交了更新)。
  • 幻读:同一事务内两次执行相同查询,返回的行数不一致(其他事务插入或删除了数据)。

SQL 标准定义了四种隔离级别,MySQL 全部支持:

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED(读未提交) 可能 可能 可能
READ COMMITTED(读已提交) 不可能 可能 可能
REPEATABLE READ(可重复读,MySQL 默认) 不可能 不可能 InnoDB 下基本不可能
SERIALIZABLE(串行化) 不可能 不可能 不可能

MySQL 的默认隔离级别是 REPEATABLE READ 。InnoDB 在该级别下通过 MVCC 保证快照读的一致性,并通过 Next-Key Lock(记录锁 + 间隙锁) 在很大程度上避免了幻读,这也是 MySQL 与部分其他数据库的一个差异点。

查看与设置隔离级别:

sql 复制代码
-- 查看当前会话与全局隔离级别
SELECT @@transaction_isolation;
SELECT @@global.transaction_isolation;

-- 设置当前会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 设置全局隔离级别(新连接生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

五、事务的基本用法

5.1 显式事务

MySQL 默认开启自动提交(autocommit = 1),每条 SQL 都是一个独立事务。要手动控制事务,可以使用以下语句:

sql 复制代码
-- 方式一:显式开启事务
START TRANSACTION;

UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;

-- 全部成功则提交
COMMIT;

-- 出现异常则回滚
-- ROLLBACK;

也可以使用 BEGIN 代替 START TRANSACTION,两者在 InnoDB 中效果基本一致。此外还可以设置事务的隔离级别和读写属性:

sql 复制代码
-- 开启一个只读事务,适合报表类查询
START TRANSACTION READ ONLY;

-- 开启事务并指定隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;

5.2 保存点(SAVEPOINT)

当事务较长时,可以设置保存点,实现部分回滚:

sql 复制代码
START TRANSACTION;

INSERT INTO orders (id, user_id, amount) VALUES (1001, 1, 199.00);
SAVEPOINT sp1;

INSERT INTO order_items (order_id, sku, qty) VALUES (1001, 'SKU-A', 2);
SAVEPOINT sp2;

-- 假设明细数据有误,只回滚到 sp1,保留订单主表记录
ROLLBACK TO SAVEPOINT sp1;

COMMIT;

注意:ROLLBACK TO SAVEPOINT 只是回滚到某个保存点,事务本身仍然处于开启状态,需要最终 COMMIT 或 ROLLBACK 结束。

5.3 自动提交控制

sql 复制代码
-- 关闭自动提交,后续语句需手动 COMMIT
SET autocommit = 0;

UPDATE account SET balance = balance - 100 WHERE id = 1;
COMMIT;

-- 恢复自动提交
SET autocommit = 1;

在实际开发中,更推荐使用显式的 START TRANSACTION,而不是长期关闭自动提交,避免连接池复用时出现"忘记提交"的隐患。

六、编程语言中的事务用法

6.1 Java(JDBC)

java 复制代码
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class TransferService {

    public void transfer(long fromId, long toId, int amount) throws SQLException {
        String url = "jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=Asia/Shanghai";
        try (Connection conn = DriverManager.getConnection(url, "root", "password")) {
            // 关闭自动提交,开启事务
            conn.setAutoCommit(false);
            try (PreparedStatement deduct = conn.prepareStatement(
                         "UPDATE account SET balance = balance - ? WHERE id = ? AND balance >= ?");
                 PreparedStatement add = conn.prepareStatement(
                         "UPDATE account SET balance = balance + ? WHERE id = ?")) {

                deduct.setInt(1, amount);
                deduct.setLong(2, fromId);
                deduct.setInt(3, amount);
                int rows = deduct.executeUpdate();
                if (rows != 1) {
                    throw new SQLException("余额不足或账户不存在");
                }

                add.setInt(1, amount);
                add.setLong(2, toId);
                add.executeUpdate();

                conn.commit();
            } catch (SQLException e) {
                conn.rollback();
                throw e;
            } finally {
                conn.setAutoCommit(true);
            }
        }
    }
}

关键点有三个:关闭自动提交 、成功时 commit 、异常时 rollback 。同时要注意连接池归还连接前必须恢复 autocommit,否则会影响后续使用者。

6.2 Spring 声明式事务

java 复制代码
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TransferService {

    private final AccountMapper accountMapper;

    public TransferService(AccountMapper accountMapper) {
        this.accountMapper = accountMapper;
    }

    @Transactional(rollbackFor = Exception.class)
    public void transfer(long fromId, long toId, int amount) {
        int rows = accountMapper.deduct(fromId, amount);
        if (rows != 1) {
            throw new IllegalStateException("余额不足");
        }
        accountMapper.add(toId, amount);
    }
}

使用 @Transactional 时需要注意几个常见坑:

  • 默认只对 RuntimeException 和 Error 回滚,受检异常需显式指定 rollbackFor。
  • 同类内部方法调用不会经过代理,事务注解会失效。
  • 方法必须是 public,且类需要被 Spring 管理。
  • 事务方法内应避免远程调用、长时间等待等耗时操作,防止长时间持有锁。

七、事务与锁、MVCC 的关系

理解事务行为,离不开锁和 MVCC:

  • 快照读 :普通 SELECT 在 REPEATABLE READ 下读取 undo log 构建的一致性视图,不加锁。
  • 当前读 :SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE、DELETE 读取最新数据并加锁。
  • 行锁:InnoDB 基于索引加锁,若查询条件没有命中索引,可能退化为锁全表扫描范围,导致并发性能骤降。
  • 间隙锁:在 REPEATABLE READ 下防止其他事务在区间内插入数据,从而抑制幻读。
sql 复制代码
-- 当前读,锁定该行,防止并发修改
START TRANSACTION;
SELECT * FROM account WHERE id = 1 FOR UPDATE;
UPDATE account SET balance = balance - 100 WHERE id = 1;
COMMIT;

因此,事务中的 SQL 应尽量走索引、缩小锁定范围,并保持事务短小。

八、事务使用的最佳实践

  1. 事务尽量短:只包裹必要的数据库操作,避免在事务中做 HTTP 调用、文件读写、消息发送等耗时动作。
  2. 控制事务粒度:不要一个事务处理成百上千条记录,必要时分批提交。
  3. 统一加锁顺序:多表或多行更新时按固定顺序加锁,降低死锁概率。
  4. 合理使用索引:避免无索引更新导致锁范围扩大。
  5. 捕获并处理死锁:InnoDB 检测到死锁会回滚其中一个事务,应用层应具备重试机制。
  6. 避免长事务:长事务会阻塞 undo 清理,导致 undo 表空间膨胀,也会加剧锁等待。
  7. 明确隔离级别:根据业务一致性要求选择,不要盲目追求最高隔离级别。

查看当前运行中的事务:

sql 复制代码
-- MySQL 8.0
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;

九、常见问题与排查思路

现象 可能原因 排查方向
更新语句长时间不返回 行锁被其他事务占用 查看 innodb_trx、performance_schema.data_locks,定位阻塞源
报错 Deadlock found 两个事务交叉持有锁 查看 SHOW ENGINE INNODB STATUS 中的 LATEST DETECTED DEADLOCK
数据出现"半更新" 未使用事务或异常未回滚 检查代码是否 commit/rollback,是否吞掉异常
同一事务两次查询结果不同 隔离级别为 READ COMMITTED 确认隔离级别,必要时改为 REPEATABLE READ 或使用当前读
undo 表空间持续增长 存在长事务 排查 innodb_trx 中运行时间过长的事务

十、总结

MySQL 事务是保证数据一致性的核心机制。InnoDB 通过 undo log 实现原子性,通过 redo log 实现持久性,通过锁与 MVCC 实现隔离性,三者共同支撑起一致性这一最终目标。

在实际使用中,掌握 START TRANSACTION、COMMIT、ROLLBACK、SAVEPOINT 这些基础语句只是起点,更重要的是理解隔离级别带来的行为差异,保持事务短小、SQL 走索引、加锁顺序一致,并在应用层做好异常回滚与死锁重试。只有把事务当作一种需要设计的资源来对待,才能真正发挥它的价值。

相关推荐
独隅2 小时前
Claude API 实战教程:从入门到完整项目
android·java·数据库·claude·claude code
_孤傲_2 小时前
快速掌握 MySQL 语法
数据库·后端·mysql
数字新视界4 小时前
一体化机柜在数据中心运维中的创新应用与优势分析
大数据·服务器·数据库·动环监控系统·动力与环境监控系统
SelectDB4 小时前
从 StarRocks 走向 Apache Doris:如何实现引擎平滑替换与架构升级?
大数据·数据库·数据分析
为思念酝酿的痛4 小时前
MySQL的操作
数据库·mysql
猫咪很可爱5 小时前
Oracle ORA-4031 错误分析与处理:SGA 内存不足导致 update/exp/expdp 失败
数据库·oracle
小豆庄5 小时前
视频素材处理时的隐私盲区:云处理与本地处理的边界在哪
数据库·图像处理·视频·敏捷流程
Umi·6 小时前
MySQL库表操作
数据库·mysql
风哥2号7 小时前
数据库教程FGMT69:PostgreSQL运维诊断与监控分析
运维·数据库·postgresql