TIDB——TIKV——分布式事务与MVCC

声明:本文基于两个以及以上实例讨论;并且所讨论的锁皆为悲观锁

注:

  • 大部分时候,节点就是实例,即一个TIKV节点就是一个独立运行的 TiKV 进程实例,实例对应着一台物理机:TiKV 服务,包含完整的存储引擎、Raft 协议模块、分布式事务模块等核心组件,但当在单台服务器部署多个 TiKV 进程时具体区别

|-------------|---------------------|
| TiKV 节点 | 运行 TiKV 服务的服务器 / 容器 |
| TiKV 实例 | 一个独立的 TiKV 进程 |

  • 只有在提交时,锁才会被感知,在begin与prewrite之间都不会被感知,称为乐观(锁)事务,要是想让begin与prewrite之间能感知到,提前将锁信息写入tikv中,悲观锁

一、分布式事务

1、事务定义

数据库中一组不可分割的++操作++集合

例子

begin

<1,xxx>-><1,lucy>

<3,xxx>-><3,frack>

<4,xxx>-><3,jack>

commit;

2、事务怎么在ITKV中存储的

1、流程主要任务

  • begin:获得事务开始时间;将数据读取到TIDB Server中
  • prewrite:修改数据;锁信息
  • commit:获取事务结束时间;提交信息;锁清理

2、事务具体提交流程

一、 事务启动阶段
  1. 事务begin 触发后,TiDB Server 向 PD 组件 请求获取时间戳 TSO ,该 TSO 作为事务的 start timestamp(开始时间戳)
  2. TiDB Server 读取需要修改的数据,并在内存中执行数据的增删改操作
二、 事务提交阶段(二阶段提交 - 2PC)
阶段 1:Prewrite(预提交)
  1. TiDB Server 将修改后的数据、锁信息,发送至对应的 TiKV 节点 进行持久化
  2. TiKV 会通过 3 个 Column Family(列簇) 存储 相关信息,分工如下:
    • default CF :存储修改后的数据 。数据的 Key 格式为 key_start timestamp,无论增删改查操作,均以数据版本的形式存储
    • lock CF :存储事务的锁信息,采用主锁机制
      • 仅为事务修改的第一行数据(主键行) 添加主锁:pk
      • 其余行的锁信息不单独存储,而是通过 @key 的形式指向主锁
      • 锁的作用:若其他事务尝试读取或写入这些数据,会被直接阻塞,保证事务隔离性
    • write CF :暂存事务的预提交相关信息
