MySQL(六):MySQL之MVCC

1、事务的引入

  事务是数据库管理系统(DBMS)执行过程中的一个逻辑单位(不可再进行分割),由一个有限的数据库操作序列构成(多个DML语句),要不全部成功,要不全部不成功。

  如:A 给 B 划钱,A 的账户-100元, B 的账户就要+100元,这两个update 语句必须作为一个整体来执行,不然A 扣钱了,B 没有加钱这种情况就是错误的。那么事务就可以保证A 、B 账户的变动要么全部一起发生,要么全部一起不发生。

2、事务的特性

  事务具有ACID特性:原子性、一致性、隔离性、持久性。

2.1、原子性(atomicity)

  一个事务必须被视为一个不可分割的最小单元,整个事务中所有的操作要么全提交成功,要么全部失败。

  A 给 B 转账 100,A 的账户 -100,B 的账户 +100,整个事务的操作要么全部成功,要么全部失败,不能出现 A的账户金额扣除,B的账户金额不扣除,原子性不能得到保证,会出现一致性问题。

2.2、一致性(consistency)

  一致性是指事务将数据库从一种一致性转换到另外一种一致性状态,在事务开始之前和事务结束之后数据库中数据的完整性没有被破坏。

  A 给 B 转账 100,A 的账户 -100,B 的账户 +100。A账户扣除的钱(-100),B账户增加的钱(+100)相加应该为0,两个账户的钱加起来前后应该不变。

2.3、隔离性(isolation)

2.3.1、概念

  一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

2.3.2、隔离性的导致的问题

  若不能保证隔离性,会导致何种问题?

  A账户的原始金额1000,转账两次,每次都是100,B账户的原始金额500,从理论上,转账完成后,A账户的金额有800,B账户的金额有700。

  将两次转账操作分别称为T1和T2,T1和T2的操作可能交替执行,执行顺序可能是:

0

  T1和T2交替执行,若按照上述执行顺序来进行两次转账,最终的结果:A账户余额900,B账户余额700。A账户相当于只扣了100元,B账户却多了200元。

  这种状态转换对于的某些数据库操作来说,不仅要保证这些操作以原子性的方式执行完成,而且要保证其它的状态转换不会影响到本次状态转换,这个规则称之为隔离性。

2.3.3、事务并发引发的问题

  MySQL是一个客户端/服务器架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,可以称之为一个会话(Session)。每个客户端都可以在自己的会话中向服务器发出请求语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说可能同时处理多个事务。

  事务由隔离性的特性,理论上在某个事务对某个数据进行访问时,其他事务应该排队,当该事务提交之后,其他事务才可以继续访问这个数据,如此并发事务的执行就变成了串行化执行。

  串行化执行性能影响太大,当既想保持事务一定的隔离性,又想保证服务器在处理访问同一数据多个事务的性能。当舍弃隔离性时,会带来什么样的数据问题?

3.1、脏读

  当一个事务读物到另一个事务修改但未提交的数据,被称为脏读。

  0

  在事务A执行过程中,事务A对数据进行了修改,事务B读取了事务A修改后的数据;

  由于某些原因,事务A并未完成提交,发生了RollBack操作,则事务B读取的数据就是脏数据。

  这种读取到另一个事务未提交的数据的现象就是脏读(Dirty Read)。

3.2、不可重复读

  当事务内相同的记录被检索两次,且两次得到的结果不同时,此现象称为不可重复读。

  0

  事务B读取了两次是数据资源,在这两次读取的过程中事务A修改了数据,导致事务B在这两次读取出来的数据不一致。

3.3、幻读

  在事务执行过程中,另一个事务将新纪录添加到正在读取的事务中,会发生幻读。

  0

  事务B前后两次读取同一个范围的数据,在事务B两次读取的过程中事务A新增了数据,导致事务B后一次读取到前一次查询没有看到的行。

  幻读重点强调了读取到了之前读取没有获取到的记录。

2.4、持久性(durability)

  事务一旦提交,则其所做的修改就会永久保存到数据库中。即使系统崩溃,已经提交的修改数据也不会丢失。

3、事务的隔离级别

  对于不同的隔离级别,并发事务可以发生不同严重程度的问题,具体情况如下:

隔离级别
含义
脏读
不可重复读
幻读
READ UNCOMMITTED
读未提交
READ COMMITTED
已提交读
-
REPEATABLE READ
可重复读
-
-
SERIALIZABLE
可串行化
-
-
-

  MySQL的默认隔离级别为 REPEATABLE READ。

