DDR爱好者之家 Design By 杰米

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 加锁处理分析。作者本人水平有限,如果有什么错误,还请指正。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。

DDR爱好者之家 Design By 杰米
广告合作:本站广告合作请联系QQ:858582 申请时备注:广告合作(否则不回)
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
DDR爱好者之家 Design By 杰米

《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线

暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。

艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。

《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。