重新认识Innodb

打从第一天上班开始,就在与Mysql打交道,当时也啃过几本评分很高的讲mysql的书。记得曾经看过书中讲的眼花缭乱的复杂sql后也跃跃欲试的想要在项目中大展拳脚,但转眼就被各种慢查询和数据不一致问题虐的体无完肤。经历过各种尴尬,看过不少前辈的分享后,发现其实常见的互联网项目,大量的逻辑可以很好的在应用代码这一侧就完成,不仅能保证效率,有利于维护,而且未来的优化空间对一般开发人员来说也比较大,不至于面对数据库这个黑匣子一脸懵逼。所以从业一段时间后,自己接受的项目中就已经很少见什么子查询,各种join,更直接就禁用了存储过程等等。

我相信,不少人和我一样,停留在这个阶段。我之所以用“停留”这个词,就是想表达:这种程度还远远不够。我们可以让mysql这样的DB只用于持久化,但这并不代表就可以完全无视它的基础法则。如果你觉得你的sql足够简单,就可以完全不在意mysql的“复杂玩法”,那你就只能等着挨打了~~

写这么多,想必应该看得出,刚刚被打的,正是在下吧~ 最近在帮同事排查问题的时候,发现自己在mysql方面有一些基础知识缺失了,导致从问题表象上看,各种反直觉。所以就决定花时间先把基础知识“复习”一下。

接下来我们就直奔主题,把这次复习到的内容全部总结出来(现学现卖),有兴趣的童鞋一定要认真看本文最后的“参考资料”,那里面会有大量的干货和实例。

Innodb

通常情况下我们选择使用mysql这样的传统db,多半离不开ACID,而要获取这样的保证,就肯定要基于事务,而mysql下支持事务的引擎就是Innodb,所以咱们本篇内容就只围绕Innodb来讲。

一般常识下,大家都知道Innodb支持行锁,有更好的并发性能,但其实这个理解中存在着很大的认知缺陷。
首先,Innodb的行锁是基于索引的,假如你的where条件无法使用索引的前提下,就会产生“锁表”(也有文章提到说是临键值锁,只是区间覆盖全表)。所以可见索引的重要性,可不仅仅是针对查询提速哦。

其次,一般情况下我们并不是“生硬”的直接去inert/update/delete,而是根据db中现有的数据值基础上进行一些数据变更(直白点讲说,就是使用select…for update),如果在事务中没有明确的申请查询锁(共享锁/排它锁)话,根据不同的事务隔离设置,会有截然不同的逻辑,如 快照读。

事务隔离

咱们先来捋顺这个概念,mysql的事务隔离有四种:

  • 读未提交
  • 读已提交(推荐)
  • 可重复读(默认)
  • 串行化

不同的事务隔离设置主要是针对业务对“脏读,不可重复读,幻读”的接受度来考量的。当然,通常我们的项目肯定是不接受“脏读”的(即提前读到其他事务未commit的数据变更),但是否一定要求“可重复读”,这一点就要大家好好根据自身的业务需要好好思考了

何为“可重复读”,主要就是指在事务中多次读取(未明确申请任何锁的前提下)相同的数据范围,得到的数据集是相同的,不会发生变化。mysql主要是依靠“快照读”来做到的,这里一定要注意一些细节:快照版本和事务开启的时刻无关,仅和该事务中第一次读操作的时刻有关,即从第一次读操作发生的时刻开始,其它任何事务的任何晚于这个时刻的提交,本事务都无法“感知”。这个特点给mysql在保证可重复读的前提下带来的很高的并发性能(避免了读写锁阻塞)。

各种锁

如果你在事务中明确申请锁,那你的读取操作就不会在是快照读了,而是会尝试加锁获取当前数据

接下来我们来看看,常见的锁:

  • 共享锁/排它锁
  • 间隙锁
  • 记录锁(行锁)
  • 临键锁(间隙锁+记录锁)
  • 自增锁

共享锁其实就是读锁(如 select…lock share mode),解决读读并发问题。
排它锁就是写锁(如 select…for update, update, delete),排斥一切,包括自己。
其实还有“意向共享锁/意向排它锁”,它们是基于表的,存在的价值就是提速锁判断。具体大家可以自己阅读相关文献。

根据sql的where条件的范围,我们可以一次性获取 N 条数据的 读或写锁。这些锁,可能是记录锁(如 where id=1),也可能是临键锁(如 where id > 1),即 记录锁 + 间隙锁。

记录锁很容易理解,就是我们一直挂嘴边的行锁。
那什么是间隙锁呢?它主要是来解决幻读的,假如事务A锁定一个数据范围(如 where id between 1 and 10),事务B尝试insert一条记录(如 id = 5,假如之前不存在id=5的记录)时,由于事务A在这个范围上加了间隙锁,事务B只能等待。当然,事务B更不可能update/delete任何记录,因为也都被事务A加上了记录锁。
这里要额外叮嘱一下:此时事务B如果进行select id=5 for update的时候,是不会阻塞的,而是会直接返回记录不存在。但随后可能的insert肯定会阻塞。

另外要注意哦,前面我们提到当使用的where条件没有命中索引的时候,mysql会进行全表范围的间隙锁哦~~

最后咱们在来看一下自增锁,这个主要是来对那些业务需要保证多次插入得到连续的自增值的场景,默认mysql是没有开启的。但若开启后,任何一个事务开启后,相当于会在整个表上增加一个自增锁,用来阻塞其他事务的insert操作。
根据这个定义,存在一个有意思的事儿:假设我们有一张新空表test,保持默认设置(即未开启自增锁),然后先开启A事务,然后插入一条数据,不提交,再开启B事务,也插入一条数据,此时提交B事务,你觉得B事务插入的记录的自增字段的值会是多少?
答案是:2。
值会基于事务A未提交的那条记录的自增值+1,即便是在事务B提交前,事务A回滚也如此

有点意思吧。

orderby + limit 下的任性

最后,我们来看一个更“冷”的知识。在我们处理分页的时候,如果你的orderby时,刚巧碰到了多条记录该排序字段的值是相同的。你猜mysql会如何做?
随机排序,吃惊么?其实一般场景下对这种处理并不敏感,甚至无视。

但加上limit设置的话,好死不死刚好随机发生在limit条件上,对应的分页结果就会表现的随机性。这还不是最狗血的,假如你的业务比较冷门,要基于当前表中的记录按照指定排序条件获取期望的特定数据值时(limit 0,1),恐怕这种随机性会让你抓狂。

我们的某个遗留系统中,就碰到这种问题,明明开了事务,加了锁,但按照创建时间倒序,想要获取最新的一条记录的订单号,在基于该订单号+1的逻辑创建新的订单号时,在压力测试(会发生同一秒创建多笔订单)的时候还是会出现订单号重复的bug。

我们先不管业务订单号必须连续的合理性,以及sql语句的严谨性之外,单单这个mysql的随机返回结果的特性,都让排查问题的时候基本抓瞎。

总结

了解了上面的基础知识后,相信在使用innodb的时候,尤其是处理一些“异常”的时候,会有思路一些吧。
强烈建议大家把下面分享的文献读一下,很开阔视野。

参考资料

挖坑,InnoDB的七种锁
4种事务的隔离级别,InnoDB如何巧妙实现?
InnoDB的快照读,到底和什么相关?
数据库索引,到底是什么做的?
58到家MySQL军规升级版
Mysql order by与limit混用陷阱

Author: kazaff
Link: https://blog.kazaff.me/2020/12/25/重新认识Innodb/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
微信打赏
支付宝打赏