mysql之事务、锁、隔离级别与MVCC

参文章 《【MySQL】当前读、快照读、MVCC》 《正确的理解MySQL的MVCC及实现原理》 《MySQL 8.0 MVCC 源码解析》 《mysql幻读》 《细谈数据库表锁和行锁》 《什么是乐观锁,什么是悲观锁》 《MySQL事务和隔离》   写在开头:本文为学习后的总结,可能有不到位的地方,错误的地方,欢迎各位指正。   一、 一段连续的不可拆分的业务逻辑的组合称为事务 举例,比如银行转账,A的账户-50,B的账务+50,那么这2个操作必须都完成才行,称为事务 基于上面的例子,事务的基本属性A(原子性)C(一致性)I(隔离性)D(持久性)   二、事务的隔离级别分为 (1) 读未提交 会出现脏读,会读取到其他事务未提交的数据 (2)读已提交 事务A的2个查询期间事务B对一条数据做了修改,导致2次数据的读取不一致。 解决方案是加锁,表锁、行锁都可以,行锁颗粒度小,执行开销大,好处是并发度大 (3)可重复读(mysql的默认隔离级别) 当前读模式下,因为加的是行锁,因此没办法防止住insert语句,导致2次 查询的语句不一致。对于快照读,使用MVCC解决,对于当前读,使用间隙锁解决 (4)串行化 这是最高的隔离级别,它强制事务都是串行执行的,使之不可能相互冲突,从而解决幻读问题。 换言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。     三、 锁 从概念上分有2种锁,悲观锁与乐观锁 悲观锁,是一种对数据的修改持有悲观态度的并发控制方式。总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据, 因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。悲观锁的实现:   1. 传统的关系型数据库使用这种锁机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。 2. Java 里面的同步 synchronized 关键字的实现。   悲观锁主要分为共享锁和排他锁:   共享锁【shared locks】又称为读锁,简称 S 锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁, 都能访问到数据,但是只能读不能修改。 排他锁【exclusive locks】又称为写锁,简称 X 锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁, 其他事务就不能再获取该行的其他锁,包括共享锁和排他锁。获取排他锁的事务可以对数据行读取和修改。   行锁、表锁是共享锁、排他锁的执行粒度。 1、表锁(解锁语句 UNLOCK TABLES) (1)读锁 lock table table_name read 对该表加读锁后,自己也不能对其进行修改;自己和其他线程只能读取该表 (2)写锁 lock table table_name write 当对某个表执加上写锁后,该线程可以对这个表进行读写,其他线程对该表的读和写都受到阻塞 2、行锁 行锁是在引擎层由各个引擎自己实现的,有的引擎并不支持行锁,比如MyISAM就不支持行锁,这意味着:   并发控制只能使用表锁,对于这种引擎(MyISAM)的表,同一张表上任何时刻只能有一个更新在执行,这严重影响了并发度; InnoDB是支持行锁的,这也是MyISAM被InnoDB代替的主要原因;   首先注意:InnoDB的行锁是针对索引加的锁,不是针对记录加的锁,并且该索引不能失效,否则都会从行锁升级为表锁   update table1 set bbb='2' where aaa='1' 事务开启后,事务A更新table1的1行,但未提交,则事务B中无法更新这一行(会阻塞,事务A提交后才行,阻塞一段时间后 还未获得行锁则会自动放弃更新),但可以更新其他行。   需注意,上面这条语句改成update table1 set bbb='2' where ccc='1' 则不会加行锁,但是事务B依然会阻塞, 原因是行锁降级成了表锁,因此事务B依然无法update(参考上文的写锁)。 在InnoDB事物中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事物提交了才会释放,这个就是两阶段锁协议。   3间隙锁 首先要了解下幻读这个问题,我们开启事务A后读取一条sql得到了10行数据,此时事务B无法对这10行做update、delete操作, 但是此时使用的是行锁,也就是锁的粒度是加在每个数据行上的,这也就意味着我们可以向这些数据中进行插入操作, 比如事务B向这张表插入了1条数据,导致事务A再次查询时得到了11条记录,这就是幻读。mysql的应对方案是增加间隙锁。 将两行记录间的空隙加上锁,阻止新记录的插入;这个锁称为间隙锁。         悲观锁的特性: 悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面, 处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性, 一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。     乐观锁采取了更加宽松的加锁机制。也是为了避免数据库幻读、业务处理时间过长等原因引起数据处理错误的一种机制, 但乐观锁不会刻意使用数据库本身的锁机制,而是依据数据本身来保证数据的正确性。乐观锁的实现:
  1. CAS 实现:Java 中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式。
  2. 版本号控制(MVCC):一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会 +1。
