TIDB——TIKV——读写与coprocessor

一、读写 ------仅讨论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 节点(按顺序执行)

  1. ① 提议(Proposal):Raftstore Pool 接收写请求后,把修改指令包装成 Raft 日志,写入 WAL(预写日志文件)
  2. ② 持久化(append):Raftstore Pool 将该 Raft 日志写入 RocksDB Raft 实例(专门存储 Raft 日志的 RocksDB,非用户数据)完成持久化
  3. ③ 复制(Replicate):Raftstore Pool 把持久化后的 Raft 日志同步给该 Region 的其他 Follower 节点
  4. ④ 提交(Committed):Raftstore Pool 收到多数节点(超过一半)的日志复制确认后,标记该日志 "已提交",并更新集群 commitIndex(已提交日志的最大索引)
  5. ⑤ 应用(Apply):Raftstore Pool 将已提交的日志推送给 Apply PoolApply Pool 解析日志中的 Key-Value 操作,写入存储用户数据的 RocksDB ,同时更新 applyIndex(已应用到用户数据的最大日志索引)

随:Raftstore PoolApply Pool 都是线程池,本质都是设定好的执行容器,当然其中的配置信息,你可以设置,需要注意的是这两个都是每个node中都有的

二、读取流程(两种方式,读 "已提交的完整数据")

方法 1:ReadIndex(从 Leader 读,保证数据最新)

  1. 用户发起读取请求 → 与 TiDB Server 交互 → TiDB Server 向 PD 请求目标 Key 所属 Region 的 Leader 节点路由信息,PD 返回位置信息
  2. TiDB Server 携带路由信息,向目标 Leader 节点发起读请求
  3. Leader 节点确认:
    • 基础方案:向集群其他节点发送心跳确认自己仍是当前 Region 的 Leader(会引入网络延迟)
    • 优化方案(Lease Read 本地读):检查当前时间是否在租约有效期 [当前时间, 当前时间+election timeout] 内,若在则直接确认 Leader 身份,无需心跳
  4. 核心步骤:确定 ReadIndex 保证线性一致性
    • 原理:Region 内的所有写请求会生成按序排列的 Raft 日志,日志索引单调递增(先提交的日志索引小,后提交的索引大)。读请求需要确保读取到所有在它之前提交的写操作结果,因此需要一个 "最小安全索引" 作为 ReadIndex
    • 实现:Leader 直接取当前的 commitIndex(集群已提交的最大 Raft 日志索引)作为 ReadIndex------ 这等价于 "挑一个比所有已提交写请求 ID 都大的标尺",无需额外查找
  5. Leader 节点等待本地的 applyIndex(已应用到 RocksDB 的日志索引)追上 ReadIndex(即 applyIndex >= ReadIndex),确保所有已提交的写日志都已被 Apply Pool 解析并写入 RocksDB
  6. 待条件满足后,Leader 节点直接从本地 RocksDB 中查找目标 Key 值并返回结果

方法 2:Follower Read(从 Follower 读,减轻 Leader 压力)

  1. Follower 先从 Leader 同步最新的 "已提交日志索引"(commitIndex)
  2. 等自己把所有已提交的日志都应用到 RocksDB 后,再返回数据

注:哪怕 Follower 处理得比 Leader 快,也会等 Leader 的最新提交信息,保证读的数据和 Leader 一致

问题聚合

问题1

问题:从 TiDB Server 获取 Leader 节点路由信息,到实际去该节点读取数据的这段时间内,如何保证这个节点仍然是路由所指的leader呢,即合法性?(毕竟集群可能会因热点负载均衡或手动操作触发 Leader 切换)

解决方法
  1. 基础方案:心跳确认 Leader 有效性,读取请求到达目标节点后,该节点会先向集群内其他节点发送心跳,确认自己还是当前 Region 的 Leader。这种方式能保证 Leader 身份准确,但会引入额外的网络延迟,影响读取性能。

  2. 优化方案:Lease Read(本地读),消除心跳延迟Leader 节点会记录两个关键时间:

    • 当前时间
    • Raft 协议的 election timeout(选举超时时间)Leader 会划定一个租约有效期[当前时间, 当前时间 + election timeout]。在这个时间段内,集群不会触发新的 Leader 选举,因此该节点可以直接确认自己的 Leader 身份,无需发送心跳。

问题2

当用户 A 修改数据的写请求执行到 "Committed(提交)" 阶段但未完全落地时,若用户 B 此时读取该数据,如何避免读到旧数据?是否必须等待用户 A 的写请求完全提交落地后,用户 B 的读请求才能执行?

核心解决思路:通过 ReadIndex 机制保证读取的线性一致性

线性一致性:后发起的读请求,必须能读到先发起的已提交写请求的结果(即用户 B 读数据,必定拿到用户 A 修改后的最新数据)。

具体实现逻辑:

  1. 写请求的有序性基础:Region 内所有写请求会生成<Region号_ID, 写入请求ID>的唯一标识,且按 ID 从小到大严格排序 ------ID 越小,写请求越先被集群提交(Committed)。
  2. ReadIndex 的选取规则:为读请求选取一个 "最小安全索引(ReadIndex)",这个索引是比当前所有已提交写请求 ID 更大的数值 (TiDB 中直接取 Leader 节点当前的commitIndex,即集群已提交的最大写日志索引),并将该 ReadIndex 记录在 Raftstore Pool 中。
  3. 读请求的执行条件:读请求不会立即执行,必须等待本地 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的压力

相关推荐
李广坤12 小时前
MySQL 大表字段变更实践(改名 + 改类型 + 改长度)
数据库
初次攀爬者2 天前
ZooKeeper 实现分布式锁的两种方式
分布式·后端·zookeeper
爱可生开源社区2 天前
2026 年,优秀的 DBA 需要具备哪些素质?
数据库·人工智能·dba
随逸1772 天前
《从零搭建NestJS项目》
数据库·typescript
加号32 天前
windows系统下mysql多源数据库同步部署
数据库·windows·mysql
シ風箏2 天前
MySQL【部署 04】Docker部署 MySQL8.0.32 版本(网盘镜像及启动命令分享)
数据库·mysql·docker
李慕婉学姐2 天前
Springboot智慧社区系统设计与开发6n99s526(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
百锦再2 天前
Django实现接口token检测的实现方案
数据库·python·django·sqlite·flask·fastapi·pip
tryCbest2 天前
数据库SQL学习
数据库·sql
jnrjian2 天前
ORA-01017 查找机器名 用户名 以及library cache lock 参数含义
数据库·oracle