大数据TiDB初识
前言
Github:https://github.com/HealerJean
一、TiDB 简介
TiDB是一个开源的分布式NewSQL数据库,采用HTAP(混合事务分析处理) 架构,同时支持OLTP(在线事务处理)和OLAP(在线分析处理)场景。其核心设计目标是 水平扩展、高可用、强一致性,并兼容MySQL协议,降低用户迁移成本

1、核心组件详解
+-----------------------+
| Application |
+-----------+-----------+
|
v
+-----------------------+
| TiDB Server | (SQL Layer, 无状态)
+-----------+-----------+
|
v
+-----------------------+
| PD Server | (元数据管理 & 调度)
+-----------+-----------+
|
v
+-----------------------+
| TiKV Server | (行式存储, OLTP)
+-----------+-----------+
|
v
+-----------------------+
| TiFlash Server | (列式存储, OLAP)
+-----------------------+
1)TiDB Server(SQL 层)
功能:
- 兼容
MySQL 5.7+协议,支持JDBC、ORM框架(如MyBatis、Hibernate)。 - 负责
SQL解析、优化、执行计划生成,将SQL转换为分布式Key-Value操作(TiKVAPI)。 - 无状态设计,可通过负载均衡(如
LVS、HAProxy)水平扩展。
关键特性:
- 分布式执行引擎:将复杂查询(如
Join、Aggregation)拆分为并行任务下发到TiKV/TiFlash。 - 事务管理:基于
PD分配全局事务ID,协调TiKV完成分布式事务(2PC)。 - 统计信息:收集表数据分布,优化查询计划。
2)PD (Placement Driver) Server(调度中心)
功能:
- 集群的 “大脑”,负责 元数据管理、调度、全局事务
ID分配。 - 提供
TiDBDashboard(内置监控和管理界面)。
核心机制:
Region调度:- 负载均衡:监控节点负载,迁移
Region使存储和流量均匀。 - 热点调度:检测热点
Region(如频繁访问的KeyRange),自动分裂或迁移。 - 副本修复:节点宕机时,在其他健康节点上补足副本。
- 负载均衡:监控节点负载,迁移
- 全局路由表:
- 记录所有
Region的分布信息(Key → Region → TiKV节点)。 - 客户端(
TiDB)通过PD定位Key所在的Region和Leader副本位置。
- 记录所有
- 高可用:
- 基于
Raft协议实现多副本(建议3/5节点部署)。 - 领导者故障时自动切换。
- 基于
关键数据结构:
Region映射表:记录每个KeyRange对应的TiKV节点。- 全局时间戳(
TSO):为分布式事务提供单调递增的时间戳。
3)TiKV Server(分布式 Key-Value 存储)
功能:
- 分布式行式存储引擎,默认 3 副本,通过
Raft协议保证数据一致性。 - 支持
ACID事务(默认快照隔离级别)。
核心设计:
- 数据分片(
Region):- 每个
Region管理一段连续的KeyRange(如[a, b))。 - 默认
96MB,超过一定阈值触发分裂(Split),过小则合并(Merge)。
- 每个
- 分布式事务:
- 采用
Percolator模型(2PC+ 乐观锁)。 - 事务流程:
- 从
PD获取start_ts(事务开始时间戳)。 - 写入数据时暂存到锁(
Lock),提交时通过2PC确认。 - 清理锁并标记提交成功(
commit_ts)。
- 从
- 采用
- 存储引擎:
- 基于
RocksDB(LSM-Tree),适合高吞吐写入。
- 基于
4)TiFlash Server(列式存储引擎)
功能:
- 列式存储,加速分析型查询(如
GROUP BY、SUM)。 - 通过
RaftLearner机制从TiKV异步同步数据(近实时)。
关键特性:
-
MPP(MassivelyParallelProcessing)引擎:- 将复杂查询拆分为多个并行任务,下推到
TiFlash节点执行。
- 将复杂查询拆分为多个并行任务,下推到
-
智能选择执行引擎:
-
优化器自动判断使用
TiKV(OLTP)或TiFlash(OLAP)。 -
用户也可强制指定:
SELECT /*+ READ_FROM_STORAGE(TIFLASH[t]) */ * FROM t;
-
2、数据读写流程示例
1)写入流程(INSERT)
- 客户端 发送
SQL到TiDBServer。 TiDB解析SQL,向PD查询目标数据的Region位置。PD返回LeaderTiKV节点地址。TiDB向TiKVLeader发起写请求:TiKV通过Raft协议同步数据到Follower。- 写入成功后返回客户端
ACK。
2) 查询流程(SELECT)
TiDB解析SQL,判断使用TiKV(点查)或TiFlash(分析)。- 若是聚合查询:
TiDB向TiFlash下发MPP任务,并行计算后汇总结果。 - 若是事务查询:从
TiKV读取最新已提交数据(基于MVCC)。
3、特性和应用场景
1)推荐场景
- 数据量预计快速增长至亿级以上,需避免分库分表。
- 要求高可用、强一致事务(如金融、支付)。
- 需同时支持
OLTP(交易)与OLAP(分析)的混合场景。 - 希望降低分布式数据库开发与运维成本。
2)不推荐场景
- 数据量小(百万级以下)、业务逻辑简单,
MySQL单机即可满足。 - 对
MySQL生态深度依赖(已使用分库分表、使用类似MyCat等中间件) - 无扩展需求的传统业务。
1)5 大核心特性
a、一键水平扩缩容:
得益于 TiDB 存储计算分离的架构的设计,可按需对计算、存储分别进行在线扩容或者缩容,扩容或者缩容过程中对应用运维人员透明。
b、高可用:
数据采用多副本存储,数据副本通过 Muli-Raft 协议同步事务日志,多数派写入成功事务才能提交,确保数据强一致性且少数副本发生故障时不影响数据的可用性。可按需配置副本地理位置、副本数量等策略,满足不同容灾级别的要求。
c、实时 HTAP
(Hybrid Transactional/Analytical Processing(混合事务分析处理)):提供行存储引擎 [TiKV]、列存储引擎 [TiFlash] 两款存储引擎,TiFlash 通过 Multi-Raft Learner 协议实时从 TiKV 复制数据,确保行存储引擎 TiKV 和列存储引擎 TiFlash 之间的数据强一致。TiKV、TiFlash 可按需部署在不同的机器,解决 HTAP 资源隔离的问题。
d、云原生的分布式数据库:
专为云而设计的分布式数据库,通过 [TiDB Operator]可在公有云、私有云、混合云中实现部署工具化、自动化。
e、兼容 MySQL 协议和 MySQL 生态:
兼容 MySQL 协议、MySQL 常用的功能、MySQL 生态,应用无需或者修改少量代码即可从 MySQL 迁移到 TiDB。提供丰富的[数据迁移工具]帮助应用便捷完成数据迁移。
2)4 大应用场景
a、金融行业场景:
金融行业对数据一致性及高可靠、系统高可用、可扩展性、容灾要求较高。传统的解决方案的资源利用率低,维护成本高。TiDB 采用多副本 + Multi-Raft 协议的方式将数据调度到不同的机房、机架、机器,确保系统的 RTO (<= 30s 及 RPO = 0。
RTO(RecoveryTimeObjective,恢复时间目标):指系统从故障发生到 完全恢复服务 的最大允许时间。≤ 30s表示必须在 30秒内 让业务重新正常运行。RPO(RecoveryPointObjective,恢复点目标):指系统恢复后允许的 数据丢失量 的最大时间窗口。= 0表示要求 零数据丢失,即故障前的最后一笔操作也必须恢复。
b、海量数据及高并发的 OLTP 场景:
传统的单机数据库无法满足因数据爆炸性的增长对数据库的容量要求。TiDB 是一种性价比高的解决方案,采用计算、存储分离的架构,可对计算、存储分别进行扩缩容,计算最大支持 512 节点,每个节点最大支持 1000 并发,集群容量最大支持 PB 级别。
- 业务快速扩展场景:初创公司或业务增长迅速的企业,
TiDB的弹性扩展能力可避免频繁重构数据库架构。 - 海量数据在线业务:电商订单、金融交易(亿级数据),需实时查询与统计,
MySQL分库分表难以满足,TiDB可直接应对。 - 高并发读写场景:秒杀、社交平台
Feed流,TiDB的分布式架构可承载百万级 ` QPS,避免MySQL` 单机瓶颈。
c、实时 HTAP 场景:
-
TiDB适用于需要实时处理的大规模数据和高并发场景。TiDB在4.0版本中引入列存储引擎TiFlash,结合行存储引擎TiKV构建真正的HTAP数据库,在增加少量存储成本的情况下,可以在同一个系统中做联机交易处理、实时数据分析,极大地节省企业的成本。 -
TiDB支持HTAP(混合事务与分析处理),无需数据同步至数仓,直接在生产库执行复杂分析查询。
d、数据汇聚、二次加工处理的场景:
TiDB 适用于将企业分散在各个系统的数据汇聚在同一个系统,并进行二次加工处理生成 T+0 或 T+1 的报表。与 Hadoop 相比,TiDB 要简单得多,业务通过 ETL 工具或者 TiDB 的同步工具将数据同步到 TiDB,在 TiDB 中可通过 SQL 直接生成报表。
三、TiDB 数据库的存储

1、Key-Value
TiKV是TiDB的底层存储引擎,其核心设计是一个 分布式的、有序的Key-Value存储系统。它的数据模型和实现方式与传统关系型数据库(如MySQL)完全不同,而是更接近底层存储系统(如RocksDB、HBase)。以下是关键点的深入解析:
1)TiKV 的 Key-Value 存储模型
(1)核心设计:有序的 Key-Value Map,TiKV 的数据模型可以抽象为一个 全局有序的 Key-Value Map,具备以下特性:
Key和Value都是字节数组([]byte),没有预定义的结构。Key按字典序(二进制顺序)排列,支持高效的范围查询(RangeScan)。- 提供原子性操作:如
Get、Put、Delete、Scan。
(2)与 SQL 表的区别::TiKV 本身不感知“表”、“行”、“列”等 SQL 概念,它只认识 Key和 Value。
TiDB的 ` SQL层负责将表数据编码为Key-Value,再存储到TiKV` 中。- 例如,一行
SQL数据可能被拆解为多个Key-Value对(如行数据 + 索引数据)。
2)TiKV 的 Key-Value 存储实现
a、底层存储引擎:RocksDB
TiKV 使用 RocksDB(基于 LSM-Tree 的高性能存储引擎)作为单机存储,其特点:
- 所有
Key-Value按Key排序存储,支持高效的Seek和Next操作。 - 写入先写入内存(
MemTable),再刷盘(SSTable),适合高吞吐场景。 - 支持快照(
Snapshot)和事务(Transaction)。
b、分布式扩展:Multi-Raft
TiKV将数据分片为Region,每个Region是一个独立的Raft组。- 每个
Region的多个副本(通常3个)分布在不同的TiKV节点上,通过Raft协议保证一致性。
3)问题
问题1:TiDB 的 Key 和 value 的组成是什么样的
(1)Key 的组成:TiDB 中每个 Key 是一个 编码后的字节序列,包含多层信息(以表数据为例):
Key = [TablePrefix][TableID][RecordPrefix][RecordID]
TablePrefix:标识这是一个表数据(而非索引或其他元数据)。TableID:表的唯一ID。RecordPrefix:标识这是行数据(Row)。RecordID:行的主键值(或 TiDB 隐式分配的_tidb_rowid)。
示例:假设表 users的 TableID=123,某行主键 user_id=100,则其 Key 可能编码为:
Key = [0x74][0x0000007B][0x72][0x00000064]
│ │ │ └── 主键值 100
│ │ └── 行数据标识
│ └── 表 ID 123
└── 表数据前缀
(2)Value 的组成:Value 存储的是 行的实际数据,编码为二进制格式(如 Protocol Buffers):
Value = [Column1][Column2][Column3]...
-
如果表
users有name(字符串)、age(整数)两列,Value 可能存储为:Value = ["Alice"][25]
问题2:数据如何存储在 Region 中?
Region管理的是Key的区间(如[start_key, end_key)),但实际存储的是 完整的Key-Value对。- 查找流程:
TiDB计算某行数据的Key(如主键user_id=100对应的 Key)。- 通过
PD查询该Key所属的Region(如Region1负责[0x50, 0xA0))。 - 从该
Region的Leader副本中读取或写入对应的Key-Value数据。
问题3:为什么只提 Key 不提 Value
Region的分片依据是Key:- 数据分片(
Region)的边界由Key的区间决定(如[a, b)),而Value是跟随Key存储的。 - 调度和负载均衡时,
PD只需关注Key的分布,无需感知Value的内容。
- 数据分片(
Value是“被动存储”的:只要Key定位到正确的Region,其对应的Value自然会被存取。
问题4:TiDB 的 Key 编码是什么样的
1)表数据(Row)
-
Key:[TablePrefix][TableID][RecordPrefix][PrimaryKey] -
Value:行的所有列数据(如{"name": "Alice", "age": 25})。-
Key = [0x74][0x0000007B][0x72][0x00000064] │ │ │ └── 主键值 (100) │ │ └── 行数据标识 (RecordPrefix) │ └── 表 ID (123) └── 表数据前缀 (TablePrefix)
-
(2)唯一索引
-
Key:[TablePrefix][TableID][IndexPrefix][IndexValue][PrimaryKey]- 例如对
email字段建唯一索引,Key 可能包含"alice@example.com"。
- 例如对
-
Value:空(因为可通过索引直接定位到行数据)。-
[0x69][0x0000007B][0x69][0x00000001]["Alice"][0x00000064] │ │ │ │ │ └── 主键值(100) │ │ │ │ └── 索引列值("Alice") │ │ │ └── 索引 ID(假设为 1) │ │ └── 索引标识('i') │ └── 表 ID(123) └── 索引前缀(0x69)
-
(3)非唯一索引
Key:[TablePrefix][TableID][IndexPrefix][IndexValue][PrimaryKey]Value:空(因为可通过索引直接定位到行数据)。
问题5:为什么这样涉及Key的编码
- 高效定位数据:
- 通过前缀(
TablePrefix+TableID)快速过滤出某张表的数据。 - 通过
RecordPrefix区分行数据和索引数据。
- 通过前缀(
- 支持范围查询:
Key按字典序排列,查询user_id BETWEEN 10 AND 20时,直接扫描连续Key区间即可。 - 兼容索引:索引的
Key类似,但前缀和结构略有不同(例如索引Key会包含索引列的值)。
问题6:为什么选择 Key-Value 模型
- 灵活性:无需预定义
Schema,适合分布式扩展。 - 高性能:有序
Key存储 +LSM-Tree结构,写入和范围查询效率高。 - 分布式友好:易于分片(
Region)和负载均衡。
2、Region
为了便于理解,假设所有的数据都只有一个副本。可以将
TiKV看作一个巨大而有序的KVMap,为了实现存储的水平扩展,数据将被分散在多台机器上。对于一个KV系统,将数据分散在多台机器上有两种比较典型的方案:
Hash:按照Key做Hash,根据Hash值选择对应的存储节点。Range:按照Key分Range,某一段连续的Key都保存在一个存储节点上。
TiKV 选择了第二种方式,将整个 Key-Value 空间分成很多段,每一段是一系列连续的 Key,将每一段叫做一个 Region,可以用 [StartKey,EndKey) 这样一个左闭右开区间来描述。每个 Region 中保存的数据量默认维持在 256 MiB 左右(可以通过配置修改)。
1)Region 的核心特性
- 分片单位:
- 每个
Region负责一段连续的KeyRange(如[start_key, end_key)),默认 96MB~256MB(可配置)。 - 当
Region超过阈值时自动 分裂(Split),过小时可能 合并(Merge)。
- 每个
- 数据分布:
PD调度器确保Region均匀分布在集群所有TiKV节点上。- 新增节点时,
PD自动从其他节点迁移Region到新节点(无需人工干预)。
2)Region 与 Raft 的协同设计
| 角色 | 写请求 | 日志生成 | 读请求(默认) | 选举参与 | 数据同步 | 用途场景 |
|---|---|---|---|---|---|---|
Leader |
✅ | ✅ | ✅ | ❌ | 无 | 处理所有读写请求 |
Follower |
❌ | ❌ | ✅(可配置) | ✅ | 是 | 数据同步,分担读负载 |
Learner |
❌ | ❌ | ❌ | ❌ | 是 | 副本迁移或扩容的过渡阶段 |
a、Raft Group 组成:
- 每个
Region对应一个独立的RaftGroup,包含多个副本(通常3个)。 - 副本角色:
Leader:处理所有读写请求,日志复制Follower:同步数据,Leader故障时参与选举。Learner:Learner是Raft协议中的临时角色,主要用于副本扩容或迁移
b、读写流程:
- 写请求:
Leader接收写请求(如Put(key, value))。- 通过
Raft日志复制到多数Follower。MemTable写满后触发flush(默认128MB)- 将日志数据顺序写入磁盘
SST文件 - 更新元数据标记已持久化的日志索引
- 提交成功后返回客户端。
- 读请求:直接由
Leader处理(保证强一致性),无需Raft交互。

