MVCC如何让数据库实现高并发?揭秘多版本并发控制的底层逻辑

MVCC如何让数据库实现高并发?揭秘多版本并发控制的底层逻辑

MVVC的实现依赖于:行记录隐藏的列、ReadView、undolog

1. 基本概念

1.1. 行记录隐藏的列

  1. ROW_ID:隐藏的自增id,如果没用设置主键或者唯一非空索引,MySQL会自动生成一个
  2. TRX_ID:最后修改当前记录的事务id(每开启一个事务,就会从数据库获取一个事务id,这个id是自增的,可以以此判断事务的时间顺序)
  3. ROLL_PTR:undolog回滚指针,存放着指向上一次记录的旧数据

1.2. ReadView

img

ReadView中的几个重要字段:

  1. creator_trx_id:创建ReadView的事务的事务id
  2. m_ids:创建ReadView时,当前数据库中“活跃且未提交”的事务id列表
  3. min_trx_id:创建ReadView时,当前数据库中“活跃且未提交”的事务中最小的事务id
  4. max_trx_id:创建ReadView时,当前数据库中“活跃且未提交”的事务中最大的事务id

img

img

ReadView的一些可见性规则:

  1. 如果记录中的trx_id小于ReadView中的min_trx_id,表示访问的记录已经在创建快照之前启动事务生成的,数据可见
  2. 如果记录中的trx_id大于等于ReadView中的max_trx_id,表示访问的记录在创建快照之后才启动事务生成的,数据不可见
  3. 如果记录中的trx_id介于min_trx_id和max_trx_id之间
    • 如果trx_id在m_ids中不存在,表示事务已经提交,数据可见
    • 如果trx_id在m_ids中存在,表示事务尚未提交,数据不可见

1.3. undolog

MVVC使用过的快照会被存储到undolog中,通过回滚指针将一个个历史执行记录形成一个版本链

2. 快照读与当前读

2.1. 快照读

  1. 定义:快照读基于MVVC机制,读取的是历史数据(即ReadView创建时查询到的数据),而非最新数据
  2. 特点:
    • 非阻塞:普通的SELECT语句属于快照读,不会阻塞其他读写操作
    • 数据一致性:只要事务未提交,读取的都是ReadView创建时查询到的数据,即使其他事务已经提交修改,读取的也是历史数据
  1. 使用场景:
    • RR级别:执行第一条SELECT语句时创建一个ReadView快照,后续查询相同记录都读取历史数据
    • RC级别:执行每一条SELECT语句时创建一个ReadView快照

2.2. 当前读

  1. 定义:当前读读取的是最新的实时数据,并对记录加锁,其他事务无法进行修改
  2. 特点:
    • 锁机制:通过锁机制(共享锁、排他锁)保证数据的一致性
    • 数据实时性:获取最新的实时数据
  1. 使用场景:
    • RR级别:使用Next-Key Lock解决幻读问题
    • Serializable:每一条普通的SELECT语句强制采取当前读
对比项快照读当前读
数据版本历史版本(事务启动时的快照)最新已提交版本
加锁不加锁加共享锁(S锁)或排他锁(X锁)
隔离级别RR、RC(部分支持)RR、Serializable(强制使用)
操作类型普通SELECTSELECT ... FOR UPDATEUPDATEDELETE
幻读处理依赖MVCC历史版本,可能部分规避通过Next-Key Lock完全防止
性能影响低开销(无需锁)高开销(锁竞争)

3. 可重复读是如何工作的?

假设先启动事务A(事务id为51),再启动事务B(事务id为52)

两个事务的ReadView创建和假设数据库中某条记录如下:

img

3.1. 接着我们来执行下面一系列操作

  1. 事务B读取id为1账户内的balance余额 , 为100w
  2. 事务A修改id为1账户内的balance余额为200w , 但未提交
  3. 事务B再次读取id为1账户内的balance余额 , 为100w
  4. 事务A提交
  5. 事务B再次读取id为1账户内的balance余额 , 为100w

3.2. 接下来具体分析一下每个步骤

结合图来理解

img

  1. 事务B读取id为1账户内的balance余额
    • 事务B查询数据库内记录的隐藏列trx_id为50,比min_trx_id(即51)小 , 证明在创建事务B的快照时就已经提交,所以直接读取到100w
  1. 事务A修改id为1账户内的balance余额为200w , 但未提交 :
    • 事务A修改数据库中的数据
    • 同时也修改了该记录隐藏列中的trx_id为事务A的事务id(即51)
    • 同时将roll_pointer指向修改前的旧数据
  1. 事务B再次读取id为1账户内的balance余额 :
    • 事务B再次查询该记录时 , 发现隐藏列中的trx_id为51 , 介于min_trx_id(即51)和max_trx_id(即52)之间
    • 接着判断是否存在于m_ids中 , 发现存在 , 则表示事务尚未提交就创建了事务B的快照 , 其修改对于事务B不可见
    • 所以事务B顺着roll_pointer对应的版本链读取到最近一条对于事务B可见的记录(也就是trx_id为50的记录),获取记录为100w
  1. 事务A提交

  2. 事务B再次读取id为1账户内的balance余额

    • 过程和3类似

4. 读提交是如何工作的?

我们同样先后启动两个A,B两个事务

4.1. 接下来执行具体操作:

  1. 事务B读取id为1账户内的balance余额 , 为100w
  2. 事务A修改id为1账户内的balance余额为200w , 但未提交
  3. 事务B再次读取id为1账户内的balance余额 , 为100w
  4. 事务A提交
  5. 事务B再次读取id为1账户内的balance余额 , 为200w

4.2. 接下来还是具体分析过程

同样结合图来理解:

img

  1. 事务B读取id为1账户内的balance余额
    • 事务B查询数据库内记录的隐藏列trx_id为50,比min_trx_id(即51)小 , 证明在创建事务B的快照时就已经提交,所以直接读取到100w
  1. 事务A修改id为1账户内的balance余额为200w , 但未提交
    • 事务A修改数据库中的数据
    • 同时也修改了该记录隐藏列中的trx_id为事务A的事务id(即51)
    • 同时将roll_pointer指向修改前的旧数据
  1. 事务B再次读取id为1账户内的balance余额
    • 事务B再次查询该记录时 , 发现隐藏列中的trx_id为51 , 介于min_trx_id(即51)和max_trx_id(即52)之间
    • 接着判断是否存在于m_ids中 , 发现存在 , 则表示事务尚未提交就创建了事务B的快照 , 其修改对于事务B不可见
    • 所以事务B顺着roll_pointer对应的版本链读取到最近一条对于事务B可见的记录(也就是trx_id为50的记录),获取记录为100w
  1. 事务A提交

img

  1. 事务B再次读取id为1账户内的balance余额
  • 所以事务B顺着roll_pointer对应的版本链读取到最近一条对于事务B可见的记录(也就是trx_id为50的记录),获取记录为100w
  1. 事务A提交

在这里插入图片描述

  1. 事务B再次读取id为1账户内的balance余额
    • 事务B第三次查询该记录时 , 发现隐藏列中的trx_id为51 , 小于min_trx_id (即52),表示该记录对于事务B可见,所以直接获取trx_id为51的记录,返回200w
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值