1.锁?
1.1何为锁
锁在现实中的意义为:封闭的器物,以钥匙或暗码开启。在计算机中的锁一般用来管理对共享资源的并发访问,比如我们java同学熟悉的Lock,synchronized等都是我们常见的锁。当然在我们的数据库中也有锁用来控制资源的并发访问,这也是数据库和文件系统的区别之一。
1.2为什么要懂数据库锁"color: #ff0000">2.InnoDB
2.1mysql体系架构
小明没有着急去了解锁这方面的知识,他首先先了解了下Mysql体系架构:
可以发现Mysql由连接池组件、管理服务和工具组件、sql接口组件、查询分析器组件、优化器组件、 缓冲组件、插件式存储引擎、物理文件组成。
小明发现在mysql中存储引擎是以插件的方式提供的,在Mysql中有多种存储引擎,每个存储引擎都有自己的特点。随后小明在命令行中打出了:
show engines \G;
一看原来有这么多种引擎。
又打出了下面的命令,查看当前数据库默认的引擎:
show variables like '%storage_engine%';
小明恍然大悟:原来自己的数据库是使用的InnoDB,依稀记得自己在上学的时候好像听说过有个引擎叫MyIsAM,小明想这两个有啥不同呢"htmlcode">
mysql> show variables like 'innodb_autoinc_lock_mode'; +--------------------------+-------+ | Variable_name | Value | +--------------------------+-------+ | innodb_autoinc_lock_mode | 2 | +--------------------------+-------+ 1 row in set (0.01 sec)
在MySQL中innodbautoinclock_mode有3种配置模式:0、1、2,分别对应”传统模式”, “连续模式”, “交错模式”。
- 传统模式:也就是我们最上面的使用表锁。
- 连续模式:对于插入的时候可以确定行数的使用互斥量,对于不能确定行数的使用表锁的模式。
- 交错模式:所有的都使用互斥量,为什么叫交错模式呢,有可能在批量插入时自增值不是连续的,当然一般来说如果不看重自增值连续一般选择这个模式,性能是最好的。
2.4InnoDB锁算法
小明已经了解到了在InnoDB中有哪些锁类型,但是如何去使用这些锁,还是得靠锁算法。
2.4.1 记录锁(Record-Lock)
记录锁是锁住记录的,这里要说明的是这里锁住的是索引记录,而不是我们真正的数据记录。
- 如果锁的是非主键索引,会在自己的索引上面加锁之后然后再去主键上面加锁锁住.
- 如果没有表上没有索引(包括没有主键),则会使用隐藏的主键索引进行加锁。
- 如果要锁的列没有索引,则会进行全表记录加锁。
2.4.2 间隙锁
间隙锁顾名思义锁间隙,不锁记录。锁间隙的意思就是锁定某一个范围,间隙锁又叫gap锁,其不会阻塞其他的gap锁,但是会阻塞插入间隙锁,这也是用来防止幻读的关键。
2.4.3 next-key锁
这个锁本质是记录锁加上gap锁。在RR隔离级别下(InnoDB默认),Innodb对于行的扫描锁定都是使用此算法,但是如果查询扫描中有唯一索引会退化成只使用记录锁。为什么呢"htmlcode">
CREATE TABLE `user` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(11) CHARACTER SET utf8mb4 DEFAULT NULL, `comment` varchar(11) CHARACTER SET utf8 DEFAULT NULL, PRIMARY KEY (`id`), KEY `index_name` (`name`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
然后插入了几条实验数据:
insert user select 20,333,333; insert user select 25,555,555; insert user select 20,999,999;
数据库事务隔离选择了RR
3.1 实验1
小明开启了两个事务,进行实验1.
时间点
事务A
事务B
1
begin;
2
select * from user where name = '555' for update;
begin;
3
insert user select 31,'556','556';
4
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
小明开启了两个事务并输入了上面的语句,发现事务B居然出现了超时,小明看了一下自己明明是对name = 555这一行进行的加锁,为什么我想插入name=556给我阻塞了。于是小明打开命令行输入:
select * from information_schema.INNODB_LOCKS
发现在事务A中给555加了Next-key锁,事务B插入的时候会首先进行插入意向锁的插入,于是得出下面结论:
可以看见事务B由于间隙锁和插入意向锁的冲突,导致了阻塞。
3.2 实验2
小明发现上面查询条件用的是普通的非唯一索引,于是小明就试了一下主键索引:
时间点
事务A
事务B
1
begin;
2
select * from user where id = 25 for update;
begin;
3
insert user select 26,'666','666';
4
Query OK, 1 row affected (0.00 sec)
Records: 1 Duplicates: 0 Warnings: 0
居然发现事务B并没有发生阻塞,哎这个是咋回事呢,小明有点疑惑,按照实验1的套路应该会被阻塞啊,因为25-30之间会有间隙锁。于是小明又祭出了命令行,发现只加了X记录锁。原来是因为唯一索引会降级记录锁,这么做的理由是:非唯一索引加next-key锁由于不能确定明确的行数有可能其他事务在你查询的过程中,再次添加这个索引的数据,导致隔离性遭到破坏,也就是幻读。唯一索引由于明确了唯一的数据行,所以不需要添加间隙锁解决幻读。
3.3 实验3
上面测试了主键索引,非唯一索引,这里还有个字段是没有索引,如果对其加锁会出现什么呢?
时间点
事务A
事务B
1
begin;
2
select * from user where comment = '555' for update;
begin;
3
insert user select 26,'666','666';
4
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
5
insert user select 31,'3131','3131';
6
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
7
insert user select 10,'100','100';
8
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
小明一看哎哟我去,这个咋回事呢,咋不管是用实验1非间隙锁范围的数据,还是用间隙锁里面的数据都不行,难道是加了表锁吗?
的确,如果用没有索引的数据,其会对所有聚簇索引上都加上next-key锁。
所以大家平常开发的时候如果对查询条件没有索引的,一定进行一致性读,也就是加锁读,会导致全表加上索引,会导致其他事务全部阻塞,数据库基本会处于不可用状态。
4.回到事故
4.1 死锁
小明做完实验之后总算是了解清楚了加锁的一些基本套路,但是之前线上出现的死锁又是什么东西呢?
死锁:是指两个或两个以上的事务在执行过程中,因争夺资源而造成的一种互相等待的现象。说明有等待才会有死锁,解决死锁可以通过去掉等待,比如回滚事务。
解决死锁的两个办法:
- 等待超时:当某一个事务等待超时之后回滚该事务,另外一个事务就可以执行了,但是这样做效率较低,会出现等待时间,还有个问题是如果这个事务所占的权重较大,已经更新了很多数据了,但是被回滚了,就会导致资源浪费。
- 等待图(wait-for-graph): 等待图用来描述事务之间的等待关系,当这个图如果出现回路如下:
就出现回滚,通常来说InnoDB会选择回滚权重较小的事务,也就是undo较小的事务。
4.2 线上问题
小明到这里,基本需要的基本功都有了,于是在自己的本地表中开始复现这个问题:
时间点
事务A
事务B
1
begin;
begin;
2
delete from user where name = '777';
delete from user where name = '666';
3
insert user select 27,'777','777';
insert user select 26,'666','666';
4
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
Query OK, 1 row affected (14.32 sec) Records: 1 Duplicates: 0 Warnings: 0
可以看见事务A出现被回滚了,而事务B成功执行。 具体每个时间点发生了什么呢"text-align: center">
4.3 修复BUG
这个问题总算是被小明找到了,就是因为间隙锁,现在需要解决这个问题,这个问题的原因是出现了间隙锁,那就来去掉他吧:
- 方案一:隔离级别降级为RC,在RC级别下不会加入间隙锁,所以就不会出现毛病了,但是在RC级别下会出现幻读,可提交读都破坏隔离性的毛病,所以这个方案不行。
- 方案二:隔离级别升级为可序列化,小明经过测试后发现不会出现这个问题,但是在可序列化级别下,性能会较低,会出现较多的锁等待,同样的也不考虑。
- 方案三:修改代码逻辑,不要直接删,改成每个数据由业务逻辑去判断哪些是更新,哪些是删除,那些是添加,这个工作量稍大,小明写这个直接删除的逻辑就是为了不做这些复杂的事的,所以这个方案先不考虑。
- 方案四:较少的修改代码逻辑,在删除之前,可以通过快照查询(不加锁),如果查询没有结果,则直接插入,如果有通过主键进行删除,在之前第三节实验2中,通过唯一索引会降级为记录锁,所以不存在间隙锁。
经过考虑小明选择了第四种,马上进行了修复,然后上线观察验证,发现现在已经不会出现这个Bug了,这下小明总算能睡个安稳觉了。
4.4 如何防止死锁
小明通过基础的学习和平常的经验总结了如下几点:
- 以固定的顺序访问表和行。交叉访问更容易造成事务等待回路。
- 尽量避免大事务,占有的资源锁越多,越容易出现死锁。建议拆成小事务。
- 降低隔离级别。如果业务允许(上面4.3也分析了,某些业务并不能允许),将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁。
- 为表添加合理的索引。防止没有索引出现表锁,出现的死锁的概率会突增。
最后
由于篇幅有限很多东西并不能介绍全如果感兴趣的同学可以阅读《Mysql技术内幕-InnoDB引擎》第6章 以及 何大师的MySQL 加锁处理分析。作者本人水平有限,如果有什么错误,还请指正。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。
更新日志
- 小骆驼-《草原狼2(蓝光CD)》[原抓WAV+CUE]
- 群星《欢迎来到我身边 电影原声专辑》[320K/MP3][105.02MB]
- 群星《欢迎来到我身边 电影原声专辑》[FLAC/分轨][480.9MB]
- 雷婷《梦里蓝天HQⅡ》 2023头版限量编号低速原抓[WAV+CUE][463M]
- 群星《2024好听新歌42》AI调整音效【WAV分轨】
- 王思雨-《思念陪着鸿雁飞》WAV
- 王思雨《喜马拉雅HQ》头版限量编号[WAV+CUE]
- 李健《无时无刻》[WAV+CUE][590M]
- 陈奕迅《酝酿》[WAV分轨][502M]
- 卓依婷《化蝶》2CD[WAV+CUE][1.1G]
- 群星《吉他王(黑胶CD)》[WAV+CUE]
- 齐秦《穿乐(穿越)》[WAV+CUE]
- 发烧珍品《数位CD音响测试-动向效果(九)》【WAV+CUE】
- 邝美云《邝美云精装歌集》[DSF][1.6G]
- 吕方《爱一回伤一回》[WAV+CUE][454M]