InnoDB的锁机制可以解决并发控制,但开销大,常常与MVCC结合使用,在大多数情况下代替行级锁,降低开销。只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作
基本原理:
通过保存数据在某个时间点的快照来实现,当我们对某条记录做了变更时,老版本的数据被放在Undo Log里,并且以指针的形式关联起来,形成一个链表。这样,在查找老的版本时,需要按链表顺序查找,直到找到created_by_txn_id <= 当前事务ID的最新那条记录即可。
版本链
InnoDB 聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的,我们创建的表中有主键或者非NULL唯一键时都不会包含row_id列):
trx_id:每次对某条聚簇索引记录进行改动时,会把对应的事务id赋值给trx_id隐藏列。
roll_pointer:每次改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
假设表中只含有一条插入记录:
之后两个id分别为100、200的事务对这条记录进行UPDATE操作,操作流程如下:
注: 两个事务中不能交叉更新同一条记录。
每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表:
对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。
回滚段中的undo logs分为: insert undo log 和 update undo log
insert undo log : 事务对insert新记录时产生的undo log, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。
update undo log : 事务对记录进行delete和update操作时产生的undo log, 不仅在事务回滚时需要, 一致性读也需要,所以不能随便删除,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。
ReadView
用于判断版本链中的哪个版本是当前事务可见的,主要包含4个比较重要的内容:
m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也
就是m_ids中的最小值。
max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。
creator_trx_id:表示生成该ReadView的事务的事务id。
只需要按照下边的步骤判断记录的某个版本是否可见:
如果被访问版本的trx_id属性值与creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
如果被访问版本的trx_id属性值小于min_trx_id,表明生成该版本的事务在生成ReadView前已经提交,所以该版本可以被当前事务访问。
如果被访问版本的trx_id属性值大于或等于max_trx_id,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
如果被访问版本的trx_id属性值在min_trx_id 和 max_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
在MySQL中,READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成 ReadView 的时机不同。
READ COMMITTED — 每次读取数据前都生成一个ReadView
比如现在系统里有两个id分别为100、200的事务在执行:
#Transaction 100
BEGIN;
UPDATE t SET c = ‘关羽’ WHERE id = 1;
UPDATE t SET c = ‘张飞’ WHERE id = 1;
#Transaction 200
BEGIN;
#更新了一些别的表的记录
…
假设现在有一个使用READ COMMITTED隔离级别的事务开始执行:
#使用READ COMMITTED隔离级别的事务
BEGIN;
#SELECT1:Transaction 100、200未提交
SELECT * FROM t WHERE id = 1; # 得到的列c的值为’刘备’
这个SELECT1的执行过程如下:
在执行SELECT会先生成一个ReadView,ReadView的m_ids列表的内容就是[100, 200]。
然后从版本链中挑选可见的记录,从图中看出,最新版本的内容是’张飞’,该版本的trx_id 值为100,在m_ids列表内,不符合可见性要求,根据roll_pointer跳到下一个版本。
下一个版本的内容是’关羽’,该版本的trx_id值也为100,也在m_ids列表内,不符合。
下一个版本的列c的内容是’刘备’,该版本的trx_id值为80,小于m_ids列表中最小的事务id100,所以这个版本是符合要求的,最后返回给用户的版本就是这条列c为’刘备’的记录。
若我们把事务id为100的事务提交一下,然后再到事务id为200的事务中更新一下表t中id为1的记录:
此时m_ids列表的内容就是[200],在使用READ COMMITTED隔离级别的事务中查询表t中id值为1的记录时,得到的结果就是’诸葛亮’了。使用READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。
REPEATABLE READ — 在第一次读取数据时生成一个ReadView
对于使用REPEATABLE READ隔离级别的事务来说,只会在第一次执行查询语句时生成一个ReadView,之后的查询就不会重复生成了。
#SELECT1:Transaction 100、200均未提交
SELECT * FROM t WHERE id = 1; # 得到的列c的值为’刘备’
#SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM t WHERE id = 1; # 得到的列c的值仍为’刘备’
在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。