并发事务带来的一致性
问题:
写-写情况可能会造成脏写问题,这是任何一种隔离级别都不允许发生的。所以在多个未提交事务相继对一条记录进行改动时,需要让他们排队执行 ,这个排队过程其实就是通过加锁来实现的
读写或写读情况在不同隔离级别下可能出现脏读、不可重复读、幻读现象
MySQL在可重复读的隔离级别下很大程度避免了幻读
对于脏读、不可重复读、幻读现象,有两种解决方案:
事务利用MVCC进行的读取操作称为一致性读(Consisten Read),或称一致性无锁读,一致性读并不会对表中任何记录进行加锁操作,其他事务可以自由对表中记录进行改动。所有普通的SELECT语句在READ COMMITTED
和REPEATABLE READ
隔离级别下都是一致性读
对于读-写情况可以使用MVCC也可以采用加锁读的方式,接下来介绍MySQL的锁
S锁和X锁的兼容关系:
MySQL中锁定读的语句:
对读取的记录加S锁,共享锁
SELECT ... LOCK IN SHARE MODE;
对读取的记录加X锁,排他锁
SELECT ... FOR UPDATE;
Innodb支持多粒度锁定,这种锁定允许事务在行级和表级上的锁同时存在
为了支持不同粒度上进行加锁,InnoDB支持一种额外的锁方式:意向锁
若将上锁的对象看成一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁
,那么首先需要对粗粒度的对象上锁
如图,如果需要对页上的记录r进行上X锁,那么分别需要对数据库A、表、页上意向锁IX,最后对记录r上X锁。
InnoDB意向锁设计比较简练,其意向锁即为表级别的锁 。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型
MySQL中支持多种引擎,不同引擎对锁的支持也是不一样的,接下来还是重点讨论InnoDB中的锁
对于MyISAM、MEMORY、MERGE这些存储引擎来说,它们只支持表级锁 ,而且这些存储引擎并不支持事务
所以当我们为使用这些存储引擎的表加锁时,一般都是针对当前会话 来说的
不会为这个表加 S 锁或 X 锁的
Metadata Lock(MDL,元数据锁)
实现的,一般情况下不会用S锁和X锁AUTO_INCREMENT
修饰的列进行递增赋值采用的实现方式有两个
AUTO_INCREMENT
修饰的字段分配递增的值,在语句结束后释放锁生成自增列的值时
获取到这个锁,然后当自增列用完这个值后就立即释放掉,不用等语句完全执行完再释放行级锁也称记录锁,这是InnoDB锁中的重头戏
记录锁就是仅仅把一条记录锁上,官方类型为:LOCK_REC_NOT_GAP
如图,比如我们给id为8的元素加上Record Lock锁
Record Lock分为S锁和X锁,和表级的S锁与X锁的兼容性规则一致
间隙锁,顾名思义就是给记录的间隙加上锁,不如记录插入这个间隙,官方类型为LOCK_GAP
如图,我们给区间(3,8)加上间隙锁,即不允许记录插入(3,8)之间
Gap锁的作用是防止插入幻影记录,所以虽然gap锁有共享锁和排他锁之分,但是它们的作用是相同的,且并不会限制其他事务对这条记录加Record锁或者Gap锁。
再强调一遍,Gap锁的作用是防止插入幻影记录
我们都知道,数据页中有两条伪记录,Infimum记录:表示最小记录
,Supremum记录:表示最大记录
为了防止其他事务插入id在(20,+∞)的新纪录,我们可以在id为20的记录和Supremum记录之间的间隙加上Gap锁
有时候,我们既想锁住某条记录,又想阻止其他事务在该记录前的间隙插入记录。InnoDB中就有这样的锁:Next-Key Lock,官方的类型为:LOCK_ORDINARY
next-key锁的本质就是一个Record锁和一个gap锁的合体 。它既能保护该条记录,又能阻止别的事务将新记录插入到被保护记录前面的间隙中。
一个事务在插入
一条记录时需要判断一下插入位置是不是被别的事务加了 gap锁
( next-key锁 也包含 gap锁 )
间隙
中 插入
新记录,但是现在在等待。Insert Intention Locks
,官方的类型名称为: LOCK_INSERT_INTENTION
,我们称为 插入意向锁
。举个例子,一个事务首先插入了一条记录(此时没有与该记录相关联的锁结构),然后另一个事务执行如下操作:
这时候事务id又要起作用了
情景1: 对于聚簇索引记录 来说,有一个trx_id
隐藏列记录着最后改动该记录的事务的事务id。
当前事务的事务id
。trx_id
隐藏列代表的事务是否是当前的活跃事务
。
is_waiting
属性为false
;然后为自己也创建一个锁结构 ,该锁结构的is_waiting
属性为true
,之后自己进入等待状态。情景2: 对于二级索引记录 来说,本身并没有trx_id隐藏列
,但是在二级索引页面的Page Header部分有一个 PAGE_MAX_TRX_ID
属性,该属性代表对该页面做改动的最大的事务id
PAGE_MAX_TRX_ID
属性值小于
当前最小的活跃事务id
,那就说明对该页面做修改的事务都已经提交了隐式锁起到了延迟生成锁结构的用处。如果别的事务在执行过程中不需要获取与该隐式锁相冲突的锁,就可以避免在内存中生成锁结构
先来看一条语句:
SELECT * FROM hero LOCK IN SHARE MODE;
很显然,这条语句需要为hero表中的所有记录进行加锁。那么,是不是需要为每条记录都生成一个锁结构呢?
如果一个事务要获取10,000条记录的锁,要生成10,000个这样的结构就太亏了吧!所以设计InnoDB的大叔本着勤俭节约的美德,决定在对不同记录加锁时,如果符合下面这些条件,这些记录的锁就可以放到一个锁结构
中:
如图为InnoDB中的锁结构
锁所在的事务信息: 无论是表级锁还是行级锁,一个锁属于一个事务
索引信息: 对于行级锁,需要记录一下加锁的记录属于哪个索引
表锁/行锁信息: 表级锁和行级锁结构在这个位置上是不同的
type_mode: 这是 32 比特的数,被分成 lock_mode
、lock type
、rec_lock_type
3个部分,如图:
LOCK_IS
(十进制的 0 ):表示共享意向锁,也就是 IS锁LOCK_IX
(十进制的 1 ):表示独占意向锁,也就是 IX锁LOCK_S
(十进制的 2 ):表示共享锁,也就是 S锁LOCK_X
(十进制的 3 ):表示独占锁,也就是 X锁LOCK_AUTO_INC
(十进制的 4 ):表示 AUTO-INC锁LOCK_TABLE
(十进制的 16 ),也就是当第5个比特位置为1时,表示表级锁。LOCK_REC
(十进制的 32 ),也就是当第6个比特位置为1时,表示行级锁。LOCK_REC
时才会有
LOCK_ORDINARY
(十进制的 0 ):表示 next-key锁 。LOCK_GAP
(十进制的 512 ):也就是当第10个比特位置为1时,表示 gap锁 。LOCK_REC_NOT_GAP
(十进制的 1024 ):也就是当第11个比特位置为1时,表示正经记录锁 。LOCK_INSERT_INTENTION
(十进制的 2048 ):也就是当第12个比特位置为1时,表示插入 意向锁。type_mode
这个32 位的数字中:
LOCK_WAIT
(十进制的 256 ) :当第9个比特位置为 1 时,表示 is_waiting 为 true ,也就是当前事务尚未获取到锁,处在等待状态;当这个比特位为 0 时,表示 is_waiting 为 false ,也就是当前事务获取锁成功。其他信息:为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表。
一堆比特位:如果是 行锁结构
的话,在该结构末尾还放置了一堆比特位,比特位的数量是由上边提到的 n_bits
属性表示的。
记录头信息
中都包含一个 heap_no
属性,伪记录 Infimum 的 heap_no 值为 0 , Supremum 的 heap_no 值为 1 ,之后每插入一条记录, heap_no 值就增1。 锁结构
最后的一堆比特位就对应着一个页面中的记录,一个比特位映射一个 heap_no
,即一个比特位映射到页内的一条记录可以通过information_schema数据库下的INNODB_TRX
、INNODB_LOCKS
、INNODBLOCK_WAITS
表来查看事务和锁的相关信息,也可以通过SHOW ENGINE INNODB STATUS语句查看事务和锁的相关信息。
不同事务由于互相持有对方需要的锁而导致事务都无法继续执行的情况称为死锁。 死锁发生时,InnoDB会选择一个较小的事务进行回滚
。可以通过查看死锁日志
来分析死锁发生过程。