一、锁简介
锁这个词是比较常见的,生活中我们使用锁来保证一个房间或者一个资源的安全,因为开锁需要钥匙,而钥匙保存在我们手里,其他人是无法正常获取到的。程序中,当我们的程序需要多线程去访问操作共享资源时,为了保证一致性,我们需要使用锁机制来防止并发原因出现的问题,同样,数据库会使用这种锁机制来保证资源的共享安全性,比如当两个事务都需要更改同一条记录时,就需要锁机制来保证一致安全性。
下面根据加锁的范围来看下innodb支持的锁类别:
1.记录锁(行锁)
行级锁是特定引擎所支持的,比如Innodb是支持行锁的,这也是它的一个特性,可以在数据记录级别上锁,粒度是比较细的,记录锁在加锁类别可以分成两种锁:
共享锁(s锁):事务可以读取数据,如果数据加了记录共享锁,可以继续加共享锁;
排他锁(x锁):事务可以更改数据,如果事务加了记录排他锁,则不可以在家共享锁和排他锁;
两种锁的排斥关系如下:
锁 | 共享锁(s) | 排他锁(x) |
共享锁(s) | 兼容 | 不兼容 |
排他锁(x) | 不兼容 | 不兼容 |
2.范围锁
范围锁是指加在区间上的锁,其锁定的是一个空间,比如锁定范围为(-∞,1】或(10,30)这种方式,Innodb中这种锁叫做gap锁。
间隙锁(gap lock):工作在事务隔离级别中的RR级别,在索引之间加入锁,它使得RR级别避免了幻读;
next key lock:这种锁描述的在Innodb中会出现行锁和gap lock结合使用的场景,把这种加锁方式叫做next key;
3.粗粒度锁
Innodb还支持粗粒度的锁,可以建立在表以及段、区、页上的。其中意向锁是表级别的锁,当想要在数据记录上加x锁时,需要先在表加上IX锁。意向锁之间是兼容的,当我们给表加表锁的时候,如果没有意向锁的话,需要遍历每一条数据是否有排他锁,如果没有才能加表锁,但是有了意向锁,直接判断表上是否有意向排他锁就可以了。意向锁是Innodb引擎自我管理的,无需DBA去操作或者管理配置,是一种自我属性。类别上也是分为两种:
意向共享锁(IS):在数据记录加排他锁之前,需要在上级也就是表上加意向排他锁;
意向排他锁(IX):在数据记录加共享锁之前,需要在上级也就是表上加意向共享锁;
意向锁和记录锁之间的排斥关系如下:
锁类别 | 共享锁 | 排他锁 | 意向共享锁 | 意向排他锁 |
共享锁 | 兼容 | 不兼容 | 兼容 | 不兼容 |
排他锁 | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
意向共享锁 | 兼容 | 不兼容 | 兼容 | 兼容 |
意向排他锁 | 不兼容 | 不兼容 | 兼容 | 兼容 |
注:IS或者IX锁此时是与表级的X锁或S锁比较排斥关系
二、加锁情况
在Innodb引擎中,使用多版本并发控制来处理并发的问题,也就是MVCC(Multi-version Concurrency Control),这个控制方法不仅仅使用在Innodb中,其实已经很多数据库,以及其他很多的引擎也支持使用了这中控制协议,其最大的有点是读不加锁,读写不冲突,因此减少了锁的开销,提高了读写性能,可以理解他是行级锁的一个变种。
MVCC工作在Innodb的RC和RR事务隔离级别下,因为RU每次都读取最新的行,Seriallizable是单线程串行,无需加锁操作。
在Innodb的MVCC中读操作分为两种:快照读和当前读,快照读读取的数据可见版本或者历史版本,无需加锁操作,当前读需要读取数据的最新数据行,并且这些返回的数据行都会加锁,防止其他事物更改,两种读操作包含的sql语句结构:
快照读:select * from table where ***;简单的读取操作;
当前读:
select * from table where ? lock in share mode;//加S锁,其他事物可以读取;
select * from table where ? for update;
insert into table values(...);
update table set ...where ?;
delete from table where ?;
除了第一条加的是S锁,其他语句都是读取数据的最新行,并且需要加X锁,以防止其他事物更改或者读取。
通过一条update语句,分析sql执行的简单过程:
1.首先客户端收到一条sql,客户端将这个sql发到server层,server经过权限验证、查询缓存(开启)、sql优化、指定执行计划等步骤之后,会把该sql发送到引擎层;
2.引擎层接收到该sql后,会查找符合该条件的记录,先去内存查找,如果没有,则到磁盘读取放入内存,找到符合条件的记录返回到server层执行器,并给该记录执行加锁操作;
3.执行器根据该返回记录,生成新的行,调用引擎的写入接口,将该记录写入;
4.引擎层接收到该更新插入请求,更新内存,添加日志,将更新成功结果返回到server层,一条记录更新完成;
5.引擎继续返回满足条件的行,直到所有满足条件更新完毕,就会告诉执行器所有记录更新完成,可以提交;
6.server层发起提交请求,引擎层执行提交操作,完成;
可以看出更新的过程是有一个当前读加锁操作,插入记录如果需要检查unique key冲突,也需要当前读的验证;
三、锁情况分析
锁的定义了解之后,我们需要知道我们应用中sql执行的内部流程包括加锁条件 ,这样才能有方向的进行优化,提升速度。假如我们分析一条语句的加锁条件,例如:delete from T1 where id = 10;执行的过程受很多因素参数影响,比如当前引擎的事务隔离级别、id是否有索引,如果有索引,索引的类别是什么?是主键?是唯一索引?下面列出几种情况,讲述锁的使用方式,可以通过阅读文章《Mysql—Innodb引擎 索引》了解innodb索引信息:
假设有表T1,有字段id,name:
RC级别下:
1.RC级别、id主键
id是主键,直接在主键上加x锁就可以
2.RC级别、id辅助唯一索引
id为唯一辅助索引,则需要在辅助索引x锁,在通过主键x锁,便可确定
3.RC级别、id辅助非唯一索引
将符合条件的非唯一辅助索引上加x锁、通过辅助索引找到对应的主键并加x锁
4.RC级别、id无索引
id列无索引,则通过全表扫描找到对应的数据记录,这个过程会使得全部记录加x锁。因为全部加x锁会严重影响性能,后期优化后,在寻找过程中,不符合记录的记录会去掉x锁
RR级别下:
1.RR级别、id主键
和RC级别下,id主键的情况相同,都是在主键上加x锁;
2.RR级别、id辅助唯一索引
和RC级别下,id辅助唯一索引的情况相同,都是在对应辅助索引和主键上加x锁;
3.RR级别、id辅助非唯一索引
此种情况下会加入gap锁,加上记录锁,也就是在辅助索引上加next key lock,在对应主键x加锁
4.RR级别、id无索引
全变扫描全部x锁加gap锁,优化后,不符合记录的记录去掉x锁,也不会加gap锁;