阶段 2:Commit(提交)与锁清理
  1. 当 Prewrite 阶段全部成功后,TiDB Server 再次向 PD 请求**commit timestamp(** 提交时间戳****。
  2. TiDB Server 向 TiKV 发送 Commit 指令,TiKV 在 write CF 中写入最终提交记录 ,Key 格式为 key_commit timestamp,Value 关联事务的 start timestamp,标记数据版本的有效性。
  3. 锁清理操作 :TiKV 在 lock CF 中插入对应锁的 delete 标记,解除对数据的锁定,允许其他事务访问。
三、 TiKV 读流程逻辑

当其他事务读取数据时,会遵循以下步骤:

  1. 先在 write CF 中,根据 Key 查找最近的已提交记录时间 ,获取对应的 start timestamp 和 key_commit timestamp
  2. 根据 keystart timestamp的格式,到 default CF 中读取对应版本的具体数据。

++划重点:write列中,写入的一行数据长度小于255B,存储到write cf否则存储到default cf中
default:存储超过255字节长度的数据(既有提交信息又有修改数据)++

3、TiDB 跨 TiKV 节点分布式事务问题及解决方案

明确定义:当所有需要修改的数据在同一个节点中,即为集中式,反之多个实例中,即为分布式

一、 问题场景

  1. 节点操作不一致:事务涉及的部分 TiKV 节点(如 node1)预提交成功,另一部分节点(如 node2)因宕机、网络异常等原因提交失败。
  2. 节点宕机后的数据恢复冲突 :TiKV node2 宕机重启后,发现:
    • 自身在预提交阶段(Prewrite) 已持久化的数据和信息仍存在;
    • 在lock找自身节点,有指向锁,即主锁所在的 node1 中,node1中数据的锁已经被删除(因为事务在其他节点已完成 Commit 或 Rollback);
    • 此时 node2 根据node1的锁信息判断,需要补全事务操作以保证全局一致性。

总结:即一个事务的成功或者是不成功就看第一行的主锁是否完成

二、 TiDB 的核心解决机制:基于主锁的事务状态仲裁 + 二阶段提交(2PC)强一致性

TiDB 解决上述问题的核心逻辑,是以主锁的状态作为事务全局状态的唯一判断依据,配合重试、清理机制保证事务最终一致性。

1. 预提交阶段(Prewrite):主锁绑定全局事务状态
  • 事务在多个 TiKV 节点执行预提交时,仅在主键行所在的 TiKV 节点(如 node1) 写入主锁 ,其他节点(如 node2)的锁信息均通过 @key 指向主锁。
  • 预提交成功的判定标准:所有涉及的 TiKV 节点都必须预提交成功 。只要有一个节点(如 node2)预提交失败,TiDB Server 会立即发起全局回滚,所有已预提交节点删除数据和锁信息。
  • 作用:从源头避免 "部分节点成功、部分失败" 的不一致状态。
2. 宕机节点重启后的事务补偿机制

当指向主锁节点宕机重启后,会触发锁清理与事务状态校验流程,核心步骤如下:

  1. 节点自检 :节点扫描自身 lock CF 中未清理的锁信息,发现这些锁的 @key 指向 node1 的主锁。
  2. 主锁状态仲裁 :指向主锁节点向主锁所在的节点 发起请求,查询主锁的当前状态:
    • 情况 1:主锁已删除,且 write CF 存在对应 Commit 记录(prewrite)
      • 判定:事务已全局提交。
      • 操作:指向主锁节点在自身 write CF 补写提交记录(关联 commitTSstartTS),然后删除 lock CF 中的锁信息,完成事务提交。
    • 情况 2:主锁已删除,且无 Commit 记录
      • 判定:事务已全局回滚。
      • 操作:node2 删除自身 default CF 中预提交的数据,同时清理 lock CF 中的锁信息,完成事务回滚。
    • 情况 3:主锁仍存在
      • 判定:事务处于未完成状态(可能是 TiDB Server 宕机或网络超时)。
      • 处理操作:触发事务超时机制,由 PD 选出的事务协调者节点,根据主锁超时时间决定提交或回滚。
3. 关键补充:锁超时与异步清理机制
  • 锁超时保护:所有锁都有超时时间,若主锁超时仍未被清理,集群会自动触发仲裁流程,避免事务长期阻塞。
  • 异步 GC 清理:事务完成后,旧版本数据不会立即删除,而是由 GC 线程定期清理,既保证读一致性,又避免存储膨胀。

总结

  • 事务成功的唯一标准:主锁所在节点完成 Commit,且所有涉及节点补全提交记录
  • 事务回滚的唯一标准:主锁被删除且无 Commit 记录,所有涉及节点清理预提交数据

二、MVCC(多版本控制)


一、 无 MVCC 时的性能问题

当 TiKV 未引入 MVCC 机制时,跨节点分布式事务 会面临严重的并发性能问题,核心表现为:

  1. 读写阻塞 :事务对跨 TiKV 节点的数据执行修改 操作时,会在主锁所在节点加主锁,其他节点的锁信息指向主锁。此时,其他事务访问数据时,都会直接读取到锁信息并被阻塞,集群退化为串行执行模式
  2. 性能灾难 :当处理 GB 级等更高级别数据时,大量数据被加锁,会导致后续所有相关事务排队等待,系统吞吐量 急剧下降
  3. 一致性与性能的矛盾:无 MVCC 时,只能通过 "加锁阻塞" 保证事务隔离性

引入:从上述缺乏MVCC机制来说,对于其它事务想读或者写入正在事务数据皆不可能,无法兼顾并发读写需求 ,那如何解决呢?MVCC 可以使其它事务可以读取正在运行中的数据的不同版本

二、 MVCC :读写不阻塞

场景

  • 事务1:未提交
  • 事务2:已提交(部分与事务1相同的数据)

MVCC 的本质是将数据复制多份 ,通过时间戳标记版本的不同,让读写操作互不干扰。根据上述场景,MVCC 的优化逻辑如下:

  1. 写操作不覆盖旧数据 :事务 1 修改相同数据 时,不会删除或覆盖事务 2 已提交的旧版本数据,而是生成一个新的版本数据 ,并绑定事务 1 的 start timestamp
  2. 读操作选择可见版本 :其他事务读取数据时,不会直接访问被锁的新版本数据,而是根据自身的 start timestamp,读取事务 2 已提交的旧版本数据,无需等待事务 1 提交或回滚
  3. 锁只阻塞写冲突,不阻塞读 :只有当其他事务尝试修改 同一数据时,才会被锁阻塞;读操作完全不受影响,从根本上提升并发性能

三、 TiKV 中 MVCC 的具体实现(结合跨节点事务场景与方法)

TiKV 基于 default CFwrite CFlock CF 三个列簇,配合 PD 提供的全局时间戳,实现了支持跨节点事务的 MVCC 机制,具体落地逻辑如下:

1. 数据版本存储:default CF 多版本化

  • 存储格式 :所有数据版本按 key_startTS( key_startstamp``) 的格式存储在 default CF 中,同一 key 对应多个不同 startTS 的版本
    • 事务 2 已提交:数据版本为 key_TS2TS2 是事务 2 的 commit timestamp),且在 write CF 中有提交记录
    • 事务 1 未提交:生成新版本 key_TS1TS1 是事务 1 的 start timestamp),仅写入 default CF,未在 write CF 生成提交记录,同时在 lock CF 加锁
  • 跨节点存储 :不同版本的同一 key 可能分布在不同 TiKV 节点,但 startTScommitTS 是 PD 分配的全局时间戳,保证跨节点版本的有序性

