目录
一. MySQL 锁基础
- MySQL 使用InnoDB存储引擎时,默认使用可重复读隔离级别,基于可重复读隔离级别使用MVCC解决了脏读,不可重复读,一定程度上解决了幻读问题, 那么真正是如何解决幻读的,或者说怎么保证数据安全的,引出了锁
- 或者说在快照读时基于MVCC可以读取undo版本日志中的历史版本,但是一些操作需要我们读取最新的版本,所以提出了锁
- 在不加锁执行读取操作时使用MVCC又称为快照度,一致性读
- 加锁读取时称为LBCC锁定读或当前读,LBCC(Lock-Based Concurreny Control)基于锁的并发控制
- MySQL不同存储引擎使用的锁不同
- MyISAM时采用的表锁,表锁下又细分为读锁,写锁
- InnoDB时根据是否命中索引添加的是行锁间隙锁,如果没命中索引则使用表锁
Mysql 锁分类
- 在操作角度分为: 读锁,写锁
读锁: 又叫共享锁,针对同一份数据,多个读操作可以同时进行互不影响
写锁: 又叫排它锁,在当前操作没完成之前,会阻断其它的读锁和写锁
- 在锁粒度角度分为:表锁,行锁,页锁(简单理解即可)
表锁(偏读锁,是不是可以理解为使用表锁是通常使用的是表锁中的读锁): 偏向MyISAM存储引擎,优点是:开销小, 加锁快, 无死锁, 锁粒度大,发生锁冲突竞争的概率高,并发度低(整个表都锁的原因)
行锁(偏写锁): 偏向InnoDB存储引擎,开销大,加锁慢,会出现死锁现象,锁粒度小,发生死锁概率低,并发度搞
- 在锁模式上又分为:
- 共享锁 Shared Locks
- 排它锁 Exclusive Locks
- 意向锁 Intention Locks: 意向锁又分为,意向共享锁Intention Shared Locks,意向排它锁 Intention Exclusive Locks
- 在锁算法上又分为
- 记录锁 Record Locks
- 间隙锁 Gap Locks
- 临键锁 Next Key Locks
二. InnoDB 下的锁
- 首先在使用InnoDB存储引擎时,如果对锁进行细分,内部包含
- 共享锁又叫乐观锁
- 互斥锁又叫排它锁
- 意向锁
- 记录锁
- 间隙锁
- 记录锁+间隙锁的临键锁Next-key Locks
- 插入意向锁
- 自增锁
- 空间索引预测锁
增删改查操作时底层的加锁处理
- InnoDB对于普通select查询不加锁,基于MVCC机制解决脏读,不可重复读,一定程度上解决幻读问题
- 手动针对select语句加锁示例:
共享锁(S):SELECT * FROM table_name WHERE … LOCK IN SHARE MODE
排他锁(X) :SELECT * FROM table_name WHERE … FOR UPDATE
- 对于UPDATE,DELETE和INSERT操作时,不考虑索引是否生效的情况下,可以概括的说为会给涉及到的数据添加X排它锁,X排它锁,因为索引失效时会锁表,并且直接执行insert操作,或者底层执行insert操作时特殊情况下会添加隐式锁,下面有分析
- 在delete操作时会添加X排它锁
- insert操作时一般是不加锁的,特殊情况下为了保证事物的隔离性会添加隐式锁(下面会讲解)
- update操作时要考虑更新前后数据的大小与是否更新主键等情况
- 如果更新前后数据大小相同,在mysql存储时可以理解为原地更新,此时添加的是X排它锁
- 如果更新后的数据大于更新前的,mysql底层会先删除就记录(delete_flag标记为删除状态),然后插入一条新记录,删除阶段添加的是X排它锁,插入新记录时跟insert操作相同使用隐式锁
- 更新主键时可以理解为组合了delete操作与insert操作,delete操作添加X排它锁,insert操作添加隐式锁
表级锁
1. 意向锁
- 首先意向锁IntentionLock,又分为意向共享锁IS, 与意向排它锁IX 是表锁
- 我理解的意向锁,在上层使用角度看并不是真实加锁,而是在表级加了一个锁标识,是为了提高加锁效率提出的意向锁
- 在没有意向锁时,假设事务A想获取某个表的表锁的时,就需要对该表下的每一行记录进行遍历,查看是否有其它事务进行了锁的获取,如果存在排它锁,则需要等待这些锁释放后才能获取,遍历成本较大
- 提出意向锁后,mysql底层在加锁时,如果加排它锁,会在表级别上添加一个意向排它锁,如果加共享锁首先会在表级别上添加意向共享锁,当下一个事物需要获取锁时,通过添加的意向锁标识就可以判断能不能获取到锁,不需要遍历判断
- 并且在添加意向锁时,当发现获取的资源已经被锁,需要判断当前加的是共享锁还是排它锁,被锁定的使用的是共享锁还是排它锁,如果两个都是共享锁,可以在表上面再次添加一个意向共享锁,意向共享锁可以同时并存多个,意向排他锁同时只能有一个存在
- InnoDB四种锁定模式下:共享锁S, 排他锁X, 意向共享锁IS, 意向排他锁IX之间的冲突关系
- 注意意向锁不能手动添加是InnoDB自动加的
- 怎么解释意向锁的表锁行锁共存: 根据2,3,意向锁相当于一个锁标识,结合锁冲突,去解释(有些博客中说意向锁就是为了解决表锁行锁共存所产生的)
2. AUTO-INC锁
- 给某个列加入自增长属性,例如自增主键,在并发操作时这个自增是怎么保证线程安全的,就是通过这个AUTO-INC锁加锁保证的,这个AUTO-INC也算是一个表级锁
- 在老版本时以自增主键为例,在插入数据时会在表级别上添加AUTO-INC锁,获取自增主键数据插入成功后再释放锁,可以认为锁定的范围是整个插入流程,进而保证自增主键的安全
- 在后续版本中考虑AUTO-INC在加锁与释放锁时的性能问题,进行了优化提出了轻量级锁,通过innodb_autoinc_lock_mode来设置,提供了3个模式(默认是consecutive 1)
- traditional: 设置为 0时,在该模式下,insert语句在开始时都会获得一个表锁autoinc_lock.该锁会一直持有到insert语句执行结束才会被释放,主从复制时,基于这种模式从库主库生成的主键id相同且有序
- consecutive: 设置为1时, 该模式下,对于简单的插入语句如果能够获取到插入数据条数,insert语句在开始时会获得一个表锁autoinc_lock,批量获取自增值,然后释放锁,不会等到数据插入完毕再释放,而对于复杂插入例如无法得到插入数据的条数,仍会加锁到数据插入完毕再释放,这种模式下获取的自增值是有序的,主从复制表现跟traditional下一样
- interleaved: 设置为2时, 该模式下,会评估的去批量获取自增值,对于能够判断出插入数据量的简单insert语句能保证ID是连续的,但是复杂的insert的ID则可能有断续出现,主从同步时,从库基于该模式可能造成id与主库不同切断续
id不连续对主从同步的影响
- mysql主从同步架构中,有两种同步模式:
- statement level基于SQL语句的复制
- row level基于一条记录的复制
- statement level与 row level 优缺点: statement模式下通过执行sql语句进行赋值,可以减少Binlog日志量,节约了IO成本,而row模式下是复制数据变更的内容,基于行的复制可能会产生大量的日志内容,IO成本比较高
- id不连续造成的问题: 如果我们主从同步中使用statement 基于sql语句进行复制,并且插入数据时的自增id是强制批量获取,当插入数据量不能预知,底层通过msyql自己预判生成批量获取id,是不可控的,就可能造成主库执行sql获取到id为1,3,5,当sql复制给从库,从库执行预判拿到了2,4,6的id,最终造成id不一致
3. 其它表锁
- 手动添加表锁
#读锁(共享锁S)
Lock table t Read;
#写锁(排它锁X)
Lock table t Write;
- 元数据锁: 在执行DDL语句对表结构,索引等等信息进行修改时,底层会添加一个元数据锁(Metadata Locks 简称MDL )是Server级别的锁,这也是表锁,用来保证读写的正确性
行锁分析
- 行锁在锁模式上分为共享锁与排它锁两种
- 共享锁又称读锁,当一个事务对某几行上读锁时,允许其他事务对这几行进行读操作,但不允许其进行写操作,也不允许其他事务给这几行上排它锁,但允许上读锁。
- 排它锁又称写锁,当一个事务对某几个上写锁时,不允许其他事务写,但允许读,更不允许其他事务给这几行上任何锁,包括写锁
- 在锁粒度上又分为: 记录锁、间隙锁、临键锁、插入意向锁
- 行锁的优缺点
- 优点: 锁定粒度小,发送锁冲突概率低,可以实现的并发度高
- 缺点: 锁的开销比较大,加锁会比较慢,容易出现死锁情况
- 使用InnoDB存储引擎创建 test_innoDB表,该表中有a,b两个字段,并分别对这两个字段添加索引,插入数据
- 学习测试中,关闭数据库自动提交
set autocommit=0;
- 行锁必须有索引才能实现,否则会自动锁全表,会自动降级为表锁,两个事务不能锁同一个索引
#事务A先执行:
select math from zje where math>60 for update;
#事务B再执行:
select math from zje where math<60 for update;
#这样的话,事务B是会阻塞的。如果事务B把 math索引换成其他索引就不会阻塞,
#但注意,换成其他索引锁住的行不能和math索引锁住的行有重复。
1. 记录锁 Record Locks
- 记录锁是行锁,仅锁定一行,官方称为LOCK_REC_NOT_GAP, 记录锁内部也分为共享记录锁S, 排它记录锁X
- 记录锁锁定的是主键索引,如果没有主键会用唯一索引锁定,如果也没有会使用rowid
2. 间隙锁 Gap Locks
- MySQL在可重复读的隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用MVCC,也可以采用加锁方案解决,用来解决幻读的锁就是间隙锁,因为在执行查询时可能出现幻读的位置上还没有数据,只能针对一定的范围添加间隙锁
- 间隙锁的产生条件
- 必须在RR可重复读隔离级别下
- 检索条件必须有索引(主键索引,或一个唯一索引除外),因为条件更新时若没有索引,行锁会升级为表锁,锁定整张表,包括不存在的所有记录,此时其他事务不能修改删除添加等操作)
- 间隙锁锁定的范围: 会根据检索条件向左寻找最靠近检索条件的记录值A,作为左区间,向右寻找最靠近检索条件的记录值B作为右区间,即锁定的间隙为
- 如下图以执行SQL :“update table set table.id=100 where number=5”为例解释,假设number上有索引, 此时间隙锁的范围向左4,向右11, 如果number=11,此时间隙锁的锁定范围向左5,向右无穷大,假设number=13,表中没有13这条数据,但是也存在间隙锁,向左11,向右无穷大,在锁定范围内的再去操作会阻塞等待
- 通过范围更新再次举例间隙锁 假设当前表中存在id为1,4,5,6记录,假设当前执行update,where的条件是id大于1并且id小于6的记录,这个时候,虽然中间没有id为2,3的记录,但是也会给上间隙锁,如果此时第二个请求过来查询id为2,或3的记录,就会阻塞等待(实际这块应该说是临建锁)
- 间隙锁的危害: 当锁定一个范围时,即使范围中间的某些键值不存在也会被加锁,这样就造成无法插入锁定键值范围内的任何数据,造成阻塞
- 问题: 执行更新操作,where条件中命中普通索引,在更新成功前有间隙锁吗,有,于命中普通索引,数据是根据普通索引进行排序加锁,如果在更新成功前执行插入操作,插入数据中对应普通索引字段值刚好是间隙锁的范围,则会阻塞等待
3. 临键锁 Next-Key Locks
- 临键锁是记录锁与间隙锁的组合体,实现了锁住指定记录 ,并且阻止其它事务在该记录前边的间隙插入新记录
- 临键锁针对 REPEATABLE READ 隔离级别下
- 以执行范围update更新操作为例,在执行更新时,InnoDB首先会给符合条件的已有数据的索引项添加记录锁,同时会锁定一个范围就叫做间隙,会针对这个间隙加锁,假设当前表中存在id为1,4,5,6记录,假设当前执行update,where的条件是id大于1并且id小于6的记录,这个时候,虽然中间没有id为2,3的记录,但是也会给上间隙锁,如果此时第二个请求过来查询id为2,或3的记录,就会阻塞等待
4. 插入意向锁
- 这里可以问为什么rr级别下next key锁能避免幻锁的原因
- 在执行插入操作时,会先检查当前插入的记录对应到落库的索引上是否存在锁对象(判断是否有间隙锁)如果锁住了,如果有,当前插入操作进行等待,并生成一个"插入意向锁"结构,插入意向锁也就是配合间隙锁或者临键锁一起防止幻读
- 我是这样理解插入意向锁的,一个锁结构,锁标识,表示一种意向,可以同时存在多个插入意向锁,多个插入操作打到同一个间隙锁上进行等待时都会生成插入意向锁,多个插入意向锁按顺序等待执行
5. 隐式锁
- 此处也可以问:insert操作会加锁吗?, 默认情况下是不加锁的,也可以说成会加一个隐式锁
- 在执行插入时,默认会给添加一个隐式锁,此时时无锁状态,当其它事物执行通过trx_id判断当前记录的事物还在活跃中,并且要获取该记录的X排它锁或者S共享锁时,这个隐式锁会升级为显示的X排它锁,只有当可能会产生冲突的时候才会加锁,减少锁的数量,提高系统的性能
- 隐式锁应该与插入意向锁一块来说:
- 隐式锁: INSERT 语句在执行插入之前,如果判断被插入数据落库的索引上有间隙锁,会先在这个间隙上插入意向锁,并且如果是唯一索引会进行重复 Key 判断,如果唯一索引值重复,会加排它锁与共享锁,(因为重复唯一索引后续可能会回滚删除,防止发生死锁)然后等待,获取到锁后进行插入
- 数据插入时会插入隐式锁, 在该插入事物未提交完毕时,其它事物要针对这条记录申请加锁时,这个隐式锁会升级为显示的X排它锁,有点像synchronized偏向锁到轻量级锁的过程
6. 加锁算法
- MySQL是针对索引进行加锁的,遵守以下加锁算法
- 主键索引: 如果我们加锁的行上存在主键索引, 会在这个主键索引上加 Record Lock 记录锁
- 辅助索引: 如果我们加锁的行上存在辅助索引, 会在这行的辅助索引上添加 Next-Key Lock,并在这行之后的辅助索引上添加一个 Gap Lock, 辅助索引上的 Next-Key Lock 和 Gap Lock 都是针对可重复读隔离模式存在的,这两种锁都是为了防止幻读现象的发生。
- 唯一的辅助索引: 也就是辅助索引是唯一索引时,MySQL 会将 Next-Key Lock 降级为 Record Lock,只会锁定当前记录。
- 如果唯一索引由多个列组成的,而我们只锁定其中一个列的话,那么此时并不会进行锁降级,还会添加 Next-KeyLock 和 GapLock。
- Insert 语句: 在 InnoDB 存储引擎中,对于 Insert 的操作,其会检查插入记录的下一条记录是否被锁定,若已经被锁定,则不允许查询。
- 未添加主键问题: 如果 InnoDB 表在创建的时候没有设置任何主键索引,那么 Record Lock 会锁住隐式的主键。
- 在加锁时,一条记录一个锁吗,不是, 有锁共享的条件,同一个事物,同一个数据页,同一个锁类型共享一把锁,否则会生成新锁
InnoDB 行锁变降级为表锁
- InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据才使用行级锁,否则会降级为表锁,所以在执行增删改操作时要注意是否能命中索引,有没有索引失效的问题
- 当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,不论是使用主键索引,唯一索引还是普通索引,InnoDB都会通过行锁来对数据加锁
- 进而引出一个问题: 由于MySQL的行锁不是针对记录加的锁,而是针对索引加的锁,所以即使是访问不同行的记录,如果使用了相同的索引键,也会出现锁冲突问题
- 还有一个问题索引是否生效是由MySQL通过执行优化器来觉得的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此在分析锁冲突时,别忘了检查SQL的执行计划,以确认是否真正使用了索引
锁冲突
- 什么是锁冲突: 假如对某一行数据加锁未释放,此时第二个请求需要操作这个被锁定的数据,第二个请求会阻塞等待前面的提交释放锁后才可以执行,这个阻塞等待,我们称为锁冲突
- 考虑锁冲突要了解InnoDB锁实现原理,是通过索引实现的,假如当前执行sql没有索引,则行锁变为表锁,表锁下如果第一个请求未释放,第二个请求无法更新该表数据会一直阻塞等待
- 如果一条执行更新的sql用到了索引,会根据该索引值锁定,如果此时第二个操作数据请求过来,有两种情况如果刚好用到被锁定的数据,会阻塞等待,如果用不到会正常执行
- 如果一条执行更新的sql用到了索引,并且是范围查询,如果此时第二个操作数据请求过来刚好命中这个范围内的数据(假设实际数据是1,4,5,范围是大于1小于等于5,就是第二个请求操作的是2,3也会阻塞,因为有间隙锁)
如何分析行锁定
- 执行"show status like ‘innodb_row-lock%’ " 查看是否有行锁,重点关注(Innodb_row_lock_time_avg等待的平均时长,Innodb_row_lock_waits等待的总次数,Innodb_row_lock_time等待总时长,如果等等次数很高,并且等待时长也不小的时候,就需要使用show proFile进行分析优化)
Innodb_row_lock_current_waits: 当前正在等待锁定的数量
Innodb_row_lock_time: 从系统启动到现在锁定总时间长度
Innodb_row_lock_time_avg: 每次等待所花费平均时间
Innodb_row_lock_time_max: 从系统启动到现在等待最长一次所花费时间
Innodb_row_lock_waits: 系统启动后到现在总共等待次数
- 还有默认情况下,看不到事物对哪些数据加了锁,需要通过"innodb_status_output_locks" 设置为"ON"(很少使用)
- 然后执行"show engine innodb status\G " 查看状态时,就可以获取到加锁的详细信息(可以获取到锁的哪个数据页, 锁的哪个索引列,锁类型等等信息)(很少使用)
三. MyISAM与表锁
- 使用MyISAM 存储引擎创建mylock表并添加数据
- 使用InnoDB存储引擎,创建book表(此处不加示例了)
- 加锁命令,read表示读锁, write表示写锁
#对一张表加锁
LOCK TABLE 表名 read/write;
#一次对多张表加锁
LOCK TABLE 表名 read/write, 表2名 read/write;
4. 查看库中表是否被加锁命令
show open tables;
5. 释放锁命令
#执行完该命令后原则上当前库中的所有锁都会被释放
unlock tables;
1. 读锁
- 解释: 启动两个session会话对使用MyISAM存储引擎的mylock表添加读锁,查看当前session下,锁释放前能否再次读,能否修改数据,能否读取其它表数据,查看另外一个session会话能否读当前被加锁的数据,能否读取其它表数据,能发修改当前被加锁的数据,
- 当前加锁session会话执行示例:加锁后,发现,可以再次读取当前表数据,但是不能读取其它表数据,并且不能对当前加锁表进行增删改操作
- 其它session会话可以读取加读锁表的数据,可以读取其它不加锁的表数据,但是如果其它session会话对加锁表进行增删改操作会阻塞等待
2. 写锁
- 解释: 启动两个session会话对使用MyISAM存储引擎的mylock表添加写锁,
- 加写锁后,当前session会话下,可以再次读取当前表数据,可以更新当前表数据,但是无法更新,读取其它表数据
- 加锁后其它session会话,可以读取其操作其它表数据,但是读取与更新被加锁的表数据,会一直阻塞
3. 如何分析表锁定
- 执行" show status like ‘table%’ " 命令,通过table_locaks_waited 和table_locks_immediate状态变量来分析系统上的表锁定
Table_locks_immediate: 产生表级锁定的次数,表示可以立即获取锁的查询此处,每理解获取锁值加1
Table_locks_waited: 出现表级锁定,争用而发生的等待次数(不能立即获取锁的次数,每等待一次锁值加1),次值越高说明存在较严重的表级锁竞争情况
4. 总结
- MyISAM在执行查询语句(SELECT)前,会自动给所涉及表添加读锁,在执行增删改操作前会自动给涉及表添加写锁
- MySql表级锁有两种模式: 表共享读锁, 表独占写锁
- 根据上面的示例总结出
- MyISAM 执行查询操作时,会自动添加读锁,不会阻塞其它进程对当前表的读操作,会阻塞其它进程对当前表的更新操作,并且当前加锁进程无法再次读取当前表与其它表操作,也无法对当前表与其它表进行更新操作
- MySAM 执行增删改操作时会自动对该表添加写锁,写锁下,会阻塞其它进程对当前表的任何操作,但是当前进程是可以再次读取当前表的数据(无法读取其它表数据)
- 简单来说: 读写会阻塞写不会阻塞读,写锁时读与写都会阻塞
- MyISAM的读写锁调度是"写优先",不适合做以写为主的表的引擎,因为写锁后,其它线程不可以对该表进行任何操作,大量的更新操作可能会造成查询请求无法获取到锁,一直阻塞等待
四. 死锁排查
- 产生死锁的四个必要条件:(跟jdk中的死锁产生原因一样)
- 互斥: 首先时互斥锁
- 请求和保持: A先获取到锁一致持有,并请求获取B的锁
- 不剥夺: 进程已经持有的资源在未使用完毕时不会强制释放
- 环路等待: 线程1持有a锁等待获取线程2持有的b锁, 线程2持有b锁等待获取线程1持有a锁,形成环
- InnoDB发现死锁之后,会计算出两个发生死锁的事务各自操作的数据量(增删改)来判定两个事务的大小,数据较小的事务回滚,较大的事务成功完成.也就是说改变的记录条数越多在死锁中就越不会被回滚掉
- 但是需要注意,当产生死锁的场景中涉及到不止InnoDB存储引擎,InnoDB是没办法检测到该死锁的,这时候就只能通过锁定超时限制参数InnoDB_lock_wait_timeout来解决。这个参数并不是只用来解决死锁问题,在并发访问比较高的情况下,如果大量事务因无法立即获得所需的锁而挂起,会占用大量计算机资源,造成严重性能问题,甚至拖跨数据库。我们通过设置合适的锁等待超时阈值,可以避免这种情况发生。
- 通常来说,死锁都是应用设计的问题,通过调整业务流程、数据库对象设计、事务大小,以及访问数据库的SQL语句,绝大部分死锁都可以避免。下面就通过实例来介绍几种避免死锁的常用方法:
- 执行"SHOW ENGINE INNODB STATUS;"来查看死锁日志,显示
------------------------
LATEST DETECTED DEADLOCK
------------------------
170219 13:31:31
##1.TRANSACTION: 表示当前执行中的事物
*** (1) TRANSACTION:
TRANSACTION 2A8BD, ACTIVE 11 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 376, 1 row lock(s)
##2.关注点: root updating后是实际执行的sql
MySQL thread id 448218, OS thread handle 0x2abe5fb5d700, query id 18923238 renjun.fangcloud.net 121.41.41.92 root updating
delete from test where a = 2
##3.WAITING FOR THIS LOCK TO BE GRANTED: 等待这个锁被授予(也就是当前执行的sql正在等待的锁)
##index `a` of table `oauthdemo`.`test` trx id 2A8BD lock_mode X waiting Record loc : 表示在等待oauthdemo库下的test表中的索引列为a,的x锁
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 923 n bits 80 index `a` of table `oauthdemo`.`test` trx id 2A8BD lock_mode X waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 4; hex 00000002; asc ;;
1: len 4; hex 00000002; asc ;;
##4.表示当前第二个执行中的事物
*** (2) TRANSACTION:
TRANSACTION 2A8BC, ACTIVE 18 sec inserting
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1248, 3 row lock(s), undo log entries 2
MySQL thread id 448217, OS thread handle 0x2abe5fd65700, query id 18923239 renjun.fangcloud.net 121.41.41.92 root update
##5.当前实际执行的sql
insert into test (id,a) values (10,2)
##6. HOLDS THE LOCK(S):记录了事物2中持有的锁(index `a` of table `oauthdemo`.`test` 表示oauthdemo库中的tst表的a索引的锁) Record lock 记录锁
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 0 page no 923 n bits 80 index `a` of table `oauthdemo`.`test` trx id 2A8BC lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 4; hex 00000002; asc ;;
1: len 4; hex 00000002; asc ;;
##7.(2) WAITING FOR THIS LOCK TO BE GRANTED: 事物2中在申请的锁(index `a` of table `oauthdemo`.`test` trx id 2A8BC lock mode S waiting Record lock 表示事物2持有a列的锁,正在申请s锁"lock mode S waiting")
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 923 n bits 80 index `a` of table `oauthdemo`.`test` trx id 2A8BC lock mode S waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 32
0: len 4; hex 00000002; asc ;;
1: len 4; hex 00000002; asc ;;
*** WE ROLL BACK TRANSACTION (1)
- 死锁日志解释: 在日志中,每个动作都会进行分块记录,每个块中包含"TRANSACTION", “WAITING FOR THIS LOCK TO BE GRANTED”, " HOLDS THE LOCK(S)"大概这三个部分,通过前缀的序号来区分,序号相同的代表是同一个完整操作记录
#1. " *** (1) TRANSACTION: ": 可以看为第一次事物执行,里面包含了当前事物执行的sql语句,操作的库表信息
#2. " *** (1) WAITING FOR THIS LOCK TO BE GRANTED: ": 等待这个锁被授予(当前事物正在等待的锁),记录了等待的哪个库表中的锁,锁类型,
#3. " *** (2) HOLDS THE LOCK(S): ": 当前事物持有的锁,锁在哪个库表,锁的类型等前面的"(2)"对应第二个事物操作
- 根据上方的死锁日志分析为什么出现死锁:
- 通过 “(1) WAITING FOR THIS LOCK TO BE GRANTED” 分析到事物1中正在等待oauthdemo库下的test表中的索引列为a,的x锁
- 通过 “(2) HOLDS THE LOCK(S):” 分析到事物2正在持有oauthdemo库下的test表中的索引列为a锁,也就是事物1正在等待的锁
- 通过 “(2) WAITING FOR THIS LOCK TO BE GRANTED” 分析到事务2正在申请S锁,也就是共享锁,
- 重点: 为什么事物2要申请s锁,而不是x锁, s锁该锁是insert into test (id,a) values (10,2)语句申请的。insert语句在普通情况下是会申请排他锁,也就是X锁,但是这里出现了S锁。这是因为a字段是一个唯一索引,所以insert语句会在插入前进行一次duplicate key的检查,为了使这次检查成功,需要申请S锁防止其他事务对a字段进行修改。
- 重点: 为什么出现死锁,是因为申请s锁失败,为什么失败: 对同一个字段的锁的申请是需要排队的。S锁前面还有一个未申请成功的X锁,所以S锁必须等待
- 所以事物1在等待a的x锁,事物2持有a的x锁,在等待S锁,事物2的s锁获取前需要先拿到一个x锁,但由于x锁需要排队,形成了循环等待,死锁出现了
推荐优化
- 在检索查询过滤数据时尽可能使用索引完成,避免无索引行锁降级为表锁
- 合理设计索引,InnoDB通过索引加锁,尽量缩小锁的范围
- 尽可能少的过滤条件,避免间隙锁
- 尽量控制事物大小,减少锁定的资源量,时间长度
- 保证业务功能不受影响的前提下尽可能的降低事物隔离级别