InnoDB引擎使用索引组织表,每个表的数据都放在一个对应的索引中,该索引称为聚集索引(clusteredindex),使用索引组织表的目的是:
动态地组织磁盘文件结构,维护数据记录有序;借助索引快速定位记录;除了clusteredindex,一个表中的其它索引称为二级索引(secondaryindexes)。二级索引的每个record除了包含本身的columns,还包含其对应的数据行的primarykey,InnoDB可以利用primarykey去主索引找到完整的row。
InnoDB使用B-tree作为索引的数据结构,B-tree本质是多级索引,扁平且平衡的树结构可以保证单次访问数据的IO次数较小且固定。
InnoDB实现的B-tree结构有几点特性:
实际数据全部存储在leaf层(即B+tree,降低树高度、优化顺序访问);non-leaf层只存储索引项(key,pageno),每个索引项指向唯一一个child节点;一个索引项的key为P,它的child节点只能存=P并且P1的记录,其中P1是下一个索引项的key;每层节点通过双向链表串起来;B-tree并发控制如果把B-tree索引看成一个黑盒,不关心内部具体的数据结构,外面看来只有有序排列的record,所有的读取、插入、删除等操作都能原子地一步完成。这时多线程并发操作不会感知到索引结构,只需要考虑事务层面的约束,例如InnoDB中一个事务对record加逻辑锁(lock)阻止其它事务访问。
但是实际的B-tree操作不是原子的,例如,一个线程在做结构调整(SMO)时会涉及多个页面的改动,此时另外一个线程进来会访问到不正确的B-tree结构而产生错误,另外,页面内部的数据结构改动也不能多线程并发执行。
InnoDB是采用page加锁的方式对B-tree进行并发控制,每个page有一个对应的物理读写锁(rwlatch),线程读取一个page要先加Slatch,修改page时要加Xlatch。
因为B-tree索引的访问和修改流程是确定的,所以InnoDB有一套设计好的加锁规则,防止多线程死锁。与之对应的是,事务锁(lock)是用户层面的,加锁顺序不可控,因此需要死锁检测机制。
并发瓶颈InnoDB对B-tree的并发控制实现细节可以参考InnoDBbtreelatch优化历程这篇文章,可以看到多次优化改进的目的都是为了提高B-tree的并发访问能力,即尽量减小每个操作的加锁范围和时间。
目前8.0版本在B-tree并发上还有一些限制:
SMO无法并发:同一时刻只允许有一个SMO进行(SMO线程持有indexSXlatch),导致在大批量插入场景下indexlatch会成为全局的瓶颈点,MySQL官方性能测试人员Dimitrick也指出TPCC场景下最大的瓶颈在于indexlockcontention(MySQLPerformance:TPCC"Mystery"[SOLVED])。设想一下,如果将这个限制放开,多个做SMO的线程会同时进入B-tree,每个线程要拿多个page,如何设计加锁规则避免线程间死锁,有很多细节需要处理。加锁范围大:为了避免死锁,乐观读写操作要持有遍历路径上所有non-leafpage的Slatch,SMO操作要持有所有可能修改的page的Xlatch。单一操作的加锁范围比较大,并发操作越多越会加剧锁竞争,在某些关键节点(例如,root、internalnode)的竞争会更明显。理论上采用latchcoupling的方式可以减小加锁范围,遍历过程中最多同时持有2个page的锁(即沿着一个指针从一个节点到另一个节点时,不能有其他线程去改变这个指针,因此要拿到后一个节点的latch再放开前一个latch),有利于降低并发操作的竞争。PolarDB并发控制优化PolarDB解决了InnoDB在B-tree并发控制上的限制:
提升并发度:去除index锁,允许所有操作并发访问B-tree,线程间冲突只在page级别;降低锁粒度:所有操作都实现了latchcoupling,减小锁范围,大大降低线程间冲突;PolarDB设计了页级别的B-tree并发控制,对索引页的加锁规则如下:
规则1:所有操作采用latchcoupling形式遍历B-tree,遍历过程中对非目标节点加Slatch(相当于读取节点指针),直到找到目标节点,根据操作读/写类型对目标节点加Slatch或Xlatch(SMO操作涉及邻居节点更新,还需要加左右邻居节点的latch);
为了避免死锁,规定加锁方向为,自上而下,从左到右。而SMO操作是自下而上的,即SMO的加锁方向是破坏规则的,会造成死锁(InnoDB的SMO操作不使用latchcoupling方式,开始就从上到下拿到所有相关节点的latch,从而避免死锁)。
规则2:在SMO操作中间,申请父节点或左邻居节点的latch之前,对当前持有Xlatch的page做SMO标记,并释放这些latch。
这里借鉴了B-link的设计,将SMO操作划分为两个阶段,第一阶段完成提前释放latch,避免了SMO操作反向加锁。这样,在SMO中间状态没有持有任何latch。
SMO中间状态节点设置一个指向其右侧节点的指针(sidelink),其它读操作访问到SMO中间状态的节点,并可以同时在其rightpage中检索,解决了B-tree结构不完整的问题。
其它写操作不应该修改处于SMO中间状态的节点,因为之前SMO操作还没提交。因此,其它线程的写操作遇到SMO节点,要等待其SMO完成。
规则3:其它线程遍历到有SMO标记的节点,并想要修改该节点时:立即释放掉已持有的latch,等待该节点SMO完成再从root重试;
如果其它线程不释放已持有的latch,相当于跟SMO线程之间互相占有并等待,造成死锁。
性能对比采用上述的B-tree并发控制机制,在高并发TPCC场景(warehouse)下:
当并发小于64时,InnoDB与Polarindex性能结果接近,InnoDB在并发时达到性能峰值;Polarindex在并发时达到峰值,可以看到Polarindex相比于InnoDB,峰值提高%;原文链接: