MySQL OLTP 场景 TPS 瓶颈分析与 lock_sys 锁拆分
发表于 2026/03/17
0
背景
在 OLTP 场景里,当 update、insert、delete 等写操作占比较高时,系统吞吐量不一定能够达到理论峰值,可能会受到 MySQL 内的全局 latch 影响,从而达到性能瓶颈。
在实际业务中,通常可以观察到以下现象:
- CPU 利用率较低,并且随着并发数增加基本没有明显变化。
- TPS 或 QPS在到达一定峰值后无法继续提高。
- 使用perf进行火焰图分析,发现 off-cpu 中锁相关热点函数占比大。
这些现象说明,性能瓶颈可能不在 SQL 本身,而在数据库内部的并发控制。
图 1 sysbench 只写 off-cpu

下面将先解释 lock_sys 的工作方式,再看为什么锁拆分可以缓解这个问题。
原理
MySQL 中的每个表和行都可以看作是一种资源,事务可以请求访问资源。但是并发的事务对资源的访问可能造成冲突,因此 MySQL 设计了 Lock-sys 用于管理对表和行的访问。
Lock-sys 维护多个锁队列,管理事务对表和行资源的占用与等待。
当一个新请求需要申请某个资源的时候,Lock-sys需要完成:
1. 在相应队列里查询资源是否已被占用;
2. 将请求入队(已授权或等待);将锁请求插入到相应的队列中(不管请求的资源是否被占用,分别标记为已授权或等待锁请求)。
3. 更新数据等待状态。
以上所有的查询与插入步骤都需要进行队列加锁保护。
数据库中,lock 和 latch 有时都被称为锁,但意义不同:
- lock:数据库对象锁(表锁、行锁),用于事务隔离语义。
- latch:内存结构保护锁,用于保护内部共享结构。
在过去,所有队列的访问均由一个 latch 管理,这意味着即使只要访问一个队列,其他所有的队列也会被锁住。这种实现方式在高并发场景下效率低下,为了解决这个问题,可以引入一种更细粒度的 latch 锁定方法。
从这里开始,核心问题就变成了:如何减少“不同队列之间的互相等待”。
1. 旧 latch
在 MySQL 中,所有的队列访问由同一个全局 latch 管理。
即使两个事务访问的是不同队列,但也要先竞争同一把锁,导致其他所有的队列也会被锁住,这种实现方式在高并发场景下效率低下。
2. 新 latch
新的 latch 锁定方法是在原来全局大锁的基础上,把队列分为固定数量的 shard,每个 shard 用自己的 mutex 保护;同时保留一个全局大锁 global latch(读写锁)。
可以满足如下场景需求:
- 普通访问:对各个队列进行访问,先拿 global latch 的共享锁(s-latch),再获取相应 shard 的 mutex。(例如 MySQL 访问一条记录时,先对表加意向锁,再对相应记录加锁)
- 全局场景:在一些特殊场景下需要锁住所有队列,直接拿 global latch 的排他锁(x-latch)。
不同事务访问不同队列时,不需要再被同一个全局锁锁定,可以独立于对其他队列进行操作。
图 2 全局 latch 管理关系

新的 latch 锁效率提升如下图所示。在过去,如果事务 A 和事务 B 分别要访问队列 1 和队列 2,由于都需要对全局大锁上锁,这时事务 B 就会被阻塞;而在新方案里,事务 A 和事务 B 可以同时申请 global latch 的共享锁,再分别申请 mutex1 和 mutex2,也就是说事务 A 和事务 B 可以同时进行,并发度提升。
图 3 全局锁与分片锁并发效率对比

设计与实现
基于上面的原理,我可以通过以下步骤来优化锁,实现功能。
1. 双队列访问
访问两个队列获取两条记录时,过程如下:
1. 对 global_latch 执行 s-latch;
2. 标识记录所属的两个页;
3. 标识包含给定页队列的两个哈希桶;
4. 标识包含这两个桶的两个 shard id;
5. 按地址顺序对两个分片 mutex 上锁。
以上所有步骤(除步骤2外,因为我们通常已经知道这些页)可通过一行代码完成:
locksys::Shard_latches_guard guard{*block_a, *block_b};2. 全局暂停场景
如果需要 stop-the-world,只需对 global latch 上 x-latch:
locksys::Exclusive_global_latch_guard guard{};结论
从“背景现象 -> 原理分析 -> 设计实现”串起来看,锁拆分的目标就是减少无关队列之间的串行等待,提升高并发写场景下的有效吞吐。
通过锁拆分技术可使 TPC-C 综合性能提升 10%。


