声明:本文基于两个以及以上实例讨论;并且所讨论的锁皆为悲观锁
注:
- 大部分时候,节点就是实例,即一个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、事务具体提交流程
一、 事务启动阶段
- 事务begin 触发后,TiDB Server 向 PD 组件 请求获取时间戳 TSO ,该 TSO 作为事务的 start timestamp(开始时间戳)
- TiDB Server 读取需要修改的数据,并在内存中执行数据的增删改操作
二、 事务提交阶段(二阶段提交 - 2PC)
阶段 1:Prewrite(预提交)
- TiDB Server 将修改后的数据、锁信息,发送至对应的 TiKV 节点 进行持久化。
- TiKV 会通过 3 个 Column Family(列簇) 存储 相关信息,分工如下:
- default CF :存储修改后的数据 。数据的 Key 格式为
key_start timestamp,无论增删改查操作,均以新数据版本的形式存储 - lock CF :存储事务的锁信息,采用主锁机制 :
- 仅为事务修改的第一行数据(主键行) 添加主锁:pk
- 其余行的锁信息不单独存储,而是通过
@key的形式指向主锁 - 锁的作用:若其他事务尝试读取或写入这些数据,会被直接阻塞,保证事务隔离性
- write CF :暂存事务的预提交相关信息
- default CF :存储修改后的数据 。数据的 Key 格式为
阶段 2:Commit(提交)与锁清理
- 当 Prewrite 阶段全部成功后,TiDB Server 再次向 PD 请求**
commit timestamp(** 提交时间戳**)**。 - TiDB Server 向 TiKV 发送 Commit 指令,TiKV 在 write CF 中写入最终提交记录 ,Key 格式为
key_commit timestamp,Value 关联事务的start timestamp,标记数据版本的有效性。 - 锁清理操作 :TiKV 在
lock CF中插入对应锁的 delete 标记,解除对数据的锁定,允许其他事务访问。
三、 TiKV 读流程逻辑
当其他事务读取数据时,会遵循以下步骤:
- 先在 write CF 中,根据 Key 查找最近的已提交记录时间 ,获取对应的
start timestamp和 key_commit timestamp。 - 根据
key与start timestamp的格式,到 default CF 中读取对应版本的具体数据。
++划重点:write列中,写入的一行数据长度小于255B,存储到write cf否则存储到default cf中
default:存储超过255字节长度的数据(既有提交信息又有修改数据)++
3、TiDB 跨 TiKV 节点分布式事务问题及解决方案
明确定义:当所有需要修改的数据在同一个节点中,即为集中式,反之多个实例中,即为分布式
一、 问题场景
- 节点操作不一致:事务涉及的部分 TiKV 节点(如 node1)预提交成功,另一部分节点(如 node2)因宕机、网络异常等原因提交失败。
- 节点宕机后的数据恢复冲突 :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. 宕机节点重启后的事务补偿机制
当指向主锁节点宕机重启后,会触发锁清理与事务状态校验流程,核心步骤如下:
- 节点自检 :节点扫描自身
lock CF中未清理的锁信息,发现这些锁的@key指向 node1 的主锁。 - 主锁状态仲裁 :指向主锁节点向主锁所在的节点 发起请求,查询主锁的当前状态:
- 情况 1:主锁已删除,且 write CF 存在对应 Commit 记录(prewrite)
- 判定:事务已全局提交。
- 操作:指向主锁节点在自身
write CF补写提交记录(关联commitTS和startTS),然后删除lock CF中的锁信息,完成事务提交。
- 情况 2:主锁已删除,且无 Commit 记录
- 判定:事务已全局回滚。
- 操作:node2 删除自身
default CF中预提交的数据,同时清理lock CF中的锁信息,完成事务回滚。
- 情况 3:主锁仍存在
- 判定:事务处于未完成状态(可能是 TiDB Server 宕机或网络超时)。
- 处理操作:触发事务超时机制,由 PD 选出的事务协调者节点,根据主锁超时时间决定提交或回滚。
- 情况 1:主锁已删除,且 write CF 存在对应 Commit 记录(prewrite)
3. 关键补充:锁超时与异步清理机制
- 锁超时保护:所有锁都有超时时间,若主锁超时仍未被清理,集群会自动触发仲裁流程,避免事务长期阻塞。
- 异步 GC 清理:事务完成后,旧版本数据不会立即删除,而是由 GC 线程定期清理,既保证读一致性,又避免存储膨胀。
总结
- 事务成功的唯一标准:主锁所在节点完成 Commit,且所有涉及节点补全提交记录
- 事务回滚的唯一标准:主锁被删除且无 Commit 记录,所有涉及节点清理预提交数据
二、MVCC(多版本控制)
一、 无 MVCC 时的性能问题
当 TiKV 未引入 MVCC 机制时,跨节点分布式事务 会面临严重的并发性能问题,核心表现为:
- 读写阻塞 :事务对跨 TiKV 节点的数据执行修改 操作时,会在主锁所在节点加主锁,其他节点的锁信息指向主锁。此时,其他事务访问数据时,都会直接读取到锁信息并被阻塞,集群退化为串行执行模式
- 性能灾难 :当处理 GB 级等更高级别数据时,大量数据被加锁,会导致后续所有相关事务排队等待,系统吞吐量 急剧下降
- 一致性与性能的矛盾:无 MVCC 时,只能通过 "加锁阻塞" 保证事务隔离性
引入:从上述缺乏MVCC机制来说,对于其它事务想读或者写入正在事务数据皆不可能,无法兼顾并发读写需求 ,那如何解决呢?MVCC 可以使其它事务可以读取正在运行中的数据的不同版本
二、 MVCC :读写不阻塞
场景
- 事务1:未提交
- 事务2:已提交(部分与事务1相同的数据)
MVCC 的本质是将数据复制多份 ,通过时间戳标记版本的不同,让读写操作互不干扰。根据上述场景,MVCC 的优化逻辑如下:
- 写操作不覆盖旧数据 :事务 1 修改相同数据 时,不会删除或覆盖事务 2 已提交的旧版本数据,而是生成一个新的版本数据 ,并绑定事务 1 的
start timestamp - 读操作选择可见版本 :其他事务读取数据时,不会直接访问被锁的新版本数据,而是根据自身的
start timestamp,读取事务 2 已提交的旧版本数据,无需等待事务 1 提交或回滚 - 锁只阻塞写冲突,不阻塞读 :只有当其他事务尝试修改 同一数据时,才会被锁阻塞;读操作完全不受影响,从根本上提升并发性能
三、 TiKV 中 MVCC 的具体实现(结合跨节点事务场景与方法)
TiKV 基于 default CF、write CF、lock CF 三个列簇,配合 PD 提供的全局时间戳,实现了支持跨节点事务的 MVCC 机制,具体落地逻辑如下:
1. 数据版本存储:default CF 多版本化
- 存储格式 :所有数据版本按
key_startTS(key_startstamp``)的格式存储在default CF中,同一 key 对应多个不同 startTS 的版本- 事务 2 已提交:数据版本为
key_TS2(TS2是事务 2 的commit timestamp),且在write CF中有提交记录 - 事务 1 未提交:生成新版本
key_TS1(TS1是事务 1 的start timestamp),仅写入default CF,未在write CF生成提交记录,同时在lock CF加锁
- 事务 2 已提交:数据版本为
- 跨节点存储 :不同版本的同一
key可能分布在不同 TiKV 节点,但startTS和commitTS是 PD 分配的全局时间戳,保证跨节点版本的有序性
2. 版本索引与可见性判断:write CF
write CF 是 MVCC 的版本元数据索引 ,存储格式为 (key_commitTS,对应版本的 startTS) ,当其他事务读取跨节点数据时,执行以下可见性判断逻辑:
- 读取事务先获取自身的
startTS(由 PD 分配) - 针对每个 TiKV 节点的目标
key,在write CF中查找最大的commitTS且满足已提交版本的 commitTS 必须小于读事务的 startTS的记录,(需记住,所有的顺序都是从上到下,id大的在下,id小的在上,时间也是一样) - 根据该记录的
startTS,到对应 TiKV 节点的default CF中读取key_startTS的数据版本。- 对于事务 1 未提交的数据:
write CF中无对应的commitTS记录,存储格式(key,startTS),不读取 - 对于事务 2 已提交的数据:
write CF中存在有效commitTS记录,存储格式(key_commitTS,对应版本的startTS),正常读取
- 对于事务 1 未提交的数据:
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存储膨胀