本文主要涉及如下三个部分:mysql
1. 为何要加锁sql
2. 锁的分类数据库
3. 常见语句的加锁分析服务器
4. 如何分析死锁session
5. 如何预防死锁数据结构
先列出我本地的运行环境并发
数据库版本是5.7,隔离级别是Repeatable-Read(可重复读),不一样的数据库版本和隔离级别对语句的执行结果影响很大。讨论锁的时候不指明版本和隔离级别,都是耍流氓。spa
1、为何要加锁线程
数据库是一个多用户使用的共享资源。当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的状况。若对并发操做不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。日志
锁是用于管理对公共资源的并发控制。也就是说并发的状况下,会出现资源竞争,因此须要加锁。
举个例子,转帐操做。简单来讲,张三给李四转帐x元,能够分为三步:
1,先查询张三的帐户余额y是否大于x
2,张三的余额 y = y - x元
3,李四的余额 x = z + x元
假设张三帐户余额有1000元,李四余额也有1000元,若是不加锁的话,同时有两个请求,A要求转500元,B要求转600元,第一步查询余额都是足够的,第2步和第3步也能执行成功,可是最终结果倒是错误,第二个请求可能会覆盖掉第一个请求。
这种问题叫作 丢失更新: 多个事务操做同一行,后面的更新覆盖了前面的更新值。须要在应用级别加锁来避免。
数据库有ACID原则,其中I是隔离性,标准SQL规范中定义了四种隔离级别:
越往下,隔离级别越高,问题越少,同时并发度也越低。隔离级别和并发度成反比的。
MySQL中的隔离级别以下:
和标准SQL规范相比,MySQL中可重复读解决了幻读,实现了串行化隔离级别的功能,同时没有严重影响并发。是经过加锁、阻止插入新数据,来解决幻读的。
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锁是能够是表锁,也能够是行锁
索引组织表
先理解下索引组织表。
辅助索引
汇集索引
Innodb中的索引数据结构是 B+ 树,数据是有序排列的,从根节点到叶子节点一层层找到对应的数据。普通索引,也叫作辅助索引,叶子节点存放的是主键值。主键上的索引叫作汇集索引,表里的每一条记录都存放在主键的叶子节点上。当经过辅助索引select 查询数据的时候,会先在辅助索引中找到对应的主键值,而后用主键值在汇集索引中找到该条记录。举个例子,用name=Alice来查询的时候,会先找到对应的主键值是18 ,而后用18在下面的汇集索引中找到name=Alice的记录内容是 77 和 Alice。
表中每一行的数据,是组织存放在汇集索引中的,因此叫作索引组织表。
了解索引数据结构的目的是为了说明,行锁是加在索引上的。
1.select * from user where id=10 for update
一条简单的SQL。在user表中查找id为10的记录,并用for update加X锁。
这里User表中,有3个字段, 主键id 和 另一个字段name。下面的表格是B+树索引的简化表达。第一行id是索引的节点,第二行和第三行是这行记录,包含了姓名和性别。
如图所示,经过锁住汇集索引中的节点来锁住这条记录。
汇集索引上的锁,比较好理解,锁住id=10的索引,即锁住了这条记录。
2. select * from user where name=‘b’ for update
查询user表中name为d的记录,并用for update加X锁
这里的name上加了惟一索引,惟一索引本质上是辅助索引,加了惟一约束。因此会先在辅助索引上找到name为d的索引记录,在辅助索引中加锁,而后查找汇集索引,锁住对应索引记录。
为何聚簇索引上的记录也要加锁?试想一下,若是有并发的另一个SQL,是直接经过主键索引id=30来更新,会先在汇集索引中请求加锁。若是只在辅助索引中加锁的话,两个并发SQL之间是互相感知不到的。
3. select * from user where name=‘b’ for update
查询user表中name为b的记录,并用for update加X锁。这里name上加了普通的索引,不是惟一索引。普通索引的值是能够重复的。会先在辅助索引中找到name为b的两条记录,加X锁,而后获得主键值7和30,到汇集索引中加X锁。
事情并无那么简单,若是这时有另外一个事务,插入了name=b,id=40的记录,却发现是能够插入的。
位置在途中红色线条标注的间隙内,这样就会出现幻读,两次查询获得的结果是不一致的,第一次查到两条数据,插入以后获得三条数据。
为了防止这种状况,出现了另外一种锁,gap lcok 间隙锁。锁住的是索引的间隙。
即图中,红色线条标识的空隙。由于新插入name=b的记录,可能出如今这三个间隙内。
这张图里出现了三种锁
记录锁:单行记录上的锁
间隙锁:锁定记录之间的范围,但不包含记录自己。
Next Key Lock: 记录锁+ 间隙锁,锁定一个范围,包含记录自己。
4. 意向锁( Intention Locks )
InnoDB为了支持多粒度(表锁与行锁)的锁并存,引入意向锁。意向锁是表级锁,
IS: 意向共享锁
IX: 意向排他锁
事务在请求某一行的S锁和X锁前,须要先得到对应表的IS、IX锁。
意向锁产生的主要目的是为了处理行锁和表锁之间的冲突,用于代表“某个事务正在某一行上持有了锁,或者准备去持有锁”。好比,表中的某一行上加了X锁,就不能对这张表加X锁。
若是不在表上加意向锁,对表加锁的时候,都要去检查表中的某一行上是否加有行锁,多麻烦。
意向锁的兼容性矩阵
5. 插入意向锁(Insert Intention Lock)
Gap Lock中存在一种插入意向锁,在insert操做时产生。
有两个做用:
3、常见语句的加锁分析
后面会有多个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. 按索引类型
elect * 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锁了),不会阻止其余并发的事务往这条记录以前插入 。
7. 先查询后插入
相似于这样的insert
insert into target_table select * from source_table ... create target_table select * from source_table ...
将select查询的结果集,插入到另外一张表中,或者使用结果集,建立一个新表。
和以前简单插入的状况相似,已插入成功的数据加X锁,间隙加上一个插入意向锁。
对于select的源表中的记录,会加共享的 next-key 锁。这是为了防止主从同步出问题。
举个例子:
session1 先开启事务,而后查询user2表的结果集,插入到user表中,session2开启事务,在插入user2中插入数据,所插入的数据恰好是session1能查询到的数据,若是不加锁的话,session2能够插入成功,而后session2提交事务,接着session1提交数据。这样看起来是没问题的,可是session2先提交的,因此bin log中会这样记录,先在user2表中插入数据,而后在user中插入数据,这样的bin log在从库执行的时候,就会出问题。
主库: user2插入一条数据,user 插入一条数据
从库: user2插入一条数据,user 插入两条数据
user表会比主库多一条数据。因此须要锁住select查询表中加next-key锁,不容许user2表中新增数据。
4、分析当前锁的状况
先说一下死锁的定义,死锁是指两个或两个以上的事务在执行过程当中,因争夺资源而形成的一种互相等待的现象。这个定义适用于数据库,有几个重点,两个或两个以上的事务,一个事务是不会出现死锁的。争夺的资源通常都是表或者记录。
出现死锁了会怎样,正常状况下,mysql会检查出死锁,并回滚某一个事务,让另外一个事务正常运行。
Mysql 会回滚反作用小的事务,断定的标准是执行的时间以及影响的范围。
1.如何知道系统有没有发生过死锁,如何去查看发生过的锁
show status like ‘innodb_row_lock%'; 从系统启动到如今的数据
Innodb_row_lock_current_waits:当前正在等待锁的数量;
Innodb_row_lock_time :锁定的总时间长度,单位ms;
Innodb_row_lock_time_avg :每次等待所花平均时间;
Innodb_row_lock_time_max:从系统启动到如今等待最长的一次所花的时间;
Innodb_row_lock_waits :从系统启动到如今总共等待的次数。
平均时间和锁等待次数比较大的话,说明可能会存在锁争用状况
2. show engine innodb status
展现innodb存储引擎的运行状态
经过这个命令显示的内容比较多,其中有一项lasted detected deadlock 显示最近发生的死锁。
图中红色线条标注的是执行的SQL,以及加了什么锁,能够看出是在这行记录上加了X锁,没有gap锁。
3. 错误日志中查看历史发生过的死锁
set global innodb_print_all_deadlocks=1;
上一个命令,只能看到最近发生的锁,若是我想看历史发生的锁怎么办? 执行这一句,更改innodb 的一个配置,innodb_print_all_deadlocks,打印全部的死锁。会将死锁的信息输出到mysql的错误日志中,默认是不输出,格式和show engine innodb status 是差很少的。
4. 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值。
5. information_schema.innodb_lock_waits
查看等待中的锁
requesting_trx_id:申请锁资源的事务id。
requested_lock_id:申请的锁的id。
blocking_trx_id:阻塞的事务id,当前拥有锁的事务ID。
blocking_lock_id:阻塞的锁的id,当前拥有锁的锁ID
6. information_schema.innodb_trx
查看已开启的事务
trx_id:innodb存储引擎内部事务惟一的事务id。
trx_state:当前事务的状态。
trx_started:事务开始的时间。
trx_requested_lock_id:等待事务的锁id,如trx_state的状态为LOCK WAIT,那么该值表明当前事务以前占用锁资源的id,若是trx_state不是LOCK WAIT的话,这个值为null。
trx_wait_started:事务等待开始的时间。
trx_weight:事务的权重,反映了一个事务修改和锁住的行数。在innodb的存储引擎中,当发生死锁须要回滚时,innodb存储引擎会选择该值最小的事务进行回滚。
trx_mysql_thread_id:正在运行的mysql中的线程id,show full processlist显示的记录中的thread_id。
trx_query:事务运行的sql语句
5、预防死锁
1. 以相同的顺序更新不一样的表
这样执行的话,会出现锁等待,但不容易出现死锁。
假设有这么两个接口,增长老师和学生的幸运值、减小老师和学生的幸运值,这个需求是我造出来的,先别管需求是否是合理。
如今有两个请求,一个增长幸运值,一个下降幸运值,若是更新顺序不一样的话,就是这样,第一个事务先给老师加幸运值,第二个接口给学生减幸运值,而后第一个事务给学生加幸运值,由于锁已经被第二个事务持有了,因此第一个事务等待。而后第二个事务给老师幸运值,这时就互相等待锁,出现了死锁。
2. 预先对数据进行排序
好比一个接口批量操做数据,若是乱序的话,并发的状况下,也是有可能出现死锁的。给学生批量加分的接口,按照表格中的执行顺序的话,第一个事务,持有A的锁,请求B的锁,第二个事务持有B的锁,请求A的锁,出现死锁。
3. 直接申请足够级别的锁,而非先共享锁,再申请排他锁。
好比这种状况,两个事务,先申请共享锁,共享锁是兼容的,而后申请互斥锁的时候,须要互相等待,就出现了死锁。
4. 事务的粒度及时间尽可能保持小,这样锁冲突的几率就小了,也就不容易出现死锁。不建议在数据库的事务中执行API调用。
5. 正确加索引。没有索引会引发全表扫描,相似于锁表。
六:总结:
1,正确的加索引,尽可能先查询,而后使用主键去加锁,等于操做来加锁,而尽可能避免辅助索引,或者不是范围比较来加锁。
2,出现了锁的问题,根据数据库已有的信息,分析死锁。
3,举了几个例子,可能不少都是上线以后才发现的,最好能在开发阶段就避免死锁。