深入 OLTP 锁与并发:从行锁到分布式事务的一致性权衡

大家好,我是小耶,写功课是为了我踩过的坑,你们别再踩了!

你有没有遇到过这种情况:数据库 TPS 明明不高,但偶尔会卡死。查慢查询日志,没发现什么大 SQL。查 CPU 和 IO,都挺正常。最后发现是死锁。

更诡异的是,死锁自动回滚了,业务也没报错,但就是偶尔响应慢。

这不是玄学。这是 OLTP 并发控制的底层机制在"暗中作祟"。


一、为什么 OLTP 的并发控制这么难?

OLTP 的核心挑战是:大量事务同时操作同一批数据,既要保证隔离性(互不干扰),又要尽可能提高并发度。

这就像早高峰的十字路口------每辆车都想 fastest 通过,但如果没有红绿灯,结果就是谁都走不了。

数据库的"红绿灯"就是并发控制机制。但比红绿灯复杂一万倍,因为:

  • 事务有优先级之分(不是所有车都一样)
  • 有的事务只看数据不加锁(有的车只是路过不转弯)
  • 有的事务既要读又要写(有的车要掉头)
  • 有的事务要同时锁定多行(有的车要占两条车道)

二、MVCC:OLTP 高并发的关键技术

2.1 MVCC 是什么?

MVCC(多版本并发控制)是现代 OLTP 存储引擎的核心并发机制。它的核心思想:数据的每个修改都保留历史版本,读操作不需要加锁。

InnoDB 的每一行记录都包含两个隐藏列:

sql 复制代码
Row Structure:
  [trx_id]     -- 创建或修改此行的事务 ID
  [roll_ptr]   -- 指向 Undo Log 中的历史版本
  [data...]    -- 用户数据

当事务读取数据时:

  1. 事务启动时生成一个 Read View(包含当前活跃事务 ID 列表)
  2. 读取行记录时,检查 trx_id 是否在活跃列表中
  3. 如果是未提交事务创建的版本,就沿着 roll_ptr 回溯历史版本

2.2 MVCC 的实战影响

优势:

  • 读操作不需要加锁,大幅提升并发读性能
  • 可重复读隔离级别下,不需要范围锁来防止幻读
  • 快照读和当前读分离

代价:

  • 历史版本占用额外存储空间
  • 版本链过长时,回溯开销增大
  • purge 线程必须及时清理旧版本

2.3 长事务是 OLTP 杀手

我在厂里排查过一次性能问题:MES 系统的报工接口突然从 50ms 变成 2 秒。查了一圈:

  • Buffer Pool 命中率正常(98%+)
  • 没有慢查询
  • 锁等待时间也很短

最后发现是一个后台报表查询跑了 3 分钟没提交。因为 Read View 一直挂着,Undo Log 里的旧版本无法清理,版本链越来越长,后续的读写操作都要遍历很长的版本链才能找到可见数据。

教训:长事务不提交,MVCC 的版本链就会越堆越长,最终拖垮整个 OLTP 系统。一定要设事务超时限制。

另外还有一个容易被忽视的点:事务号耗尽。老版本 MySQL 用 32 位事务号,在高并发 OLTP 系统里(日均千万级事务),事务号可能不到一年就耗尽,届时需要停机维护。金仓 KES 用了 64 位事务号,从架构层面规避了这个问题。在做 Oracle 迁移的时候,如果源端 Oracle 的并发量很高,目标端的事务号位宽一定要提前评估,否则迁移完成运行一段时间就可能出现性能骤降。

2.4 怎么监控 MVCC 健康状态?

sql 复制代码
-- 查看 Undo Log 版本链长度
SHOW GLOBAL STATUS LIKE 'Innodb_history_list_length';

这个值持续增长,说明 purge 跟不上,需要排查长事务。


三、锁:写操作的保护伞

MVCC 处理的是"读"的并发。对于"写",仍然需要锁。

3.1 InnoDB 的锁类型

