文章目录
(一)事务隔离级别
众所周知,MySQL中的INNODB引擎是支持事务的并发操作,但是并发就会带来各种的问题—例如:脏读,幻读等 所以我们首先来看一下MySQL中并发可能会产生的问题
1.并发产生的问题
- 脏读:两个事务同时在执行,A修改了一个数据但是还没有提交,B读取并使用了A修改后的数据,但是A事务之后又回滚,那么B读到的这个数据就是不合法的,是脏的.这种情况就被称为脏读
- 不可重复读:两个事务在执行,A在一开始读取到一个数据为x,然后B将这个数据更改为y,更改之后A又去读取该数据,结果却发现数据为y.和最开始的x不同.也就是说一个事务在过程中多次读取同一个数据,结果发现前后结果不同.这就被称为不可重复读.
- 幻读: 两个事务在执行,A执行了一个语句比如说 select id from student where id > 1.查询出结果有4条,B这个时候插入了一条数据 **insert into student values(100) **.插入完成之后,A再执行之前相同的语句,却发现一个有5条记录! 平白无故多了一条记录.就像出现了幻觉.这个情况就被称为是幻读.
tip:不可重复读和幻读的小区别:不可重复读强调的是对原有的数据进行了修改.而幻读则强调增加或减少数据
2.事务隔离级别
隔离级别 | 描述 | 解决问题 | 实现方案 |
---|---|---|---|
读未提交(read uncommitted) | 当前事务可以读取其他事务没有提交的数据 | 什么都没解决 | 什么都没做 |
读已提交(read committed) | 当前事务只能读取其他事务已提交的数据 | 脏读 | MVCC(undo log + read view) |
可重复读(repeatable-read) | 可以保证事务执行期间对一个数据的多次读取结果是相同的 | 脏读,不可重复读 | MVCC(undo log + read view) |
可串行化(serializable) | 最高的隔离级别,事务之间是一个一个的顺序执行,事务不能并发,自然也就没有并发问题. | 什么都解决了 | 加锁 |
需要注意的是:在MySQL中,Repeatable Read 是Mysql的默认隔离级别,但是它这个Repeatable Read有点特殊,MySQL通过next_key lock解决了幻读问题.
(二)MVCC
MVCC,全称Muti-Version Concucurrency Control,翻译过来就是多版本并发控制.根据它的名字我们就可以知道: 它的目的是解决并发问题,它的方式是通过多版本.
1.隐藏字段和undo log
大家都知道数据库中有一行行的数据,但是大家可能不知道每一列除了我们插入的数据,还有一些隐式定义的字段,例如: trx_id,roll_ptr等.而且每一个事务INNODB都会给它分配一个事务id,并且按照事务执行的顺序递增,也就是说先执行的事务的id小,后面的大.
- trx_id:每次当一个事务对该行数据进行一次更改之后,就会记录下它的事务id(trx_id).并且会生成一个undo log.这个undo log 会记录数据在更改之前的模样.
- roll_ptr:roll_ptr相当于一个指针,它会指向该数据执行之前的undo log.并且每一条undo log也有一个roll_ptr.
下面我们以实际例子来展示一下真实的场景:
假设我们现在有一张表:
mysql> desc mvcc_test;
+-------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(20) | YES | | NULL | |
+-------+-------------+------+-----+---------+----------------+
2 rows in set (0.00 sec)
其中的数据为
mysql> select * from mvcc_test;
+----+--------+
| id | name |
+----+--------+
| 1 | JackMa |
+----+--------+
1 row in set (0.00 sec)
那么此时的情况应该是这样的,
现在我们对其进行更改
//这是A事务,假设事务id为 20
update mvcc_test set name = 'PonyMa'
//这是B事务,假设事务id为30
update mvcc_test set name = 'BillGates'
更改完之后,情况就会变成这样,这样一个通过roll_ptr连接起来的链表我们把它称作为 版本链
2.Read View
上面我们看到了undo log的作用,通过roll_ptr我们就可以访问到之前的数据,但是我们需要对这些数据进行一个限制,要不然如果所有事务都可以随便访问之前的数据,那么undo log就没有了作用.Read View包含了系统当前包括了哪些活跃的事务,并且将它们的事务id放在一个列表中,并且命名为m_ids.这样当一个事务在访问某条记录的时候,就可以通过一定的规则的规则去查看记录的某个版本是否可见:
-
如果被访问版本的trx_id属性值小于m_ids列表中最小的事务id,表明生成该版本的事务在生成ReadView前已经提交,所以该版本可以被当前事务访问。
-
如果被访问版本的trx_id属性值大于m_ids列表中最大的事务id,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。
-
如果被访问版本的trx_id属性值在m_ids列表中最大的事务id和最小事务id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
3.Read Committed的实现
还是上面的那个例子,假设事务A(trx_id = 20)已经执行完成了,而事务B(trx_id = 30)还没有执行完成,现在又有一个事务隔离级别为Read Committed的事务C来执行下列操作
//事务C,假设事务id为60,事务A(20)已经提交,事务B(30)没有提交
select * from mvcc_test where id = 1; # 得到结果为PonyMa,满足ReadCommitted的要求
// 一分钟后,事务B(30)也提交了,mysql系统中没有活跃的事务
select * from mvcc_test where id = 1; # 得到结果为BillGates
那么这个过程中,是怎样通过Read View + Undo Log来解决的呢?
Read_Committed中,每执行一条语句就会生成一个Read View
- 事务C在执行第一次 select * from mvcc_test where id = 1 会生成一个Read View,其中的m_ids是[ 30 ],因为这个时候只有事务B(30)还是活跃的.
- 然后我们从版本链中寻找,最新的数据 trx_id 为30 大于等于 m_ids中的最小值.所以我们不使用这个值
- 通过roll_pointer跳转到版本链中的下一条记录中,下一条记录中的trx_id为20 小于 m_ids中的 最小值,所以符合要求.最终返回的结果为 PonyMa
- 第二次执行 select * from mvcc_test where id = 1 会再生成一个 Read View,其中的m_ids为[],此时没有事务活跃
- 然后我们从版本链中查找,最新的数据 trx_id为30,不在m_ids,我们读取到就是 BillGates
4.Repeatable-Read的实现
照样是上面的例子,不过事务的执行顺序变一下
//这是A事务,假设事务id为 20,A执行并提交
update mvcc_test set name = 'PonyMa'
//这是C事务,最先执行,假设事务Id为 25
select * from mvcc_test where id = 1; # 得到结果为 PonyMa
//这是B事务,假设事务id为 30,B执行并提交
update mvcc_test set name = 'BillGates'
//这是C事务,在A,B事务执行完成之后再次执行一条语句
select * from mvcc_test where id = 1; # 得到结果为 PonyMa,结果符合repeatable read的要求
与Read Committed不同,Repeatable Read只会在第一次执行查询语句的时候生成一个ReadView,之后会一直使用这个ReadView
- 事务C在执行第一次 select * from mvcc_test where id = 1的时候,生成了一个ReadView,m_ids为[20],读取最新的数据,最新数据的trx_id为1,小于m_ids的最小值,所以返回的就是JackMa.
- 上面的查询语句执行完成之后,事务A,B分别执行并提交,生成最新的版本链
- 事务C再次执行 select * from mvcc_test where id = 1,但是这个时候不会再生成一个ReadView,而会使用第一次生成的Read View,m_ids为[20]. 此时最新的数据的trx_id为30,大于m_ids中的最小值,所以不满足要求
- 通过roll_pointer转到下一条版本中,下一条版本的trx_id为20,存在于m_ids.所以返回 PonyMa.
(三)next_key lock解决幻读问题
MySQL底层通过next_key lock解决了幻读问题,其实next_key lock可以看成是`
next key lock = gap lock + record lock
1.record lock
首先我们先了解一下 record lock
,假设我们有这样一张表
mysql> select * from mvcc_test;
+----+---------+
| id | name |
+----+---------+
| 1 | JackMa |
| 5 | MarkLau |
+----+---------+
2 rows in set (0.00 sec)
现在我们执行一条语句
# 事务A
select * from mvcc_test where id > 1 and id < 5 for update
# 这样数据库就会对id = 5 的数据上record锁,别的事务在事务A的执行过程就不能对该条记录进行写操作
# 这就是record锁
2.gap lock
然后我们在了解一下gap lock
,gap就是间隙的意思,顾名思义它会锁住间隙,假设还是上面的那个表,我们再执行一条语句
# 事务B,此时 数据库中的间隙有(-无穷,1),(1,5),(5,正无穷)
select * from mvcc_test where id = id > 2 and id < 6 for update
# 这个时候如果是gaplock,我们命中了id=5的记录,就会锁住5左右的两个区间,(1,5),(5,正无穷),这时候如果我们插入一条数据
insert into mvcc_test values(6,'ElonMask');
#此时该语句会被阻塞,因为(5,正无穷)被锁住了.
下面我们应该就了解,next key lock
是gap lock + record lock
的结合体,所以在命中数据的时候,不仅会锁住本行数据,还会锁住相邻区间.
候如果我们插入一条数据
insert into mvcc_test values(6,‘ElonMask’);
#此时该语句会被阻塞,因为(5,正无穷)被锁住了.
下面我们应该就了解,`next key lock`是`gap lock + record lock`的结合体,所以在命中数据的时候,不仅会锁住本行数据,还会锁住相邻区间.