3.1、读未提交(read uncommitted)

  事务A在修改表中数据时,可以看到事务B未提交的数据。安全级别最低的隔离级别,会产生脏读(dirty read)。

3.2、读已提交(read committed)

  事务A和事务B同时操作同一张表,有一条数据是id为1姓名为CPP的数据,此时事务A对这个数据修改为id为2姓名为ZYC(未提交),事务B再次查询得到的数据一直都是 1 CPP,当事务A修改完成commit提交后,此时事务B再次查询得到的数据 2 ZYC。

  0

  读已提交,只能读取已提交的数据,该隔离级别解决了脏读的问题,但缺点是不可重复读。

3.3、可重复读(REPEATABLE READ)

  可重复读是MySQL的默认隔离级别。

  0

  事务A和事务B同时操作同一张表,有一条数据是id为1姓名CPP的数据,此时事务A对这个数据修改为id为1姓名ZYC(未提交),此时事务B查询id为1的记录,姓名仍为CPP。事务A提交完成后,事务B再次查询id为1的记录姓名仍为CPP。

  在事务B未commit前,不管查询多少次结果id为1的记录,姓名都为CPP。只有在事务B提交后,再次查询id为1的记录,才会查出最新结果,姓名为ZYC。

  可重复读的隔离级别解决了不可重复读的问题,但会出现幻读。如当查询某条数据,可能显式的是1 CPP,但此时可能有个人正好修改好了这条数据,当你提交后再次查询时,会发现变成了2 ZYC。

3.4、序列化

  最安全的默认隔离级别,但不支持并发,相当于单线程,不允许并发操作数据库,事务开启后,只允许一个人进行操作,直到提交后下一个人才可以进入。

4、MVCC

  MVCC全称Multi-Version Concurrency Control,即多版本并发控制,主要是为了提高数据库的并发性能。

  每个不同的事务访问查询同一行数据时,每个事务修改的都是这行数据的不同版本,InnoDB只需要去记录这个数据的访问链,就可以实现一个SELECT操作的并发执行。

  同一行数据同时发生读写请求时,会上锁阻塞住。但MVCC用更好的方式去处理读--写请求,做到在发生读--写请求冲突时不用加锁。这个读指的是快照读,而不是当前读,当前读是一种加锁操作,是悲观锁。

  如何做到读--写不用加锁的,快照读和当前读指什么?

  MVCC的实现主要依赖于记录中的隐藏字段,undolog,readview来实现的。

4.1、版本链

  使用InnoDB存储引擎的表的聚簇索引记录中都包含两个必要的隐藏列:trx_id 和 roll_pointer。

  trx_id:每次一个事务对某条聚簇索引记录进行改动时,会把该事务的事务id赋值给trx_id隐藏列;

  roll_pointer:每次对某条聚簇索引记录进行改动时,会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

  0

  假设现有一个事务B,事务ID为10,要对这条记录进行修改,把CPP的age从12改成了20,那么此时Undo Log会发生啥?

  此时,这条id为1的记录,trx_id就变为了10,trx_id此时记录了修改这条记录的事务ID,而对应的roll_pointer指针,就指向了上次事务A的操作对应的Undo Log:

  0

  trx_id是记录修改了每条聚簇索引的事务id;roll_pointer是一个指针,指向每一个历史操作版本的数据存储的地址;每一次修改操作都会生成一个Undo Log版本,每个版本之间是隔离的。

  undo日志:为了实现事务的原子性,InnoDB存储引擎在实际进行增、删、改一条记录时,都需要先把对应的undo日志记下来。一般每对一条记录做一次改动,就对应着一条undo日志,但在某些更新记录的操作中,也可能会对应着2条undo日志。一个事务在执行过程中可能新增、删除、更新若干条记录,也就是说需要记录很多条对应的undo日志,这些undo日志会被从0开始编号,也就是说根据生成的顺序分别被称为第0号undo日志、第1号undo日志、...、第n号undo日志等,该编号也称为undo no。

  该记录每次更新后,都会将旧值放到一条undo日志中,是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,将这个链表称之为版本链,版本链的头节点就是当前记录最新的值,每个版本中还包含生成该版本时对应的事务id。可以利用这个记录的版本链来控制并发事务访问相同记录的行为,这种机制被称之为多版本并发控制(Mulit-Version Concurrency Control MVCC)。

  若有事务C,事务D等一直对这条记录进行修改,那么这条记录的roll_pointer指针就会一直这样递归修改下去,最终形成一个关于修改和删除操作的Undo Log版本链。

  修改和删除操作才会生成Undo Log版本链,查询操作不会生成Undo Log版本链。

  InnoDB有两种版本链:insert undo log(插入操作产生)和 update undo log(更新操作产生)。

  MySQL的读操作和写操作是分离的,写操作去生成版本链,而读操作只需要根据规则去查看对应的某一个版本,即快照读。