锁类型 作用范围 说明
行锁(Record Lock) 具体的索引记录 UPDATE/DELETE 对特定行加锁
间隙锁(Gap Lock) 索引记录之间的间隙 防止其他事务在间隙中插入新行
Next-Key Lock 行锁 + 间隙锁 InnoDB RR 级别下的默认锁
意向锁(Intention Lock) 表级别 声明事务打算对表中哪些行加锁

3.2 悲观锁 vs 乐观锁

悲观锁:假设冲突经常发生,操作前先加锁。

sql 复制代码
-- SELECT ... FOR UPDATE:对查询结果加排他锁
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

乐观锁:假设冲突很少发生,提交时检查是否有冲突。

ini 复制代码
-- 通过版本号字段实现
UPDATE accounts SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 5;

选型建议:高冲突场景用悲观锁,低冲突场景用乐观锁。电商库存扣减通常用悲观锁,因为并发冲突概率高。


四、死锁:检测、排查与预防

4.1 死锁是怎么形成的?

当两个事务互相等待对方释放锁时,就会发生死锁:

css 复制代码
事务 A:持有 row1 的锁,等待 row2 的锁
事务 B:持有 row2 的锁,等待 row1 的锁
→ 死锁

InnoDB 使用等待图(Wait-For Graph) 检测死锁:将事务和锁依赖关系建模为有向图,检测到环就回滚代价较小的事务。

4.2 怎么排查死锁?

sql 复制代码
SHOW ENGINE INNODB STATUS\G

在输出的 LATEST DETECTED DEADLOCK 段可以看到:

  • 哪个事务被回滚了
  • 每个事务持有的锁和等待的锁
  • 涉及的 SQL 语句

4.3 死锁预防策略

策略一:固定访问顺序

sql 复制代码
-- 所有事务都按 id 升序访问
UPDATE accounts SET balance = balance - 100 WHERE id IN (1, 2) ORDER BY id;

这样就不会出现 T1 锁 row1 等 row2、T2 锁 row2 等 row1 的情况。

策略二:降低隔离级别

RC 级别的 InnoDB 不使用 Gap Lock,只使用 Record Lock,死锁概率低于 RR 级别。但需要接受不可重复读的可能性。

策略三:缩短事务持有锁的时间

sql 复制代码
-- ❌ 长事务,锁持有时间长
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- ... 执行大量非数据库操作(2秒)...
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

-- ✅ 短事务,锁持有时间短
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- ... 非数据库操作放在事务外 ...

五、OLTP 性能基准:TPC-C 与实战方法

5.1 TPC-C 是什么?

TPC-C 是 OLTP 领域最权威的性能基准,模拟一个批发供应商的业务系统,包含 5 种事务:

事务 占比 描述
New-Order 45% 创建新订单,涉及多表写入
Payment 43% 处理客户付款
Order-Status 4% 查询最近订单
Delivery 4% 批量发货
Stock-Level 4% 查询库存水平

核心指标:

  • tpmC:每分钟完成的新订单事务数
  • Price/tpmC:每 tpmC 的成本(性价比)

5.2 实用 OLTP 压测方法

ini 复制代码
sysbench oltp_read_write \
  --table-size=1000000 \
  --tables=16 \
  --threads=64 \
  --time=300 \
  --db-driver=mysql \
  run

关注指标:

  • transactions:总事务数和 TPS
  • latency:avg / min / max / 95th / 99th percentile
  • thread fairness:各线程执行时间分布(均匀说明无热点)

5.3 生产环境性能基线

指标 小型 OLTP 中型 OLTP 大型 OLTP
TPS 500-2,000 2,000-10,000 10,000-100,000+
P99 延迟 <200ms <100ms <50ms
并发连接 50-200 200-1,000 1,000-10,000+

六、分布式 OLTP 的并发控制

6.1 CAP 定理的取舍

