论文阅读:Contention-Aware Lock Scheduling for Transactional Databases,原文章:
http://www.vldb.org/pvldb/vol11/p648-tian.pdfwww.vldb.org该文提出了竞争感知的锁调度算法,先上脑图。
问题定义
在数据库中锁协议主要有:
- 锁的两个类型,即:共享锁和排他锁;
- 两阶段封锁;
在数据库中一般会采用了锁依赖图(Dependency Graph)进行管理,如做死锁检查等,依赖图中点和边如下:
- 锁依赖图中的节点有两类:事务和数据元素;
- 边也有两个类:从事务到元素的边表示该事务占有了该元素的(共享/排他)锁;从元素到事务的边表示,该元素将要被该事务申请,但是还没有占有。
锁调度(Lock Scheduling)是指在数据库中当一个锁资源被释放/或者一个事务申请锁时,从等待队列中选择一个事务执行的选择过程,即调度过程。最常用的First In First Out(FIFO)策略,或者基于FIFO的一些变形。
该文提出了一个新的锁调度策略,即竞争感知的锁调度。
竞争感知的锁调度
该调度算法已经在MySQL 8.0.3+开始作为缺省的调度算法了。
该文章主要列举了如下可参考使用的竞争感知可选项,即把这些作为调度的感知值,但在最后实验时主要是采用bLDFS进行实验的。具体如下:
Most Locks First(MLF,最多锁优先)
方法如下:从所有依赖于刚释放元素的事务中,选择使用到对象锁最多中的一个(Number of locks held)。在下图中,当o1对象锁被释放后,会选择t1作为下一个事务;
Most Blocking Locks First(MBLF,最多阻塞锁优先)
MLF会把没被其他线程使用的对象锁也算进来了,造成后续其他事务被阻塞。例如,上一个例子中,会延迟t2后的三个事务。为了解决这个问题,可以采用MBLF,即只计算元素锁中被其他事务依赖的个数(Number of locks that block other transaction),在上一个例子中,则会优先执行事务t2。类似地,在下面的例子中,会优先执行t2事务。
Deepest Dependency First(DDF,最深依赖优先)
但是在上一个例子中,MBLF又会把事务t3之后的事务都阻塞,导致深度比较大的事务,要等待很久。为了解决这个问题,可以采用DDF,即基于深度依赖优先的调度策略。如果采用该策略,对于上个例子中,则会优先执行事务t1。
类似地,对于下一个例子,则会优先执行事务t1,但是如果先执行t2,其实是可以提前增加并法度的。
Largest Dependency Set First(LDFS,最大依赖集优先)
为了解决以上问题,终于到了本文的核心方法,即LDFS,即基于依赖集的大小的方法,如下图所示。t1事务被五个事务(含自身)依赖,t2被四个事务依赖,因此会优先调度事务t1。
对于不同的锁(排他锁和共享锁),LDFS会把所有共享锁作为一个集合,然后做并集运算,得到依赖对象o的共享锁事务集合的个数。如果该数值比排他锁中最大依赖事务集合大小还要小,则优先执行共享锁的所有事务,即把对象o共享锁给这些事务;否则,执行排他锁中依赖集最大的一个事务。具体算法如下:
b(atched) LDSF(批量LDFS)
LDFS算法存在一个问题,即当共享锁的事务集合太大时,那么就会造成其他排他锁事务等待,而且会依赖于最后一个共享锁的事务执行完成。
另外,基于引理4,对于事务个数k,当k越大时,这些事务中最长事务的平均时长是呈递增趋势的。为了解决以上问题,作者又提出了一个分批处理的LDFS。即,对于共享锁的事务,拆分集合,并选择满足第5个操作的事务集合。
其中,f(k)为延迟因子(delay factor)。
总结
本文提出了竞争感知的锁调度算法,该算法在测试结果中有明显的优势,而且也在MySQL 8.0.3+中实际应用,该算法比FIFO算法还是有较大的优势的。
另外,作者在实现时,也介绍了如何避免饿死(Starvation Avoidance)的情况,为了解决这个问题,在实现上,在每一个锁等待队列中加一个barrier,所有在这个barrier之后加入的不参与调度计算。这就可以有效地避免饿死的情况。