前言
在学习编程的过程中,锁,恐怕是我们经常要遇到的一种技术了。几乎每一种技术里都有锁的存在,锁的目的就是解决多进程或多线程情况下对共享资源的争抢问题。并发编程里我们学过各种锁,那MySQL的锁又是怎么做的呐?今天我们就来一探究竟!
一、锁介绍
在数据库中,除了传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。在实际的数据库系统中,每时每刻都在发生锁定,当某个用户在修改一部分数据时,MySQL会通过锁来防止其他用户读取同一数据。
二、锁分类
1 、从性能上分为:
乐观锁(用版本对比或CAS机制)和悲观锁,乐观锁适合读操作较多的场景,悲观锁适合写操作较多的场景,如果在写操作较多的场景使用乐观锁会导致比对次数过多,影响性能;
2 、从对数据操作的粒度分为:
全局锁: 锁住整个Database,由MySQL的SQL layer层实现;
表锁: 锁住某个表,由MySQL的SQL layer层实现;
页锁: 在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中有多个行记录;
间隙锁: 锁的是两个值之间的空隙,间隙锁是在可重复读隔离级别下才会生效;****
行锁: 锁某一数据Row的索引,也可锁定行索引之间的间隙(即间隙锁),由存储引擎实现;
3 、从对数据库操作的类型分为:
读锁: 又叫共享锁,针对同一份数据,多个读操作可以同时进行而不会互相影响;
写锁: 当前写操作没有完成前,它会阻断其他写锁和读锁,数据修改操作都会加写锁,查询也可以通过for update加写锁;
意向锁: 又称I锁,针对表锁,主要是为了提高加表锁的效率,是MySQL数据库自己加的。当有事务给表的数据行加了共享锁或排他锁,同时会给表设置一个标识,代表已经有行锁了,其他事务要想对表加表锁时,就不必逐行判断有没有行锁可能跟表锁冲突了,直接读这个标识就可以确定自己该不该加表锁。
三、锁详解
1 、全局锁
全局锁是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的DML的写语句,DDL语句,事务提交语句都将被阻塞。其典型的使用场景是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。
加全局锁的命令:flush tables with read lock;
释放全局锁的命令:方式一:flush tables with read lock;方式二:断开加锁的session连接,便可自动释放全局锁;
2 、表锁
每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;一般用在整表数据迁移的场景。可分为:
1) 表读锁
添加表读锁命令:lock table t read;删除表锁命令:unlock tables;加表锁后,不阻塞其他session对加读锁的表的读,但会阻塞任何session对加读锁的表的写;并且为表加读锁的当前session不能查询其他没有锁定的表。
2) 表写锁
添加表写锁命令:lock table t write;删除表锁命令:unlock tables;为表添加写锁的当前session对锁定表可以进行查询、更新、插入操作;会阻塞其他session对锁定表的读和写操作。
3 、行锁
每次操作锁住一行数据。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最高。
InnoDB相对于MYISAM的最大不同有两点:一、InnoDB支持事务(TRANSACTION);二、InnoDB支持行级锁;
注意:InnoDB的行锁实际上是针对索引加的锁(在索引对应的索引项上做标记),不是针对整个行记录加的锁。并且该索引不能失效,否则会从行锁升级为表锁。(RR级别会升级为表锁,RC级别不会升级为表锁)
比如我们在RR级别执行如下sql语句:select * from account where name = 'lilei' for update; (where条件里的name字段无索引)则其它Session对该表任意一行记录做修改操作都会被阻塞住。
关于可重复读隔离级别行锁升级为表锁的原因分析:
因为在可重复读隔离级别下,需要解决不可重复读和幻读问题,所以在遍历扫描聚集索引记录时,为了防止扫描过的索引被其它事务修改(不可重复读问题)或间隙被其它事务插入记录(幻读问题),从而导致数据不一致,所以MySQL的解决方案就是把所有扫描过的索引记录和间隙都锁上,这里要注意,并不是直接将整张表加表锁,因为不一定能加上表锁,可能会有其它事务锁住了表里的其它行记录。
4 、页锁
只有BDB存储引擎支持页锁,页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录。当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。
5 、间隙锁
间隙锁,锁的是两个值之间的空隙,间隙锁是在可重复读隔离级别下才会生效。Mysql默认级别是可重复读,有幻读问题,间隙锁是可以解决幻读问题的。
假设一张user表里数据如下:
上表中的间隙就有id为 (3,10),(10,20),(20,正无穷) 这三个区间,在Session_1下面执行sql语句:select * from account where id = 18 for update;则其他Session没法在这个(10,20)这个间隙范围里插入任何数据。如果执行下面这条sql语句:select * from account where id = 25 for update;则其他Session没法在这个(20,正无穷)这个间隙范围里插入任何数据。也就是说,只要在间隙范围内锁了一条不存在的记录会锁住整个间隙范围,不锁边界记录,这样就能防止其它Session在这个间隙范围内插入数据,就解决了可重复读隔离级别的幻读问题。
6 、意向锁
又称I锁,针对表锁,主要是为了提高加表锁的效率,是mysql数据库自己加的。当有事务给表的数据行加了共享锁或排他锁,同时会给表设置一个标识,代表已经有行锁了,其他事务要想对表加表锁时,就不必逐行判断有没有行锁可能跟表锁冲突了,直接读这个标识就可以确定自己该不该加表锁。特别是表中的记录很多时,逐行判断加表锁的方式效率很低。而这个标识就是意向锁。
意向锁主要分为:
意向共享锁,IS锁,对整个表加共享锁之前,需要先获取到意向共享锁。
意向排他锁,IX锁,对整个表加排他锁之前,需要先获取到意向排他锁。
7 、临键锁
临键锁是行锁与间隙锁的组合,【左开右闭区间】,例如(5,8]; 默认情况下,InnoDB使用临键锁来锁定记录,但在不同的场景中会退化;当查询的索引含有唯一属性的时候,临键锁会进行优化,将其降级为记录锁,即仅锁住索引本身,不是范围。
四、加锁规则
主键索引
等值条件,命中,加记录锁;
等值条件,未命中,加间隙锁;
范围条件,命中,包含where条件的临键区间,加临键锁;
范围条件,没有命中,加间隙锁;
辅助索引
等值条件,命中,命中记录的辅助索引项+主键索引项加记录锁,辅助索引项两侧加间隙锁;
等值条件,未命中,加间隙锁;
范围条件,命中,包含where条件的临键区间加临键锁。命中记录的id索引项加记录锁;
范围条件,没有命中,加间隙锁;
五、InnoDB和MyISAM存储引擎下锁的区别
InnoDB在执行查询语句select查询语句时(非串行隔离级别),不会加锁。但是update、insert、delete操作会加行锁;
MyISAM在执行查询语句select查询语句前,会自动给涉及的所有表加读锁,在执行update、insert、delete操作会自动给涉及的表加写锁。
读锁会阻塞写,但是不会阻塞读。而写锁则会把读和写都阻塞。
InnoDB存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一下,但是在整体并发处理能力方面要远远优于MyISAM的表级锁。当系统并发量高的时候,InnoDB的整体性能和MyISAM相比就会有比较明显的优势了。但是,InnoDB的行级锁定同样也有其脆弱的一面,当我们使用不当的时候,可能会让InnoDB的整体性能表现不仅不能比MYISAM高,甚至可能会更差。
六、锁分析实战
锁的理论知识基本介绍完了,现在我们来进行一下实战分析,先看下面两条SQL语句:
1、 select * from t where id = 9;
2、 delete from t where id = 9;
以上两条SQL会加锁么?如果会,那么加的是什么锁?
各位先自己想想答案再继续往下看。
对于以上两条SQL语句是否加锁问题,我们可以从一下几个方面进行回答:
由于在InnoDB存储引擎下,读已提交和可重复读隔离级别下select操作均不加锁,所以接下来我们着重分析SQL语句2。
1 、在读已提交隔离级别下
1 )id是主键时
如果id是主键,那读已提交隔离级别下,给定SQL2:delete from t where id = 9 ; 只需要将主键上id=9的记录加上写锁即可。
2 )id不是主键,而是唯一索引时
由于id是二级唯一索引,因此delete语句会选择走id列的索引进行where条件的过滤,在找到id=9的记录后,首先会将二级唯一索引上的id=9索引记录加上写锁,同时,会读取到该二级唯一索引树叶子节点上的主键值,然后根据主键值去聚簇索引中找到对应的主键索引项并加写锁。
3 )id为非唯一索引(即普通索引)时
若id列上有非唯一索引,那么对应的所有满足SQL查询条件(即id=9)的记录,都会被加锁。同时,这些记录在主键索引(即聚簇索引)上的记录,也会被加锁。
4 )id列上无索引时
若id列上没有索引,SQL会走聚簇索引的全扫描进行过滤,由于过滤是由MySQL Server层面进行的。因此每条记录,无论是否满足条件,都会被加上写锁。但是,为了效率考量,MySQL做了优化,对于不满足条件的记录,会在判断后放锁,最终持有的,是满足条件的记录上的锁,但是不满足条件的记录上的加锁/放锁动作不会省略。
2 、在可重复读隔离级别下
1 )id是主键时
与读已提交隔离级别下id是主键时的情况一致。
2 )id不是主键,而是唯一索引时
与读已提交隔离级别下id不是主键,而是唯一索引时的情况一致。
3 )id为非唯一索引(即普通索引)时
此情况下,对应SQL语句:delete from t where id = 9; 首先,通过id索引定位到第一条满足where条件的记录,加记录上的写锁,加GAP上的间隙锁,然后加主键聚簇索引上的记录写锁,然后返回;然后读取下一条,重复进行。直至进行到第一条不满足条件的记录,此时,不需要加记录写锁,但是仍旧需要加间隙锁,最后返回结束。
4 )id列上无索引时
如果进行全表扫描的当前读,那么会锁上表中的所有记录,同时会锁上聚簇索引内的所有间隙,杜绝所有的并发“更新/删除/插入”操作。
七、死锁
1 、产生情况
情况1
情况2
以上两种情况下都会发生死锁,死锁的发生与否,并不在于事务中有多少条SQL语句,关键在于:两个(或以上)的Session加锁的顺序不一致。
2 、如何避免死锁情况
大多数情况mysql可以自动检测死锁并回滚产生死锁的那个事务,但是有些情况MySQL没法自动检测死锁,这种情况我们可以通过日志分析找到对应事务线程id,可以通过kill杀掉。
1)尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
2)合理设计索引,尽量缩小锁的范围
3)尽可能减少检索条件范围,避免间隙锁
4)尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的SQL尽量放在事务最后执行
5)尽可能用低的事务隔离级别
锁理论及锁分析的介绍到此就结束了,欢迎各位批评指正。