不出意外,这就是基础部分的最后一篇了,会尝试结合实际的案例来学习比较细节的点,随处可见的东西遇上也应该不会写,希望能更多的从源码的角度分析问题,希望吧
什么是锁
锁是数据库系统区别于文件系统的一个关键特性,锁机制用于管理对共享资源的并发访问
在innodb系统中,最常见的当然是对行数据的锁,除此之外,缓冲池的LRU列表,删除,添加LRU元素等存在并发操作,又不要保持一致性的场景,都需要锁的介入
lock和latch
这两者都是锁,lock是上文提到的锁,而latch是一种轻量级的锁,latch可分为mutex(互斥量)和rwlock(读写锁),其中mutex在group commit中使用过。
latch的特点
latch 的锁定时间一般很短,一旦长时间锁会导致性能的急剧下降,latch是用于保证并发线程操作共享资源的正确性,并且没有死锁检测,比如在group cmmit中,flush sync commit三阶段就是通过mutex来控制。latch 的信息可以通过 show engine innodb mutex查看
lock的特点
lock 的对象是事务,用于锁定数据库中的对象,比如数据,页等。有死锁检测机制。lock的信息可以通过 show engine innodb status 或者 information_schema.innodb_trx,innodb_locks,innodb_lock_waits。
二阶段锁
二阶段锁只的是,当需要访问资源时,先加锁,再访问;访问后不释放锁,只到事务commit / rollback 时再释放锁
锁的类型
共享锁(s lock),允许事务读取一行数据
排他锁(x lock),允许事务修改一行数据
易知 x lock和x lock之间时无法兼容的,其他情况xs,sx,ss都是可以兼容的,这里的sx锁锁的都是行锁
但是我们知道行数据都是放在页上,页在表里,表在库里,那么如果想对库A表B页C上的D数据上锁,那么对D数据加s/x锁,对ABC就不能加s/x锁,不然另一个事务想访问页C上的E数据就会被block住,这显然影响并发。索引innodb引入了意向锁(intention lock),意向锁是为了支持细粒度更小的锁,如上例,在库A表B页C上加意向锁(IX/IS)在数据D/E上加s/x锁。意向锁分为以下两类
意向共享锁(IS lock),事务想要获得一张表中数据的共享锁
意向排他锁(IX lock),事务想要获得一张表中数据的排他锁
我们可以想一下这四种锁(IX,IS,X,S)互相之间是否兼容的关系,首先XS之间兼容关系已经说过了;IX和IS之间显然也是互相兼容的,因为IXIS锁住的不是事务真正要读取或修改的对象(我的理解是IXIS是XS的锁降级);接下来如果存在X锁可以和IXIS兼容么?显然是不行的,假设X锁住了表A,这时另一个事务需要获取表A中某一行的数据,需要给表A加IXIS,此时X锁已经锁住了全表的数据,X锁本质是行锁,实际上是把全表的数据都锁了一遍,这时另一个事务显然不能再获取其中任何数据的锁了,所以X锁和X/S/IX/IS全部不兼容;最后S锁可以和IXIS兼容么,假设S锁锁住了表A,等于给表A所有数据加上了S锁,此时另一个事务给表A某些数据加S锁(此时给表A加的是IS),此时是可行的,因为SS是兼容的,如果给表A某些数据加X锁(此时给表A加的是IX锁),此时是不行的,因为SX锁不兼容,最终得到如下锁之间的兼容性表
innodb引擎下锁之间的兼容性表
|
IS
|
IX
|
S
|
X
|
IS
|
兼容
|
兼容
|
兼容
|
不兼容
|
IX
|
兼容
|
兼容
|
不兼容
|
不兼容
|
S
|
兼容
|
不兼容
|
兼容
|
不兼容
|
X
|
不兼容
|
不兼容
|
不兼容
|
不兼容
|
一致性非锁定读 和 一致性锁定读
在RR级别下,在使用 一致性非锁定读时(默认不加锁),读取的总是事务开始的那个版本数据,数据可能不是最新的;在使用 一致性锁定读时(加x锁或者s锁,具体语句是select .... for update(加X锁),select ... lock in share mode(加S锁),读取的总是当前最新的数据;也就是说在RR隔离级别中一致性锁定读 与 一致性非锁定读读取到的数据不一定一样(在开启事务后,有其他事务修改了数据时不一致,否则一致),多使用在对数据一致性,实时性不高的情况下(数据可能不是最新的,不会破坏事务隔离性)
在RC级别下,一致性锁定读 与 一致性非锁定读读取到的数据总是一样的。因为即使在开启事务后,有其他的事务对数据进行了修改,一致性非锁定读总是读取最新的那个快照,而一致性锁定读本身就是读取最新的那个快照。多使用在对数据一致性,实时性比较高的情况下(会破坏事务隔离性,比较慢,性能低)
锁的算法
innodb引擎有三种行锁的算法
1,record lock 记录锁,锁住一行数据,即记录本身
2,gap lock 间隙锁,锁住一个范围,不包括记录本身,锁范围是左开右开
3,next-key lock 是record lock + gap lock,锁定一个范围,并且锁定记录本身,锁范围是左开右闭
在read-uncommited隔离级别下,存在脏数据问题(不是脏页),即在事务A中可以看到事务B修改的数据,即使事务B没有提交,这违反了事务的隔离性,并且事务B可能再次修改数据,也可能rollback,所以此时事务A读到的数据是脏数据,脏数据指的就是未提交的数据,这个问题就是脏读问题
为了解决脏读问题,需要将隔离级别提升到read commited,在此级别下无法读取未提交事务的数据
因为在read-uncommited隔离级别在读操作不加锁,写操作加X锁,所以当事务B修改数据加X锁时,事务A读操作根本不加锁,自然不会有兼容问题,也就读取到了为提交的数据,而在read-commited级别下,读操作需要加S锁,这样就避免了脏读问题,是通过每条select sql执行时新建一个read view(详见MVCC章节有描述),所以避免了脏读问题。
在read-commit隔离级别下,存在不可重复读问题,即在同一事务的两次读中,读到了不同的数据,这里不同特指发生了update操作修改了数据,如果是insert操作增加了数据,这种称之为幻读,之后会说。即在事务A中读取了部分数据,同时事务B修改这部分数据并且commit,然后事务A执行同样的读操作发现数据发生了变化,这个问题就是不可重复读问题,这本质上上也破坏了事务的一致性。为了解决不可重复度问题,需要将隔离级别提升至 repeatable-read
在repeatable-read隔离级别下,在第一条select执行时创建read view,并且整个事务期间都使用此视图。所以即使期间有其他事务修改并提交,由于read view不变,所以不会对select的结果有影响。
在repeatable-read 级别下存在幻读问题,此问题提通过将隔离级别提升至串行化解决,但是innodb通过next-key lock + mvcc在rc级别下解决了幻读问题。所谓的幻读就是在同一事务中,使用同一条select sql两次执行中读取到了其他事务insert的行,看起就就像幻觉一样,所以称之为幻读。
如何在repeatable-read级别下解决幻读问题呢?当 读为快照读时,是通过MVCC解决,此时即使其他事务有insert数据,但是由于read view没有更新,所以实际时通过MVCC读取读的历史数据,不存在幻读问题。当为 当前读时,innodb引入了next-key lock / gap lock ,这时select 时锁住的不是孤立的记录,而是满足条件的范围,这导致其他事务的insert会进入block状态,例sql : select * from t where a>20,此时获取的不是a=20这条数据的record lock,而是(20,+无穷)范围内的gap lock,那么此时如果有事务insert into t values(21)就会被block,那么再次执行select * from t where a>20就不会出现幻读的问题了。
加锁的规则
首先要先了解以下规则(此规则为丁奇大佬总结,强烈推荐《MySQL实战45讲》)
加锁规则里面,包含了两个“原则”、两个“优化”和一个“bug”。
原则 1:加锁的基本单位是 next-key lock
原则 2:查找过程中访问到的对象才会加锁。
优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
下面结合不同的例子来加深对加索规则的理解,例子有来源于《MySQL实战45讲》,这里我会通过例子自己去分析加锁的流程,
表结构及数据如下所示
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
例子1,等值查询间隙锁
1,sessionA 需要给id = 7的数据加上排他锁,根据原则1,加锁的范围是(5,10]
2,id为主键,且为等值查询,符合优化2,next-key lock退化为间隙锁,因为显然7 != 10,所以此时的加索范围为(5,10)
3,由上可知,sessionB中往(5,10)之间插入数据,会被阻塞,而更新id=10的数据可以正常执行
例子2,非唯一索引等值锁
1,sessionA 中在字段C索引上,加(0,5]共享锁,在主键索引上加(0,5]排他锁
2,由于优化1,主键上的(5,10]退化为行锁[5],由于优化2,c索引上的锁退化为间隙锁(0,5),好像不对啊,这样sessionB应该会阻塞,sessionC 不阻塞才对
//////////////////////我是分割线///////////////////////////////
1,sessionA 中 c索引上加(0,5]共享锁,但是由于c索引不是唯一索引,所以在得到c=5后还需要接着往右查,知道发现c>5的数才会停,根据原则2,访问到的都加索,所以(5,10]也会加上锁,根据优化2,(5,10]退化为(5,10),此时c索引上的锁为(0,5] 和 (5,10)。又由于sessionA的sql是覆盖查询,不需要回表,所以根据优化2,主键上没有加任何锁。
2,此时sessionB的update需要主键上(0,5]的排他锁,可以执行,sessionC的insert 需要主键上的(5,10]的排他锁和c索引上的(5,10]的排他锁,所以被阻塞了。
ps:如果sessionA 的sql是for update的话,系统会认为是需要修改数据的,此时就会给主键上加索
3 主键索引范围锁
1,根据原则1,加锁范围为 (5,10]和(10,15]
2,当寻找第一个满足条件的id值时是等值查找,根据优化1,(5,10]退化为行锁[10],实际锁范围是[10,15]
3,sessionB中insert id=8成功,insert id=13失败
4,sessionC中update id=5失败
4,非唯一索引范围
1,由于是for update,故是会对主键加索的,对c索引,加索范围为(5,10]和(10,15],主键上的锁为[5]行锁和(10,15]next-key lock锁
2,sessionB insert id=8会被c索引的锁阻塞
3,sessionC update c=15会被c索引的锁阻塞
5,唯一索引范围锁bug
1,sessionA 显然会在(10,15]上加锁,但是由于bug1,需要继续向右找到不满足条件的值,所以还会加上(15,20]锁
2,所以sessionB和sessionC都会被阻塞
6,next-key lock的加锁规则
1,sessionA先加锁(5,10],由于c不是唯一索引,所以需要向右找到第一个不满足条件的值即15,又由于优化2,实际加锁为(10,15)。
2,sessionB 需要加锁为 (5,10],所以被阻塞
3,sessionA 需要插入数据,结果发生死锁, 此处实际是由于insert语句被sessionB上的(5,10)间隙锁阻塞,导致出现的死锁
next-key lock是由行锁和间隙锁合并而来,在实际加锁时,也是分开添加的,需要注意的是间隙锁是不存在阻塞概念的,会阻塞的只有行锁,因为行锁才是锁住了实际的数据。如上例,sessionB的sql已经被阻塞了,但实际上(5,10)的间隙锁已经加上了,所以(5,10)这个范围内sessionA和sessionB都加了锁。
锁的学习长路漫漫,此处只做入门学习,故到此为止,以后遇到实际的问题再具体分析