4.2、Read View

  Read View 是事务进行快照读操作时产生的读视图,在该事务执行快照读的那一刻,会生成一个数据系统当前的快照,记录并维护系统当前活跃事务的id,事务的id是递增的。

  Read View的最大作用是用来做可见性判断的,即当某个事务在执行快照读时,对该记录创建一个Read View的视图,把它当作条件去判断当前事务能够看到哪个版本的数据,有可能读取到的是最新的数据,也有可能读取的是当前行记录的undolog中某个版本的数据。

  Read View 存放着一个列表,这个列表用来记录当前数据库系统中活跃的读写事务,也就是已经开启了,正在进行数据操作但是还未提交保存的事务。可以通过这个列表来判断某一个版本是否对当前事务可见。其中,有四个重要的字段:

creator_trx_id
创建当前Read View所对应的事务ID
m_ids
所有当前未提交事务的事务ID,即活跃事务的事务id列表
min_trx_id
m_ids里最小的事务id值
max_trx_id
InnoDB 需要分配给下一个事务的事务ID值

  0

  如何判断当前版本对每一个事务是否可见,就是拿它们的值进行区间比较。

  Read View遵循的可见性算法主要是将要被修改的数据的最新记录中的DB_TRX_ID(当前事务id)取出来,与系统当前其他活跃事务的id去对比,如果DB_TRX_ID跟Read View的属性做了比较,不符合可见性,那么就通过DB_ROLL_PTR回滚指针去取出undolog中的DB_TRX_ID做比较,即遍历链表中的DB_TRX_ID,直到找到满足条件的DB_TRX_ID,这个DB_TRX_ID所在的旧记录就是当前事务能看到的最新老版本数据。

4.3、MVCC实现可重复读

  假设现在事务A和事务B同时对主键id = 1进行操作。事务A的id为20,事务B的id为30。事务A将age更新成15,事务B将age更新成30。

  0

  事务A和事务B创建各自的Read View,此时事务A的creator_trx_id = 10,事务B的creator_trx_id = 20。

事务A
事务id为10
事务B
事务id为20
creator_trx_id = 10
creator_trx_id = 20
m_ids = [10,20]
m_ids = [10,20]
min_trx_id = 10
min_trx_id = 10
max_trx_id = 21
max_trx_id = 21

  当前有两个活跃的事务,事务id分别为10和20。事务列表中最小的事务id是事务A,min_trx_id为10,max_trx_id值为事务B的下一个id,也就是21。

  事务A去读取主键id为1的数据,找到了记录后就会去查看该记录的trx_id,事务A查看到该记录的trx_id的值为5。

  0

  将事务A的creator_trx_id与当前记录的trx_id的值进行比较:

  0

  id=1的数据记录 trx_id=5 < 事务A的 creator_id=10,判断该记录到的事务id不存在于活跃的事务列表中并且小于事务A的事务id,表示本次记录的值是在事务A查询之前提交的,可以放心读取。读取完毕,会将该记录的trx_id修改为自己的事务id。

  0

  事务A将cpp的age从10又改成了15。

  0

  同时另一个隐藏字段也会被修改,roll_pointer指针。会指向被事务A修改之前的版本,也就是cpp的年龄还是15时候的地址,为了用来记录,方便下次被查询:

  0

  之后,事务B参与进来,事务B也对主键id为1的update操作,将cpp的age从15更改为30。此时再进行一次trx_id的比较过程,去判断自己的creator_trx_id是否大于这条记录对应的trx_id,若大于则去修改这条记录的值,将age从30修改为50:

  0

  0

  若事务A再次去读取主键id=1记录的值,这条记录的trx_id已经变成了20,会再次进行值的区间比较:发现事务A下数据记录的 trx_id(10)<主键id=1数据记录的 trx_id(20)<max_trx_id(21),并且trx_id为20的值存在于m_ids中,代表自己读取到的是和事务a同一时间范围内一块启动的另一个未提交的活跃事务b所修改的值。< div="">

  0

  此时事务A不会去读取这条记录对应的数据,会通知Undo Log上的roll_pointer指向的地址去查找上一个1旧版本的记录,直到找到第一条trx_id小于等于自己的事务id并且不存在于m_ids列表中的记录,即为别的事务已经提交的最后一条记录并读取它。

  0

  每一个事务去读取或者修改同一个记录时,只能操作已经提交了的数据,未提交的数据是不能读取到的。

  本质上就是通过Read View的字段判断这行记录对自己是否可见,如果不可见的话再去找Undo Log里面记录的对自己可见的数据。

