三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

【数据库】tdsql(MySQL )的事务隔离级别

【数据库】tdsql(MySQL )的事务隔离级别

MySQL 的事务隔离级别是数据库并发控制的核心概念,用于解决多个事务同时执行时可能产生的数据不一致问题。

1. 默认值是

MySQL InnoDB 默认的事务隔离级别是:REPEATABLE READ(可重复读)。


2. 四大隔离级别

三个读异常(脏话、不可重复、幻读)解释:

  • 脏读(Dirty Read):读到别的事务尚未提交的数据(万一对方回滚,这数据就是废的)。
  • 不可重复读(Non-Repeatable Read):在同一事务内,两次读取同一条记录,因为别的事务修改并提交了,导致两次结果不一致(侧重于“改”)。
  • 幻读(Phantom Read):在同一事务内,两次执行同一个范围查询,因为别的事务插入或删除了数据,导致第二次多出或少了几行(侧重于“增删”)。

隔离级别与异常的关系

注意:按照 SQL 标准,REPEATABLE READ允许幻读发生的。但 MySQL InnoDB 通过强大的 Next-Key Lock(间隙锁+行锁)机制,在这个级别下就杜绝了幻读。


3.MySQL 默认选择 REPEATABLE READ

绝大多数数据库(如 Oracle、PostgreSQL、SQL Server)默认都是READ COMMITTED, MySQL 偏要与众不同。

背后的原因不是“拍脑袋”,而是由MySQL 主从复制(基于 Binlog)的历史架构性能权衡共同决定的。我们可以把原因归结为三大支柱

一:历史包袱与二进制日志(Binlog)的安全隐患(最核心原因)

在 MySQL 5.0 及更早版本,以及当下的某些配置中,Binlog 的格式默认是STATEMENT(基于语句的复制)。也就是记录UPDATE t SET a=1 WHERE id>10;这样的 SQL 语句,然后发给从库执行。

如果此时隔离级别是READ COMMITTED,会产生一个致命的主从数据不一致风险,我们称之为“Binlog 惊魂”

让我们用一个经典场景复现:

假设主库并发执行以下两个事务(TxA 删除,TxB 插入):

  • 时间线 1:TxA 执行DELETE FROM users WHERE age > 20;(此时表中有 id=1(age=25) 和 id=2(age=30),TxA 扫到了这两行,准备删除)。
  • 时间线 2:TxB 执行INSERT INTO users (id=3, age=28);提交
  • 时间线 3:在READ COMMITTED下,由于 TxB 已提交,TxA 重新评估条件age>20新插入的 id=3 不会被删除(因为 InnoDB 的删除是通过隐藏字段标记,此时读视图变化了)。
  • 时间线 4:TxA 提交。

主库结果:id=1, id=2 被删,id=3 存活。

Binlog 记录顺序(STATEMENT 格式):INSERT(TxB)的记录先写,DELETE(TxA)的记录后写。

从库重放:先执行INSERT(插入 id=3),后执行DELETE FROM users WHERE age>20;。在从库执行 DELETE 时,它会无情地把刚刚插入的 id=3 也一起删掉!

  • 从库结果:id=1, id=2, id=3 全部被删。

结果:主从数据严重不一致!

如何解决?MySQL 选择了REPEATABLE READ。在这个级别下,TxA 在开始时创建了一致性读视图(快照),直到事务结束。即使 TxB 提交了,TxA 依然看不到新插入的 id=3,所以 DELETE 语句只作用于它一开始看到的 id=1 和 id=2。这条 DELETE 记录到 Binlog 后,在从库执行时,也会基于从库的 MVCC 快照(或锁机制)只删除这两条,从而保证主从一致。

二:MVCC 实现的读视图(Read View)生成时机不同

为了理解这一点,我们需要看一眼 InnoDB 的多版本并发控制(MVCC)机制。

  • READ COMMITTED每一次执行普通SELECT语句,都会重新生成一个新的 Read View(读视图)。
  • REPEATABLE READ事务内第一次执行SELECT时生成一个 Read View,并复用到事务结束。

这种机制天然保证了“可重复读”,并且为基于 STATEMENT 的复制提供了确定性。虽然生成 Read View 的代价很小,但REPEATABLE READ减少了生成的次数,在极高并发下对 CPU 也算一种微小的优化。

三:间隙锁(Gap Lock)带来的“伪串行化”优势

虽然REPEATABLE READ带来了更高的数据一致性保障,但它也有代价——间隙锁。为了阻止幻读,InnoDB 不仅锁住命中的行(Record Lock),还会锁住索引记录之间的“间隙”(Gap Lock)。

TxA (REPEATABLE READ)开启事务执行范围查询WHERE id BETWEEN10 AND 20获得间隙锁锁定 (10,20)之间的所有间隙再次查询结果集不变,幻读被阻止提交事务释放间隙锁TxB (试图插入)尝试插入 id=15阻塞等待(因间隙锁未释放)等待中...TxA提交后插入成功,但此时已不影响TxA的一致性事务并发与幻读防范(间隙锁作用)

MySQL 选择REPEATABLE READ,核心是为了兼容早期的 STATEMENT 格式 Binlog 以保证主从一致性。虽然现在有了更安全的ROW(基于行)格式 Binlog(推荐使用),但这个默认级别作为历史传承和“更安全”的默认配置,被保留了下来。


4. 现代视角下的权衡

既然 ROW 格式 Binlog 解决了主从不一致问题,为什么 MySQL 8.0 依然不改默认值?

  1. 兼容性与平滑升级:对于无数遗留系统,修改默认隔离级别可能引发未知的性能回退或死锁变化,作为基础软件,MySQL 必须保持高度的向下兼容。
  2. 业务语义更严谨:对于金融、报表统计等需要事务内多次读取同一批数据的场景,REPEATABLE READ直接提供了开箱即用的强一致性,减少了应用层处理“不可重复读”的代码负担。

但也要注意它的“副作用”
由于间隙锁的存在,在高并发写入(尤其是高冲突的插入场景)下,REPEATABLE READREAD COMMITTED更容易产生死锁。因为间隙锁是“范围锁”,两个事务很容易相互等待。


5. MySQL 的隔离级别决策逻辑

READ UNCOMMITTED

READ COMMITTED

REPEATABLE READ

SERIALIZABLE

用户发起事务

当前隔离级别?

无锁读,性能极致
但如同走钢丝,易脏读
极少生产环境使用

Oracle/PostgreSQL默认
每次查询生成新快照
无间隙锁,并发高,但主从需配合ROW格式Binlog

MySQL默认
首次查询生成快照并复用
加间隙锁防幻读
主从兼容性好(STATEMENT/ROW均可)
代价:更高死锁风险

所有读加共享锁
完全串行化
性能最差,仅用于极端严格场景

结束事务

结论

“MySQL 默认是 REPEATABLE READ。这主要源于早期的 STATEMENT 模式 Binlog 复制,如果使用 READ COMMITTED 会导致主从数据不一致。虽然现在我们可以通过设置 binlog_format=ROW 并切换到 READ COMMITTED 来获得更高并发和更少死锁,但 InnoDB 为了向后兼容和提供更稳健的默认行为,在 8.0 版本中依然保留了 REPEATABLE READ 作为默认值。”

← 返回列表