c、典型场景
- 正常运行:
Leader处理写请求,Follower同步数据,提供读服务(若开启follower-read)。 - 副本调度:
PD将Learner副本加入RaftGroup,待其同步后晋升为Follower。 - 故障恢复:
Leader故障时,Raft协议从Follower中选举新的Leader,确保服务连续性。
d、关键设计原则
- 强一致性:通过 Raft 协议保证数据在 Leader 和 Follower 之间的一致性。
- 高可用性:多副本(默认 3 个)和自动故障转移机制(Leader 选举)。
- 灵活扩展:通过 Learner 副本实现无缝的副本迁移和负载均衡。
3)region 大小
- 稳态目标:
256MB(PD 调度的理想参考值) - 分裂触发:
- 渐进式分裂:
Region达到144MB(96MB×1.5)开始准备 - 强制分裂:
Region达到384MB(region-max-size)必须分裂
- 渐进式分裂:
-
合并触发:相邻
Region均小于54MB(region-merge-size)时可能合并 - 影响:
Region设置过小可能导致Region数量过多,增加资源开销和调度复杂度。Region设置过大可能导致单个Region负载过重,影响查询性能和系统稳定性。
3)问题分析
a、为什么必须通过 Leader
Raft要求所有写操作必须由Leader发起,避免多Leader导致数据冲突(脑裂问题)。- 读操作可以直接走
Leader(保证强一致性),或配置为从Follower读(可能读到旧数据)。
b、数据一致性如何保证?
- 写一致性:日志必须复制到多数派(
N/2+1)才会提交,即使部分节点宕机也不丢数据。 - 读一致性:默认从
Leader读,总能读到最新已提交的数据。
c、故障场景怎么处理?
- 短期宕机:
RaftLeader重试,依赖多数派维持服务。 - 长期宕机:
PD调度器介入,补足副本并移除故障节点。 - 设计哲学:
Raft保证 短期一致性,PD保证 长期高可用。两者协同实现生产级分布式存储。
| 行为 | Raft 协议原生能力 |
PD 调度器的增强 |
|---|---|---|
Follower 短暂宕机 |
Leader 重试,不补副本 |
不干预 |
Follower 长期宕机 |
无法自动修复 | 自动补副本,移除宕机节点 |
| 数据一致性保障 | 依赖多数派存活 | 确保最终副本数达标(如 3 副本) |
| 触发条件 | 超时机制(选举/心跳) | 定时健康检查 + 策略配置 |
3、RocksDB
RocksDB作为TiKV的核心存储引擎,用于存储Raft日志以及用户数据。每个TiKV实例中有两个RocksDB实例,两个RocksDB实例是 并行运行在同一个进程(TiKV进程)中,但它们的数据是物理隔离的
- 一个用于存储
Raft日志(通常被称为raftdb)- 一个用于存储用户数据以及
MVCC信息(通常被称为kvdb)。
kvdb中有 4 个ColumnFamily:raft、lock、default和write:
问题:为什么要这样设计?
| 目的 | 说明 |
|---|---|
| 性能隔离 | Raft 日志写入频繁且对延迟敏感,与用户数据混合可能互相影响。 |
| 运维灵活 | 可以为 raftdb 和 kvdb 配置不同的磁盘路径(比如 raftdb 放在更快的 SSD 上)。 |
| 资源控制 | 可以分别设置 raftdb 和 kvdb 的内存缓存大小、线程数、压缩策略等。 |
1)双 RocksDB 实例
TiKV 采用双 RocksDB 实例设计,实现数据与日志的物理隔离:
| 存储内容 | RocksDB 实例 |
I/O 模式 | 主要用途 |
|---|---|---|---|
用户数据(Key-Value) |
kvdb |
宏观:随机机写入 | 存储所有 Region 的实际数据 |
Raft 日志 |
raftdb |
顺序追加 | 存储 Raft 协议的操作日志 |
raft列:用于存储各个Region的元信息。仅占极少量空间,用户可以不必关注。lock列:用于存储悲观事务的悲观锁以及分布式事务的一阶段Prewrite锁。- 当用户的事务提交之后,
lockcf中对应的数据会很快删除掉,因此大部分情况下lockcf中的数据也很少(少于1GB)。 - 如果
lockcf中的数据大量增加,说明有大量事务等待提交,系统出现了bug或者故障。
- 当用户的事务提交之后,
write列:用于存储用户真实的写入数据以及MVCC信息(该数据所属事务的开始时间以及提交时间)。- 当用户写入了一行数据时,如果该行数据长度小于或等于
255字节,那么会被存储write列中,否则该行数据会被存入到default列中。 - 由于
TiDB的非unique索引存储的value为空,unique索引存储的value为主键索引,因此二级索引只会占用writecf的空间。
- 当用户写入了一行数据时,如果该行数据长度小于或等于
default列:用于存储超过255字节长度的数据。
2)RocksDB 的内存占用
a、Block、BlockCache、Region关系
-
Region是数据分布式管理的基本单位:TiKV将整个Key-Value空间按范围划分成多个小段,每一段就是一个 `Region -
Block是磁盘存储的基本单元:TiKV使用RocksDB作为底层存储引擎,RocksDB将存储在磁盘上的文件按照一定大小切分成Block,默认大小是64KB。Block是数据在磁盘上物理存储的最小单位,包含实际的键值对数据。 Region包含多个Block:一个Region中的数据会存储在磁盘上的多个文件中,这些文件又会被切分成多个Block。Block是构成Region数据存储的底层单元。BlockCache:作为内存缓存。读取Region数据时,会以Block为单位从磁盘读取并加载到内存中的BlockCache,若BlockCache中存在所需Block,则可直接从内存读取,无需访问磁盘,从而提高读取性能
b、Block 与 BlockCache 的设计逻辑
BlockCache的作用:作为内存缓存,它利用LRU(最近最少使用)算法优先保留高频访问的Block,大幅降低磁盘IO次数。TiKV默认分配系统总内存的45%给BlockCache(上限建议60%),这是因为读取性能对数据库体验至关重要,而内存资源的合理分配能最大化缓存命中率。
c、MemTable 的内存控制
写入
RocksDB中的数据会写入MemTable
- 写入流程:数据先写入
MemTable(内存结构),避免直接落盘的性能开销。当单个MemTable达到128MB时切换到MemTable,旧MemTable会异步刷盘为SST文件。 - 内存上限控制:
TiKV包含2个RocksDB实例(分别对应kv和raft数据),共 4 个ColumnFamily(CF),每个CF最多允许5个MemTable(含活跃和待刷盘的)。按单个128MB计算, 4 × 5 × 1 28MB =2.5GB的上限设计,既能保证写入吞吐量(避免频繁刷盘),又能防止内存占用过高导致OOM,这部分配置确实不建议轻易修改。

| 设计选择 | 优点 | 缺点 | 解决方案 |
|---|---|---|---|
先写WAL |
保证数据安全 | 额外I/O开销 | 顺序追加优化 |
| 内存缓冲 | 高速响应写入 | 内存占用受限 | 多MemTable轮流工作 |
| 异步刷盘 | 不阻塞客户端 | 可能短暂数据风险 | 后台线程加速处理 |
3)RocksDB 的空间占用
RocksDB的空间放大(≈1.11)是LSM-tree结构为优化写入性能的固有特性,通过合并机制控制在较低水平。
TiKV的额外空间开销来自MVCC的多版本存储,由GC机制动态平衡(旧版本及时回收则总放大可控)。- 两者共同构成了 TiKV 的空间占用特性,是 “高性能写入” 与 “事务支持” 权衡的结果。
-
多版本
-
版本追加:
L0层的SST文件由MemTable刷盘生成,由于写入顺序是 “追加” 而非 “原地更新”,新写入的 key 会直接生成新的SST文件,导致L0中不同SST的key范围可能重叠(同一 key 可能有多个版本)。这是为了保证写入性能(避免随机写磁盘),但会暂时保留旧版本。 -
版本合并:当
L0文件数量达到阈值,会触发与L1的合并(compaction)。合并时会按 key 范围重新整理,删除重复版本(只保留最新的),因此L1及更深层的 SST 文件范围不重叠,同一 key 仅存一个版本。当某一层级的 SST 文件总大小超过配置的目标大小时,会触发该层级与下一层级的 Compaction -
简言之,多版本仅 “临时” 存在于
L0,深层则通过合并保证版本唯一性,平衡了写入性能和空间效率。 -
MemTable无序 多版本 ~128MB<1% L0 可能重叠 多版本 不定 ~1% L1 不重叠 单版本 8MB ~9% L2+ 不重叠 单版本 递增 ~90%
-
- 空间放大
- 层级合并:当某一层级的
SST文件总大小超过配置的目标大小时,会触发该层级与下一层级的Compaction,随着不断的Compaction,数据会从高层级逐渐向下层级移动,越老的数据越容易被移动到更低的层级,经过多次这样的合并操作后,最终会到达LN层,LN 层存储着相对最老、最冷的数据 - 空间放大:指 “实际存储占用” 与 “有效数据大小” 的比值。
RocksDB中,每层总大小是上一层的N倍(TiKV默认N=10),因此空间放大系数约为 1.11
- 层级合并:当某一层级的
MVCC机制的额外影响:- 异步删除:
TiKV会通过异步GC(垃圾回收) 定期清理过期的旧版本(默认每10分钟触发一次),删除不再需要的版本数据 - 空间占用:未被
GC回收的旧版本(最近 10 分钟内的写入)会占用额外空间。
- 异步删除:
4)RocksDB 后台线程与 Compact
RocksDB 中,将内存中的 MemTable 转化为磁盘上的 SST 文件,以及合并各个层级的 SST 文件等操作都是在后台线程池中执行的。后台线程池的默认大小是 8,当机器 CPU 数量小于等于 8 时,则后台线程池默认大小为 CPU 数量减一。通常来说,用户不需要更改这个配置。如果用户在一个机器上部署了多个 TiKV 实例,或者机器的读负载比较高而写负载比较低,那么可以适当调低 rocksdb/max-background-jobs 至 3 或者 4。
5)WriteStall
RocksDB 的 L0 与其他层不同,L0 的各个 SST 是按照生成顺序排列,各个 SST 之间的 key 范围存在重叠,因此查询的时候必须依次查询 L0 中的每一个 SST。为了不影响查询性能,当 L0 中的文件数量过多时,会触发 WriteStall 阻塞写入。
如果用户遇到了写延迟突然大幅度上涨,可以先查看 Grafana RocksDB KV 面板 WriteStall Reason 指标,如果是 L0 文件数量过多引起的 WriteStall,可以调整下面几个配置到 64
rocksdb.defaultcf.level0-slowdown-writes-trigger
rocksdb.writecf.level0-slowdown-writes-trigger
rocksdb.lockcf.level0-slowdown-writes-trigger
rocksdb.defaultcf.level0-stop-writes-trigger
rocksdb.writecf.level0-stop-writes-trigger
rocksdb.lockcf.level0-stop-writes-trigger
4、Titan
Titan是基于RocksDB的高性能单机key-value存储引擎插件。当value较大(1 KB 以上或 512 B 以上)的时候,Titan在写、更新和点读等场景下性能都优于RocksDB。但与此同时,Titan会占用更多硬盘空间和部分舍弃范围查询。随着SSD价格的降低,Titan的优势会更加突出,让用户更容易做出选择。
1)核心特性
- 支持将
value从LSM-tree中分离出来单独存储,以降低写放大。 - 已有
RocksDB实例可以平滑地升级到Titan,这意味着升级过程不需要人工干预,并且不会影响线上服务。 100%兼容目前TiKV所使用的所有RocksDB的特性。
2)适用场景
Titan 适合在以下场景中使用:
- 前台写入量较大,
RocksDB大量触发compaction消耗大量 I/O 带宽或者CPU资源,造成 TiKV 前台读写性能较差。 - 前台写入量较大,由于
I/O带宽瓶颈或CPU瓶颈的限制,RocksDBcompaction进度落后较多频繁造成writestall。 - 前台写入量较大,
RocksDB大量触发compaction造成I/O写入量较大,影响SSD盘的寿命。
开启 Titan 需要考虑以下前提条件:
(1)Value 较大。即 value 平均大小比较大,或者数据中大 value 的数据总大小占比比较大。目前 Titan 默认 1KB 以上大小的 value 是大 value,根据实际情况 512B 以上大小的 value 也可以看作是大 value。
(2)没有范围查询或者对范围查询性能不敏感。Titan 存储数据的顺序性较差,所以相比 RocksDB 范围查询的性能较差,尤其是大范围查询。在测试中 Titan 范围查询性能相比 RocksDB 下降 40% 到数倍不等。
- 数据分离导致访问路径变长(
Value分离)- 当进行范围扫描时,
Titan不仅需要读取key的顺序,还需要根据每个key去BlobFile中查找对应的 value,这就引入了额外的I/O操作和随机读,降低了效率。
- 当进行范围扫描时,
- 缓存命中率下降:
RocksDB使用BlockCache来缓存SST中的block,一个block包含多个key-value对,适合范围查询复用。Titan的BlobCache是针对单个value的缓存,无法很好地支持连续value的访问。
BlobFile没有良好的局部性- RocksDB 的 SST 文件是按 key 排序的,在做 range scan 时,利用操作系统的预读机制,高效地批量读取连续的数据块。
- Titan 的 BlobFile 是按写入顺序存储的,key 的顺序和 value 在 BlobFile 中的位置没有关联
(3)磁盘剩余空间足够,推荐为相同数据量下 RocksDB 磁盘占用的两倍
Titan降低写放大是通过牺牲空间放大达到的key和value被分开存储,每个 key 都需要保存一个指向 value 的指针,这会占用额外空间。BlobFile中存在垃圾数据(旧版本value),在GC前不会被回收,也会导致空间占用上升Titan逐个压缩value,压缩率比RocksDB差- RocksDB 是按 block(通常是几十 KB)为单位进行压缩的,利用相邻数据的重复性可以获得更高的压缩率。
- Titan 是按单个 value 进行压缩的,缺乏上下文信息,压缩效率较低
优势:
- 减少写放大:在
RocksDB中,当存储大量数据时,特别是在执行compaction操作时,可能会产生显著的写放大问题。Titan通过将大value存储在独立的BlobFile中,并仅在LSM-tree中存储指向这些值的小型索引,减少了compaction过程中需要移动的数据量,从而降低了写放大。 - 提高写入性能:由于减少了写放大,
Titan能够提供更高的写入性能,特别是在高并发写入场景下。这对于需要频繁更新和插入大量数据的应用程序来说是一个重要的改进。 -
更好的缓存使用效率:
Titan设计允许更高效的缓存使用,尤其是针对小key和大value的情况。LSM-tree只存储较小的键值对,使得同等内存容量下可以缓存更多的索引、布隆过滤器和block,进而提高了点查询性能。 - 更有效的空间管理:
Titan引入了垃圾回收机制来清理不再使用的BlobFile数据,这有助于更有效地利用磁盘空间,避免了由于无效数据占据的空间浪费。
3)核心原理
Titan在Flush和Compaction的时候将value分离出LSM-tree,这样写入流程可以和RocksDB保持一致,减少对RocksDB的侵入性改动。

a、键值分离存储:减少写入放大
Titan将key和value分离存储,核心逻辑如下:
- 分离策略
- 当
value大小 ≥min_blob_size(默认32KB)时,value被写入独立的BlobFile,SST文件仅存储key和指向BlobFile的索引(类似指针)。 - 小
value仍直接存储在SST文件中,保持对RocksDB接口的兼容。
- 当
BlobFie结构- 有序存储:
BlobFile中的key-value按顺序排列,支持预读取(prefetch)优化顺序读取性能。 - 元数据管理:每个
BlobFile包含meta block和meta index block,记录文件属性和索引信息,便于快速定位数据。
- 有序存储:
- 写入流程:写入时,大
value先写入WAL(Write-Ahead Log),再通过TitanTableBuilder生成BlobFile,SST文件仅记录key和BlobFile索引。这种设计避免了大value在LSM-tree中被多次重写,将写入放大降低 50% 以上
b、智能 GC 机制:高效回收空间
由于
value被分离存储,当key过期或删除时,对应的value不会立即被移除,因此需要通过GC来回收这些不再使用的空间。Titan有两种GC策略:传统GC和LevelMerge默认)
- 传统
GC- 原理:
Titan定期扫描各个BlobFile,统计它们里面有多少是“有用的”数据,有多少是“垃圾”。 - 选择策略:选择那些垃圾最多的
BlobFile来进行回收。 - 操作方式:把这些
BlobFile中有用的value提取出来,写入新的BlobFile,跳过无效的垃圾数据,从而释放空间。 - 缺点:这种
GC是独立运行的,可能会在系统忙的时候也运行,造成额外的I/O负载。
- 原理:
LevelMerge(层级合并)-
原理:利用
RocksDB本身做LSM-tree compaction(合并) 的过程,在合并SST文件时,同步处理对应的BlobFile。 - 优势:
- 不是单独运行,而是在
compaction时顺便清理BlobFile,效率更高。 - 高效:不需要额外扫描所有
BlobFile,只处理当前compaction涉及的数据 - 低开销:复用
compaction的I/O和CPU资源,减少系统压力。 - 空间利用率高:及时清理无效数据,避免磁盘浪费。
- 不是单独运行,而是在
- 操作方式:只清理那些在
SST合并过程中涉及的BlobFile,避免全盘扫描。
| 特性 | 传统GC | Level Merge |
|---|---|---|
| 是否独立运行 | 是(单独运行) | 否(配合 compaction) |
| 回收效率 | 一般 | 更高 |
| 系统开销 | 较大 | 更低 |
| 适合场景 | 垃圾较多、低负载时 | 高并发、写入密集型场景 |
4)配置
Titan对RocksDB兼容,也就是说,使用 RocksDB 存储引擎的现有 TiKV 实例可以直接开启 Titan。
a、数据迁移
当你在 TiDB 中启用 Titan 存储引擎后,并不会立刻把所有数据迁移到 Titan 中。数据会逐步迁移,主要通过以下两个方式:
- 前台写入:新写入的数据如果满足条件(
value大于min-blob-size,默认32KB),会直接写入 Titan。 - 后台
Compaction:RocksDB在做compaction(合并SST文件)时,会检查数据大小,把符合条件的大value分离到Titan的BlobFile中
b、常用参数
| 参数分类 | 参数名 | 推荐值 | 适用场景 | 注意事项 |
|---|---|---|---|---|
| 存储分离阈值 | min-blob-size |
1KB-128KB | - 1KB:高频点查 - 32KB:混合负载(默认) - 128KB:顺序扫描为主 | 值越小写性能越好,但会降低扫描性能 |
| 压缩策略 | blob-file-compression |
zstd |
- none:超高频更新 - lz4:平衡场景 - zstd:冷数据/高压缩比需求 |
需配合zstd-dict-size(建议32KB)使用 |
| 缓存配置 | blob-cache-size |
总内存×50% - BlockCache |
大Value频繁读取场景 |
需确保BlockCache足够容纳LSM-Tree热数据 |
| GC回收机制 | discardable-ratio |
0.3(30%) | 增大:减少GC频率,但增大空间, 减小:降低空间放大,但是频繁 GC |
低于0.2可能导致GC过频繁 |
| 并发控制 | max-background-gc |
4-8线程 | GC压力大的场景(CPU>70%) | 每线程需预留1-2核CPU资源 |
I/O 限流 |
rate-bytes-per-sec |
100-200MB/s | 高负载场景防止I/O饱和 | 需根据磁盘类型调整(NVME可设更高) |
| 共享缓存(v8.0+) | shared-blob-cache |
true |
内存受限环境 | 当开启共享缓存时,Block 文件具有更高的优先级,TiKV 将优先满足 Block 文件的缓存需求,然后将剩余的缓存用于 Blob 文件 |
5、Partitioned Raft KV
在 TiKV 中,这是从 v6.6.0 版本开始引入的一种新的存储引擎架构,它的核心思想是:每个 Region 使用一个独立的 RocksDB 实例来存储数据,而不是像以前那样所有 Region 共享一个 RocksDB 实例。所以,它也被称为 Partitioned Raft KV,即“分区 Raft KV”。
问题1:为什么引入它?
在旧版本中(v6.6.0 之前),TiKV 使用单个 RocksDB 实例来管理所有 Region 的数据,这在面对大规模集群、高写入负载时,会带来一些性能瓶颈。写放大和读放大严重,扩缩容速度慢,Region 之间资源争抢,性能互相影响,难以支持更大规模的数据和集群
1)核心优势
| 优势 | 说明 |
|---|---|
| ✅ 更好的写入性能 | 每个 Region 独立写入自己的 RocksDB,减少写放大,提高吞吐 |
| ✅ 更快的扩缩容 | Region 搬迁更轻量,不涉及整个 RocksDB 的操作 |
| ✅ Region 间物理隔离 | 数据操作互不干扰,提升稳定性和可预测性 |
| ✅ 支持更大集群 | 更好地利用内存和 CPU 资源,相同硬件下支持更多数据和更大的集群 |
| ✅ 资源控制更精细 | 可以为每个 RocksDB 实例设置不同的配置,如内存、压缩策略等 |
a、更好的写入性能
- 减少写放大:在传统的单个
RocksDB实例中,所有Region的数据共享同一个LSM-tree结构,进行compaction操作时会涉及大量数据的重写,导致写放大问题。而在Partitioned Raft KV中,每个Region独立拥有一个 RocksDB 实例,compaction 只影响单个Region的数据,减少了不必要的数据重写,从而降低了写放大。 - 并行处理能力增强:由于每个
Region数据独立管理,多个Region的写入可以并行执行,充分利用多核CPU资源,提高整体写入吞吐量。
b、更快的扩缩容速度
- 轻量化搬迁:当需要进行节点间的数据迁移(如扩容或缩容)时,只需迁移特定
Region对应的RocksDB实例,而不需要像以前那样处理整个RocksDB实例的数据,大大简化了搬迁过程,提高了迁移效率。
c、Region 间物理隔离
- 减少相互影响:在传统模式下,如果某个
Region发生频繁的写入或读取操作,可能会对其他Region的性能造成影响。而通过为每个Region分配独立的RocksDB实例,实现了真正的物理隔离,确保各个 Region 的操作不会互相干扰,提升了系统的稳定性和可预测性。 - 减少资源争用:由于不同
Region的数据操作互不干扰,避免了因共享RocksDB实例导致的资源竞争问题,使得扩容或缩容操作更加平滑和高效。
d、支持更大集群
- 更高效的内存利用:虽然每个
Region 需要维护自己的RocksDB实例,但可以通过合理配置(如调整缓存大小),使得整个TiKV节点上的内存使用更加均衡,支持更多的Region和数据量。 - 分布式计算友好:这种架构有利于分布式计算任务的执行,因为每个
Region的数据独立,便于并行处理和分布式调度。
e、资源控制更精细
-
动态调整:根据实际运行情况,可以动态调整某些
Region的RocksDB配置,以适应不断变化的工作负载,提升整体系统性能。 -
更细粒度的资源控制:可以根据每个
Region的具体需求,灵活配置对应的RocksDB参数(如缓存大小、压缩策略等),实现更精细化的资源管理和优化。
2)使用限制(实验特性)
注意:该功能在引入时为实验特性,存在一定限制:
| 限制项 | 说明 |
|---|---|
| ❌ 不支持基于 EBS 的快照备份 | 暂不支持 BR 等工具的某些备份方式 |
| ❌ 不支持 Online Unsafe Recovery 和 Titan | 与部分高级功能不兼容 |
| ❌ 不支持部分 tikv-ctl 命令 | 如 unsafe-recover、raw-scan 等 |
| ❌ 不兼容 TiFlash | 暂不支持与 TiFlash 联合使用 |
| ❌ 初始化后不能开关 | 一旦启用,不能关闭;初始化前也不能中途启用 |
4、MVCC
TiKV 支持多版本并发控制。假设有这样一种场景:某客户端 A 在写一个 Key,另一个客户端 B 同时在对这个 Key 进行读操作。如果没有数据的多版本控制机制,那么这里的读写操作必然互斥。在分布式场景下,这种情况可能会导致性能问题和死锁问题。
有了 MVCC,只要客户端 B 执行的读操作的逻辑时间早于客户端 A,那么客户端 B 就可以在客户端 A 写入的同时正确地读原有的值。即使该 Key 被多个写操作修改过多次,客户端 B 也可以按照其逻辑时间读到旧的值。
TiKV 的 MVCC 是通过在 Key 后面添加版本号来实现的。没有 MVCC 时,可以把 TiKV 看作如下的 Key-Value 对:
Key1 -> Value
Key2 -> Value
……
KeyN -> Value
有了 MVCC 之后,TiKV 的 Key-Value 排列如下:
Key1_Version3 -> Value
Key1_Version2 -> Value
Key1_Version1 -> Value
……
Key2_Version4 -> Value
Key2_Version3 -> Value
Key2_Version2 -> Value
Key2_Version1 -> Value
……
KeyN_Version2 -> Value
KeyN_Version1 -> Value
……
注意,对于同一个 Key 的多个版本,版本号较大的会被放在前面,版本号小的会被放在后面(见 [Key-Value]一节,Key 是有序的排列),这样当用户通过一个 Key + Version 来获取 Value 的时候,可以通过 Key 和 Version 构造出 MVCC 的 Key,也就是 Key_Version。然后定位到第一个大于等于这个 Key_Version 的位置。
四、TiDB 数据库的计算
分布式
SQL的核心:计算靠近数据(下推) + 并行处理(分片) + 结果合并(Reduce)。
1、表数据与 Key-Value 的映射关系
1)表数据与 Key-Value 的映射关系
唯一KEY:在关系型数据库中,一个表可能有很多列。要将一行中各列数据映射成一个 (Key, Value) 键值对,需要考虑如何构造 Key。首先,OLTP 场景下有大量针对单行或者多行的增、删、改、查等操作,要求数据库具备快速读取一行数据的能力。因此,对应的 Key 最好有一个唯一 ID(显示或隐式的 ID),以方便快速定位。其次,很多 OLAP 型查询需要进行全表扫描。如果能够将一个表中所有行的Key 编码到一个区间内,就可以通过范围查询高效完成全表扫描的任务。
设计思想:基于上述考虑,TiDB 中的表数据与 Key-Value 的映射关系作了如下设计:
- 表
Id:为了保证同一个表的数据放在一起,方便查找,TiDB会为每个表分配一个表ID,用TableID表示。表ID是一个整数,在整个集群内唯一。 - 行
Id:TiDB会为表中每行数据分配一个行ID,用RowID表示。行ID也是一个整数,在表内唯一。对于行ID,TiDB 做了一个小优化,如果某个表有整数型的主键,TiDB会使用主键的值当做这一行数据的行ID。
每行数据按照如下规则编码成 (Key, Value) 键值对:
Key: tablePrefix{TableID}_recordPrefixSep{RowID}
Value: [col1, col2, col3, col4]
2)索引数据和 Key-Value 的映射关系
唯一索引:对于主键和唯一索引,需要根据键值快速定位到对应的 RowID,因此,按照如下规则编码成 (Key, Value) 键值对:
Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue
Value: RowID
二级索引:对于不需要满足唯一性约束的普通二级索引,一个键值可能对应多行,需要根据键值范围查询对应的 RowID。因此,按照如下规则编码成 (Key, Value) 键值对:
Key: tablePrefix{TableID}_indexPrefixSep{IndexID}_indexedColumnsValue_{RowID}
Value: null
3)映射关系小结
上述所有编码规则中的 tablePrefix、recordPrefixSep 和 indexPrefixSep 都是字符串常量,用于在 Key 空间内区分其他数据,定义如下:
tablePrefix = []byte{'t'}
recordPrefixSep = []byte{'r'}
indexPrefixSep = []byte{'i'}
2、SQL 层简介
TiDB的SQL层,即TiDBServer,负责将SQL翻译成Key-Value操作,将其转发给共用的分布式Key-Value存储层TiKV,然后组装TiKV返回的结果,最终将查询结果返回给客户端。这一层的节点都是无状态的,节点本身并不存储数据,节点之间完全对等。
1)SQL 运算
最简单的方案就是通过 表数据与-key-value-的映射关系)方案,将 SQL 查询映射为对 KV 的查询,再通过 KV 接口获取对应的数据,最后执行各种计算。
比如 select count(*) from user where name = "TiDB" 这样一个 SQL 语句,它需要读取表中所有的数据,然后检查 name 字段是否是 TiDB,如果是的话,则返回这一行。具体流程如下:
- 存储-构造
KeyRange:一个表中所有的RowID都在[0, MaxInt64)这个范围内,使用0和MaxInt64根据行数据的Key编码规则,就能构造出一个[StartKey, EndKey)的左闭右开区间。 - 查询-扫描
KeyRange:根据上面构造出的KeyRange,读取TiKV中的数据。 - 过滤数据:对于读到的每一行数据,计算
name = "TiDB"这个表达式,如果为真,则向上返回这一行,否则丢弃这一行数据。 - 计算
Count(*):对符合要求的每一行,累计到Count(*)的结果上面。
这个方案是直观且可行的,但是在分布式数据库的场景下有一些显而易见的问题:
- 在扫描数据的时候,每一行都要通过
KV操作从TiKV中读取出来,至少有一次RPC开销,如果需要扫描的数据很多,那么这个开销会非常大。 - 并不是所有的行都满足过滤条件
name = "TiDB",如果不满足条件,其实可以不读取出来。 - 此查询只要求返回符合要求行的数量,不要求返回这些行的值。
2)分布式 SQL 运算
为了解决上述问题,计算应该需要尽量靠近存储节点,以避免大量的 RPC 调用。
1、首先,SQL 中的谓词条件 name = "TiDB" 应被下推到存储节点进行计算,这样只需要返回有效的行,避免无意义的网络传输。
2、然后,聚合函数 Count(*) 也可以被下推到存储节点,进行预聚合,每个节点只需要返回一个 Count(*) 的结果即可
3、再由 SQL 层将各个节点返回的 Count(*) 的结果累加求和。

3、查询流程
SELECT department, AVG(salary) FROM employees WHERE age > 30 GROUP BY department
1) 数据分布假设
- 表
employees被分片到 3 个TiKV节点(按主键范围):- Region1(TiKV-1):
[Alice, Bob] - Region2(TiKV-2):
[Carol, Dave] - Region3(TiKV-3):
[Eve, Frank]
- Region1(TiKV-1):
- 每个
Region存储部分数据(包含department,salary,age列)。
2)分布式执行流程图

3)分步流程
阶段 1:SQL 解析与优化(TiDB Server)
- 解析
SQL:识别出需下推的操作:- 过滤条件:
age > 30 - 聚合函数:
AVG(salary) - 分组键:
department
- 过滤条件:
- 生成分布式计划:将过滤和聚合下推到
TiKV节点,TiDB仅负责汇总。
阶段 2:分布式计算(TiKV 节点):每个 TiKV 节点 并行执行:
- 谓词下推(
FilterPushdown):- 扫描本地数据,过滤出
age > 30的行。 - 例如
TiKV-1保留[Alice(HR,50K), Bob(Eng,60K)]。
- 扫描本地数据,过滤出
- 聚合下推(Aggregation Pushdown):
- 按
department分组计算AVG(salary):TiKV-1:HR=50K,Eng=60KTiKV-2:Eng=60KTiKV-3:HR=55K
- 按
阶段 3:结果汇总(TiDB Server)
-
接收部分结果:从
TiKV节点收集预聚合结果:TiKV-1: [(HR, 50K), (Eng, 60K)] TiKV-2: [(Eng, 60K)] TiKV-3: [(HR, 55K)] -
全局聚合(
Reduce阶段):合并相同分组的结果:HR:(50K + 55K) / 2 = 52.5KEng:(60K + 60K) / 2 = 60K
-
返回最终结果:
+------------+-------------+
| department | AVG(salary) |
+------------+-------------+
| HR | 52.5K |
| Eng | 60K |
+------------+-------------+
4)问题分析
问题1: 为什么 TiDB Server 需要参与部分计算?
TiDBServer参与计算:负责SQL解析、分布式计划生成、跨节点数据合并、复杂运算。-
分布式协调需求:跨节点数据合并(如
JOIN、UNION)需中央节点协调。 -
SQL复杂性:存储层(TiKV/TiFlash)不支持所有SQL语法(如递归 CTE)。 -
事务一致性:全局事务管理(如多表更新)需
TiDB协调。
-
TiDBServer不参与计算:已下推的操作(如谓词过滤、单表聚合、索引扫描)由存储层完成。- 设计目标:计算尽量靠近数据(下推),仅必要时由 TiDB 协调,实现高性能分布式查询。
问题2:为什么 TiDB 不直接在 TiKV 上完成最终汇总计算
- 存算一体 vs 存算分离:
-
目标场景不同:
TiDB优先保证OLTP事务性能,Doris优先OLAP分析性能。 - 架构复杂度:在
TiKV上实现全局聚合会引入分布式协调开销,违背其“简单存储”的设计初衷。-
Doris:采用的是存算一体的架构,这意味着存储和计算资源通常是绑定在一起的。这种设计的一个优点是在处理大规模数据聚合时可以直接在存储节点(Backend节点)之间进行Shuffle操作来完成全局聚合,减少了中央节点的数据汇总压力,并且可以更高效地利用本地数据。 -
TiDB:采用的是存算分离的架构,即计算层(TiDB Server)和存储层(TiKV)是分开的。这样的设计有助于实现更高的灵活性和可扩展性,例如TiDB Server可以独立扩缩容而不影响底层存储,同时也简化了系统的设计,避免了存储节点之间的直接通信。
-
- 结果汇总位置的不同:
- 在
Doris中,由于其存算一体的特点,结果可以在Backend节点间通过网络传输来完成最终的聚合工作,这使得它能够有效地支持复杂查询的执行。 - 而在
TiDB中,由于采用了存算分离的策略,TiDB Server负责从多个TiKV节点收集中间结果并进行最后的合并。这样做不仅简化了架构,也确保了TiKV可以专注于高性能的存储和简单的计算任务。
- 在
- 协议兼容性和
SQL语义的支持:TiDB兼容MySQL协议,因此需要支持复杂的SQL特性,如事务、触发器等。这些复杂逻辑通常难以完全下推到存储层处理,这也是为什么TiDB选择在计算层完成最终的结果汇总。- 此外,考虑到
TiKV作为行存引擎,在处理复杂聚合查询(如多阶段GROUP BY,JOIN等)时效率较低,这也进一步说明了为何不在TiKV上做全局汇总。
问题3:Tidb region 副本不参与查询?
- 架构设计目标:
TiDB是面向OLTP场景设计的,强调一致性、事务支持,因此牺牲了部分查询并发能力以换取稳定性。TiDB查询只访问Raft Leader,确保读取到的数据是最新的(强一致性)如果是flower副本的话,少数派确认可能没有数据Doris的Tablet多副本机制中,所有副本都可用于查询,多个副本之间可以分担查询压力。Doris主要面向分析型场景OLAP,更关注查询性能和并发能力,对实时一致性的要求相对较低。
| 特性 | TiDB | Doris |
|---|---|---|
| 存储模型 | 行存(TiKV) |
列存(BE) |
| 副本是否参与查询 | 默认不参与(仅 Leader) |
所有副本均可参与 |
| 是否支持读副本 | 支持(需手动开启) | 天然支持 |
| 架构类型 | 存算分离 | 存算一体 |
| 查询调度位置 | TiDB Server 统一汇总 |
BE 节点本地执行 |
| 典型使用场景 | OLTP + 实时分析 |
OLAP 分析 |
五、TiDB 数据库的调度
PD是TiDB集群的“大脑”,负责全局调度和元数据管理,通过实时监控集群状态,动态调整数据分布和副本策略,解决您提到的所有复杂场景问题。以下是PD如何应对各类问题的详细设计:
| 问题场景 | 流程 |
|---|---|
| 数据分布不均 | 根据 Region 的空间占用对副本进行合理的分布 |
| 跨机房容灾 | 集群进行跨机房部署的时候,要保证一个机房掉线,不会丢失 Raft Group 的多个副本 |
| 节点扩缩容 | 添加一个节点进入 TiKV 集群之后,需要合理地将集群中其他节点上的数据搬到新增节点 |
| 副本管理 | 短暂掉线要评估、长时间掉线要调度;副本不够要补充、副本过多要删除 |
Leader 集中 |
读/写通过 Leade进行,Leader 的分布只集中在少量几个节点会对集群造成影响 |
| 热点集中 | 并不是所有的 Region 都被频繁的访问,可能访问热点只在少数几个 Region,需要通过调度进行负载均衡 |
| 资源竞争 | 集群在做负载均衡的时候,往往需要搬迁数据,这种数据的迁移可能会占用大量的网络带宽、磁盘 IO 以及 CPU,进而影响在线服务。 |
1、调度的需求
为了满足上面这些需求,需要收集足够的信息,比如每个节点的状态、每个 Raft Group 的信息、业务访问操作的统计等;其次需要设置一些策略,PD 根据这些信息以及调度的策略,制定出尽量满足前面所述需求的调度计划;最后需要一些基本的操作,来完成调度计划。以上问题可归为以下两类:
| 需求类型 | 核心问题 | 实现目标 |
|---|---|---|
| 第一类:容灾功能(必需) | ||
| 副本数量管理 | 副本过少(节点宕机)或过多(节点恢复) | 确保每个 Raft Group 始终有 N 个副本 |
| 拓扑感知分布 | 副本集中导致单点故障(如同一机房) | 跨机房/机架分布,满足多数派存活 |
| 节点异常处理 | 节点宕机(短暂或永久) | 快速恢复数据可用性 |
| 第二类:资源优化(扩展性) | ||
Leader 分布均衡 |
Leader 集中在少数节点导致负载不均 |
避免单节点过载 |
| 存储容量均衡 | 部分节点磁盘满,其他节点闲置 | 提高存储利用率 |
| 热点访问均衡 | 少数 Region 被频繁访问(如热门商品) |
分散热点压力 |
| 负载均衡速率控制 | 数据迁移占用带宽/CPU,影响在线服务 |
平衡迁移速度与业务稳定性 |
| 节点状态管理 | 手动维护节点(上线/下线) | 灵活扩缩容 |
2、调度的基本操作
调度的基本操作指的是为了满足调度的策略。上述调度需求可整理为以下三个操作:
- 增加一个副本
- 删除一个副本
- 将
Leader角色在一个RaftGroup的不同副本之间transfer(迁移)
3、信息收集
调度依赖于整个集群信息的收集,简单来说,调度需要知道每个
TiKV节点的状态以及每个Region的状态。TiKV集群会向PD汇报两类消息,TiKV节点信息和Region信息:
1)TiKV 节点汇报
每个 TiKV 节点会定期向 PD 汇报节点的状态信息
- 总磁盘容量,可用磁盘容量
- 承载的
Region数量 - 数据写入/读取速度
- 发送/接受的
Snapshot数量(副本之间可能会通过Snapshot同步数据) - 是否过载
labels标签信息(标签是具备层级关系的一系列Tag,
a、节点状态流程

b、节点状态含义
(1)Up:正常服务
-
含义:
TiKV Store正常运行,提供读写服务。 -
触发条件:
- 节点启动并成功注册到 PD。
- 心跳持续正常(默认每 10 秒上报一次)。
(2)Disconnect:心跳中断
- 含义:
PD与TiKV Store心跳丢失,可能因网络抖动或节点短暂故障 -
触发条件:心跳丢失超过 20 秒。
- 恢复路径:
- 心跳恢复:自动回到
Up状态。 - 持续中断:超过
max-store-down-time(默认 30 分钟)转为Down。
- 心跳恢复:自动回到
(3)Down(节点不可用)
-
含义:节点长时间不可用,
PD判定需补副本。 -
关键行为:
PD在其他健康节点上补足缺失的Region副本(通过Raft Snapshot)。- 若节点恢复:需同步最新数据,确认无冲突后回到
Up。 - 若未恢复:补足副本后,原节点标记为
Tombstone。
(4)Offline(手动下线中)
- 含义:管理员主动下线节点,迁移其数据。
- 触发条件:通过
pd-ctl执行store delete <store_id>。 - 关键行为:
PD逐步将该节点的Region迁移到其他Up节点。- 完成条件:
leader_count和region_count均为 0(通过pd-ctl store <store_id>查看)。 - 注意事项:
- 在
Offline状态下,禁止关闭该Store服务以及其所在的物理服务器,否则可能导致数据迁移失败。 - 如果集群里不存在满足搬迁条件的其它目标
Store(例没有足够的Store能够继续满足集群的副本数量要求),该Store将一直处于Offline状态
- 在
(5)Tombstone(完全下线)
- 含义:节点数据已全部迁移,可安全移除。
- 清理方式:
- 手动清理:执行
pd-ctl remove-tombstone。 - 自动清理:
Tombstone状态满 1 个月后 PD 自动删除记录。
- 手动清理:执行
2)Raft Group 的 Leader 汇报
每个 Raft Group 的 Leader 和 PD 之间存在心跳包,用于汇报这个 [Region 的状态],主要包括下面几点信息:
Leader的位置Followers的位置- 掉线副本的个数
- 数据写入/读取的速度
4、调度的策略
1)一个 Region 的副本数量正确
当 PD 通过某个 Region Leader 的心跳包发现这个 Region 的副本数量不满足要求时,需要通过 Add/Remove Replica 操作调整副本数量。出现这种情况的可能原因是:
- 某个节点掉线,上面的数据全部丢失,导致一些
Region的副本数量不足 - 某个掉线节点又恢复服务,自动接入集群,这样之前已经补足了副本的
Region的副本数量过多,需要删除某个副本 - 管理员调整副本策略,修改了 [max-replicas]的配置
2)一个 Raft Group 中的多个副本不在同一个位置
注意这里用的是『同一个位置』而不是『同一个节点』。在一般情况下,PD 只会保证多个副本不落在一个节点上,以避免单个节点失效导致多个副本丢失。在实际部署中,还可能出现下面这些需求:
- 多个节点部署在同一台物理机器上
TiKV节点分布在多个机架上,希望单个机架掉电时,也能保证系统可用性TiKV节点分布在多个IDC中,希望单个机房掉电时,也能保证系统可用性
这些需求本质上都是某一个节点具备共同的位置属性,构成一个最小的『容错单元』,希望这个单元内部不会存在一个 Region 的多个副本。
3)副本在 Store 之间的分布均匀分配
由于每个 Region 的副本中存储的数据容量上限是固定的,通过维持每个节点上面副本数量的均衡,使得各节点间承载的数据更均衡。
4)Leader 数量在 Store 之间均匀分配
Raft 协议要求读取和写入都通过 Leader 进行,所以计算的负载主要在Leader 上面,PD 会尽可能将 Leader在节点间分散开。
5)访问热点数量在 Store 之间均匀分配
每个 Store 以及 Region Leader 在上报信息时携带了当前访问负载的信息,比如 Key 的读取/写入速度。PD 会检测出访问热点,且将其在节点之间分散开。
6)各个 Store 的存储空间占用大致相等
每个 Store 启动的时候都会指定一个 Capacity 参数,表明这个 Store 的存储空间上限,PD 在做调度的时候,会考虑节点的存储空间剩余量。
7)控制调度速度,避免影响在线服务
调度操作需要耗费 CPU、内存、磁盘 IO 以及网络带宽,需要避免对线上服务造成太大影响。PD 会对当前正在进行的操作数量进行控制,默认的速度控制是比较保守的,如果希望加快调度(比如停服务升级或者增加新节点,希望尽快调度),那么可以通过调节 PD 参数动态加快调度速度。
8)调度的实现
信息收集:PD 不断地通过 Store 或者 Leader 的心跳包收集整个集群信息,并且根据这些信息以及调度策略生成调度操作序列。
信息反馈:每次收到 Region Leader 发来的心跳包时,PD 都会检查这个 Region 是否有待进行的操作,然后通过心跳包的回复消息,将需要进行的操作返回给 Region Leader,并在后面的心跳包中监测执行结果。
调度执行:注意这里的操作只是给 Region Leader 的建议,并不保证一定能得到执行,具体是否会执行以及什么时候执行,由 Region Leader 根据当前自身状态来定。
a、整体流程
1. Region Leader (Store 1) 发送心跳给 PD
└─ 携带信息:Region 负载高、Store 1 CPU 使用率80%
2. PD 分析后决定:
└─ 建议将 Leader 转移到 Store 3(负载较低)
3. PD 在心跳回复中携带:
└─ 调度建议:TransferLeader(Region 123, From Store1, To Store3)
4. Region Leader 收到后:
└─ 检查 Store 3 是否可用
└─ 若无冲突,开始 Leader 转移流程
└─ 若正处理重要事务,可能延迟执行
5. 下次心跳时:
└─ 报告调度执行结果(成功/失败/延迟)
└─ PD 根据结果决定是否重新调度
b、普通 Follower 副本迁移流程
- 添加
Learner:PD在目标TiKV节点上为RaftGroup添加一个Learner副本 - 数据同步:
Learner从Leader异步复制数据(不参与写确认) - 提升为
Follower:当Learner数据接近最新时,Leader将其提升为正式Follower - 移除旧副本:从原
TiKV节点移除旧的Follower副本
c、Leader 副本迁移流程
- 完成上述 1-3 步:先在目标节点建立
Learner并提升为Follower Leader转移:PD发起LeaderTransfer命令:- 旧
Leader主动让位 - 新
Follower发起选举成为新Leader
- 旧
- 清理旧
Leader:新Leader将原Leader副本降级并移除(保证正确的副本数量)
六、TSO
TSO(TimeStampOracle) 是TiDB分布式数据库实现全局一致性的核心组件它为整个分布式系统提供严格递增、唯一的时间戳服务。
1、TSO 的核心作用
- 全局事务排序:为所有分布式事务分配唯一时间戳
MVCC版本控制:支持多版本并发控制机制- 一致性保证:确保分布式事务的线性一致性
2、TSO 的架构设计
1)物理组成
PD(PlacementDriver) 内置:TSO服务内嵌在PD组件中- 高可用部署:通常部署
3-5个PD节点组成集群 Leader提供服务:只有PDLeader节点提供TSO服务
2)逻辑结构
每个 TSO 由两部分组成: |
物理时间戳(高位) | 逻辑计数器(低位) |
- 物理部分:毫秒级精度系统时间
- 逻辑部分:每毫秒内的递增序列号
- 用于需要在一毫秒内使用多个时间戳的情况,或某些事件可能触发时钟进程逆转的情况。在这些情况下,物理时间戳会保持不变,而逻辑时间戳保持递增。该机制可以确保
TSO时间戳的完整性,确保时间戳始终递增而不会倒退。
- 用于需要在一毫秒内使用多个时间戳的情况,或某些事件可能触发时钟进程逆转的情况。在这些情况下,物理时间戳会保持不变,而逻辑时间戳保持递增。该机制可以确保
3、获取 TiDB 当前的 TSO
TSO 时间戳是按事务分配的,所以需要从包含 BEGIN; ...; ROLLBACK 的事务中获取时间戳。TSO 时间戳是一个十进制数
BEGIN; SET @ts := @@tidb_current_ts; ROLLBACK;
Query OK, 0 rows affected (0.0007 sec)
Query OK, 0 rows affected (0.0002 sec)
Query OK, 0 rows affected (0.0001 sec)
SELECT @ts;
+--------------------+
| @ts |
+--------------------+
| 443852055297916932 |
+--------------------+
1 row in set (0.00 sec)
TSO 时间戳的二进制细节:
0000011000101000111000010001011110111000110111000000000000000100 ← 该值是二进制形式的 443852055297916932
0000011000101000111000010001011110111000110111 ← 前 46 位是物理时间戳
000000000000000100 ← 后 18 位是逻辑时间戳
七、TiDB 数据倾斜问题解决方案
1、数据倾斜的本质与影响
本质:数据在分布式集群中分布不均,导致部分 TiKV 节点负载过高(如存储量、读写请求数远超其他节点)。
影响:
- 热点节点成为性能瓶颈,导致整体集群吞吐量下降;
Raft日志同步延迟增加,影响事务一致性;- 部分节点资源(
CPU/IO/ 内存)利用率过高,可能触发服务降级。
2、数据倾斜的常见原因
TiDB 解决数据倾斜需从 设计(表结构)、调度(PD 策略)、业务(流量打散) 三层入手:
- 预防优先:设计阶段避免自增主键、合理选择分片键;
- 动态调度:通过
PD自动迁移热点Region,配合参数优化提升调度效率; - 业务适配:对高频操作添加缓存或分片查询,减轻集群压力。
通过上述方案,可有效将数据倾斜率(单节点负载 / 平均负载)控制在 1.5 倍以内,保障集群性能稳定性。
1)表结构设计问题
- 主键或分区键选择不当(如使用自增
ID、时间字段未分片),导致数据集中在少数Region; - 未创建合适的二级索引,查询或写入操作集中在特定数据范围。
2)集群调度策略不足
PD(PlacementDriver)调度参数未优化,Region迁移不及时或策略不合理。
3)业务访问模式倾斜
- 高频操作(如更新、查询)集中在某段时间或某个值域(如订单表按日期查询近期数据);
- 缓存失效或业务突发流量导致瞬时访问集中。
3、TiDB 数据倾斜解决方案
1) 表结构与索引优化
a、主键设计:避免自增 ID 与热点键
-
反模式:使用自增主键(如
AUTO_INCREMENT),导致新数据始终写入最后一个Region。 -
优化方案
-
哈希分片主键:将主键与哈希值结合(如
id + MOD(id, 100)),打散数据分布; -
复合主键:使用业务字段 + 随机数组合
CREATE TABLE t ( user_id BIGINT, rand_num INT, -- 其他字段 PRIMARY KEY (user_id, rand_num) ); -
UUID主键:使用UUID()或UUID_TO_BIN()生成无序主键(需注意UUID索引查询效率)。
-
b、分区表与分片规则
- 范围分区(适用于时间序列数据)按时间字段分区(如年 / 月),并结合哈希分片避免单分区过热:
REATE TABLE t (
id BIGINT,
create_time DATETIME,
-- 其他字段
PRIMARY KEY (id, create_time)
)
PARTITION BY RANGE COLUMNS(create_time) (
PARTITION p2024 VALUES LESS THAN ('2025-01-01'),
PARTITION p2025 VALUES LESS THAN ('2026-01-01'),
-- 更多分区
);
- 哈希分区(适用于均匀分布场景):对高频访问字段添加哈希分片(如用户 ID):
CREATE TABLE t (
user_id BIGINT,
-- 其他字段
PRIMARY KEY (MOD(user_id, 100), user_id)
);
c、二级索引优化
-
主键打散的是“主表数据”的物理分布,但二级索引是独立存储的结构,它的分布由“索引键”决定!
-
对高频查询 / 更新字段创建索引时,避免索引键倾斜:
- 反例:对
status字段(值分布不均,如90%为active)创建索引,导致查询集中在少数Region; - 优化:添加随机后缀或组合字段(如
(status, RAND())),打散索引访问。
- 反例:对
2)集群调度与参数优化
a、PD 调度参数调整
- 通过
pd-ctl命令优化Region迁移策略
开启负载均衡:
pd-ctl config set enable-rebalance-hot-region true
pd-ctl config set hot-region-schedule-limit 4 # 热点 Region 调度并发数
调整 Region 大小阈值:
pd-ctl config set max-region-size 1048576 # 单位 KB,默认 96MB,可减小至 64MB 提升分片密度
pd-ctl config set max-region-keys 200000 # 单个 Region 最大键数,默认 20 万,可根据数据大小调整
热点 Region 强制迁移:
pd-ctl hot-region schedule table table_id # 对指定表的热点 Region 触发调度
b、TiKV 存储层优化
- 调整
TiKV存储参数,提升热点Region处理能力:
[rocksdb]
max-sub-compactions = 4 # 并发压缩任务数,提升写入性能
max-background-jobs = 20 # 后台任务数,默认 16,可增加至 20
3)业务层面优化
a、读写请求打散
- 查询场景:对时间范围查询添加随机偏移(如查询近
7天数据时,按用户ID分片查询); - 写入场景:对高频写入表添加中间缓存(如
Redis),批量写入TiDB(减少瞬时流量冲击)。
b、冷热数据分离
- 将高频访问的热数据与历史冷数据分表存储:
- 热数据表:采用高效分片策略,部署在高性能节点;
- 冷数据表:降低资源配置,或归档至 TiDB 归档集群(如使用 TiDB-Dump + 离线导入)。
5)主键、分区与二级索引
三者协同的本质是:通过主键打散物理存储,分区管理逻辑分组,索引优化访问路径,从 “存储分布→数据管理→查询效率” 形成闭环。
- 主键:决定数据在分布式集群中的基础分布(全局打散);
- 分区:在主键分片基础上,进一步按业务维度(如时间、地域)分组,降低单分区数据规模;
- 二级索引:优化查询路径,避免因索引键倾斜导致的访问集中。
问题1:region 和分区关系?
可以把分区表看作 “文件夹”,**Region **看作 “文件夹里的文件块”:
- 一个数据库表被分成多个 “文件夹”(分区 p2024、p2025),每个文件夹存放特定时间的数据。
- 每个文件夹里的文件(数据)被进一步拆分成多个 “文件块”(
Region),每个文件块分散存储在不同硬盘(TiKV 节点)上。 - 主键打散相当于给每个文件块编号(MOD 运算),让文件块均匀分布在硬盘上,避免单个硬盘过载。
- 二级索引相当于每个文件夹的 “目录”,查询时直接查对应文件夹的目录,快速定位文件块。
a、主键
主键:分布式存储的 “基石”—— 决定数据打散粒度
案例:电商订单表
-
反模式:使用自增主键
order_id,新订单全部写入最后一个Region,导致写入倾斜; -
优化设计
-- 主键 = 业务主键 + 哈希分片键 PRIMARY KEY (order_id, MOD(order_id, 100)) -
协同逻辑
- 哈希分片键(
MOD(order_id, 100))将数据分散到100个逻辑桶中,每个桶对应独立Region,避免写入集中; - 分区与索引基于此主键分布进一步优化(如按桶分区、对分片键创建索引)。
- 哈希分片键(
b、分区
分区:在主键分片上 “逻辑分组”—— 缩小倾斜影响范围
场景:按时间维度分区
-
表结构设计
CREATE TABLE orders ( order_id BIGINT, create_time DATETIME, -- 其他字段 PRIMARY KEY (order_id, MOD(order_id, 100)), -- 主键打散 KEY idx_create_time (create_time) -- 二级索引 ) PARTITION BY RANGE COLUMNS(create_time) ( -- 按时间分区 PARTITION p2024 VALUES LESS THAN ('2025-01-01'), PARTITION p2025 VALUES LESS THAN ('2026-01-01') ); -
协同逻辑
- 主键打散:每个时间分区内的数据通过
MOD(order_id, 100)分散到100个Region,避免单分区内写入倾斜; - 分区隔离:历史分区(如 p2024)与当前分区(p2025)物理隔离,热点仅影响当前分区,不波及全量数据;
- 索引优化:二级索引
idx_create_time在每个分区内独立维护,查询时按分区过滤,减少跨分区扫描。
- 主键打散:每个时间分区内的数据通过
c、二级索引
二级索引:优化访问路径 —— 避免索引键导致的查询倾斜
场景:用户表按 status 字段高频查询
-
问题:若
status取值分布不均(如90%为active),索引查询会集中访问少数Region; -
协同优化方案
CREATE TABLE users ( user_id BIGINT, status VARCHAR(20), rand_suffix INT, -- 主键与分区 PRIMARY KEY (user_id, MOD(user_id, 50)), -- 主键打散 -- 带随机后缀的二级索引 (user_id 避免回表) KEY idx_status (status, rand_suffix, user_id) ) PARTITION BY HASH(user_id) PARTITIONS 10; -- 按用户 ID 哈希分区 -
协同逻辑
- 主键与分区:
user_id哈希分片确保用户数据均匀分布在50个Region和10个分区中; - 索引优化:
status索引添加随机后缀rand_suffix,将同类status的查询分散到不同Region(如status='active'的查询会访问rand_suffix为0-99的多个Region); - 查询改写:业务查询时添加随机后缀条件(如
WHERE status='active' AND rand_suffix = FLOOR(RAND()*100)),进一步打散流量。
- 主键与分区:


