InnoDB 相比 MyISAM 有两大特点,一是支持事务而是支持行级锁,事务的引入带来了一些新的挑战。相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率
,提高数据库系统的事务吞吐量,从而可以支持可以支持更多的用户。但并发事务处理也会带来一些问题,主要包括以下几种情况:
更新丢失(Lost Update)
:当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题 ——最后的更新覆盖了其他事务所做的更新。如何避免这个问题呢,最好在一个事务对数据进行更改但还未提交时,其他事务不能访问修改同一个数据。脏读(Dirty Reads)
:一个事务正在对一条记录做修改,在这个事务并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些尚未提交的脏数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。不可重复读(Non-Repeatable Reads)
:在事务A中先后两次读取同一个数据,但是两次读取的结果不一样。脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。幻读(Phantom Reads)
:在事务A中按照某个条件先后两次查询数据库,两次查询结果的行数不同,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。以上是并发事务过程中
会存在的问题,解决更新丢失可以交给应用,但是后三者需要数据库提供事务间的隔离机制
来解决。实现隔离机制的方法主要有两种:
定义
MVCC (Multiversion Concurrency Control)
叫多版本并发控制
,是在并发访问数据库时,通过对数据做多版本管理,避免因为写锁的阻塞而造成读数据的并发阻塞问题。(MVCC 是无锁操作的一种实现方式
)
通俗的讲就是MVCC通过保存数据的历史版本,根据比较版本号来处理数据的是否显示,从而达到读取数据的时候不需要加锁就可以保证事务隔离性的效果
特征
MVCC实现的核心知识点
自增长的事务ID
,可以从事务ID判断事务的执行先后顺序。1、DB_TRX_ID
: 记录操作该数据事务的事务ID,大小为 6 个字节;
2、DB_ROLL_PTR
:指向上一个版本
数据在undo log
里的位置指针
,大小为 7 个字节;(在下一节具体说明如何组织 undo Log 链)
3、DB_ROW_ID
:行标识(隐藏单调自增 ID),大小为 6 字节,如果表没有主键,InnoDB 会自动生成一个隐藏主键,因此会出现这个列。每条记录的头信息(record header)里都有一个专门的bit(deleted_flag)来表示当前记录是否已经被删除。
撤销所有已经成功执行的sql语句
。
以update操作为例:当事务执行update时,其生成的undo log中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到update之前的状态。
实例:
模拟一次数据修改的过程来了解下事务版本号、表格隐藏的列和undo log他们之间的使用关系
。
(1)首先准备一张原始原始数据表
ID | Name | DB_TRX_ID | DB_ROLL_PTR |
---|---|---|---|
1 | zhnagsan | 103 | 0xxxxxxx |
(2)开启一个事务A: 对user_info
表执行 update user_info set name =“李四”where id=1
会进行如下流程操作
start transaction;
update user_info set name =“lisi” where id=1
commit;
DB_ROW_ID = 103
的这行记录加排他锁
user_info
表修改前的数据
拷贝到undo log
,DB_TRX_ID 和 DB_ROLL_PTR
都不动user_info表 id=1
的数据,这时产生一个新版本,更新 DATA_TRX_ID
为修改记录的事务 ID(104)
,将 DATA_ROLL_PTR
指向刚刚拷贝到 undo log
链中的旧版本记录,这样就能通过 DB_ROLL_PTR
找到这条记录的历史版本。
UPDATE
,Undo Log
会组成一个链表,遍历这个链表可以看到这条记录的变迁redo log
,包括 undo log
中的修改INSERT
会产生一条新纪录,它的 DATA_TRX_ID
为当前插入记录的事务 ID
;DELETE
某条记录时可看成是一种特殊的 UPDATE
,其实是软删,真正执行删除操作会在 commit
时,DATA_TRX_ID
则记录下删除该记录的事务 ID
。RU 隔离级别(读未提交)
下,直接读取版本的最新记录就 OK,对于 SERIALIZABLE
隔离级别,则是通过加锁互斥来访问数据,因此不需要MVCC 的帮助。RC(读已提交)
和 RR(可重复读)
这两个隔离级别下,当 InnoDB 隔离级别设置为二者其一时,在 SELECT 数据时就会用到版本链核心问题是版本链中哪些版本对当前事务可见?
InnoDB 为了解决这个问题,设计了 readview(可读视图)的概念。在InnoDB中每个SQL语句执行前都会得到一个readview。副本主要保存了当前数据库系统中正处于活跃(没有commit)的事务的ID号,其实简单的说这个副本中保存的是系统中当前不应该被本事务看到的其他事务id列表。
trx_ids
: 当前系统活跃(未提交)事务版本号集合。low_limit_id
: 创建当前read view 时“当前系统最大事务版本号+1
”。up_limit_id
: 创建当前read view 时“系统正处于活跃事务最小版本号
”creator_trx_id
: 创建当前
read view的事务版本号;(1)数据事务ID
如果数据事务ID小于read view中的最小活跃事务ID,则可以肯定该数据是在当前事务启之前就已经存在了的,所以可以显示。
(2)数据事务ID>=low_limit_id 则不显示
如果数据事务ID大于read view 中的当前系统的最大事务ID,则说明该数据是在当前read view 创建之后才产生的,所以数据不予显示。
(3) up_limit_id <数据事务ID
如果数据的事务ID大于最小的活跃事务ID,同时又小于等于系统最大的事务ID,这种情况就说明这个数据有可能是在当前事务开始的时候还没有提交的。所以这时候我们需要把数据的事务ID与当前read view 中的活跃事务集合trx_ids 匹配:
(4)不满足read view条件时候,从undo log里面获取数据
当数据的事务ID不满足read view条件时候,从undo log里面获取数据的历史版本,然后数据历史版本事务号回头再来和read view 条件匹配 ,直到找到一条满足条件的历史数据,或者找不到则返回空结果;
下面我们通过开启两个同时进行的事务来模拟MVCC的工作流程。
(1)创建user_info表,插入一条初始化数据
(2)事务A和事务B同时对user_info进行修改和查询操作
事务A:update user_info set name =”李四”
事务B:select * fom user_info where id=1
问题: 先开启事务A ,在事务A修改数据后但未进行commit,此时执行事B。最后返回结果如何。
执行流程说明:
(1)事务A:开启事务,首先得到一个事务编号102;
(2)事务B:开启事务,得到事务编号103;
(3)事务A:进行修改操作,首先把原数据拷贝到undolog,然后对数据进行修改,标记事务编号和上一个数据版本在undo log的地址。
(6)事务B: 把数据与read view进行匹配,
发现不满足read view显示条件,所以从undo lo获取历史版本的数据再和read view进行匹配,最后返回数据如下。
在 READ COMMITTED 隔离级别下,会使用 MVCC。在开启一个读取事务之后,它会在每一个 select 操作
之前都生成一个 Read View
。
REPEATABLE READ 与 READ COMMITTED 的区别只有在生成 Read View 的时机
上。
只会在第一次执行 select 操作时生成一个 Read View
,直到该事务提交之前,所有的select 操作都是使用第一次生成的 Read View。RR(重复读)级别下的一个事务里只会获取一次read view副本,从而保证每次查询的数据都是一样的。
具体更详细说明:请参考
该隔离级别不会使用 MVCC
。如果使用的是普通的 select 语句,它会在该语句后面加上 lock in share mode,变为一致性锁定读
。假设一个事务读取一条记录,其他事务对该记录的更改都会被阻塞
。
读写操作变为了串行操作
。1、https://zhuanlan.zhihu.com/p/52977862
2、https://zhuanlan.zhihu.com/p/64576887
3、https://zhuanlan.zhihu.com/p/149640067
4、https://baijiahao.baidu.com/s?id=1629409989970483292&wfr=spider&for=pc
5、https://zhuanlan.zhihu.com/p/115912936