本文主要涉及以下几个个部分:

1. 为什么要加锁

2. 锁的分类

3. 常见语句的加锁分析

4. 如何分析死锁

5. 如何预防死锁

先列出本地的运行环境

数据库版本是5.7,隔离级别是Repeatable-Read(可重复读),不同的数据库版本和隔离级别对语句的执行结果影响很大。所以需要说明版本和隔离级别

一、为什么要加锁

数据库是一个多用户使用的共享资源。当多个用户并发地存取数据时,在数据库中加锁是为了保证数据库的一致性。

数据库有ACID原则,其中I是隔离性,

  • 脏读:读未提交的数据
  • 不可重复读:读已修改的数据
  • 虚读:读提交了插入/删除的数据
 
事务隔离机制

和标准SQL规范相比,MySQL中可重复读解决了幻读,实现了串行化隔离级别的功能,同时没有严重影响并发。是通过加锁、阻止插入新数据,来解决幻读的。

二、锁的分类

 

我们听说过读锁、写锁、共享锁、互斥锁、行锁等等各种名词,简单对这些锁进行了分类。

 
锁的分类

加锁机制:

1、乐观锁:先修改,保存时判断数据是否被更新过,若被其他人更新过则不能保存,属于应用级别

2、悲观锁:先获取锁,防止其它线程修改数据,再操作修改,数据库锁级别

锁粒度:

表级锁:表锁时Mysql中最基本的锁策略,并且时开销最小的策略。表锁会锁定整张表。一个用户在对表进行写操作(插入、删除、更新等),

  需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁是,其他读取的用户才能获得读锁,读锁之间是不相互阻塞的。

  优点和缺点:开销小,加锁快,粒度大,锁冲突概率大,并发度低,适用于读多写少的情况。

页级锁:

页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,

  一次锁定相邻的一组记录。BDB 支持页级锁。

  特点:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。

行级锁:行级锁可以最大程度地支持并发处理(同时也带来了最大的锁开销)。行级锁只在存储引擎层实现,而Mysql服务器层没有实现。

  服务器层完全不了解存储引擎中的锁实现。

  特点:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

兼容性:

S锁,也叫做读锁、共享锁,对应于我们常用的 select * from users where id =1 lock in share mode

X锁,也叫做写锁、排它锁、独占锁、互斥锁,对应对于select * from users where id =1 for update

下面这个表格是锁冲突矩阵,可以看到只有读锁和读锁之间兼容的,写锁和读锁、写锁都是冲突的。

 
共享锁和排斥锁

冲突的时候会阻塞当前会话,直到拿到锁或者超时

这里要提到的一点是,S锁 和 X锁是可以是表锁,也可以是行锁

接下来是面试必备的。

记录锁:单行记录上的锁,行锁是加在索引上的。

间隙锁:锁定记录之间的范围,但不包含记录本身。

Next Key Lock: 记录锁+ 间隙锁,锁定一个范围,包含记录本身。

4. 意向锁( Intention Locks )

InnoDB为了支持多粒度(表锁与行锁)的锁并存,引入意向锁。意向锁是表级锁,

IS: 意向共享锁

IX: 意向排他锁

事务在请求某一行的S锁和X锁前,需要先获得对应表的IS、IX锁。

意向锁产生的主要目的是为了处理行锁和表锁之间的冲突,用于表明“某个事务正在某一行上持有了锁,或者准备去持有锁”。比如,表中的某一行上加了X锁,就不能对这张表加X锁。

如果不在表上加意向锁,对表加锁的时候,都要去检查表中的某一行上是否加有行锁,多麻烦。

