想象你是一位超大城市的总图书管理员,手底下有上百家分馆,市民不管从哪个分馆借书,看到的都必须是同一套“最新藏书记录”。为了保证这一点,你规定每本书至少在三个分馆各存一份;分馆之间必须有一份“抄本核对协议”,确认哪一本才是当前权威版本;每一次借还、批注都要留下修改历史,让读者在任何时刻都能看到一致的视图;当某个分馆的藏书超过一万册时,还要把书架拆成若干小区域,分给更多分馆共同负担。这些图书馆管理员的日常烦恼,正是 TiKV 这个分布式事务 KV 存储引擎每天都在解决的问题。 TiKV 是 TiDB 的存储底座,负责把海量的键值对数据可靠地分散在上百台机器上。它用 Raft 算法让每个数据分片拥有多个副本,用 MVCC 让读和写互不阻塞,用类 Percolator 的两阶段提交让跨节点的修改拥有原子性,又用 Region 的自动分裂与调度,让系统在扩容、故障时持续保持平衡。这篇文章就像一次馆长培训课,从书架怎么摆、分馆怎么核对,一路讲到整座联盟图书馆如何自动运转。
对 AI 从业者而言,弄懂 TiKV 等于拿到了分布式数据库内核的“操作手册”。无论是做分布式系统架构、性能调优,还是参与 TiDB 开源社区,你都能一眼看穿存储系统在每个时刻正在做什么、为什么这样做。
可视化演示
演示TiKV核心机制,包括Raft多副本一致性、MVCC多版本并发控制、Percolator分布式事务模型和Region分裂调度,通过交互式可视化展示分布式存储底层原理。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Raft多副本一致性 Leader接收写请求 日志复制到Follower 多数派确认后提交 三副本保证数据不丢失
总体架构
想象你是那座超大城市联盟图书馆的第一任总馆长。市民不需要知道书放在哪个分馆,他们只面对一个统一的“检索台”。为了让这套系统能无限扩张,你不会把全城书目抄成一本大厚册子,而是把书架按照编号切成许多连续的区块,每块配三位馆员负责。这正是 TiKV 的顶层设计思路:把数据切成一格一格的 Region,每个 Region 就是一段连续键值的“书架”。
从数据模型看,TiKV 是一个 Key-Value 存储:每个数据项都是一对 (key, value),其中 key 是二进制字符串,value 是字节数组。所有 key 按字典序排列,被切分到不同的 Region 中。每个 Region 默认由 3 个副本组成,这 3 个副本分布在不同的物理节点上,构成一个 Raft 复制组,共同对外提供“这一个 Region 不丢数据”的承诺。
整套系统在逻辑上分为四层:最底层用 RocksDB 做单机持久化存储;往上是 Raft 层,负责副本间的日志复制与一致性;再往上是事务层,实现分布式事务;最顶上是 PD(Placement Driver),负责记录每个 Region 的位置、管理分裂与故障迁移。分层设计的妙处在于,每一层都可以独立演进,就像图书馆的采购、编目、馆际互借各有专人负责一样。
回到图书馆的比喻:只有当每格书架都边界清晰、责任到人,整座联盟图书馆才能永远保持“无论书在哪个分馆,市民总能找到它”的承诺。TiKV 的顶层架构,归根结底做的就是这件事。
- 键值对集合Region 是一组
(key, value)条目的集合,就像一格书架上的所有藏书。 - 区间边界条件
start\_key ≤ key < end\_key是左闭右开的范围限制,保证任意 key 恰好属于一个 Region,不会重叠。 - 整体覆盖所有 Region 的并集等于全量键空间,就像所有书架并在一起就是整座图书馆的馆藏。
Raft一致性
三个分馆各存了一本《国富论》,但其中一本被读者做了批注,另外两本没有。你作为馆长必须规定:到底哪一本才是“官方版本”?如果每次有读者来问,都临时开三方会议,效率太低。于是你指定:平时由“主馆”负责对外回答,但它改书之前,必须把改动同步给其他分馆,得到多数确认后才能正式生效。这就是 Raft 协议的核心思路。
Raft 是 TiKV 中每个 Region 副本组使用的共识算法。它把一个复制组里的节点分成三种角色:Leader(主副本)、Follower(从副本)和 Candidate(竞选者)。正常情况下,只有 Leader 接收客户端写入,它把写操作追加为一条日志,并行发给所有 Follower,等多数派节点确认后,才把这条日志“提交”并应用到状态机。如果 Leader 宕机,Follower 会通过选举选出新 Leader,选举同样遵循多数派原则。
Raft 的核心价值在于“让多数派说了算”。它解决了三个子问题:Leader 选举、日志复制、成员变更。成员变更允许在不中断服务的情况下增减副本,比如把一个 Region 从 3 副本扩容到 5 副本。整个过程不需要人工指定谁当主,系统自己就能达成一致——就像图书馆联盟的总馆名号不是永久固定的,谁的能力获得多数分馆认可,谁就接班。
分馆之间核对抄本、按多数派盖章生效,听起来繁琐,却换来一个极其宝贵的性质:只要多数派节点还活着,整组服务就能继续。图书馆不会因为一家分馆临时停电就关门,TiKV 也不会因为一台机器宕机就丢数据。
- 副本总数 nn 代表当前 Raft 复制组中参与投票的副本数量,例如 3 副本或 5 副本。
- 向下取整
⌊n/2⌋表示对 n 的一半向下取整,例如 3 副本时取 1。 - 超过半数加上 1 之后,q 必然大于 n/2,这就是“多数派”的数学含义。
MVCC机制
图书馆的借阅卡片上,如果管理员每次都在同一格里涂改,借阅者就无法知道“上次归还时书是什么状态”。于是你规定:每一次借出、归还、续借都追加一行记录,标注时间和操作人。读者查询时,系统只展示“他查询那一刻之前最接近的一条有效记录”——这就是多版本并发控制(MVCC)在图书馆里的样子。
在 TiKV 中,每个 key 可以同时存在多个版本的数据,每个版本都带有一个全局递增的提交时间戳 commit_ts。当事务读取某个 key 时,它会携带一个 read_ts(读取时间戳),存储引擎只返回满足“commit_ts ≤ read_ts”的最新版本。这样,一个长时间运行的事务也能看到一致的快照,而不会被其他并发写入干扰。
MVCC 最大的收益是“读写互不阻塞”:写操作永远追加新版本,不覆盖旧版本;读操作永远在旧版本快照上进行,不需要加锁。这样一来,在线业务最常见的读多写少场景就不会因为锁竞争而卡顿。不过,版本越积越多会浪费空间,因此 TiKV 会定期做垃圾回收,清理那些“任何活跃事务都看不到”的旧版本。
图书馆的读者永远看不到“正在被涂改的那一行”,看到的是他进门那一刻之前最完整的记录。MVCC 的智慧正是如此:用历史版本的叠加代替就地修改,让并发不再是敌人,而是秩序的一部分。
- 版本 vv 表示某个 key 的一条历史版本记录,包含当时的 value 和 commit_ts。
- 读取时间戳 tsts 是读事务开始时获取的全局时间戳,决定了该事务能看到哪些版本。
- 已提交且不晚于
v.commit_ts ≤ ts是可见性判据,只有“已经提交”且“提交得足够早”的版本才被读到。
事务模型
一位市民在分馆 A 还掉旧书,同时想从分馆 B 借走另一本书。这两个操作必须“要么都成功、要么都不成功”——否则就会出现书丢了却没借到的混乱。馆长定下一个两步协议:第一步,先在两个分馆的账本上写下“此书正在处理中”的锁定标记并预登记;第二步,等两边都确认无误,再盖上“正式生效”的章,同时解除锁定。这个协议,就是 Percolator 两阶段提交模型的现实原型。
TiKV 的分布式事务基于 Google 的 Percolator 论文设计,核心目标是在没有全局锁的前提下,让跨多个 Region、多个 key 的写入拥有原子性。事务开启时先获取一个 start_ts;提交时获取一个更大的 commit_ts。流程分为两阶段:预写(Prewrite)阶段,事务向每个涉及的 key 写入一个带 start_ts 的预存值,并尝试加锁;如果任何 key 的锁已经被其他事务占用,则整个事务回滚。提交(Commit)阶段,所有预写成功后,事务用 commit_ts 打上最终时间戳,释放全部锁,数据对外可见。
这种设计最精妙的地方在于:没有任何中央协调者,每个 key 的锁自包含在自己的记录里。读操作遇到锁时,只需根据锁中的 start_ts 判断持有锁的事务是否已经超时,进而决定等待还是清理。由于 commit_ts > start_ts,所有并发事务都能依据时间戳形成唯一的历史顺序,不会出现“先提交却后可见”的错乱。
借书协议里的“先锁定、后盖章”,映射到数据库就是“先预写、后提交”。它让所有分馆的账本在任意时刻都能达成一致——哪怕没有一个中央指挥官,每个馆员也清楚自己该做什么。
- 遍历所有 key
∀k ∈ T表示对事务 T 写入的每个 key 逐一检查预写结果。 - 预写成功
prewrite(k) = ok表示该 key 成功写入预存值且锁未被他人持有。 - 时间戳递增
commit_ts > start_ts保证“提交发生在开始之后”,这是建立全局历史顺序的基石。 - 两者同时成立用逻辑与连接两个条件,任何一边不满足都不能提交。
Region管理
一万本书堆在同一格书架上,不但找书困难,搬运也极慢。馆长定下规矩:每个书架区块的书目数量超过上限,就必须自动拆成两个新块,分别挪到不同分馆。TiKV 的 Region 分裂逻辑与此几乎完全相同。
TiKV 中每个 Region 默认约 96MB 数据。当 Region 的数据量超过这个阈值时,PD 会指示该 Region 的 Leader 执行分裂操作:把键值范围从中间一分为二,生成两个大小相近的新 Region。分裂过程不需要拷贝数据,因为新 Region 只是改变了键空间的边界,底层存储仍然共享同一份 RocksDB 数据。分裂完成后,新的 Region 元数据会注册到 PD,客户端通过 PD 获取最新的路由信息。
Region 分裂的意义在于“控制单点容量上限”。单个 Region 过大会导致三个问题:一是参与复制的数据量增大,网络和 IO 压力上升;二是热点集中在一个副本组,无法发挥分布式优势;三是故障恢复时补副本的时间变长。通过自动分裂加上 PD 的负载均衡调度,TiKV 能把数据均匀地铺到整个集群中。当某个节点存储接近饱和,PD 还会把 Region 调度到空余节点,这就是所谓的“热点调度”——它会监控读写流量的分布,把热 Region 分散到更多机器上。
书架拆块看似小事,却是联盟图书馆能无限扩容的秘密。因为每一块书架都足够小,搬迁、复制、重新平衡的成本都可控;反过来,TiKV 也靠这套“拆块”机制,让上百台机器的集群像一台巨型计算机一样顺畅运转。
- 当前数据量
size(R)表示 Region R 当前承载的键值数据大小。 - 触发条件当
size(R)大于 96MB 时,分裂条件成立。 - 立即分裂条件为真时系统对 R 执行分裂,把一个 Region 变为两个。
高可用机制
某个深夜,一家重要的分馆突发水管爆裂,书架全部被淹。作为总馆长,你不会让大家等着修好——而是立刻启动预案:从其他分馆调出这本书的备份副本,在新分馆重建书架,同时对外保持“开门营业”的状态。TiKV 的高可用机制,就是这套预案的工程化实现。
在 TiKV 集群中,PD 通过心跳机制检测每个节点的健康状态。如果某个 store 节点心跳超时,PD 会判定它失联,并为其负责的每个 Region 生成“补副本”任务:选择集群中另一个健康节点,从已有副本同步数据,把新的副本建立起来。这个过程依赖 Raft 的成员变更能力,先添加新副本、再移除故障副本,整个过程对客户端透明。
那么,系统到底能容忍多少个节点同时故障?答案取决于副本数。一个 3 副本的 Region,最多容忍 1 个副本故障;5 副本的 Region,最多容忍 2 个。因为只要多数派存活,Raft 就能继续选主、继续提交日志。故障期间读写可能短暂变慢,但不会停机——这就是“无感切换”的含义。数据恢复则依靠日志的追平机制:新副本加入后,Leader 会把缺失的日志项同步过去,直到它与 Leader 状态一致。
回看图书馆的比喻:分馆可以坏,但书不能丢。TiKV 用“多副本 + 多数派 + 自动补位”三重保险,把故障从“事故”变成“日常调度里的一件小事”。这正是分布式系统高可用的本质:不是不故障,而是故障之后能自行恢复。
- 副本总数 nn 是 Region 的副本个数,通常为 3 或 5。
- 最大容忍故障数
(n-1)/2向下取整,得到的 f_max 是能容忍的故障副本上限。 - 多数派仍存活只要存活节点数 ≥ q 即
⌊n/2⌋+1,该 Raft 组仍能正常工作。
AI 应用场景
在线支付系统的分布式事务底座
阶段:在线交易数据存储层
解决的问题:传统单机数据库无法承受高并发支付请求,且跨账户转账涉及多个数据分片,必须保证原子性和一致性。TiKV 通过 Raft 多副本保证数据不丢,通过 Percolator 事务模型保证资金流转的原子性,使任意分片的读写都符合事务语义。
具体案例:某支付平台将用户余额、流水记录存入 TiKV,单笔交易同时更新“扣款方余额”和“收款方余额”两个 key。系统在预写阶段给两个 key 加锁,全部成功后再统一提交;若任一 key 被并发事务锁定,则整体回滚,用户端感受到的只是“稍后再试”。
AI特征存储与模型推理的特征服务
阶段:特征工程与在线推理
解决的问题:推荐系统中用户特征和物品特征需要高并发读写,且要求在特征更新时不影响正在进行的推理请求。TiKV 的 MVCC 让“写特征”和“读特征”互不阻塞,读请求总能拿到某一时间戳下的一致快照,非常适合作为特征在线存储。
具体案例:某短视频推荐系统把用户最近 100 个交互行为编码成 key-value 特征写入 TiKV,模型服务在线读取时携带请求到达时间戳,得到该时刻的特征快照。新行为写入不会干扰正在执行的推理批次,特征更新的延迟由 Raft 复制保证在毫秒级。
物联网设备状态海量存储
阶段:数据采集与状态管理
解决的问题:物联网平台每天接入数百万设备的上下线状态,写入量大、单条数据小,并且必须支持按设备 ID 快速查询。TiKV 的 Region 自动分裂与 PD 热点调度,能把这些高频写入均匀分散到集群中,避免单点瓶颈。
具体案例:某车联网平台将所有车辆的 GPS 坐标、电量、告警标志以“车辆ID_属性”为 key 写入 TiKV。每当某区域车辆激增,PD 会自动识别热点 Region 并将之分片后调度到其他节点,保障整个平台的路由更新和状态查询始终稳定。
Python 演示
#!/usr/bin/env python3"""TiKV核心机制模拟:极简MVCC存储 + 两阶段提交事务。""" from collections import defaultdict class MVCCStore: """用Python模拟TiKV的多版本存储与事务提交语义。""" def __init__(self): self.data = defaultdict(list) # key -> [(commit_ts, value)] self.locks = {} # key -> start_ts,模拟prewrite锁 self.ts = 0 # 全局逻辑时间戳 def next_ts(self): self.ts += 1 return self.ts def get(self, key, read_ts): """读取时间戳下最新可见版本。""" for commit_ts, value in reversed(self.data[key]): if commit_ts is not None and commit_ts <= read_ts: return value return None def prewrite(self, key, value, start_ts): """事务阶段一:加锁并写入预提交版本。""" if key in self.locks: raise RuntimeError("write-write conflict") self.locks[key] = start_ts self.data[key].append((None, value)) # 未提交版本以None标记 def commit(self, keys, start_ts, commit_ts): """事务阶段二:生效提交,并释放锁。""" for key in keys: self.locks.pop(key, None) for i, (ts, val) in enumerate(self.data[key]): if ts is None: self.data[key][i] = (commit_ts, val) break def main(): store = MVCCStore() t1 = store.next_ts() store.prewrite("user:1:age", "20", t1) store.commit(["user:1:age"], t1, store.next_ts()) read_older = store.next_ts() print("第一次读到的age:", store.get("user:1:age", read_older)) t2 = store.next_ts() store.prewrite("user:1:age", "21", t2) store.commit(["user:1:age"], t2, store.next_ts()) read_newer = store.next_ts() print("第二次读到的age:", store.get("user:1:age", read_newer)) print("旧时间戳仍读到旧值:", store.get("user:1:age", read_older)) if __name__ == "__main__": main()
self.data = defaultdict(list)每个 key 对应一个版本列表,存放(commit_ts, value)元组。def get(self, key, read_ts)从最新往旧遍历版本,返回第一个commit_ts ≤ read_ts的已提交值。if commit_ts is not None过滤掉尚未提交的预写版本,保证读操作看不到“半成品”。self.locks[key] = start_ts模拟预写阶段的锁,以start_ts记录持有者。self.data[key].append((None, value))用None标记未提交版本,对应 TiKV 中的 prewrite 数据。def commit(self, keys, start_ts, commit_ts)把None替换为commit_ts,版本正式生效。store.get("user:1:age", read_older)即使新版本已提交,旧读取时间戳依然读到旧值,这就是 MVCC 快照隔离。
常见误区
误区很多初学者把 Raft 理解为“选举大王”——选出 Leader 就完事了,选完主以后每个节点各写各的。
正解选举只是 Raft 三个子问题之一。真正的核心是日志复制:Leader 把所有写操作作为日志项广播给 Follower,只有日志被多数派节点确认并提交后,数据才算是“生效”。选举存在的意义,是为了在异常情况下选出一个拥有完整日志的新 Leader。
为何常见:初学 Raft 时最容易记住“投票选主”这个画面,而日志复制、提交、截断这些更繁琐的机制需要花时间消化。
误区“有了 MVCC,读写不阻塞,那事务之间都可以无锁并发了。”
正解MVCC 解决的是“读写互不阻塞”,它让写事务不必等待读事务完成。但写与写之间仍然可能冲突:两个事务同时修改同一个 key 时,照样要靠锁(TiKV 中表现为 prewrite 锁)来决定谁先谁后。Percolator 模型中的锁依然是两阶段提交的关键一环。
为何常见:把“读不用锁”推广成了“写也不用锁”,忽略了事务隔离中写写冲突这一基本问题。
误区“Region 超过 96MB 后就拷贝一份数据,变成两个 96MB 的 Region。”
正解分裂并不拷贝数据,它只是在逻辑上把键值范围从中间切开,两个新 Region 共享底层 RocksDB 存储文件。分裂后每个 Region 只是各自的键范围变小了,物理数据几乎不需要移动。“重新平衡”时才需要把新 Region 的副本调度到其他节点,这一步会发生跨节点数据传输。
为何常见:“分裂”这个词容易让人联想到细胞分裂式的物理复制,却忽略了 Region 本质上只是一个逻辑上的键范围概念。
误区“两阶段提交就是全局锁,提交期间所有其他事务都得停下来等着。”
正解Percolator 模型的锁是“按 key 粒度”的,不是全局锁。事务只锁它自己涉及的 key,其他 key 上的读写完全不受影响。即使遇到冲突,读操作也可以根据锁上的时间戳判断是否清理超时锁,而不是永久阻塞。
为何常见:经典分布式数据库教材强调两阶段提交的阻塞问题,Percolator 通过选主协议和时间戳优化缓解了这一点,初学者容易把两者混为一谈。
面试题
Q1. Raft 保证多副本数据一致性的核心手段是什么?
- A.随机选举领导节点
- B.日志复制到达多数派← 正确答案
- C.所有节点同步写入
- D.中心节点统一调度
答案:B。Raft 的核心是把“数据一致性”归结为“日志一致”。Leader 将写操作作为日志项复制给 Follower,只有超过半数节点确认后才提交,这正是多数派(quorum)原则。选主只是过程,不是一致性本身;中心调度不是 Raft 的做法。
Q2. 在 TiKV 中,数据的基本分片和复制单元是什么?
- A.数据表
- B.逻辑分区
- C.Region← 正确答案
- D.日志段
答案:C。TiKV 将整个键空间切分成连续的 Region,每个 Region 是一段 key 的范围,也是 Raft 复制组的基本单元。表是关系层概念,逻辑分区和日志段都不参与 TiKV 的存储分片。
Q3. MVCC 在 TiKV 中的主要作用是什么?
- A.提高磁盘写入速度
- B.避免读写互相阻塞← 正确答案
- C.压缩历史数据空间
- D.加速网络传输速度
答案:B。MVCC(多版本并发控制)让每个 key 的写操作追加新版本而非覆盖旧版本,读操作基于时间戳读取快照,从而读写互不阻塞。它并不压缩数据,也不直接加速磁盘或网络。
Q4. Percolator 分布式事务的提交流程是?
- A.先全局锁表再提交
- B.先预写加锁再提交← 正确答案
- C.直接覆盖所有数据
- D.依赖中心事务管理器
答案:B。Percolator 采用两阶段提交,第一阶段对每个涉及的 key 做 prewrite 加锁并写预存值,第二阶段用更大的 commit_ts 提交并释放锁。它没有中心事务管理器,也不直接覆盖旧数据。
Q5. 一个 5 副本的 Raft 复制组,最多能容忍几个副本同时故障?
- A.1 个
- B.2 个← 正确答案
- C.3 个
- D.4 个
答案:B。5 副本的多数派要 ⌊5/2⌋+1 = 3 个节点在线。因此最多可以容忍 5-3 = 2 个副本故障。若故障 3 个,剩余 2 个不足多数派,无法选出新 Leader 或提交日志。
Q6. PD(Placement Driver)在 TiKV 中的核心职责是什么?
- A.存储所有用户数据
- B.管理 Region 路由与调度← 正确答案
- C.执行复杂的 SQL 查询
- D.汇总全集群监控日志
答案:B。PD 是 TiKV 的调度中心,负责维护 Region 的位置信息、触发分裂、执行副本迁移与负载均衡。用户数据由各存储节点保存,SQL 查询由 TiDB 计算层处理,监控日志只是辅助功能。
Q7. TiKV 中 Region 自动分裂的典型触发条件是什么?
- A.节点 CPU 使用率超过 80%
- B.Region 数据量超过阈值← 正确答案
- C.客户端连接数超过上限
- D.磁盘 IO 达到饱和
答案:B。Region 分裂的经典触发条件是数据量超过约 96MB,这是为了避免单 Region 过大造成复制、恢复和热点问题。CPU、连接数、磁盘 IO 都不是 Region 分裂的直接依据。
Q8. 以下哪些属于 Raft 算法需要解决的子问题?
- A.Leader 选举← 正确答案
- B.日志复制← 正确答案
- C.成员变更← 正确答案
- D.哈希索引
答案:ABC。Raft 将共识问题拆解为三个子问题:Leader 选举、日志复制和成员变更。哈希索引是存储引擎的索引结构,与 Raft 的共识机制无关。
Q9. 关于 TiKV 的读写流程,以下说法正确的是?
- A.读请求可由任意副本直接响应
- B.写请求交给 Leader 处理← 正确答案
- C.事务提交前需要预写锁← 正确答案
- D.所有请求必须经过 PD 转发
答案:BC。写请求必须经 Leader 进入 Raft 日志;事务在 Percolator 模型中先预写加锁再提交。为满足线性一致读,读请求通常也在 Leader 上执行或携带时间戳校验,并非所有副本都能直接响应。客户端通过 PD 获取路由信息,但后续读写直接访问对应节点,不经过 PD 转发。
Q10. 在 Percolator 事务模型中,下列哪些是保证事务正确性的关键设计?
- A.每个 key 预写前先加锁← 正确答案
- B.commit_ts 必须大于 start_ts← 正确答案
- C.所有参与者必须共享同一物理时钟
- D.预写阶段发现锁冲突则回滚← 正确答案
答案:ABD。Percolator 用 prewrite 锁检测写写冲突,任何 key 被锁住就回滚;commit_ts 必须大于 start_ts 才能保证历史顺序正确。它并不要求所有参与者共享同一物理时钟,而是依赖分布式逻辑时间戳(如 TSO)生成全局递增时间。