不加锁的简单select读都是快照读,读的是历史数据。需要加锁的场景,读取的是记录的最新版本,加锁的select、对数据进行增删改都会进行当前读。
事务有四个隔离级别,可能存在三种并发问题:
MySQL中默认的隔离级别为可重复读,它实际上解决了脏读、不可重复读和幻读,但并不是串行化。
trx_id :每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id 隐藏列。
roll_pointer :每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
例如:
有一student表,id = 1,name = ‘张三’,class = ‘一班’
假设插入该记录的事务id为8,此条记录的结构如下:
每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer 属性( INSERT 操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表,就是版本链。
对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer 属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。
readview就是事务A在使用MVCC机制进行快照读操作时产生的读视图。当事务启动时,会生成数据库系统当前的一个快照,innodb为每个事务构造了一个数组,用来记录并维护系统当前活跃(begin了但未commit)事务的id。
mvcc只在read committed和repeatable read两个隔离级别下工作。
read committed:每次读取数据前都生成一个readview。
事务id的分配:如果是增删改行为,系统自动递增分配事务id;如果是查询行为,事务id为0.
现在有两个事务id 分别为10 、20 的事务在执行:
# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...
student表中id为1的记录得到的版本链表如图:
此时有个事务隔离级别为read committed,进行如下操作:
begin;
select * from student where id = 1;
因为transaction 10 未提交,所以这个事务查出来的结果是“1 张三 一班 8”这条记录。接下来解释原因:
接下来,把transaction 10的事务提交,
# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
COMMIT;
把transaction 20的事务中更新一下表student 中id 为1 的记录:
# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...
UPDATE student SET name="钱七" WHERE id=1;
UPDATE student SET name="宋八" WHERE id=1;
begin;
# select 1: 10 20 未提交
select * from student where id = 1; # 张三
# select 2: 10提交,20未提交
select * from student where id = 1; # 王五
只会在第一次查询时生成read view,之后的查询不会重复生成。
还是上面的例子:
# Transaction 10
BEGIN;
UPDATE student SET name="李四" WHERE id=1;
UPDATE student SET name="王五" WHERE id=1;
# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...
此时有个事务隔离级别为read committed,进行如下操作:
begin;
select * from student where id = 1;
同上,此时查到的是“张三”记录。read view中:creator_trx_id = 0(查询操作事务id为0)、trx_ids列表内容[10,20](id为10和20的两个事务活跃)、up_limit_id = 10(小的事务是10)、low_limit_id = 21(最大的事务不会超过20);
提交transaction 10,到transaction 20中更新表student,
# Transaction 20
BEGIN;
# 更新了一些别的表的记录
...
UPDATE student SET name="钱七" WHERE id=1;
UPDATE student SET name="宋八" WHERE id=1;
begin;
# select 1: 10 20 未提交
select * from student where id = 1; # 张三
# select 2: 10提交,20未提交
select * from student where id = 1; # 张三
这时不再生成新的read view,还是一开始的read view。creator_trx_id = 0(查询操作事务id为0)、trx_ids列表内容[10,20](id为10和20的两个事务活跃)、up_limit_id = 10(小的事务是10)、low_limit_id = 21(最大的事务不会超过20);
所以说,只要查询事务没有提交,用的都是第一次select时生成的read view。
幻读:student表中有一条id=1的记录,事务A开始进行第一次查询,结果显示id=1,现有事务B对student表进行更新操作,添加了id=2,id=3的两条记录并提交,接下来事务A再进行第二次查询,结果显示id=1,id=2,id=3,同一事务A前后两次查询操作的返回结果不相同,这就是出现了幻读。
在repeatable read隔离级别下,read view只在事务第一次进行查询操作时生成,此后的查询操作(只要不commit)仍用这个视图,所以就算事务B提交了更新,read view并不同步更新,trx_ids列表中仍有事务B的trx_id,认为事务B处于活跃状态,跳过更新的那条记录。
MVCC = 隐藏字段 trx_id + undo log 版本链 + read view 快照
READ COMMITTD
在每一次进行普通SELECT操作前都会生成一个ReadView。
REPEATABLE READ
只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView。
说明:执行delete语句或更新主键的update语句并不会立即把对应的记录完全从页面中删除,而是执行一个所谓的delete mark操作,相当于只是对记录上打了一个删除标志位,这就是为MVCC服务的,因为可能需要回滚。