前言
本博客主要是对《MySQL是怎样运行的》一书的内容进行整理,另外添加了自己对于锁机制与MVCC机制之间应用场景和作用的辨析以及解决幻读的方法的总结
锁机制和MVCC机制的辨析
MVCC机制主要用于处理读——写之间的冲突,因此MVCC能处理脏读、不可重复读以及快照读的幻读问题;
锁机制主要用于处理写——写之间的冲突,锁机制用于处理脏写问题(脏写一般都是通过加锁来处理),另外锁机制还用于处理当前读的幻读问题。
使用锁可以解决脏写、脏读、不可重复读、幻读这四个问题,但是问题在于锁机制的性能较差,因此通常情况会使用MVCC来处理脏写、脏读问题。
* MVCC和锁是不同的机制,二者之间不会牵扯,也无需声明;MySQL会自动根据需要调用MVCC和锁方法。
因此select * from * for update 或者select * from * for share mode时,无需再考虑版本号信息,而是直接使用锁的方式获取最新值。
事务在并发执行时访问相同记录的情况大致可以分为以下三种:
1、读——读情况:读取操作本身不会对记录产生任何影响,不会引起什么问题
2、写——写情况:并发事务相继对相同的记录进行改动,可能会引发很严重的问题。
3、读——写或写——读情况:也就是一个事务进行读取操作,另一个事务进行改动操作
写——写情况
在写——写情况下会发生脏写的线下那个。任何一种隔离级别都不允许这种现象的发生,因此在多个未提交事务相继对一条记录进行改动时,需要让他们排队进行。这个排队的过程其实就是通过为该记录加锁来实现的。
锁里有很多信息,其中最关键的只有两个
1、trx信息:表示这个锁结构是与哪个事务关联的。
2、is_waiting:表示当前事务是否在等待
当有多个事务尝试访问同一条记录时,会出现多个锁结构,如下所示:
只有一个能成功获取锁,只有当获取锁的事务提交或rollback,剩下的事务才能尝试获取这把锁执行后续任务。
读——写情况或写——读情况
怎么避免脏读、不可重复读、幻读这些现象呢?其实有两种可选的解决方案
1、 读操作使用多版本并发控制(MVCC),写操作进行加锁
2、读、写操作都使用加锁的方式
如果我们的一些业务场景不允许读取记录的旧版本,而是每次都必须去读记录的最新版本。比如在银行存款的事务中,我们需要先把账户的余额读出来,然后将其加上本次存款的数额,最后再写到数据库中。在将账户余额读取出来后,就不想让别的事务再访问该余额,知道本次存款事务执行完成后,其他事务才可以访问账户的余额。这样在读取记录的时候也就需要对其进行加锁操作,这也就意味着读操作和写操作也得像写——写操作那样排队进行。
很明显,如果采用MVCC方式,读——写操作彼此并不冲突,性能更高;如果采用加锁方式,读——写操作彼此需要排队执行,影响性能。
一般而言我们更喜欢使用MVCC,但是在某些特殊的业务场景下,我们必须采用加锁的方式来执行(就比如刚才的银行案例)。
一致性读
事务利用MVCC进行的读取操作称为一致性读(又名快照读)。所有普通的SELECT语句都算是一致性读,例如:select * from t。
锁定读
1、共享锁和独占锁
* 共享锁:简称S锁。在事务要读取一条记录时,需要先获取该记录的S锁(即读锁)。
* 独占锁:也常称为排他锁,简称X锁。在事务要改动一条记录时,需要先获取该记录的X锁(即写锁)。
S锁X锁兼容关系:
兼容性 | X锁 | S锁 |
X锁 | 不兼容 | 不兼容 |
S锁 | 不兼容 | 兼容 |
有时候我们想在读取记录时就获取记录的X锁,从而禁止别的事务读写该记录,我们把这种在读取记录前就为该记录加锁的读取方式称为锁定读,例如如下两种形式:
对读取的记录加S锁:
* select * lock in share mode
对读取的数据加X锁
* select * for update
此外,update、insert、delete等操作也对应锁定读(当前读)模式,默认读取当前最新的值。
特别值得一提的是:对insert而言,一般情况下新插入的一条记录受隐式锁保护,不需要在内存中为其生成对应的锁结构。
多粒度锁
前面提到的锁都是针对记录的,可以将其称为行级锁或者行锁。对一条记录加锁,影响的也只是这一条记录而已。我们就说这个行锁的粒度比较细。其实一个事务也可以在表级别进行加锁,自然就将其称为表锁。表锁的粒度比较粗。给表加的锁也可以分为共享锁(S锁,读锁)和独占锁(X锁,写锁)。
给表加锁的时候需要首先确定表里每行记录的行锁状态
如果要给表加S锁,要确定表里没有加X锁的记录
如果要给表加X锁,要确定表里没有加S锁或者X锁的记录
问题出现:如何确定表里每个记录的锁的状态
解决方案:意向锁(Intention Lock)
意向共享锁(Intention Shared Lock):简称IS锁,当事务准备在某条记录上加S锁时,需要先在表级别加一个IS锁。
意向独占锁(Intention Exclusive Lock):简称IX锁,当事务准备在某条记录上加X锁时,需要先在表级别加一个IX锁。
每当要读表记录加S锁时会给表顺便加个IS锁,每当要写表记录时会给表顺便加个IX锁。当执行完毕后会把意向锁给取消。
总结:IS锁、IX锁是表级锁,它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录。
也就是说,IS锁和IX锁是兼容的,IX锁和IX锁是兼容的。
兼容关系:
InnoDB存储引擎中的锁
表级锁粒度粗,占用资源较少。不过有时我们仅仅需要锁住几条记录,如果使用表级锁,效果上相当于为表中的所有记录都加锁,所以性能比较差。行级锁粒度细,可以实现更精准的并发控制,但是占用资源较多。
Gap Lock
用锁解决幻读时存在一个问题,即第一次读取时,那些幻影记录尚不存在,无法给这些幻影记录直接加上正式的锁。因此InnoDB提出了一种叫做Gap Lock的锁,可以锁住搜索区间,不让在这中间插入数据(直接加表锁过于浪费资源,这个类似于弱化版表锁)。
gap锁会阻塞插入操作,直到拥有该gap锁的事务提交了之后将该gap锁释放掉,其他事务才可以插入数据。
gap锁的作用仅仅是为了防止插入幻影记录。(幻读指多读数据而不是少数据)
Next-Key Lock
有时候我们既想锁住某条记录,又想组织其他事务在该记录前面的间隙插入新纪录,因此提出了Next-Key Lock,
next-key锁的本质就是一个正经记录锁和一个gap锁的合体。它既能保护该条记录,又能阻止别的事务将新纪录插入到被保护记录前面的间隙中。
隐式锁
在内存中生成锁结构并维护他们并不是一件零成本的事情,因此提出了隐式锁的概念。采用类似写实复制的理念,推迟生成锁结构的时间,减少生成锁的次数。
-
情景一:对于聚簇索引记录来说,有一个
trx_id
隐藏列,该隐藏列记录着最后改动该记录的事务id
。那么如果在当前事务中新插入一条聚簇索引记录后,该记录的trx_id
隐藏列代表的的就是当前事务的事务id
,如果其他事务此时想对该记录添加S锁
或者X锁
时,首先会看一下该记录的trx_id
隐藏列代表的事务是否是当前的活跃事务,如果是的话,那么就帮助当前事务创建一个X锁
(也就是为当前事务创建一个锁结构,is_waiting
属性是false
),然后自己进入等待状态(也就是为自己也创建一个锁结构,is_waiting
属性是true
)。 -
情景二:对于二级索引记录来说,本身并没有
trx_id
隐藏列,但是在二级索引页面的Page Header
部分有一个PAGE_MAX_TRX_ID
属性,该属性代表对该页面做改动的最大的事务id
,如果PAGE_MAX_TRX_ID
属性值小于当前最小的活跃事务id
,那么说明对该页面做修改的事务都已经提交了,否则就需要在页面中定位到对应的二级索引记录,然后回表找到它对应的聚簇索引记录,然后再重复情景一
的做法。
无论使用隐式锁保护记录,还是通过在内存中显式生成锁结构来保护记录,起到的作用是一样的。
InnoDB锁的内存结构
一个事务对多条记录加锁时,是不是就要创建多个锁结构呢?
答:必然不是。如果符合下面这些条件,这些记录的锁就可以放到一个锁结构中:
* 在同一事务中进行加锁操作
* 被加锁的记录在同一个页面
* 加锁的类型是一样的
* 等待状态是一样的
语句加锁分析
搜索过程不断给条件区间内访问到的数据添加next-key锁,并且不释放,以防止插入数据。
通过聚簇索引的话直接给聚簇索引记录加next-key锁,通过二级索引搜索的话给二级索引记录项加next-key锁,给实际的聚簇索引里的内容加正式记录锁。
当update时也会更新索引在,这时候会发现二级索引不允许插入产生相应回馈,update往区间插入数据失败。
死锁
不同事务由于互相持有对方需要的锁而导致事务无法继续执行的情况称为死锁。死锁发生时,InnoDB会选择一个较小的事务进行回滚。可以通过查看死锁日志来分析死锁发生过程。
所谓较小的事务,就是指在事务执行过程中插入、更新或删除记录较少的事务。