代码实现
- Latch guards的变更:
本特性新增类如下表所示。在Exclusive_global_latch_guard仅用于死锁解析等罕见事件的情况下,Shard_latch_guard,Shard_latches_guard,Exclusive_global_latch_guard已可以处理大多数用例,但在我们的代码中有一些特殊情况需要表中其他更细粒度的工具。
类名
说明
Shard_latch_guard
对global_latch执行s-latch并在其生命周期内锁住shard mutex。
Shard_latches_guard
对global_latch执行s-latch并在其生存期内按序锁住两个shard mutex。
Exclusive_global_latch_guard
在其生命周期内对global_latch执行x-latch。
Shared_global_latch_guard
仅对global_latch执行s-latch,但不锁任何shard。
说明:以后在事务锁释放阶段可以在有效地遍历事务锁列表时单独锁住shard。
Naked_shard_latch_guard
只锁一个shard,但不锁global_latch。
说明:该类与Shared_global_latch_guard结合使用。
Try_exclusive_global_latch
试图在其生命周期内对global_latch执行x-latch。
说明:该类唯一用例在srv_printf_innodb_monitor()中,其作用是试图避免在报告InnoDB监视器输出时干扰工作负载。
Unsafe_global_latch_manipulator
允许以非结构化的方式按需手动锁定和解锁独占global_latch。
说明:在如下代码实现路径下需要使用该类:
srv_printf_innodb_monitor() =>
srv_printf_locks_and_transactions() =>
lock_print_info_all_transactions() =>
lock_trx_print_locks() =>
lock_rec_fetch_page()
- trx->mutex的变更:
Lock-sys和trx->mutex-es的交互比较复杂。我们特别允许执行Lock-sys操作的线程请求另一个trx->mutex,即使它已经有一个trx的mutex。因此,必须证明在设想的等待图中不可能形成死锁循环,其中边缘从试图获取trx->mutex的线程到当前拥有trx->mutex的线程。
在过去这很简单,因为Lock-sys受global mutex保护,这意味着最多有一个线程可以尝试拥有多个trx->mutex,在只有一个节点可以同时拥有入边和出边的图中,一个trx->mutex不能形成循环。
只要多个线程发生在不同的分片中,这些线程就可以并行执行Lock-sys操作,因此我们可以拆分Lock-sys mutex。
- dict_table_t::autoinc_trx的变更:
该字段用于存储指向trx的指针,指向当前拥AUTO_INC锁的trx。由于AUTO_INC锁互斥,因此最多只能有一个事务持有AUTO_INC锁。函数row_lock_table_autoinc_for_mysql ()通过查看该字段检查当前事务是否持有AUTO_INC。如果是,它可以遵循快速路径。此类检查是一种不带锁的比较,为了保证结果正确,需要将类型更改为atomic<>,并且保证对该字段的修改只在授权和释放期间进行,另外,在线程运行事务期间不能进行锁的释放。为了更好地解释工作原理,一些关于该代码的注释、断言和字段类型也做了相应的修改。
- dict_table_t::n_rec_locks的变更:
该字段统计当前与给定表关联的记录锁(已授权或等待中)的数量。在新特性中,只要记录锁位于不同的分片中,就可以并行创建和销毁,这意味着对该字段的修改是并发性的,因此我们将该字段声明为atomic<>类型。事务如果要读取该字段的值,须获得独占的全局latch,否则,在处理结果之前,这个值可能会被修改。该字段相关的一些注释也做了相应的修改。
- hit list的变更:
为了避免高优先级事务等待记录锁,新特性引入一个中止低优先级冲突事务的机制。由于冲突事务可能处于其他Lock-sys队列,识别和中止冲突事务的过程需要独占global latch。为了避免高优先级事务可能根本不处于等待状态而独占全局latch,我们需要一种可靠的、对线程安全的方法来检查事务是否正在等待。hp_trx->lock.que_state == TRX_QUE_LOCK_WAIT即可简单地完成该操作,但其合理程度还需后续确定。
代码中有些地方修改该字段之前未获取latch,这并不安全。另一种正确的方法是使用hp_trx->lock.blocking_trx.load() != nullptr来代替,这只需要在注释中稍作改动,以声明需要在等待结束时清除该字段(已经在代码中完成,但注释未做相应说明,可能造成迷惑)。修复que_state处理不在此特性的范围内。
- lock_release()的变更:
释放事务占有的所有锁,需要对事务的锁列表进行遍历,并对每个锁在相应的锁队列中执行一些操作。获取独占的全局latch是一种简单的实现方式,但是效率比较低下。
为了获得更好的并发性,最好是只获取一个共享的全局latch,然后在遍历时逐个锁住包含特定锁的shard。这种实现方法的困难在于,锁定顺序规则要求在trx latch之前获取Lock-sys latch,而trx->mutex保护事务的锁列表。此规则能防止并发的B树修改所导致的锁在页之间(进而在分片之间)重新定位。所以,这是一个先有鸡还是现有蛋的问题:我们要知道要锁哪个shard,需要知道下一个锁是什么,但是要遍历列表,我们需要trx->latch,这只能在锁定shard之后才能得到。对于该问题,我们的解决方法是:首先锁住trx->mutex,记下列表中最后一个锁的shard id,释放trx->mutex,锁住特定shard,再锁trx->mutex。只有之前所记录的锁仍位于列表的尾部,才能继续。这可能看起来很复杂,但实际上比“stop-the-world”的方法要快得多。
- lock_trx_release_read_locks()的变更:
lock_trx_release_read_locks ()主要用于组复制应用中释放读间隙锁。如果使用独占全局latch来遍历事务的锁,该函数会成为瓶颈。与lock_release ()函数类似,我们应该获取一个共享的global latch并在遍历中逐个获取latch shard。问题在于其他线程可以并发修改锁列表(例如,由于隐式到显式转换,或B树重组),而且我们不能简单地将当前锁与尾部进行比较,因为我们并不删除所有锁,只是删除它们的子集,所以lock_release ()的解决方法在这里是不够的。对此,我们引入uint64_t trx->lock.trx_locks_version,此变量在每次trx锁列表添加或删除锁时递增。在几次重启失败之后,可以切换回旧的lock_trx_release_read_locks_in_x_mode()。
- 其他变更:
- 将整个latch锁定逻辑分离到专门的类locksys::Latches,并在其头文件中广泛记录设计。
- 所有新函数都将在locksys命名空间中。
- lock_mutex_enter()/lock_mutex_exit()的所有用法将被适当的latch guard替换,最好是locksys:Shard_latch_guard。
- table->n_rec_locks必须成为atomic类型,因为它现在为给定表创建/销毁记录锁期间,可以并行递增/递减。
- dict/mem.cc在为UNIV_LIBRARY编译时并不需要包括lock0lock.h。
- 从PSI中移除lock_mutex。
- 在PSI中添加lock_sys_global_latch_rw_lock。
- 在PSI中添加lock_sys_page_mutex。
- 在PSI中添加lock_sys_table_mutex。
- 将所有使用exclusive global latch的地方记录下来,以说明我们必须采用如此强同步的其余原因。
- table->autoinc_trx字段应该是atomic类型的,因为它在未使用latch的情况下被“窥视”。并且必须清理周围的混淆/错误的注释和断言,以声明其正确性。
- lock_rec_expl_exist_on_page()应返回bool,而不是可能指向lock_t的悬垂指针。
- lock_print_info_summary和srv_printf_innodb_monitor()的内部逻辑通常至少需要进行小的重构,以便可以使用latch guard。
- lock_mutex_own()调试谓词必须替换为更具体的owns_exclusive_global_latch()、owns_shared_global_latch()、owners_page_shard(page)、owners_page_shard(table)等等。
- 需要实现bool Sharded_rw_lock::try_x_lock。
- lock_rec_insert_check_and_lock()(及其副本lock_prdt_insert_check_and_lock)的控制流可以通过移除重复代码来简化,以便能够使用latch guard。
- 可以通过删除重复代码,并使用更结构化的latch锁定来简化lock_rec_queue_validate()的代码。
- 更新sync0debug,使其有适当的latch锁定顺序规则。