MVCC

MVCC

​ MVCC(Multi Version Concurrency Control)是在并发访问数据库是,通过对数据做多版本控制,避免因为写数据是要加写锁而阻塞读取请求,造成写数据无法读取数据的问题。

​ 通俗的将就是MVCC通过保存数据的历史版本,根据对比数据的版本号来决定数据是否显示,在不需要加读锁的情况能打到事务的隔离效果,可以同时进行数据的读取和修改,极大地提升了事务的并发性能

Innodb的MVCC实现的核心点

包括下面的四个重要属性

  1. 事务版本号
  2. 表的隐藏列
  3. undo log
  4. read view

事务版本号

​ 每次事务开启前都会从数据库获取一个自增加的事务ID,可以从事务ID判断事物的执行先后顺序

表格的隐藏列

  • DB_TRX_ID 记录该数据事务的事务ID
  • DB_ROLL_PTR 指向上一个版本的数据在undo log 里的位置指针
  • DB_ROW_ID 隐藏ID,当创建表没有合适的索引作为聚集索引时,会用该隐藏ID创建聚集索引

Undo log

Undo log 主要用于记录数据被修改之前的日志,在表之前的数据信息,在表信息修改之前会把数据存储到undo log里,当事务进行回滚时可以通过undo log里的日志进行数据还原。

  1. 保证事务进行rollback时的原子性和一致性,当事务进行回滚的时候可以用undo log进行回复
  2. 用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都用友自己独立的快照数据版本

事务版本号、表格的隐藏列、undo log的关系

模拟一行数据修改的过程来了解十五号、表格隐藏的列和undo log他们之间的关系

准备一张原始的数据表

id name DB_TRX_ID DB_ROLL_PTR
1 张三 103 none

开启事务
对user_info表执行 update user_info set name = '李四' where id = 1会进行如下操作

  1. 获取到一个事务编号104
  2. 把user_info表修改前的数据拷贝到undo log
  3. 修改user_info表id=1的数据
  4. 把修改后的数据事务版本号改成当前事务版本号,并把DB_ROLL_PTR地址指向undo log数据地址

执行完成后的效果如图

Read View

使用innodb开启事务前会创建read_view,副本主要保存当前数据库系统中正处于活跃(没有commit)事务的ID号
read view的几个重要属性

  1. trx_ids:当前系统活跃(未提交)事务版本号集合
  2. low_limit_id:创建当前read veiw 时 当前系统最大事务版本号+1
  3. up_limit_id:创建当前read view 时系统正处于活跃事务的最小版本号
  4. creator_trx_id 创建当前read view的事务版本号

read view 匹配条件

  1. 数据事务ID < up_limit_id 则显示
  2. 数据事务ID>=low_limit_id则不显示,如果数据事务ID大于read view 中当前系统最大的事务ID,则说明该数据是在当前read view创建之后才产生的,所以数据不予显示
  3. up_limit_id<=数据事务ID<low_limit_id时,则需要具体看对应的事务id
    1. 如果事务不存在与trx_ids集合(说明read view产生的事务已经commit),则可以可以显示
    2. 事务ID存在trx_ids则说明read view 产生的数据还没有提交,如果数据的事务等于creator_trx_id,那么说明这个数据就是事务自己生成的,自己生成的数据自己当然能看见,所以这种情况此数据也是也可以显示的
    3. 如果事务id既存在于trx_ids而且有不等于creator_trx_id则说明read view产生的时候数据则提交,又不是自己生成的,这种情况不能显示。
  4. 查询时不满足read view 时,则从undo log里面获取数据,当数据事务id不满足read view条件时,查找对应的undo log,再执行1-3步

innodb实现mvcc的原理

  1. 获取事务版本号
  2. 获取一个read view
  3. 查询到数据,与read view事务版本号进行匹配
  4. 不符合read view规则的从undo log里获取历史版本数据
  5. 返回符合规则的数据

模拟两个线程分别进行更新和插叙你的情况

各个事务隔离级别下的read view 工作方式

RC(read commit)级别下同一个事务里面的每一次查询都会闯将新的read view,这样就可能造成同一个事务里前后读取的数据可能不一致(重复读),RR(repeat read)级别下一个事务里只会获取到一次read view副本,从而保证查询的数据都是一样的


read uncommited 级别的事务不会获取read view副本

快照度和当前度

快照读

快照读是指读取数据时不时读取最新版本的数据,而是基于历史版本读取一个快照信息

当前读

当前读是读取的数据库最新的数据,当前读和快照读不通,因为要读取最新的数据而且要保证事务的隔离性,所以当前读是需要对数据进行加锁的,使用for update

实例python代码
mvcc简单的实现

import copy

global_trx_id = 0

datas = []
trx_ids = set()


class Row:
    def __init__(self, _id, name, DB_TRX_ID, DB_ROLL_PTR):
        self.id = _id
        self.name = name

        self.DB_TRX_ID = DB_TRX_ID  # 记录操作该数据事务的事务ID
        self.DB_ROLL_PTR = DB_ROLL_PTR  # 指向上一个版本数据在undo log 里的位置指针
    def __repr__(self):
        return "Row<id:{},name:{},DB_TRX_ID:{},DB_ROLL_PTR:{}>".format(self.id , self.name, self.DB_TRX_ID, self.DB_ROLL_PTR)


class ReadView:
    def __init__(self, low_limit_id, up_limit_id, creator_trx_id, ):
        self.low_limit_id = low_limit_id  # 创建当前read view 时“当前系统最大事务版本号+1”
        self.up_limit_id = up_limit_id  # 创建当前read view 时“系统正 处于活跃的事务 最小版本号”
        self.creator_trx_id = creator_trx_id  # 创建当前read view的事务版本号


def begin(row: Row):
    global global_trx_id
    cur_trx_id = global_trx_id
    trx_ids.add(cur_trx_id)
    global_trx_id += 1
    return cur_trx_id


def commit(trx_id):
    trx_ids.remove(trx_id)


def update_data_name(cur_trx_id, row, name):
    # cur_trx_id = begin(row)

    row_log = copy.deepcopy(row)

    row.name = name
    row.DB_TRX_ID = cur_trx_id
    row.DB_ROLL_PTR = row_log


def select_data_by_id(trx_id, id):
    read_view = get_read_view(trx_id)

    def match_rec(read_view, d):
        if d == None:
            return None
        if d.DB_TRX_ID < read_view.up_limit_id:
            return d
        else:
            if d.DB_TRX_ID not in trx_ids or (d.DB_TRX_ID in trx_ids and d.DB_TRX_ID == read_view.creator_trx_id):
                return d
            return match_rec(read_view, d.DB_ROLL_PTR)

    for d in datas:
        if d.id == id:
            # 开始进行事务的对比
            return match_rec(read_view, d)
    return None


def get_read_view(trx_id):
    rv = ReadView(global_trx_id, min(trx_ids), trx_id)
    return rv


def main():
    # update thread
    one_row = Row(1, "Foo", 99, None)
    datas.append(one_row)
    global global_trx_id
    global_trx_id = 102
    # update begin
    update_trx_id = begin(one_row)
    # select begin
    select_trx_id = begin(one_row)

    # update data
    update_data_name(update_trx_id, one_row, "Bar")

    # select get view
    read_view = get_read_view(select_trx_id)
    # select found rec
    select_get_rec = select_data_by_id(select_trx_id, 1)
    print(select_get_rec)


if __name__ == '__main__':
    main()

引用

posted @ 2022-07-24 22:25  随风而行-  阅读(101)  评论(0编辑  收藏  举报