5月27日 15:06

MariaDB 的事务隔离级别如何工作?怎样根据业务场景选择合适的隔离级别?

事务隔离级别要解决什么问题

多个事务并发执行时,如果不加任何隔离措施,会出现三类数据不一致的问题:

  • 脏读(Dirty Read):事务 A 读到了事务 B 尚未提交的数据。如果事务 B 回滚,事务 A 拿到的就是根本不存在的"脏数据"。
  • 不可重复读(Non-Repeatable Read):事务 A 两次读取同一行数据,中间事务 B 修改并提交了这行,导致两次读到的值不同。
  • 幻读(Phantom Read):事务 A 两次执行相同的范围查询,中间事务 B 插入了新行并提交,导致第二次查询多出了"幻影行"。

这三类问题逐层递进:脏读是读到了未提交的修改,不可重复读是已提交的修改导致同一行前后不一致,幻读是已提交的新增导致行数变化。SQL 标准据此定义了四种隔离级别,每种级别禁止一部分问题。

四种隔离级别

READ UNCOMMITTED(读未提交)

最低隔离级别,允许事务读取其他事务未提交的修改。在这个级别下,脏读、不可重复读、幻读都可能发生。实际业务中几乎不会使用——读到未提交的数据意味着可能基于错误数据做出决策,风险极高。

READ COMMITTED(读已提交)

只允许读取已经提交的数据,杜绝了脏读。但同一事务内两次读取同一行,可能因为其他事务的提交而得到不同结果,所以不可重复读和幻读仍然存在。

Oracle 和 PostgreSQL 默认使用这个级别。如果你的业务对同一事务内数据一致性要求不高(比如报表查询、大多数 Web 应用的读操作),READ COMMITTED 是一个性能和正确性的折中选择。

REPEATABLE READ(可重复读)

保证同一事务内多次读取同一行的结果一致,杜绝了脏读和不可重复读。按照 SQL 标准,幻读在这个级别仍然可能发生。但 MariaDB/MySQL 的 InnoDB 引擎通过 MVCC 和 Gap Lock 机制,在 REPEATABLE READ 下也避免了幻读——这比 SQL 标准更严格。

MariaDB 和 MySQL 的默认隔离级别就是 REPEATABLE READ。大多数 OLTP 场景不需要改动它。

SERIALIZABLE(串行化)

最高隔离级别,所有事务按顺序串行执行,完全杜绝脏读、不可重复读和幻读。实现方式是对所有读取的行加共享锁,其他事务无法修改这些行直到锁释放。

性能代价极大——并发度几乎归零。只在对数据一致性有极端要求的场景下使用,比如金融对账、审计等。

隔离级别与并发问题的对应关系

隔离级别脏读不可重复读幻读
READ UNCOMMITTED可能可能可能
READ COMMITTED不会可能可能
REPEATABLE READ不会不会可能(SQL 标准)/ 不会(MariaDB InnoDB)
SERIALIZABLE不会不会不会

MVCC 是怎么工作的

MVCC(Multi-Version Concurrency Control,多版本并发控制)是 InnoDB 实现 REPEATABLE READ 和 READ COMMITTED 的核心机制。它的基本思路是:每行数据保留多个版本,读操作访问的是某个历史快照,写操作创建新版本,读写互不阻塞。

InnoDB 在每行记录后添加两个隐藏列:

  • DB_TRX_ID:最后修改该行的事务 ID。
  • DB_ROLL_PTR:回滚指针,指向 undo log 中该行的前一个版本。

每个事务开始时会获得一个递增的事务 ID。读取数据时,InnoDB 根据当前事务 ID 和 undo log 链构建一个一致性视图(Read View),只返回对当前事务可见的版本。

MVCC 在两个隔离级别下的行为差异:

  • REPEATABLE READ:事务第一次读取时创建 Read View,整个事务期间复用同一个 View,所以同一行数据多次读取结果一致。
  • READ COMMITTED:每次 SELECT 都创建新的 Read View,所以能看到其他事务已提交的最新数据。

这就是为什么 READ COMMITTED 下会出现不可重复读,而 REPEATABLE READ 不会——Read View 的创建时机不同。

Gap Lock 与 Next-Key Lock

MVCC 解决了快照读(普通 SELECT)的幻读问题,但当前读(SELECT ... FOR UPDATE、UPDATE、DELETE 等加锁读)怎么办?InnoDB 的答案是 Gap Lock 和 Next-Key Lock。

  • Record Lock:锁定索引上的单条记录。
  • Gap Lock:锁定两条记录之间的间隙,阻止其他事务在该间隙中插入新行。
  • Next-Key Lock:Record Lock + Gap Lock 的组合,锁定一条记录及其前面的间隙。这是 InnoDB 在 REPEATABLE READ 下的默认加锁方式。

举个例子:表中有 id = 1、5、10 三条记录。对 id = 5 加 Next-Key Lock 时,实际锁住的范围是 (1, 5],即 id 大于 1 且小于等于 5 的区间。其他事务无法在这个范围内插入新行(比如 id = 3),从而防止了幻读。

