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 存储膨胀
相关推荐
5 小时前
TIDB——TIKV——RocksDB
数据库·分布式·tidb·分布式数据库·
Gauss松鼠会5 小时前
【openGauss】如何在openGauss中实现类似Oracle中constructor function、member function的功能
数据库·oracle·database·gaussdb
Cat God 0075 小时前
Kafka单机搭建(一)
分布式·kafka
yumgpkpm5 小时前
Cloudera CDP 7.3下载地址、方式,开源适配 CMP 7.3(或类 CDP 的 CMP 7.13 平台,如华为鲲鹏 ARM 版)值得推荐
大数据·hive·hadoop·分布式·华为·开源·cloudera
roman_日积跬步-终至千里5 小时前
【后端】预生产环境与生产环境数据库表隔离方案
数据库
Chasing__Dreams5 小时前
kafka--基础知识点--6.3--leader epoch机制
分布式·kafka
过期动态5 小时前
JDBC进阶篇:拓展功能与连接池运用详解
java·开发语言·数据库·mysql·oracle·intellij-idea·mybatis
L、2185 小时前
Flutter 与 OpenHarmony 深度集成:构建分布式多端协同应用
分布式·flutter·wpf
yuezhilangniao5 小时前
PostgreSQL vs MySQL:从零开始基础命令对比指南
数据库·mysql·postgresql