4.4、MVCC实现读已提交

  提交读能够解决脏读并发一致性问题。脏读问题本质上是一个事务读取到了另一个事务没有提交的内容。下面来看看ReadView要如何解决该问题?

  事务A和事务B同一时刻开启,事务B将同一行的记录,将cpp的age改成了35,但并未提交,此时事务A读取这条记录。

  0

  0

  事务A查看到该记录的trx_id比事务A自身的Read View列表里的creator_trx_id值大,并且修改这条记录的事务的trx_id存在于m_ids列表中,事务A就可以判断得到该记录是被另一条没有提交的事务修改的,所以事务A不会读取这条数据的内容。

  0

  事务A会继续通过Undo Log往下找第一条trx_id小于等于自己的事务id并且不再活跃事务列表m_ids里面的数据。因此,不会看到别的事务正在修改的数据,脏数据也不会产生。

  0

  0

4.5、总结

  InnoDB存储引擎中,MVCC通过 隐藏列 + Undo Log + Read View 进行数据读取,Undo Log保存了历史快照,Read View规则用于判断当前版本的数据是否可见,不需要通过加锁的方式,就可以实现提交读和可重复读这两种隔离级别。

4.5.1、RC、RR级别下的InnoDB快照读的区别

  造成RC、RR级别下快照读的结果的不同是因为Read View生成时机的不同。

  1、在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照即Read View,将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见

  2、在RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动和事务的快照,这些事务的修改对于当前事务都是不可见的,而早于Read View创建的事务所做的修改均是可见

  3、在RC级别下,事务中,每次快照读都会新生成一个快照和Read View,这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因。

  在RC隔离级别下,是每个快照读都会生成并获取最新的Read View,而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View,之后的快照读获取的都是同一个Read View。

5、其他

5.1、隐式提交

  MySQL使用START TRANSACTION或者BEGIN语句开启事务,系统变量autocommit的值设置为OFF时,事务不会进行自动提交,当输入了某些语句(create/alter),就好像输入了commit语句,这种因某些特殊的语句而导致事务提交的情况称为隐式提交,这些会导致事务隐式提交的语句包括:ALTER、CREATE、DROP、GRANT等。

  定义或修改数据库对象的数据定义语言(Datadefinition language,简称DDL)。数据库对象指的是数据库、表、视图、存储过程等。当使用CREATE、ALTER、DROP等语句去修改数据库对象时,会隐式的提交前面语句所属的事务。

BEGIN;
SELECT ... -- 事务中的一条语句
UPDATE ... -- 事务中的一条语句
... 事务中的其他语句
CREATE TABLE ...  -- 隐式提交事务

5.2、保存点

  若开启了一个事务,执行了很多语句,其中的某条语句有问题,此时需要使用ROLLBACK语句让数据库状态恢复到事务执行之前的样子,但可能根据业务和数据的变化,不需要全部回滚。

  MySQL里提出了一个保存点(savepoint)的概念,即在事务对应的数据库语句中打几个点,在调用ROLLBACK语句时可以指定回滚到哪个店,而不是回到最初的原点。定义保存点的语法:

SAVEPOINT 保存点名称;

  若想回滚到某个保存点时,可以使用如下的语句:

ROLLBACK TO [SAVEPOINT] 保存点名称;

  若ROLLBACK语句后边不跟随保存点名称,会直接回滚到事务执行之前的状态。

  若要删除某个保存点,可以使用如下语句:

RELEASE SAVEPOINT 保存点名称;

 

posted @ 2024-03-08 18:46  无虑的小猪  阅读(99)  评论(0编辑  收藏  举报