数据库的MVCC
数据库并发情况下存在的问题¶
- 脏读 事务B读取到事务A修改但没有提交的值;当事务A发生回滚时,事务B无法读到的数据将变成脏数据。
场景示例:电商库存与报表
- 事务A (用户下单):用户购买一件库存为10的商品,系统执行 UPDATE stock = 9,但事务尚未提交,正在进行其他检查。
- 事务B (库存报表):此时,一个后台报表系统读取了库存,显示为 9。
- 事务A回滚:用户的下单因故失败,事务A回滚,库存恢复为10。
-
后果:报表系统展示了一个从未真实存在过的库存数据(9),导致了数据污染。
-
不可重复读 事务B 在事务内读取同一行数据Data时,值不一致;这是由于在B执行两次查询间,有事务A对Data执行并提交了update命令。
场景示例:机票价格校验
- 事务A (用户订票):用户查询某航班机票,第一次读取价格为 500元。用户开始填写个人信息。
- 事务B (后台调价):在此期间,航空公司上调了价格,执行 UPDATE price = 550 并提交。
- 事务A (继续订票):用户填写完毕点击支付,系统为了校验,第二次读取该航班价格,发现价格已变为 550元。
-
后果:在同一个订票流程中,前后读取到的价格不一致,导致业务逻辑冲突
-
幻读 事务B在事务内多次按同一范围R查询但是返回的记录行数不同;事务A在两次查询的间隙在范围R中执行并提交了insert或者delete。
场景示例:金融系统月底对账
- 事务A (对账系统):在月底,对账系统第一次查询某商户9月份的交易记录总数,得到结果为 100条。
- 事务B (正常交易):在此期间,该商户又产生了一笔新的交易,系统执行 INSERT 操作并提交。
- 事务A (继续对账):对账系统在完成其他核对后,为了最终确认,第二次按相同范围查询,发现交易记录总数变成了 101条。
- 后果:第二次查询多出了一条“幻影”记录,导致对账程序的初步统计和最终校验结果不一致,对账失败。
#### 事务隔离等级
MVCC(多版本并发控制)的解决方案¶
mvcc简述¶
MVCC 是一种乐观并发控制 (Optimistic Concurrency Control, OCC) 思想的成功实现。
- 乐观哲学:它乐观地假设“读”和“写”操作通常不会在同一份数据上产生冲突。因此,它不像悲观锁那样,在操作前就预先加锁,而是允许它们并行进行。
- 冲突解决方式:它解决冲突的方式不是“阻塞等待”,而是“版本绕行”。当读操作遇到一个由未提交事务创建的新版本时,它不会等待,而是会绕过它,去读取一个更早的、对它可见的旧版本(快照读)。如隔离等级(读已提交和可重复读)下,MVCC通过readview不同的生成使用时机来给确定其在冲突时所见的数据版本(对于一个事务,每次快照读都构造一个当期的readview;对于一个事务只在第一次快照读时生成当前的readview,后续的快照读都沿用同一个readview。
readview的定义¶
确定readview后,如何确定输出的数据版本?版本链的构建与 Read View 的查找之旅¶
多版本并发控制 (MVCC) 是现代数据库实现高性能并发读写的核心技术。其精髓在于两大机制:版本链 (Version Chain) 的动态构建,以及 Read View (读视图) 在版本链上的精确查找。
¶
版本链是一个逻辑上的单向链表,它记录了一行数据的完整历史变更。
1. 版本链的构成(数据结构)¶
版本链由以下核心元素构成:
- 行记录中的隐藏字段:
DB_TRX_ID(事务ID): 记录最后一次修改该数据版本的事务 ID。DB_ROLL_PTR(回滚指针): 指向该版本在 Undo Log 中的前一个版本,是连接链表的关键。
- Undo Log (回滚日志):
- 历史版本的档案馆:Undo Log 中存储了数据被修改前的完整行镜像(前置镜像)。这些镜像就是版本链上的历史节点。
结构图示:
2. 版本链的构造(动态过程)¶
版本链是在数据被修改时动态构建和延长的。
-
INSERT- 链的诞生- 新行被插入。
DB_TRX_ID被设置为当前事务 ID。DB_ROLL_PTR为null,因为没有前序版本。
-
UPDATE- 链的延长 (核心) 这是一个“头插法”的过程:- 记录 Undo Log: 在修改数据之前,先将当前行的完整内容复制到 Undo Log 中,创建一个新的历史版本节点。
- 修改数据页: 在数据页的行记录上,执行更新,并将
DB_TRX_ID更新为当前事务 ID。 - 连接链表: 将数据页上该行的
DB_ROLL_PTR指向刚刚在 Undo Log 中创建的那个旧版本节点。
-
DELETE- 特殊的延长DELETE在 MVCC 中并非立即物理删除。- 它同样会先将被删除行的当前版本记录到 Undo Log 中。
- 然后,在数据页的行记录上打上一个“删除”标记,并更新
DB_TRX_ID和DB_ROLL_PTR。该记录依然是版本链的一部分,直到后台线程确认其不再需要时才被物理清除。
二、 Read View:事务的“时空地图”¶
Read View 是 MVCC 的决策核心。当事务执行快照读 (SELECT) 时,会依据 Read View 的规则在版本链上查找。
1. Read View 的构成(“地图”要素)¶
Read View 是在事务查询时,在内存中动态创建的一个快照信息对象,包含:
m_ids: 创建 Read View 时,所有活跃事务的 ID 列表。min_trx_id:m_ids中的最小 ID (可见性的“过去”边界)。max_trx_id: 预分配的下一个事务 ID (可见性的“未来”边界)。creator_trx_id: 创建此 Read View 的事务自身的 ID。
2. Read View 的创建时机(决定隔离级别)¶
-
在“读已提交” (Read Committed) 级别:
每条
SELECT语句执行时,都生成一个新的 Read View。 * 在“可重复读” (Repeatable Read) 级别:仅在第一条
SELECT语句执行时创建,后续所有SELECT复用同一个 Read View。
3. 在版本链上的查找之旅(可见性判断算法)¶
查找过程是一个从新到旧 (从版本链头部开始) 的短路查找。一旦找到第一个可见版本,立即停止并返回。
对于版本链上的任意一个版本(其事务ID为 version_trx_id),判断逻辑如下:
-
规则一:是否是自己人? (Creator Check)
version_trx_id等于creator_trx_id吗?- 是 -> 可见。✅ (事务总能看到自己的修改)
- 否 -> 进入下一规则。
-
规则二:是否是历史? (Past Check)
version_trx_id小于min_trx_id吗?- 是 -> 可见。✅ (该版本在事务开始前就已提交)
- 否 -> 进入下一规则。
-
规则三:是否是未来? (Future Check)
version_trx_id大于或等于max_trx_id吗?- 是 -> 不可见。❌ (该版本在事务快照创建后才开始) -> 继续查找下一个旧版本。
- 否 -> 进入下一规则(此时
min_trx_id <= version_trx_id < max_trx_id)。
-
规则四:是当时正在施工的吗? (Active Check)
- 在
m_ids(活跃事务列表) 中查找version_trx_id。- 如果存在于
m_ids中 -> 不可见。❌ (该版本在快照创建时还未提交) -> 继续查找下一个旧版本。 - 如果不存在于
m_ids中 -> 可见。✅ (该版本在快照创建时已提交)
- 如果存在于
- 在
这个循环会一直持续,直到找到第一个标记为 ✅ (可见) 的版本,作为最终的查询结果。
详细总结一下 是如何找到对应的数据版本的 把版本链都构成和构造 以及readview 如何在版本链上寻找的
并发控制策略核心精髓对比¶
乐观锁是在应用层面 不涉及线程操作的情况下 对于读写冲突 进行一个自定义方法的补偿操作 而读写锁模式则是采取线程操作的阻塞重试机制(更严格)
-
MVCC (多版本并发控制)¶
-
层面: 数据库内核/存储引擎层。对应用开发者透明。
- 核心思想: 乐观的,用空间换时间。通过维护数据的多个历史版本,实现高并发。
- 处理方式: 版本绕行。读操作遇到写冲突时,不等待,而是去读取一个对当前事务可见的旧版本快照。
- 主要解决: 读写阻塞问题,并在此基础上解决不可重复读和快-照读下的幻读。
- 加锁行为: 普通SELECT(快照读)不加锁。
-
适用场景: 数据库内部默认的高性能并发读取机制。
-
乐观锁 (Optimistic Locking)¶
-
层面: 应用层。需要开发者手动编码实现(如 version 字段)。
- 核心思想: 乐观的,假设并发冲突是稀有的。
- 处理方式: 事后验证 + 应用层补偿。全程非阻塞,在最后提交UPDATE时,通过WHERE子句检查数据在此期间是否被修改过。若有冲突,更新失败,由应用代码决定如何重试或放弃。
- 主要解决: 更新丢失问题,保证“读-改-写”业务流程的原子性。
- 加锁行为: 不加锁。
-
适用场景: 读多写少的场景,能最大化吞吐量。
-
读写锁 / 悲观锁 (Read-Write Lock / Pessimistic Locking)¶
-
层面: 数据库或系统底层。
- 核心思想: 悲观的,假设并发冲突是常态,必须提前预防。
- 处理方式: 事前加锁 + 阻塞等待。在访问数据前必须先获取锁。获取不到锁的线程会被挂起,进入等待队列,直到锁被释放。
- 主要解决: 在需要绝对数据一致性的场景下,强制串行化写操作,保证数据安全。
- 加锁行为: 显式加锁。SELECT ... FOR UPDATE加排他锁(写锁),SELECT ... LOCK IN SHARE MODE加共享锁(读锁)。
- 适用场景: 写多或冲突频繁的场景,或业务逻辑复杂、不容许失败重试的关键写操作。


