MySQL的事务隔离级别,如何选择?

你好,我是猿java

在 MySQL 中,事务隔离级别用于定义多个事务在并发执行时如何隔离彼此的数据操作,从而防止数据的不一致性和各种并发问题。这篇文章,我们一起来详细地分析它们以及在实际工作中该如何选择。

1. 事务隔离级别

MySQL 支持以下四种标准的事务隔离级别:

  1. 未提交读(Read Uncommitted)
  2. 提交读(Read Committed)
  3. 可重复读(Repeatable Read)
  4. 可串行化(Serializable)

下面我们将逐一介绍每种隔离级别及其特性,并通过示例说明其具体行为。

1.1 未提交读(Read Uncommitted)

特点:

  • 最低的隔离级别。
  • 事务可以读取其他事务尚未提交的数据(脏读)。
  • 可能导致脏读、不可重复读和幻读。

示例:

假设有两个事务 T1 和 T2,操作同一张表 accounts,其中有一行记录 id=1balance=1000

  • 事务 T1:

    1
    2
    3
    START TRANSACTION;
    UPDATE accounts SET balance = balance - 100 WHERE id = 1;
    -- 此时 balance 临时为 900,但尚未提交
  • 事务 T2:

    1
    2
    3
    SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
    START TRANSACTION;
    SELECT balance FROM accounts WHERE id = 1; -- T2 读取到 balance = 900(未提交)

如果 T1 最终回滚操作,T2 则读取到了一个不存在的中间状态(脏读)。

1.2 提交读(Read Committed)

特点:

  • 事务只能读取已经提交的数据,防止脏读。
  • 不保证同一事务中的多次读取一致,可能出现不可重复读。
  • 允许幻读。

示例:

  • 事务 T1:

    1
    2
    3
    START TRANSACTION;
    UPDATE accounts SET balance = balance - 100 WHERE id = 1;
    COMMIT; -- 提交后,balance = 900
  • 事务 T2:

    1
    2
    3
    4
    SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
    START TRANSACTION;
    SELECT balance FROM accounts WHERE id = 1; -- 读取到 balance = 900
    -- 如果 T1 之前已提交,那么 T2 读取到的是最新值

如果 T1 在 T2 的两个读取之间进行了提交,T2 会两次读取到不同的值(可重复读被破坏)。

1.3 可重复读(Repeatable Read)

特点:

  • 默认的隔离级别(InnoDB 默认)。
  • 保证同一事务中的多次读取结果一致,防止脏读和不可重复读。
  • 通过多版本并发控制(MVCC)实现,避免幻读大部分情况。
  • 在 InnoDB 中,使用 Next-Key Lock 机制防止幻读。

示例:

  • 事务 T1:

    1
    2
    3
    4
    5
    START TRANSACTION;
    SELECT balance FROM accounts WHERE id = 1; -- 假设读取到 balance = 1000
    -- 执行其他操作
    SELECT balance FROM accounts WHERE id = 1; -- 仍然读取到 balance = 1000
    COMMIT;
  • 事务 T2:

    1
    2
    3
    START TRANSACTION;
    UPDATE accounts SET balance = balance - 100 WHERE id = 1;
    COMMIT;

在 T1 的整个事务期间,虽然 T2 修改了 balance 并提交,但 T1 在再次读取时仍然看到的是最初的 balance = 1000,保证了可重复读。

注: 在 InnoDB 中,引入了 Next-Key Lock,可以防止新的行被插入,从而避免幻读。例如:

  • 事务 T1:

    1
    2
    3
    4
    5
    START TRANSACTION;
    SELECT * FROM accounts WHERE balance > 500; -- 读取所有 balance > 500 的行
    -- 执行其他操作
    SELECT * FROM accounts WHERE balance > 500; -- 仍然读取相同的行集,没有幻读
    COMMIT;
  • 事务 T2:

    1
    2
    3
    START TRANSACTION;
    INSERT INTO accounts (id, balance) VALUES (2, 600);
    COMMIT;

由于 Next-Key Lock 的存在,T2 无法在 T1 的事务期间插入 balance > 500 的新记录,避免了幻读。

1.4 可串行化(Serializable)

特点:

  • 最高的隔离级别。
  • 强制事务串行执行,仿佛按顺序一个接一个地执行。
  • 防止脏读、不可重复读和幻读。
  • 可能导致性能下降和更高的锁争用。

示例:

  • 事务 T1:

    1
    2
    3
    4
    5
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    START TRANSACTION;
    SELECT * FROM accounts WHERE balance > 500 FOR UPDATE; -- 加锁,防止其他事务修改或插入
    -- 执行其他操作
    COMMIT;
  • 事务 T2:

    1
    2
    3
    4
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    START TRANSACTION;
    SELECT * FROM accounts WHERE balance > 500 FOR UPDATE; -- 如果 T1 正在执行,T2 会等待或阻塞
    COMMIT;

在这种隔离级别下,事务 T2 必须等待 T1 完成后才能执行,确保事务的串行执行,避免所有并发问题。

2. 如何选择?

选择合适的事务隔离级别需要在数据一致性和系统性能之间进行权衡。一般情况下,Repeatable Read 是一个良好的默认选择,提供了较好的数据一致性和性能平衡。这里我们给出一些常见的使用建议。

大部分 Web 应用

  • 推荐隔离级别:可重复读(Repeatable Read)
  • 原因:提供良好的数据一致性,防止脏读和不可重复读,同时在 InnoDB 中通过 Next-Key Lock 减少幻读问题,适合大多数场景。

高并发读操作且数据一致性要求不高

  • 推荐隔离级别:提交读(Read Committed)
  • 原因:提高读操作的并发性,避免长时间持有读锁,适合需要高吞吐量但对一致性要求略低的应用。

分析型或报告系统

  • 推荐隔离级别:提交读(Read Committed) 或 不可重复读(视具体需求而定)
  • 原因:报告和分析操作通常对数据一致性要求不如事务性的写操作严格,可以接受一定程度的数据变化,以提高查询性能。

金融交易或库存管理等关键业务

  • 推荐隔离级别:可串行化(Serializable)
  • 原因:确保最高的数据一致性,防止所有并发问题,但应注意可能带来的性能开销。适用于对数据准确性要求极高的场景。

只进行写操作且需要避免写冲突

  • 推荐隔离级别:可串行化(Serializable) 或 可重复读(Repeatable Read)
  • 原因:避免写操作之间的冲突,确保数据完整性。

3. 总结

本文,我们分析了 MySQL的 4种标准的事务隔离级别,它们用于定义多个事务在并发执行时如何隔离彼此的数据操作,从而防止数据的不一致性和各种并发问题。最后,我们用一张图表来分析每种隔离级别的行为:

隔离级别 脏读 不可重复读 幻读 特点描述
未提交读 ✔️ ✔️ ✔️ 最低隔离级别,允许读取未提交的数据
提交读 ✔️ ✔️ 防止脏读,允许不可重复读和幻读
可重复读 部分 防止脏读和不可重复读,InnoDB 防止大部分幻读
可串行化 最高隔离级别,所有事务串行执行

4. 学习交流

如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注公众号:猿java,持续输出硬核文章。

drawing