MVCC
MVCC
MVCC(Multi Version Concurrency Control)是在并发访问数据库是,通过对数据做多版本控制,避免因为写数据是要加写锁而阻塞读取请求,造成写数据无法读取数据的问题。
通俗的将就是MVCC通过保存数据的历史版本,根据对比数据的版本号来决定数据是否显示,在不需要加读锁的情况能打到事务的隔离效果,可以同时进行数据的读取和修改,极大地提升了事务的并发性能
Innodb的MVCC实现的核心点
包括下面的四个重要属性
- 事务版本号
- 表的隐藏列
- undo log
- 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里的日志进行数据还原。
- 保证事务进行rollback时的原子性和一致性,当事务进行回滚的时候可以用undo log进行回复
- 用于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
会进行如下操作
- 获取到一个事务编号104
- 把user_info表修改前的数据拷贝到undo log
- 修改user_info表id=1的数据
- 把修改后的数据事务版本号改成当前事务版本号,并把DB_ROLL_PTR地址指向undo log数据地址
执行完成后的效果如图
Read View
使用innodb开启事务前会创建read_view,副本主要保存当前数据库系统中正处于活跃(没有commit)事务的ID号
read view的几个重要属性
- trx_ids:当前系统活跃(未提交)事务版本号集合
- low_limit_id:创建当前read veiw 时 当前系统最大事务版本号+1
- up_limit_id:创建当前read view 时系统正处于活跃事务的最小版本号
- creator_trx_id 创建当前read view的事务版本号
read view 匹配条件
- 数据事务ID < up_limit_id 则显示
- 数据事务ID>=low_limit_id则不显示,如果数据事务ID大于read view 中当前系统最大的事务ID,则说明该数据是在当前read view创建之后才产生的,所以数据不予显示
- up_limit_id<=数据事务ID<low_limit_id时,则需要具体看对应的事务id
- 如果事务不存在与trx_ids集合(说明read view产生的事务已经commit),则可以可以显示
- 事务ID存在trx_ids则说明read view 产生的数据还没有提交,如果数据的事务等于creator_trx_id,那么说明这个数据就是事务自己生成的,自己生成的数据自己当然能看见,所以这种情况此数据也是也可以显示的
- 如果事务id既存在于trx_ids而且有不等于creator_trx_id则说明read view产生的时候数据则提交,又不是自己生成的,这种情况不能显示。
- 查询时不满足read view 时,则从undo log里面获取数据,当数据事务id不满足read view条件时,查找对应的undo log,再执行1-3步
innodb实现mvcc的原理
- 获取事务版本号
- 获取一个read view
- 查询到数据,与read view事务版本号进行匹配
- 不符合read view规则的从undo log里获取历史版本数据
- 返回符合规则的数据
模拟两个线程分别进行更新和插叙你的情况
各个事务隔离级别下的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()