在 READ COMMITTED 下,Gap Lock 被禁用(外键约束检查和唯一键冲突检查除外),只使用 Record Lock。这意味着其他事务可以在已锁定记录的间隙中自由插入,并发度更高,但可能出现幻读。

InnoDB 与 MyISAM 的关键区别

讨论事务隔离级别的前提是存储引擎支持事务。MariaDB 同时支持 InnoDB 和 MyISAM,但两者在事务能力上有本质区别:

  • InnoDB:支持完整的 ACID 事务、行级锁、MVCC、外键约束和崩溃恢复。事务隔离级别的所有讨论都基于 InnoDB。
  • MyISAM:不支持事务、不支持行级锁(只有表级锁)、没有 MVCC、没有崩溃恢复。在 MyISAM 表上执行 START TRANSACTION 不会有任何效果,ROLLBACK 也不会回滚任何修改。

如果你的表使用 MyISAM 引擎,事务隔离级别的设置毫无意义。检查方法:

sql
SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'your_db' AND TABLE_NAME = 'your_table';

如果是 MyISAM,需要先转为 InnoDB:

sql
ALTER TABLE your_table ENGINE = InnoDB;

MariaDB 5.5 起默认存储引擎已经是 InnoDB,新建的表无需额外指定。

如何设置隔离级别

查看当前隔离级别

sql
-- 查看全局默认隔离级别 SELECT @@GLOBAL.transaction_isolation; -- 查看当前会话隔离级别 SELECT @@SESSION.transaction_isolation; -- 兼容写法(MariaDB 中仍可用) SELECT @@tx_isolation;

设置隔离级别

sql
-- 仅影响下一个事务 SET TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 影响当前会话的所有后续事务 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 影响所有新会话的默认隔离级别(需要 SUPER 权限) SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

注意:事务已经开始后不能修改隔离级别,否则会报错 ERROR 1568 (25001): Transaction characteristics can't be changed while a transaction is in progress

在配置文件中设置

my.cnf 中设置全局默认:

ini
[mysqld] transaction-isolation = READ-COMMITTED

重启后生效。

MariaDB 与 MySQL 的差异

MariaDB 是 MySQL 的分支,事务隔离机制基本一致,但有几个值得注意的差异:

  • tx_isolation vs transaction_isolation:MySQL 8.0.3 移除了 tx_isolation 别名,只使用 transaction_isolation;MariaDB 两者都支持。
  • WITH CONSISTENT SNAPSHOT:MariaDB 的 START TRANSACTION WITH CONSISTENT SNAPSHOT 兼容所有隔离级别,MySQL 8.0 前只支持 REPEATABLE READ。
  • Gap Lock 行为:两者在 REPEATABLE READ 下的 Gap Lock 策略相同,但具体死锁场景可能因版本不同而有差异。
  • 默认二进制日志:MySQL 8.0 默认开启 binlog,MariaDB 默认关闭。binlog 的开启与否会影响事务的提交流程和性能。
  • Aria 引擎:MariaDB 用 Aria 替代 MyISAM 作为非事务型引擎的选择,Aria 支持崩溃安全特性。

怎么选择隔离级别

选择隔离级别本质上是正确性和并发性能之间的权衡:

  • 大多数 Web 应用:保持默认的 REPEATABLE READ 即可。InnoDB 的 MVCC 让读操作不加锁,性能开销可控。
  • 高并发短事务场景(如秒杀、库存扣减):考虑降级到 READ COMMITTED。Gap Lock 在高并发下容易导致死锁,去掉 Gap Lock 可以减少锁冲突。代价是需要业务层处理不可重复读。
  • 报表和数据分析:READ COMMITTED 通常够用。报表查询对同一事务内的一致性要求不高,但需要看到最新提交的数据。
  • 金融对账和审计:SERIALIZABLE 或者在应用层加分布式锁。数据一致性优先,性能可以妥协。
  • READ UNCOMMITTED:几乎没有任何合理的使用场景。即使你不在乎一致性,它也不会比 READ COMMITTED 快多少——InnoDB 在 RC 级别下读操作同样不加锁。

一个常见的调优方向:把 REPEATABLE READ 降为 READ COMMITTED,减少 Gap Lock 带来的死锁问题。Drupal 官方就推荐使用 READ COMMITTED 替代默认的 REPEATABLE READ 来避免死锁。如果你的业务逻辑中大量使用范围查询和插入操作混合的场景,值得做这个调整。

从性能角度看,隔离级别从低到高,锁持有时间递增、锁范围递增、并发度递减。REPEATABLE READ 的 Read View 在事务期间一直持有,长事务会占用大量 undo log 空间;READ COMMITTED 每次 SELECT 创建新 Read View,undo log 压力更小。所以控制事务长度比选择隔离级别本身更重要——无论用哪个级别,都应该让事务尽可能短。

标签:MariaDB