邹平建设局网站,wordpress管理员登陆,常州培训做网站,网页制作程序书背景
最近双十一开门红期间组内出现了一次因 Mysql 死锁导致的线上问题#xff0c;当时从监控可以看到数据库活跃连接数飙升#xff0c;导致应用层数据库连接池被打满#xff0c;后续所有请求都因获取不到连接而失败
整体业务代码精简逻辑如下#xff1a;
Transaction
p…背景
最近双十一开门红期间组内出现了一次因 Mysql 死锁导致的线上问题当时从监控可以看到数据库活跃连接数飙升导致应用层数据库连接池被打满后续所有请求都因获取不到连接而失败
整体业务代码精简逻辑如下
Transaction
public void service(Integer id) {delete(id);insert(id);
}
数据库实例监控 当时通过分析上游问题流量限流解决后后续找时间又重新分析了下问题发生的根本原因现将其总结如下本篇文章会先对 Mysql 中的各种锁进行分析包括互斥锁、间隙锁和插入意向锁让大家对各种锁的使用场景有一个了解然后在此基础上再对本问题进行分析希望大家未来再碰到相似场景时能够快速的定位问题
Mysql 锁机制
在 Mysql 中为了解决对同一行记录并发写的问题引入了行锁机制多个事务不能同时对一行数据进行修改操作当需要对数据库中的一行数据进行修改时会首先判断该行数据是否加锁如果没加锁那么当前事务加锁成功可以进行后续的修改操作但如果该行数据已经被其他事务加锁则当前事务只有等待加锁的事务释放锁后才能加锁成功继续执行修改操作
本篇文章中所有实验用到的建表语句
create table test (id int(11) NOT NULL,num int(11) NOT NULL,PRIMARY KEY (id),KEY num (num)
) ENGINE InnoDB;insert intotest
values
(10, 10),
(20, 20),
(30, 30),
(40, 40),
(50, 50);
Shared and Exclusive Locks
shared(S) lock 表示共享锁当一个事务持有某行上的 S 锁后可以对该行的数据进行读操作通过语句 select ... from test lock in share mode 可以添加共享锁一般使用的较少不做过多阐述
exclusive(X) lock 表示互斥锁当一个事务对某行数据进行 update 或 delete 操作时都要先获取到该记录上的 X 锁如果已经有其他事务获取到了该记录上的 X 锁那么当前事务会阻塞等待直到上一事务释放了对应记录上的 X 锁
S 锁之间不互斥多个事务可以同时获取一条记录上的 S 锁 X 锁之间互斥多个事务不能同时获取同一条记录上的 X 锁 S 锁和 X 锁之间互斥多个事务不能同时获取同一条记录上的 S 锁和 X 锁 当多个事务同时去 update 索引上同一条记录时都需要先获取到该记录上的 X 锁所谓的锁也就是会在内存中生成一个数据结构来记录当前的事务信息、锁类型和是否等待等信息。下图中就是 T1 和 T2 同时去更新 id 30 的这行记录并且 T1 成功获取到了锁其在内存中生成的锁结构信息中字段 is_wating 为 false可以继续执行事务的后续逻辑而 T2 获取锁失败则生成的锁结构信息字段 is_wating 为 true阻塞等待 T1 上的锁释放 互斥锁在 Mysql 日志中的锁信息为lock_mode X locks rec but not gap
RECORD LOCKS space id 58 page no 3 n bits 72 index PRIMARY of table test.t
trx id 10078 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 00: len 4; hex 8000000a; asc ;;1: len 6; hex 00000000274f; asc O;;2: len 7; hex b60000019d0110; asc ;;
Gap Locks
上一小节中介绍了 Exclusive Locks该锁可以避免多个事务同时对一行记录进行更新操作但不能解决幻读的问题所谓的幻读就是指一个事务在前后两次查询同一个范围时后一次查询到了前一次没有的记录
session Asession BT1select num from test where num 10 and num 15 for update; (0 rows)T2insert into test values(12, 12);T3select num from test where num 10 and num 15 for update; (1 rows)
在上面这个场景中session A 分别在 T1、T3 时刻进行了两次范围查询session B 在 T2 时刻插入了一条该范围内的数据如果 session A 能在 T3 时刻查询出 session B 插入的数据就说明发生了幻读。此时只使用互斥锁是无法解决幻读的因为 num 12 的记录在数据库中还不存在不能给其加上互斥锁来防止 T2 时刻 session B 的插入
因此为了解决幻读问题只有引入新的锁机制也就是间隙锁(Gap Locks)。间隙锁和互斥锁不同互斥锁是行锁只会锁定一行特定的记录而间隙锁则是锁定两行记录之间的空隙防止其他事务在此间隙中插入新的记录
引入了间隙锁之后session A 在 T1 时刻会给 id 20 记录生成一个 Gap Locks之后 session B 在 T2 时刻想要插入记录时需要先判断待插入位置的后一条记录上是否存在 Gap Locks很明显此时 id 20 的记录上已经存在了 Gap Locks那么session B 就需要在 id 20 的记录上生成一个插入意向锁并进入锁等待 间隙锁在 Mysql 中的锁日志信息如下lock_mode X locks gap before rec
RECORD LOCKS space id 133 page no 3 n bits 80 index PRIMARY of table test.test trx id 38849 lock_mode X locks gap before rec
Record lock, heap no 4 PHYSICAL RECORD: n_fields 4; compact format; info bits 00: len 4; hex 8000001e; asc 30 ;;1: len 6; hex 00000000969c; asc ;;2: len 7; hex a60000011a0128; asc (;;3: len 4; hex 8000001e; asc ;;
间隙锁虽然解决了幻读问题但因每次都会锁住一段间隙大大降低了数据库整体的并发度且因间隙锁和间隙锁之间不互斥不同事务可以同时对同一间隙加上 Gap Locks这也往往是各种死锁产生的源头
Next-Key Locks
Next-Key Locks 是 (Shard/Exclusive Locks Gap Locks) 的结合当 session A 给某行记录 R 添加了互斥型的 Next-Key Locks 后 相当于拥有了记录 R 的 X 锁和记录 R 的 Gap Locks
在上面 Gap Locks 的例子中事务 1 加的就是 Next-Key Locks即同时给 id 20 的记录加了 X 锁和 Gap 锁 在可重复读隔离级别下update 和 delete 操作默认都会给记录添加 Next-Key LocksMysql 中 Next-Key Locks 的锁日志信息为lock_mode X
RECORD LOCKS space id 58 page no 3 n bits 72 index PRIMARY of table test.t
trx id 10080 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 00: len 8; hex 73757072656d756d; asc supremum;;Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 00: len 4; hex 8000000a; asc ;;1: len 6; hex 00000000274f; asc O;;2: len 7; hex b60000019d0110; asc ;
Insert Intention Locks
插入意向锁(Insert Intention Locks) 也是一种间隙锁由 INSERT 操作在行数据插入之前获取
在插入一条记录前需要先定位到该记录在 B 树中的存储位置然后判断待插入位置的下一条记录上是否添加了 Gap Locks如果下一条记录上存在 Gap Locks那么插入操作就需要阻塞等待直到拥有 Gap Locks 的那个事务提交同时执行插入操作等待的事务也会在内存中生成一个锁结构表明有事务想在某个间隙中插入新记录但目前处于阻塞状态生成的锁结构就是插入意向锁
实验模拟如下
session 1session 2session 3T1begin;T2select * from test where id 25 for update;T3insert into test values(26, 26); (blocked)T4insert into test values(26, 26); (blocked)
对于语句 select * from test where id 25 for update 因当前表中不存在该记录在可重复读隔离级别下为了避免幻读会给 (20, 30] 间隙加上 Gap Locks
从锁日志可以看出 session 1 给记录 30 添加了间隙锁(lock_mode X locks gap before rec)
RECORD LOCKS space id 133 page no 3 n bits 80 index PRIMARY of table test.test trx id 38849 lock_mode X locks gap before rec
Record lock, heap no 4 PHYSICAL RECORD: n_fields 4; compact format; info bits 00: len 4; hex 8000001e; asc 30 ;;1: len 6; hex 00000000969c; asc ;;2: len 7; hex a60000011a0128; asc (;;3: len 4; hex 8000001e; asc ;;
当 session 2 插入记录 26 时会在 B 树中先定位到待插入位置再判断插入位置的间隙是否存在 Gap Locks也就是判断待插入位置的后一记录 id 30 是否存在 Gap Locks如果存在需要在该记录上生成插入意向锁等待
RECORD LOCKS space id 133 page no 3 n bits 80 index PRIMARY of table test.test trx id 38850 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 4; compact format; info bits 00: len 4; hex 8000001e; asc 30 ;;1: len 6; hex 00000000969c; asc ;;2: len 7; hex a60000011a0128; asc (;;3: len 4; hex 8000001e; asc ;;
此时 session 2 和 session 3 都在 id 30 的记录上添加了插入意向锁等待 session 1 上的 Gap Locks 释放生成的锁记录如下 线上问题分析
在对 Mysql 中的各种锁结构有了一个清晰的了解之后回过头来再看看前面的线上问题
Transaction
public void service(Integer id) {delete(id);insert(id);
}
对于上面的业务代码可能存在下面两种情况:
•传入的参数 id 在原数据库中不存在
•传入的参数 id 在原数据库中存在
本次主要会针对 id 记录在原数据库中不存在进行分析
session 1session 2session 3T1delete from test where id 15;T2delete from test where id 15;delete from test where id 15;T3insert into test values(15, 15);T4insert into test values(15, 15);T5insert into test values(15, 15);
因 id 15 在数据库中不存在在 T1 时刻 session 1 会给其所在间隙的下一条记录添加上 Gap Locks又因 Gap Locks 不互斥 在 T2 时刻 session 2 和session 3 都会同时获取到 id 20 的 Gap 锁
下图中 tx: T1、T2、T3 分别代表 session 1、session 2 和 session 3 当在 T3 时刻 session 1 插入 id 15 的记录时会判断其插入位置的后一条记录是否存在 Gap Locks如果存在则需要在该记录上生成 Insert Intention Locks 并等待持有 Gap Locks 的事务释放锁 在 T4 时刻 session 2 执行插入语句同样会因插入位置的后一条记录中存在 Gap Locks 而需要生成 Insert Intention Locks 等待。此时很明显就形成了死锁session 1 生成插入意向锁等待 session 2 和 session 3 上的 Gap 锁释放而 session 2 同样生成插入意向锁等待 session 1 和 session 3 上的 Gap 锁释放 在 T4 时刻检测到死锁后Mysql 会选择其中一个事务进行回滚假设此时 session 2 被回滚释放了其持有的所有锁资源session 1 可以继续执行吗 很明显不可以session 1 还同时在等待 session 3 上的 Gap 锁释放继续阻塞等待
在 T5 时刻 session 3 开始执行插入语句此时同 T4 时刻死锁形成session 1 生成的插入意向锁正在等待 session 3 上的 Gap Locks 释放session 3 上生成的插入意向锁正在等待 session 1 上的 Gap Locks 释放此时 session 3 回滚释放所有锁资源后session 1 才可以最终执行成功 在完成了三个并发线程的死锁分析后可能有人会想虽然有死锁但通过死锁检测可以很快的检测出程序也可以正常的执行这有什么问题呢 其实上面没有问题主要是因为并发量较小死锁检测可以很快检测出如果此时将并发量扩大 100 倍甚至 1000 倍后还会没有问题吗
看看当时出现线上问题时接口的调用量情况 进一步在本地模拟 300 个线程并发执行因人脑并发分析所有事务的执行情况的话会非常复杂本次只以事务 1 为一个点来进行分析
从图中可以看到当 T1 在执行插入语句时需要等待 T2- T101 上持有的 Gap Locks 释放之后 T2 - T6 可能同时执行插入语句然后进行死锁检测事务回滚看着似乎只要后续有事务执行了插入语句就会执行死锁回滚正常运行但在死锁检测的过程中还会有新事务(T101 - T 200 )获取到 Gap Locks造成锁等待队列中的事务越来越多而 Mysql 的整体死锁检测时间复杂度为 O(n^2)锁等待队列中的事务较多时每一次有新事务进行锁等待死锁检测都需要遍历锁等待队列中在其之前等待的事务判断是否会因自己的加入形成环此时检测会非常消耗 CPU 资源造成数据库整体性能下降死锁检测耗时增加Mysql 活跃连接数大幅增加并且因锁等待而连接无法释放最终造成应用层连接池被打满 综上分析本次出现问题的最主要原因是在短时间内存在大并发的请求对同一行数据进行先删除再插入操作(先更新再插入同理)造成了死锁等待应用层连接池被打满大量上游请求超时重试进一步导致锁等待最终影响了所有依赖该数据库的业务
因此对于未来在业务代码中存在相似逻辑的地方一定要做好防重校验避免短时间内存在对同一行数据的先更新再插入的并发操作。同时在可重复读隔离别下更新和删除操作默认都会添加 Next-Key Locks间隙锁的引入使得死锁问题在并发情况下很容易出现这也是在业务逻辑实现上需要考虑的问题。
总结
本文以一个线上问题为背景对 Mysql 中的各种锁机制进行了详细的总结分析了各个锁的加锁时机和具体使用场景其中特别要注意间隙锁的使用因间隙锁和间隙锁之间不互斥当多个事务之间并发执行时很容易形成死锁