2. 版本索引与可见性判断:write CF

write CF 是 MVCC 的版本元数据索引 ,存储格式为 (key_commitTS,对应版本的 startTS) ,当其他事务读取跨节点数据时,执行以下可见性判断逻辑:

  1. 读取事务先获取自身的 startTS(由 PD 分配)
  2. 针对每个 TiKV 节点的目标 key,在 write CF 中查找最大的 commitTS 且满足已提交版本的 commitTS 必须小于读事务的 startTS的记录,(需记住,所有的顺序都是从上到下,id大的在下,id小的在上,时间也是一样)
  3. 根据该记录的 startTS,到对应 TiKV 节点的 default CF 中读取 key_startTS 的数据版本。
    • 对于事务 1 未提交的数据:write CF 中无对应的 commitTS 记录,存储格式(key,startTS ),不读取
    • 对于事务 2 已提交的数据:write CF 中存在有效 commitTS 记录,存储格式(key_commitTS,对应版本的 startTS) ,正常读取

3. 锁与版本的协同:lock CF 只控写、不控读

在跨节点事务中,lock CF 的作用被 MVCC 限制为仅阻塞写冲突,具体规则如下:

  • 写操作 :其他事务尝试修改 同一 key 时,会先检查 lock CF。若存在锁(事务 1 未提交),则阻塞等待,避免多版本冲突
  • 读操作 :其他事务读取时,直接忽略 lock CF 的锁信息,通过 write CF 查找历史提交版本 ,去deault CF读取数据,实现读写不阻塞
  • 跨节点锁校验 :当某 TiKV 节点宕机重启后,通过锁的 @key 指向主锁节点,校验主锁状态,再决定是补全提交版本还是清理未提交版本

4. 旧版本清理:GC 机制释放存储空间

  • 未提交的事务(如事务 1 回滚):其生成的 key_TS1 版本会被直接清理,不会占用长期存储
  • 已提交的旧版本(如事务 2 的版本):TiDB Server 会定期触发 GC 流程,清理超过 GC lifetime 的旧版本数据,避免 default CF 存储膨胀
相关推荐
初次攀爬者2 天前
ZooKeeper 实现分布式锁的两种方式
分布式·后端·zookeeper
爱可生开源社区2 天前
2026 年,优秀的 DBA 需要具备哪些素质?
数据库·人工智能·dba
随逸1772 天前
《从零搭建NestJS项目》
数据库·typescript
加号33 天前
windows系统下mysql多源数据库同步部署
数据库·windows·mysql
シ風箏3 天前
MySQL【部署 04】Docker部署 MySQL8.0.32 版本(网盘镜像及启动命令分享)
数据库·mysql·docker
李慕婉学姐3 天前
Springboot智慧社区系统设计与开发6n99s526(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
百锦再3 天前
Django实现接口token检测的实现方案
数据库·python·django·sqlite·flask·fastapi·pip
tryCbest3 天前
数据库SQL学习
数据库·sql
jnrjian3 天前
ORA-01017 查找机器名 用户名 以及library cache lock 参数含义
数据库·oracle
十月南城3 天前
数据湖技术对比——Iceberg、Hudi、Delta的表格格式与维护策略
大数据·数据库·数据仓库·hive·hadoop·spark