image.png

    如上文所述,InnoDB基于原子操作+内存屏障实现了自己的一套锁机制。为了便于读者阅读和问题理解,我们简化了相关代码。对于读写锁rw_lock_t类型,我们主要介绍writer_thread和recursive这两个变量:writer_thread表示持有锁的写线程,recursive表示这个锁是否是递归锁和writer_thread值的合法性。我们假设两个线程A和B按照以下顺序执行锁操作:step1. A成功申请了写锁,并调用rw_lock_set_writer_id_and_recursion_flag()函数,修改了writer_thread=A和recursive=true这两个变量,recursive=true表示writer_thread的值是合法的;step2. A释放了写锁,将recursive变量修改为false,表示writer_thread是非法的;step3. B申请了写锁,并调用rw_lock_set_writer_id_and_recursion_flag()函数,修改了writer_thread=B和recursive=true这两个变量;step4. A申请写锁,发现写锁已经被某线程持有。然而因为rw_lock_t是递归锁,A需要检查持有该写锁的线程是否是自己,如果是就成功获得锁。如果多线程执行无法保证step3和step4两组操作之间的执行顺序,这里的判断逻辑就会在ARM架构上引入严重bug。

    首先我们说明rw_lock_set_writer_id_and_recursion_flag()函数。由于os_compare_and_swap_thread_id原子操作包含了wmb屏障,这里的写操作逻辑在ARM上没有问题。writer_thread会先被设置,然后lock->recursive才被设置成true表示writer_thread是合法的。

    然而,ARM这类弱序模型可能打乱了lock->recursive和lock->writer_thread两个读操作的顺序。以step4为例,A先访问了lock->writer_thread,然后才访问lock->recursive。这时候如果step4和step3是交叉执行的,就会引入bug。例如A访问lock->writer_thread是在step3之前,这时候它获取到的lock->writer_thread=A(这时候lock->recursive=false,表明这个值是无效的)。然而如果这时候step3执行完成,A然后才访问了lock->recursive=true,这就导致A以为自己持有了这个写锁,就进入了后面的递归锁逻辑。这导致了临界区的混乱,两个线程可能进入了同一个临界区。这个问题导致的后果,轻则死锁,重则mysqld崩溃甚至数据写坏。

    基于上述分析,修复这个问题仅需要保证lock->recursive和lock->writer_thread两个读操作的顺序,因此我们的修复方案如下: