一、读写——仅讨论raft与rocksdb层面无mvcc与transaction
一、写入流程
涉及组件TIDB Server、PD、TIKV
各组件所做工作:
1. TiDB Server
- 接收用户写请求,解析为 Key-Value 修改指令
- 向 PD 要两个关键信息:① 该 Key 所属的 Region 及 Leader 节点地址;② 一个起始时间戳(start_ts,用于标记操作顺序)
- 直接把修改指令发给 TiKV 的 Leader 节点
2. PD
- 维护集群导航信息:谁是哪个 Region 的 Leader、数据存在哪里
- 给 TiDB Server 分配 start_ts + 返回数据的存储位置(Region + Leader 地址)
3. TiKV Leader 节点(按顺序执行)
- ① 提议(Proposal):Raftstore Pool接收写请求后,把修改指令包装成 Raft 日志,写入 WAL(预写日志文件)
- ② 持久化(append):Raftstore Pool将该 Raft 日志写入 RocksDB Raft 实例(专门存储 Raft 日志的 RocksDB,非用户数据)完成持久化
- ③ 复制(Replicate):Raftstore Pool把持久化后的 Raft 日志同步给该 Region 的其他 Follower 节点
- ④ 提交(Committed):Raftstore Pool收到多数节点(超过一半)的日志复制确认后,标记该日志 “已提交”,并更新集群
commitIndex(已提交日志的最大索引) - ⑤ 应用(Apply):Raftstore Pool将已提交的日志推送给Apply Pool→Apply Pool解析日志中的 Key-Value 操作,写入存储用户数据的RocksDB,同时更新
applyIndex(已应用到用户数据的最大日志索引)
随:Raftstore Pool与Apply Pool都是线程池,本质都是设定好的执行容器,当然其中的配置信息,你可以设置,需要注意的是这两个都是每个node中都有的
二、读取流程(两种方式,读 “已提交的完整数据”)
方法 1:ReadIndex(从 Leader 读,保证数据最新)
- 用户发起读取请求 → 与 TiDB Server 交互 → TiDB Server 向 PD 请求目标 Key 所属 Region 的 Leader 节点路由信息,PD 返回位置信息
- TiDB Server 携带路由信息,向目标 Leader 节点发起读请求
- Leader 节点确认:
- 基础方案:向集群其他节点发送心跳,确认自己仍是当前 Region 的 Leader(会引入网络延迟)
- 优化方案(Lease Read 本地读):检查当前时间是否在租约有效期
[当前时间, 当前时间+election timeout]内,若在则直接确认 Leader 身份,无需心跳
- 核心步骤:确定 ReadIndex 保证线性一致性
- 原理:Region 内的所有写请求会生成按序排列的 Raft 日志,日志索引单调递增(先提交的日志索引小,后提交的索引大)。读请求需要确保读取到所有在它之前提交的写操作结果,因此需要一个 “最小安全索引” 作为 ReadIndex
- 实现:Leader 直接取当前的
commitIndex(集群已提交的最大 Raft 日志索引)作为 ReadIndex—— 这等价于 “挑一个比所有已提交写请求 ID 都大的标尺”,无需额外查找
- Leader 节点等待本地的
applyIndex(已应用到 RocksDB 的日志索引)追上 ReadIndex(即applyIndex >= ReadIndex),确保所有已提交的写日志都已被 Apply Pool 解析并写入 RocksDB - 待条件满足后,Leader 节点直接从本地 RocksDB 中查找目标 Key 值并返回结果
方法 2:Follower Read(从 Follower 读,减轻 Leader 压力)
- Follower 先从 Leader 同步最新的 “已提交日志索引”(commitIndex)
- 等自己把所有已提交的日志都应用到 RocksDB 后,再返回数据
注:哪怕 Follower 处理得比 Leader 快,也会等 Leader 的最新提交信息,保证读的数据和 Leader 一致
问题聚合
问题1
问题:从 TiDB Server 获取 Leader 节点路由信息,到实际去该节点读取数据的这段时间内,如何保证这个节点仍然是路由所指的leader呢,即合法性?(毕竟集群可能会因热点负载均衡或手动操作触发 Leader 切换)
解决方法
基础方案:心跳确认 Leader 有效性,读取请求到达目标节点后,该节点会先向集群内其他节点发送心跳,确认自己还是当前 Region 的 Leader。这种方式能保证 Leader 身份准确,但会引入额外的网络延迟,影响读取性能。
优化方案:Lease Read(本地读),消除心跳延迟Leader 节点会记录两个关键时间:
- 当前时间
- Raft 协议的
election timeout(选举超时时间)Leader 会划定一个租约有效期:[当前时间, 当前时间 + election timeout]。在这个时间段内,集群不会触发新的 Leader 选举,因此该节点可以直接确认自己的 Leader 身份,无需发送心跳。
问题2
当用户 A 修改数据的写请求执行到 “Committed(提交)” 阶段但未完全落地时,若用户 B 此时读取该数据,如何避免读到旧数据?是否必须等待用户 A 的写请求完全提交落地后,用户 B 的读请求才能执行?
核心解决思路:通过 ReadIndex 机制保证读取的线性一致性
线性一致性:后发起的读请求,必须能读到先发起的已提交写请求的结果(即用户 B 读数据,必定拿到用户 A 修改后的最新数据)。
具体实现逻辑:
- 写请求的有序性基础:Region 内所有写请求会生成
<Region号_ID, 写入请求ID>的唯一标识,且按 ID 从小到大严格排序 ——ID 越小,写请求越先被集群提交(Committed)。 - ReadIndex 的选取规则:为读请求选取一个 “最小安全索引(ReadIndex)”,这个索引是比当前所有已提交写请求 ID 更大的数值(TiDB 中直接取 Leader 节点当前的
commitIndex,即集群已提交的最大写日志索引),并将该 ReadIndex 记录在 Raftstore Pool 中。 - 读请求的执行条件:读请求不会立即执行,必须等待本地 Apply Pool 维护的
applyIndex(已落地到 RocksDB 的最大日志索引)追上 ReadIndex(applyIndex ≥ ReadIndex)。- 这意味着:所有 ID 小于 ReadIndex 的写请求(包括用户 A 的修改请求),都已完成集群提交且落地到 RocksDB 后,读请求才会执行。
- 最终效果:读请求不会被 “堵塞” 在网络层面,而是通过索引等待机制,确保读取到的是前序所有已提交写请求修改后的最新数据,既保证线性一致性,又避免无意义的等待。
问题3
问题:在方法2中,可能会读取到「未被集群确认提交,但 Follower 本地提前落地」的数据,那如何保持数据一致呢?
可能会读取未确认数据核心产生原因:Leader 落地数据慢,Follower 反而快
1. Leader Apply 慢:写请求压力 + 多 Region 资源分摊
Leader 是 Region 的唯一写入口,需同时承担两类核心任务,导致 Apply 速度慢
简单说:Leader 既要 “存储与写请求”,在高并发场景下,让多线程分摊多个 Region 的 Apply 任务,Region 的 Apply 资源被稀释,热点 Region日志会持续生成并提交,单个Region对应的 Apply 线程来不及处理热点导致applyIndex追不上commitIndex,出现日志堆积,表现为 Apply 速度慢
2. Follower Apply 快:无写压力 + 多线程专注处理
Follower 不接收外部写请求,核心工作仅为:
- 从 Leader 同步 Raft 日志,写入本地 WAL 持久化
- 待 Leader 确认日志 “已提交” 后,更新commitindex,Apply Pool 的多线程专注处理同步过来的日志
由于无写请求干扰,Follower 的多线程可集中资源处理各 Region 的 Apply 任务,不会出现日志堆积,因此applyIndex能快速跟上已提交日志进度,甚至比 Leader 更快
总结:Follower 节点把 Raft 日志解析并写入 RocksDB 的速度,比 Leader 节点更快,导致 Follower 本地的applyIndex数值,会比 Leader 节点的applyIndex数值更大。
核心解决方法
前提:
前提 1:单个region的落地操作必须按顺序来
TiKV 里的每个 Region,把日志解析后写入 RocksDB后,必须按日志提交的顺序一步步执行。
前提 2:多线程是为了让不同分区并行干活
不管是 Leader 还是 Follower 节点,都能通过raftstore.apply-pool-size配置多个 Apply 线程。这些线程的作用是同时处理不同分区的落地任务,如线程 1 处理regionA、线程 2 处理region B,提升整个节点的工作效率,而不是让一个region的任务被多个线程同时处理即单个region串行化操作,这样能避免同时写入导致数据顺序乱掉,保证前提交前落地。
一、关键保障:Follower Apply 快但不破坏一致性
Follower 即便applyIndex更高(Apply 更快,即本地落地的日志索引比 Leader 大),也不会导致读请求获取不一致数据,核心靠两层机制保障:
1. Follower 读请求以 Leader 的 commitIndex 为 “安全标尺”
Follower Read 的核心规则:
- Follower 收到读请求后,不会直接使用自身
applyIndex判断,而是先向 Leader 同步最新的commitIndex(记为leader_commitIndex); - 即便 Follower 自身
applyIndex已超过leader_commitIndex(比如 Leader commitIndex=100,Follower applyIndex=105),也仅等待applyIndex ≥ leader_commitIndex,且只读取该标尺及之前的日志对应数据,避免读取本地提前应用但集群未确认的日志。
2. 集群一致性的核心:Leader 的 commitIndex 是全局唯一标准
- 集群 “已提交数据” 的唯一判定依据是 Leader 确认的
commitIndex(需多数节点确认日志提交),非单个节点的applyIndex - Follower 提前 Apply 的日志(如索引 101-105),本质是 “未被集群认可提交的日志”,仅为本地提前解析落地,读请求会严格过滤这类数据,确保只读取 Leader 确认的、集群一致的已提交数据
二、Coprocessor协同处理器
- 问题:如果所有数据都拉到 TiDB Server 再计算(当TIDB server接收用户的sql语句,调用node节点中的数据,由于数据分散,就会造成数据的聚合,以及需要统计信息),那么网络和 Server 负载会很大。
- 解决:让 TiKV 的 Coprocessor 先做 “初步计算”—— 比如过滤不需要的数据、统计数量(count/sum),再把结果传给 TiDB Server 做最终整合。
- 核心:把能在数据存储端做的计算,就不在 Server 端做,提升效率。
随:因为TIDB Server在TIKV之上,所以叫做计算下推,这种做法减少着TiDB Server的压力