5. 插入意向锁(Insert Intention Lock

Gap Lock中存在一种插入意向锁,在insert操作时产生。

有两个作用:

和next-key互斥,阻塞next-key 锁,防止插入数据,这样就不会幻读。

插入意向锁互相是兼容的,允许相同间隙、不同数据的并发插入

三、常见语句的加锁分析

后面会有多个SQL语句,先说明一下表结构

CREATE TABLE user (

id int(11) unsigned NOT NULL AUTO_INCREMENT,

id_no varchar(255) DEFAULT NULL COMMENT '身份证号',

name varchar(255) DEFAULT NULL COMMENT '姓名',

mobile varchar(255) DEFAULT NULL COMMENT '手机号',

age int(11) DEFAULT NULL COMMENT '年龄',

address varchar(255) DEFAULT NULL COMMENT '地址',

PRIMARY KEY (id),

UNIQUE KEY uniq_id_no (id_no),

KEY idx_name (name)

) ENGINE=InnoDB AUTO_INCREMENT=10002 DEFAULT CHARSET=utf8 COMMENT='用户表';

这里有一个user表,5个字段,其中id是主键,id_no是身份证号,加了唯一索引,name是用户姓名,可以重复的,加了普通索引,手机号、年龄、地址都没有索引。

1. 普通select

select ***** from user where id =1;

begin;

select ***** from user where id =1;

commit:

普通的select 语句是不加锁的。select包裹在事务中,同样也是不加锁的。where后面的条件不管多少,普通的select是不加锁的。

2. 显式加锁

select ***** from user where id =1 lock in share mode;

select ***** from user where id =1 for update;

显式指出要加什么样的锁。上面一个加的是共享锁,下面的是互斥锁。

这里需要强调的一点,需要明确在事务中是用这些锁,不在事务中是没有意义的。

3. 隐式加锁

update user set address '北京' where id=1;

delete from user where id=1;

update和delete也会对查询出的记录加X锁,隐式加互斥锁。加锁类型和for update 类似

后面只按照显式加锁的select for update 举例子,更新和删除的加锁方式是一样的。

4. 按索引类型

select ***** from user where id =1 for update;

select ***** from user where id_no ='a22' for update;

select ***** from user where name ='王二' for update;

select ***** from user where address ='杭州' for update;

四条SQL,区别在于where条件的过滤列,分别是主键、唯一索引、普通索引、无索引。

 
索引的锁

主键:之前提到过索引组织表,这里会在聚集索引上对查询出的记录,加X锁

唯一索引:会在辅助索引上,把在对应的id_no=a22的索引加X锁,因为是唯一的,所以不是next-key锁。然后在主键上,也会在这条记录上加X锁。

普通索引:因为不是唯一的,会在辅助索引上,把对应的id_no=a22的索引加next-key锁。然后在主键加X锁。

无索引:首先,是不推荐这种写法,没有索引的话,因为会全表扫描,数据量大的话查询会很慢。这里讨论的是,这种情况下,会加什么锁? 答案: 首先,聚簇索引上的所有记录,都被加上了X锁。其次,聚簇索引每条记录间的间隙(GAP),也同时被加上了GAP锁。在这种情况下,这个表上,除了不加锁的快照度,其他任何加锁的并发SQL,均不能执行,不能更新,不能删除,不能插入,全表被锁死。这是一个很恐怖的事情,请注意。

5. 记录不存在的情况

前面几个例子中,都是可以查到结果的。如果对应记录不存在会怎样?答案是锁住间隙,不允许插入。mysql要保证没有其他人可以插入,所以锁住间隙。

6. 普通 insert 语句

在插入之前,会先在插入记录所在的间隙加上一个插入意向锁。

insert会对插入成功的行加上排它锁,这个排它锁是个记录锁,而非next-key锁(当然更不是gap锁了),不会阻止其他并发的事务往这条记录之前插入 。

四、分析当前锁的情况

先说一下死锁的定义,死锁是指两个或两个以上的事务在执行过程中,因争夺资源而造成的一种互相等待的现象。这个定义适用于数据库,有几个重点,两个或两个以上的事务,一个事务是不会出现死锁的。争夺的资源一般都是表或者记录。

出现死锁了会怎样,正常情况下,mysql会检查出死锁,并回滚某一个事务,让另一个事务正常运行。

Mysql 会回滚副作用小的事务,判定的标准是执行的时间以及影响的范围。

1. show engine innodb status

 
 

展示innodb存储引擎的运行状态

通过这个命令显示的内容比较多,其中有一项lasted detected deadlock 显示最近发生的死锁。

图中红色线条标注的是执行的SQL,以及加了什么锁,可以看出是在这行记录上加了X锁,没有gap锁。

2. information_schema.innodb_locks

 
 

information_schema 数据库是mysql自带的,保存着关于MySQL服务器所维护的所有其他数据库的信息。其中innodb_locks表,记录了事务请求但是还没获得的锁,即等待获得的锁。

lock_id:锁的id,由锁住的空间id编号、页编号、行编号组成

lock_trx_id:锁的事务id。

lock_mode:锁的模式。S[,GAP], X[,GAP], IS[,GAP], IX[,GAP]

lock_type:锁的类型,表锁还是行锁

lock_table:要加锁的表。

lock_index:锁住的索引。

lock_space:innodb存储引擎表空间的id号码

lock_page:被锁住的页的数量,如果是表锁,则为null值。

lock_rec:被锁住的行的数量,如果表锁,则为null值。

lock_data:被锁住的行的主键值,如果表锁,则为null值。

五、预防死锁

1. 以相同的顺序更新不同的表

这样执行的话,会出现锁等待,但不容易出现死锁。

2. 预先对数据进行排序

比如一个接口批量操作数据,如果乱序的话,并发的情况下,也是有可能出现死锁的。给学生批量加分的接口,按照表格中的执行顺序的话,第一个事务,持有A的锁,请求B的锁,第二个事务持有B的锁,请求A的锁,出现死锁。

3. 直接申请足够级别的锁,而非先共享锁,再申请排他锁。

比如这种情况,两个事务,先申请共享锁,共享锁是兼容的,然后申请互斥锁的时候,需要互相等待,就出现了死锁。

4. 事务的粒度及时间尽量保持小,这样锁冲突的概率就小了,也就不容易出现死锁。不建议在数据库的事务中执行API调用。

5. 正确加索引。没有索引会引起全表扫描,类似于锁表。

六:总结:

1,正确的加索引,尽量先查询,然后使用主键去加锁,等于操作来加锁,而尽量避免辅助索引,或者不是范围比较来加锁。

2,出现了锁的问题,根据数据库已有的信息,分析死锁。


原文地址:https://www.jianshu.com/p/120fcab69de6