当线程 A 要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新, 否则重试更新操作,直到更新成功。   乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。   在乐观锁与悲观锁的选择上面,主要看下两者的区别以及适用场景就可以了。 1️. 响应效率:如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁。 乐观锁并未真正加锁,效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。 2️. 冲突频率:如果冲突频率非常高,建议采用悲观锁,保证成功率。冲突频率大,选择乐观锁会需要多次重试才能成功,代价比较大。 3. 重试代价:如果重试代价大,建议采用悲观锁。悲观锁依赖数据库锁,效率低。更新失败的概率比较低。 4️. 如果有人在你之前更新了,你的更新应当是被拒绝的,可以让用户从新操作。悲观锁则会等待前一个更新完成。这也是区别。   随着互联网三高架构(高并发、高性能、高可用)的提出,悲观锁已经越来越少的被应用到生产环境中了,尤其是并发量比较大的业务场景。   四、MVCC 多版本并发控制MVCC,利用3个隐式字段,undo日志 ,Read View 来实现的。   (1)隐式字段 每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段   DB_TRX_ID 6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID DB_ROLL_PTR 7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里) DB_ROW_ID 6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引 实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了     mysql之事务、锁、隔离级别与MVCC 如上图,DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键,DB_TRX_ID是当前操作该记录的事务ID, 而DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本。   (2)undo日志 undo log主要分为两种:   insert undo log 代表事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃 update undo log 事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要; 所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除   下图展示了1条记录经过多次事务修改的流程图   mysql之事务、锁、隔离级别与MVCC     (3)Read View(读视图) Read View是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)   所以我们知道 Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。   Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前事务ID)取出来,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_ID跟Read View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本   readview有以下3个主要属性 trx_list(一个数值列表,用来维护Read View生成时刻系统正活跃的事务ID) up_limit_id(记录trx_list列表中事务ID最小的ID) low_limit_id(ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1)   (1)首先比较DB_TRX_ID < up_limit_id, 如果小于,则当前事务能看到DB_TRX_ID 所在的记录,如果大于等于进入下一个判断 (2)接下来判断 DB_TRX_ID 大于等于 low_limit_id , 如果大于等于则代表DB_TRX_ID 所在的记录在Read View生成后才出现的, 那对当前事务肯定不可见,如果小于则进入下一个判断。 (3)判断DB_TRX_ID 是否在活跃事务之中,trx_list.contains(DB_TRX_ID),如果在,则代表我Read View生成时刻,你这个事务还在活跃, 还没有Commit,你修改的数据,我当前事务也是看不见的;如果不在,则说明,你这个事务在Read View生成之前就已经Commit了, 你修改的结果,我当前事务是能看见的     mysql之事务、锁、隔离级别与MVCC       简单总结一下,就是给每条记录增加3个隐藏字段,1个隐藏主键,1个标记当前事务ID(DB_TRX_ID),1个标记上本记录的上一个版本(DB_ROLL_PTR)。 DB_ROLL_PTR指向的上一版本记录存储在undo log中,undo log中的每条记录也都有DB_ROLL_PTR字段,于是每条记录的修改记录连在一起就形成了1个修改记录链表,最早的版本在链尾。 当我们快照读时,对于readview中记录的活跃的事务,拿这条记录当前的DB_TRX_ID,比对他们的版本,再从undo log中找出符合可见性的记录版本,然后读取。                
上一篇:记校赛第一题


下一篇:MVCC多版本并发控制机制与Innodb引擎SQL执行的BufferPool缓存机制