本文主要涉及以下几个个部分:
1. 为什么要加锁
2. 锁的分类
3. 常见语句的加锁分析
4. 如何分析死锁
5. 如何预防死锁
先列出本地的运行环境
数据库版本是5.7,隔离级别是Repeatable-Read(可重复读),不同的数据库版本和隔离级别对语句的执行结果影响很大。所以需要说明版本和隔离级别
一、为什么要加锁
数据库是一个多用户使用的共享资源。当多个用户并发地存取数据时,在数据库中加锁是为了保证数据库的一致性。
数据库有ACID原则,其中I是隔离性,
- 脏读:读未提交的数据
- 不可重复读:读已修改的数据
- 虚读:读提交了插入/删除的数据
和标准SQL规范相比,MySQL中可重复读解决了幻读,实现了串行化隔离级别的功能,同时没有严重影响并发。是通过加锁、阻止插入新数据,来解决幻读的。
二、锁的分类
我们听说过读锁、写锁、共享锁、互斥锁、行锁等等各种名词,简单对这些锁进行了分类。
加锁机制:
1、乐观锁:先修改,保存时判断是够被更新过,应用级别
2、悲观锁:先获取锁,再操作修改,数据库级别
锁粒度:
表级锁:开销小,加锁快,粒度大,锁冲突概率大,并发度低,适用于读多写少的情况。
页级锁:BDB存储引擎
行级锁:Innodb存储引擎,默认选项
兼容性:
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,出现了锁的问题,根据数据库已有的信息,分析死锁。