近日在DBAplus公众号上面转载了一位作者(下文中简称“原作者”)关于MySQL 分区表的bug分析的文章,那位作者认为mysql-5.7的分区表做索引扫描时过多地扫描了分区,从而导致了其并发性能降低,是一个bug。我对此做了分析发现并不是这样,原作者的测试用例并不能证明mysql-5.7的分区功能有问题,测例中的事务锁冲突另有原因,详见本文。如果看不懂实验分析,可以先可以下本文末尾的结论部分,然后再从头开始看。

我写本文完全为了技术交流,并且分区表这个主题也非常契合目前分库分表的业界潮流,是很多同学都会遇到的事情,所以很值得探讨分析,故有此文。我们每一个mysql的用户积极的分析使用过程中遇到的mysql的问题,并发现和汇报甚至修复bug给官方是很好的事情,利于mysql生态圈的发展。原作者的做法还是非常值得鼓励的。技术方面有错误不要紧,谁都会犯错,谁也都在学习。如果读者发现本文有任何错误或者疏漏也请不吝指教。


由于微信公众号的发文编辑器无法粘贴表格,所以下文主体部分只好输出两张图片贴上了。


MySQL分区表索引扫描及事务锁实验_java

MySQL分区表索引扫描及事务锁实验_java_02

结论

原作者的实验并不能证明mysqlpartition处理逻辑有任何问题。他的实验以及我后面增加的实验都可以帮助我们学习mysqlpartition的处理逻辑。特别地,我在此讲一下 int Partition_helper::handle_ordered_index_scan(uchar *buf)这个函数(如下图)为什么需要在如下图的一个循环中从start_part 

MySQL分区表索引扫描及事务锁实验_java_03

到 end_part获取每个分区的行。其实很简单,这个算法是这样的:假设我们的表有N个分区p1, p2... pN,我们需要从这N个分区中取出全表范围内有序的(本例中是按照主键索引的顺序)行。那么我们就需要在每个分区中开启一个index scan,取出各个分区的符合查询条件的第1行,分别放入行buffer,也就是那个 part_rec_buf_ptr,(它是一块连续大内存,放置这N个行的数据)然后,我们从这N行中,找到最小的一行。在mysql中使用了一个优先级队列做的排序(m_queue)来找到最小行。返回这行给上层调用者。假设返回的行属于分区pX,那么下面我们要从pX中获取下一行(继续这个分区的index scan),放入m_queue 优先级队列(int Partition_helper::handle_ordered_next(uchar *buf, bool is_next_same)),然后再从中返回最小的行。以此类推。当某个分区的行用尽后,就不再从它那里获取行,而是继续返回m_queue当前最小的行(void Partition_helper::return_top_record(uchar *buf)),然后就会导致我们需要从另一个分区pY中做index scan获取符合条件的下一行放入m_queue。最终所有的分区都会用尽符合查询条件的行,然后m_queue中的行也会用尽,于是扫描结束,上层调用者得到的就是全表有序的行。

在每个分区扫描行的过程中,innodb会根据事务隔离级别,做出不同的行锁定操作,在read committed隔离级别下,只锁定返回的目标行(以及索引扫描过程中扫过的其他行,本例中不存在这种情况),在repeatable read隔离级别下,还会做gap locking,锁住扫过的行之间的gap

另外,要注意的是,mysql会尽可能缩小需要扫描的partition的范围,如果知道某些partition一定不会含有目标数据,那么这些partition并不会被扫描到,这部分逻辑在 Partition_helper::common_index_read() 函数里面。比如,如果分区表使用的列正好是所扫描的索引的列集的子集,并且做的是分区列的等值查找,那么目标行必定只存在于一个分区中,此时就不需要扫描其他分区了。再比如,在满足上述超集条件下,如果做的不是等值查找而是范围查找,也可以根据分区定义排除一部分分区。