选择 代表系统 一致性 可用性 OLTP 适用性
CP Spanner、CockroachDB 强一致 分区时不可用 金融级 OLTP
AP DynamoDB、Cassandra 最终一致 分区时可用 不适合强一致 OLTP

金融级 OLTP 几乎都选 CP:宁可分区时不可用,也不能接受数据不一致。

6.2 分布式事务协议

两阶段提交(2PC) :

  • Phase 1:协调者问所有参与者"准备好了吗"
  • Phase 2:如果都准备好了就提交,否则回滚

2PC 的问题:协调者单点故障、性能开销大(两次网络往返)、锁持有时间长。

优化方案:

  • Percolator(Google):用乐观并发控制替代 2PC 的悲观锁
  • Spanner TrueTime:用硬件原子钟实现全局时序,减少锁等待

6.3 全局时钟

分布式 OLTP 需要全局时钟来给事务排序:

方案 精度 代表系统
NTP 时间同步 毫秒级 大多数分布式数据库
HLC(混合逻辑时钟) 逻辑顺序 TiDB、CockroachDB
TrueTime(硬件时钟) 微秒级 Google Spanner
TSO(集中授时服务) 毫秒级 OceanBase

七、HTAP:并发控制的新维度

HTAP 试图在同一套引擎中同时支持 OLTP 和 OLAP 负载,这给并发控制增加了新的维度。

资源隔离方案:通过资源组和连接池隔离 OLTP/OLAP 负载。金仓 KES 就是走的这条路------在同一数据库实例中,用资源组把 OLTP 和 OLAP 的 CPU/内存/IO 分开,避免分析查询拖慢事务性能。这种方案适合中小规模场景,不需要额外部署分析引擎。而且 OLTP 和 OLAP 之间通过 logical decoding 实时同步,数据一致性有保障,不用像双引擎方案那样担心同步延迟。

双引擎方案:行存 + 列存双引擎(如 TiDB 的 TiKV + TiFlash)。OLTP 写入行存,异步同步到列存,OLAP 查询走列存。隔离更彻底,但运维复杂度更高。

选型判断:如果你的系统日均事务量在百万级以下,偶尔有分析需求,资源隔离方案更经济;如果 OLAP 查询频率高且数据量大,双引擎方案更稳。


八、总结

OLTP 并发控制的核心要点:

  1. MVCC 让读写不互锁,但长事务会让版本链堆积,是 OLTP 系统的隐形杀手
  2. 死锁不是 bug,是并发系统的自然现象,关键是预防和快速恢复
  3. 隔离级别的选择是一致性与并发的权衡,RC 死锁少但可能有不可重复读
  4. 分布式 OLTP 用一致性换可用性,选型要看业务对一致性的容忍度
  5. 压测一定要看延迟分布,不能只看 TPS

👋 小耶在手,SQL不愁

相关推荐
SelectDB37 分钟前
开源替代怎么选:能力对照、三条部署路径与四类边界速查
数据库·云原生·开源
SelectDB37 分钟前
自建与托管怎么选:开源自建、私有化与云上托管三条路径速查
数据库·云原生·开源
SelectDB39 分钟前
数仓 TCO 怎么算:五项成本拆解框架速查
大数据·数据库·开源
虚拟世界AI1 小时前
Kronos 时间序列数据库新手部署与实战指南
数据库
愤怒火龙果1 小时前
AI攻防 AIRT03-RAG Exploitation & Vector Database Attacks
数据库·人工智能·网络安全
SelectDB1 小时前
高并发 BI 看板怎么撑住:隔离与加速配置速查
大数据·数据库·数据分析
l1t2 小时前
测试DuckDB v2.1.0-alpha49002的bignum乘法
数据库·sql·算法·duckdb
麦壳饼2 小时前
SonnetDB:向量召回率评估中的精确基线与参数记录
数据库·sonnetdb
ClouGence3 小时前
数据如何随时可用?广西水牛先生的 MySQL 实时同步实践
数据库·mysql·数据分析