《MySQL是怎样运行的-从根上理解MySQL》第21章 阅读笔记
一个事务对应着现实世界的一次状态转换。
事务执行之后必须保证数据符合现实世界的所有规则,这就是我们强调的一致性。
程序员只要将现实世界的状态转换锁对应的数据库操作都写到一个事务中,那么该事务执行完成之后,必然从一个一致性状态转换到另一个一致性状态。
为了保证事务的一致性,要让事务“隔离”地执行,互不干涉。这也就是事务的隔离性。
实现隔离性最粗暴的方式是串行执行,即在系统中的同一时刻最多只允许一个事务执行。 这种方式会严重降低系统吞吐量和资源利用率,增加事务的等待时间。
改进方法:
在某个事物访问某个数据时,对其他试图访问相同数据的事务进行限制,让它们进行排队。 这样可以让并发执行的事务的执行结果与穿行结果一样,我们称之为串行化执行。
两个并发事务在执行过程中访问相同数据的情况有:
- 读-读。
- 读-写。
- 写-写。
- 写-读。 只有在至少一个事务对数据进行写操作时,才会带来一致性问题。所以,我们仅需要再读-写、写-读和写-写情况时,对其进行排队(通常是通过加锁实现)。
事务并发执行时,可能会出现以下一致性问题:
- 脏写。一个事务修改了另一个未提交事务修改过的数据。
- 脏读。一个事务读取了另一个未提交事务修改过的数据。
- 不可重复读。
- 广义解释:一个事务修改了另一个未提交事务读取的数据。
- 严格解释:一个事务T1第一次读取数据后,另一个事务T2修改了该数据,之后T1再次读取该数据,第二次读取到的数据和第一次不一致。
- 幻读:一个事务先根据某些搜索条件查询出一些记录,在该事务未提交时,另一个事务写入了一些符合那些搜索条件的记录。
MySQL中的幻读:
对于MySQL来说,幻读强调的就是一个事务在按照某个相同的搜索条件多次读取记录时,在后读取时读取到了之前没有读到的记录。
SQL标准中的4种隔离级别:
- 读未提交(read uncommitted)。
- 读提交(read committed)。
- 可重复读(repeatable read)。
- 串行化(serializable)。
SQL标准中规定: 针对不同的隔离级别,并发事务执行过程中可以发生不同的现象。具体情况如下表所示:
隔离级别 脏读 不可重复读 幻读 读未提交 可能 可能 可能 读提交 不可能 可能 可能 可重复读 不可能 不可能 可能 串行化 不可能 不可能 不可能
MySQL中的4种隔离级别:
不同的数据库厂商对SQL标准中规定的4种隔离级别的支持不一样。MySQL支持4中隔离级别,但与SQL标准中的4种隔离级别并不完全一致–MySQL在可重复读隔离级别下,可以在很大程度上禁止幻读现网的发生。
MySQL的默认隔离级别为可重复读
MySQL-设置隔离级别
可以通过下面的语句修改事务的隔离级别:
set [global|session] transaction isolation level [read uncommitted|read committed|repeatable read|serializable];
- 使用global关键字,在全局范围内产生影响:
- 只对执行完该语句之后新产生的会话起作用。
- 对当前已经存在的会话无效。
- 使用session关键字:
- 对当前会话所有的后续事务有效;
- 在已经开启的事务中执行,不会影响当前正在执行的事务。
- 在事务之间执行,则对后续的事务有效。
- 不适用关键字:
- 只对当前会话中下一个即将开启的事务有效。
- 下一个事务执行完后,后续事务将恢复到之前的隔离级别。
- 该语句不能再已经开启的事务中执行,否则会报错。
版本链 对于使用InnoDB存储引擎的表来说,它的聚簇索引记录都包含下面两个必要的隐藏列:
- trx_id:一个事务每次对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列。
- roll_pointer:每次对某条聚簇索引记录进行修改时,都会把旧的版本写入到undo日志中。这个隐藏列相当于一个指针,可以通过它找到该记录修改前的信息。 row_id并不是必要的,如果创建的表中有主键时,或者有不允许为null的唯一索引时,就不会包含row_id隐藏列。
insert undo日志只在事务回滚时发生作用。当该事务提交后,该类型的undo日志就没用了,它占用的undo日志段也会被系统回收。
版本链 除了insert操作,每条undo日志都有一个roll_pointer属性(insert操作对应的undo日志没有该属性,因为insert操作的记录没有更早的版本)。每次更新聚簇索引记录后,都会将旧值放到一条undo日志中,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,这个链表成为版本链。
MVCC 我们会利用一条记录的版本链来控制并发事务访问相同记录时的行为,我们把这种机制称之为多版本并发控制(MVCC)。
update操作产生的undo日志中,只会记录一些索引列以及被更新到的列的信息,并不会记录所有的信息。
Read View 对于使用RC和RR格力级别的事务来说,都必须保证读到已提交的事务修改过的记录。如果另一个事务修改了记录但是还未提交,则不能直接读取最新版本的记录。问题是:需要判断版本链中哪个版本是当前事务可见的。
为此,InnoDB提出了ReadView(也翻译成 一致性视图)的概念。 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值域ReadView中的creator_trx_id相同,则该版本可见。
- 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id,则该版本可见。
- 如果被访问版本的trx_id属性值大于或等于max_trx_id,则该版本不可见。
- 如果被访问版本的trx_id在min_trx_id和max_trx_id之间,则需要判断该版本的trx_id是否在m_ids列表中。如果在,则该版本不可见;如果不在,则说明创建ReadView时生成该版本的事务已经被提交,则该版本可见。
如果某个版本的数据对当前事务不可见,那就顺着版本链找到下一个版本的数据,并继续执行上面的步骤来判断记录的可见性,直到版本链中的最后一个版本。
在事务执行过程中,只有在第一次真正修改记录时(比如使用insert,delete,update语句),才会分配一个唯一的事务id,而且这个事务id是递增的。
Read View的生成时机
- RC– 每次读取数据前都生成一个ReadView。 使用RC隔离级别的事务在每次查询开始时都会生成一个独立的ReadVie
- RR– 在第一次读取数据时生成一个ReadView。
二级索引与MVCC 只有在聚簇索引中才有trx_id和roll_pointer隐藏列。
如果查询优化器决定先到二级索引中定位,那么相关二级索引记录对这个事务的可见性的判断如下:
二级索引页的Page Header中有一个PAGE_MAX_TRX_ID属性,每次对该页面中的记录进行增删改操作时,都会把PAGE_MAX_TRX_ID设置为当前事务的事务id(如果当前事务的事务id更大的话)。当select语句访问某个二级索引记录时,先判断min_trx_id是否大于PAGE_MAX_TRX_ID,如果大于,则说明该记录是可见的;否则,执行步骤2,在回表之后再判断可见性。
利用二级索引记录中的主键值进行回表操作,得到对应的聚簇索引记录后再按照前面讲过的方式找到对该ReadView可见的第一个版本,然后判断该版本中相应的二级索引列的值是否与利用该二级索引查询时的值相同。如果是,就继续;否则,就跳过。
只有在进行普通的select查询时,mvcc才生效。
Purge 两件事:
- insert undo日志在事务提交之后就可以释放掉了,而update undo日志由于还需要支持mvcc,因此不能立即释放掉。 当一个事务提交之后,就会把这个事务执行过程中产生的一组update undo日志插入到History链表的头部。 但是,加入到History链表中的update undo日志所占用的存储空间没有被释放。
- 为了支持mvcc,delete mark操作仅仅是在记录上打了一个删除标记,并没有真正将记录删除。
如果能确保生成ReadView时某个事务已经提交,那么该ReadView肯定就不需要访问该事务运行过程中产生的undo日志了。
InnoDB为了做到这点:
在一个事务提交时,会为这个事务生成一个名为事务no的值,标识事务提交的顺序。 History链表是按照事务提交的顺序来排列各组undo日志的,索引History链表中的各组undo日志也是按照对应的事务no来排序的。
在生成ReadView时,会把比当前系统中足底啊的事务no值还大1的值赋给ReadView的事务no属性。
InnoDB把当前系统中所有的ReadView按照创建时间连城了一个链表。在执行purge操作时,就把最早生成的ReadView取出来,然后从各个回滚段的History链表中取出事务no值较小的各组undo日志。如果一组undo日志的事务no值小于最早ReadView的事务no值,则说明该组undo日志已经没用,就从History链表中移除,并释放其占用的存储空间。
如果某个事务使用RR隔离级别,该事务会一直复用最初产生的ReadView。如果这个事务运行了很久,那么最早生成的ReadView一直不释放,系统中的update undo日志和打了删除标记的记录就会越来越多,表空间对应的文件也会越来越大,一条记录的版本链会越来越长,从而影响系统性能。