首先放TiDB的gitbook,里面对于tidb的大部分内容都有详细的介绍:https://book.tidb.io/
其次实际的应用当然需要PingCAP的官方文档:https://docs.pingcap.com/zh/tidb/stable/overview,因为自己只是想了解下TiDB,感觉这个数据库有点火,大致了解一下工作原理,自己在上一家公司有实际使用接触过,但是因为当时自己经验不足,不觉得他有什么特别或者不同,就没有过多了解。现在在美团公司内部有自己的分布式数据库,但是我负责的业务场景实际用不到,还是以Mysql为主,所以在这里根据网络上的这些资料,对TiDB做一个简单的学习和总结。
TiDB简介
TiDB 是 [PingCAP](https://pingcap.com/about-
cn/) 公司自主设计、研发的开源分布式关系型数据库,是一款同时支持在线事务处理与在线分析处理 (Hybrid Transactional and
Analytical Processing, HTAP) 的融合型分布式数据库产品,具备水平扩容或者缩容、金融级高可用、实时
HTAP、云原生的分布式数据库、兼容 MySQL 5.7 协议和 MySQL 生态等重要特性。目标是为用户提供一站式 OLTP (Online
Transactional Processing)、OLAP (Online Analytical Processing)、HTAP 解决方案。TiDB
适合高可用、强一致要求较高、数据规模较大等各种应用场景。
五大核心特性
得益于 TiDB 存储计算分离的架构的设计,可按需对计算、存储分别进行在线扩容或者缩容,扩容或者缩容过程中对应用运维人员透明。
数据采用多副本存储,数据副本通过 Multi-Raft
协议同步事务日志,多数派写入成功事务才能提交,确保数据强一致性且少数副本发生故障时不影响数据的可用性。可按需配置副本地理位置、副本数量等策略满足不同容灾级别的要求。
提供行存储引擎 TiKV、列存储引擎
TiFlash
两款存储引擎,TiFlash 通过 Multi-Raft Learner 协议实时从 TiKV 复制数据,确保行存储引擎 TiKV 和列存储引擎
TiFlash 之间的数据强一致。TiKV、TiFlash 可按需部署在不同的机器,解决 HTAP 资源隔离的问题。
专为云而设计的分布式数据库,通过 [TiDB Operator](https://docs.pingcap.com/zh/tidb-in-
kubernetes/stable/tidb-operator-overview) 可在公有云、私有云、混合云中实现部署工具化、自动化。
- 兼容 MySQL 5.7 协议和 MySQL 生态
兼容 MySQL 5.7 协议、MySQL 常用的功能、MySQL 生态,应用无需或者修改少量代码即可从 MySQL 迁移到
TiDB。提供丰富的[数据迁移工具](https://docs.pingcap.com/zh/tidb/stable/ecosystem-tool-
user-guide)帮助应用便捷完成数据迁移。
四大核心应用场景
- 对数据一致性及高可靠、系统高可用、可扩展性、容灾要求较高的金融行业属性的场景
众所周知,金融行业对数据一致性及高可靠、系统高可用、可扩展性、容灾要求较高。传统的解决方案是同城两个机房提供服务、异地一个机房提供数据容灾能力但不提供服务,此解决方案存在以下缺点:资源利用率低、维护成本高、RTO
(Recovery Time Objective) 及 RPO (Recovery Point Objective) 无法真实达到企业所期望的值。TiDB
采用多副本 + Multi-Raft 协议的方式将数据调度到不同的机房、机架、机器,当部分机器出现故障时系统可自动进行切换,确保系统的 RTO <= 30s
及 RPO = 0。
- 对存储容量、可扩展性、并发要求较高的海量数据及高并发的 OLTP 场景
随着业务的高速发展,数据呈现爆炸性的增长,传统的单机数据库无法满足因数据爆炸性的增长对数据库的容量要求,可行方案是采用分库分表的中间件产品或者 NewSQL
数据库替代、采用高端的存储设备等,其中性价比最大的是 NewSQL 数据库,例如:TiDB。TiDB
采用计算、存储分离的架构,可对计算、存储分别进行扩容和缩容,计算最大支持 512 节点,每个节点最大支持 1000 并发,集群容量最大支持 PB 级别。
随着 5G、物联网、人工智能的高速发展,企业所生产的数据会越来越多,其规模可能达到数百 TB 甚至 PB 级别,传统的解决方案是通过 OLTP
型数据库处理在线联机交易业务,通过 ETL 工具将数据同步到 OLAP 型数据库进行数据分析,这种处理方案存在存储成本高、实时性差等多方面的问题。TiDB
在 4.0 版本中引入列存储引擎 TiFlash 结合行存储引擎 TiKV 构建真正的 HTAP
数据库,在增加少量存储成本的情况下,可以在同一个系统中做联机交易处理、实时数据分析,极大地节省企业的成本。
当前绝大部分企业的业务数据都分散在不同的系统中,没有一个统一的汇总,随着业务的发展,企业的决策层需要了解整个公司的业务状况以便及时做出决策,故需要将分散在各个系统的数据汇聚在同一个系统并进行二次加工处理生成
T+0 或 T+1 的报表。传统常见的解决方案是采用 ETL + Hadoop 来完成,但 Hadoop
体系太复杂,运维、存储成本太高无法满足用户的需求。与 Hadoop 相比,TiDB 就简单得多,业务通过 ETL 工具或者 TiDB 的同步工具将数据同步到
TiDB,在 TiDB 中可通过 SQL 直接生成报表。
TiDB整体架构
在内核设计上,TiDB 分布式数据库将整体架构拆分成了多个模块,各模块之间互相通信,组成完整的 TiDB 系统。对应的架构图如下:

- TiDB Server:SQL 层,对外暴露 MySQL 协议的连接 endpoint,负责接受客户端的连接,执行 SQL 解析和优化,最终生成分布式执行计划。TiDB 层本身是无状态的,实践中可以启动多个 TiDB 实例,通过负载均衡组件(如 LVS、HAProxy 或 F5)对外提供统一的接入地址,客户端的连接可以均匀地分摊在多个 TiDB 实例上以达到负载均衡的效果。TiDB Server 本身并不存储数据,只是解析 SQL,将实际的数据读取请求转发给底层的存储节点 TiKV(或 TiFlash)。
- PD (Placement Driver) Server:整个 TiDB 集群的元信息管理模块,负责存储每个 TiKV 节点实时的数据分布情况和集群的整体拓扑结构,提供 TiDB Dashboard 管控界面,并为分布式事务分配事务 ID。PD 不仅存储元信息,同时还会根据 TiKV 节点实时上报的数据分布状态,下发数据调度命令给具体的 TiKV 节点,可以说是整个集群的“大脑”。此外,PD 本身也是由至少 3 个节点构成,拥有高可用的能力。建议部署奇数个 PD 节点。
- 存储节点
- TiKV Server:负责存储数据,从外部看 TiKV 是一个分布式的提供事务的 Key-Value 存储引擎。存储数据的基本单位是 Region,每个 Region 负责存储一个 Key Range(从 StartKey 到 EndKey 的左闭右开区间)的数据,每个 TiKV 节点会负责多个 Region。TiKV 的 API 在 KV 键值对层面提供对分布式事务的原生支持,默认提供了 SI (Snapshot Isolation) 的隔离级别,这也是 TiDB 在 SQL 层面支持分布式事务的核心。TiDB 的 SQL 层做完 SQL 解析后,会将 SQL 的执行计划转换为对 TiKV API 的实际调用。所以,数据都存储在 TiKV 中。另外,TiKV 中的数据都会自动维护多副本(默认为三副本),天然支持高可用和自动故障转移。
- TiFlash:TiFlash 是一类特殊的存储节点。和普通 TiKV 节点不一样的是,在 TiFlash 内部,数据是以列式的形式进行存储,主要的功能是为分析型的场景加速。
存储
Key-Value Pairs (键值对)
作为保存数据的系统,首先要决定的是数据的存储模型,也就是数据以什么样的形式保存下来。TiKV 的选择是 Key-Value 模型,并且提供有序遍历方法。
TiKV 数据存储的两个关键点:
- 这是一个巨大的 Map(可以类比一下 C++ 的 std::map),也就是存储的是 Key-Value Pairs(键值对)
- 这个 Map 中的 Key-Value pair 按照 Key 的二进制顺序有序,也就是可以 Seek 到某一个 Key 的位置,然后不断地调用 Next 方法以递增的顺序获取比这个 Key 大的 Key-Value。
有人可能会问,这里讲的存储模型和 SQL 中表是什么关系?在这里有一件重要的事情需要强调:
TiKV 的 KV 存储模型和 SQL 中的 Table 无关!
现在让我们忘记 SQL 中的任何概念,专注于讨论如何实现 TiKV 这样一个高性能、高可靠性、分布式的 Key-Value 存储。
本地存储(RocksDB)
任何持久化的存储引擎,数据终归要保存在磁盘上,TiKV 也不例外。但是 TiKV 没有选择直接向磁盘上写数据,而是把数据保存在 RocksDB
中,具体的数据落地由 RocksDB 负责。这个选择的原因是开发一个单机存储引擎工作量很大,特别是要做一个高性能的单机引擎,需要做各种细致的优化,而
RocksDB 是由 Facebook 开源的一个非常优秀的单机 KV 存储引擎,可以满足 TiKV 对单机引擎的各种要求。这里可以简单的认为
RocksDB 是一个单机的持久化 Key-Value Map。
Raft 协议
接下来 TiKV 的实现面临一件更难的事情:如何保证单机失效的情况下,数据不丢失,不出错?
简单来说,需要想办法把数据复制到多台机器上,这样一台机器挂了,其他的机器上的副本还能提供服务;
复杂来说,还需要这个数据复制方案是可靠和高效的,并且能处理副本失效的情况。TiKV 选择了 Raft 算法。Raft 是一个一致性协议,它和 Multi
Paxos 实现一样的功能,但是更加易于理解。这里 是 Raft
的论文,感兴趣的可以看一下。下面对 Raft 做一个简要的介绍,细节问题可以参考论文。 Raft 提供几个重要的功能:
- Leader(主副本)选举
- 成员变更(如添加副本、删除副本、转移 Leader 等操作)
- 日志复制
TiKV 利用 Raft 来做数据复制,每个数据变更都会落地为一条 Raft 日志,通过 Raft
的日志复制功能,将数据安全可靠地同步到复制组的每一个节点中。不过在实际写入中,根据 Raft 的协议,只需要同步复制到多数节点,即可安全地认为数据写入成功。

总结一下,通过单机的 RocksDB,TiKV 可以将数据快速地存储在磁盘上;通过 Raft,将数据复制到多台机器上,以防单机失效。数据的写入是通过
Raft 这一层的接口写入,而不是直接写 RocksDB。通过实现 Raft,TiKV 变成了一个分布式的 Key-Value
存储,少数几台机器宕机也能通过原生的 Raft 协议自动把副本补全,可以做到对业务无感知。
Region
讲到这里,我们需要提到一个非常重要的概念:Region。这个概念是理解后续一系列机制的基础,请仔细阅读这一小节。 前面提到,我们将 TiKV
看做一个巨大的有序的 KV Map,那么为了实现存储的水平扩展,我们需要将数据分散在多台机器上。这里提到的数据分散在多台机器上和 Raft
的数据复制不是一个概念,在这一节我们先忘记 Raft,假设所有的数据都只有一个副本,这样更容易理解。 对于一个 KV
系统,将数据分散在多台机器上有两种比较典型的方案:
- Hash:按照 Key 做 Hash,根据 Hash 值选择对应的存储节点
- Range:按照 Key 分 Range,某一段连续的 Key 都保存在一个存储节点上
TiKV 选择了第二种方式,将整个 Key-Value 空间分成很多段,每一段是一系列连续的 Key,将每一段叫做一个 Region,并且会尽量保持每个
Region 中保存的数据不超过一定的大小,目前在 TiKV 中默认是 96MB。每一个 Region 都可以用 [StartKey,EndKey)
这样一个左闭右开区间来描述。

注意,这里的 Region 还是和 SQL 中的表没什么关系! 请各位继续忘记 SQL,只谈 KV。 将数据划分成 Region 后,TiKV
将会做两件重要的事情:
- 以 Region 为单位,将数据分散在集群中所有的节点上,并且尽量保证每个节点上服务的 Region 数量差不多
- 以 Region 为单位做 Raft 的复制和成员管理
这两点非常重要,我们一点一点来说。 先看第一点,数据按照 Key 切分成很多 Region,每个 Region
的数据只会保存在一个节点上面(暂不考虑多副本)。TiDB 系统会有一个组件(PD)来负责将 Region
尽可能均匀的散布在集群中所有的节点上,这样一方面实现了存储容量的水平扩展(增加新的节点后,会自动将其他节点上的 Region
调度过来),另一方面也实现了负载均衡(不会出现某个节点有很多数据,其他节点上没什么数据的情况)。同时为了保证上层客户端能够访问所需要的数据,系统中也会有一个组件(PD)记录
Region 在节点上面的分布情况,也就是通过任意一个 Key 就能查询到这个 Key 在哪个 Region 中,以及这个 Region
目前在哪个节点上(即 Key 的位置路由信息)。至于负责这两项重要工作的组件(PD),会在后续介绍。
对于第二点,TiKV 是以 Region 为单位做数据的复制,也就是一个 Region 的数据会保存多个副本,TiKV 将每一个副本叫做一个
Replica。Replica 之间是通过 Raft 来保持数据的一致,一个 Region 的多个 Replica 会保存在不同的节点上,构成一个 Raft
Group。其中一个 Replica 会作为这个 Group 的 Leader,其他的 Replica 作为 Follower。所有的读和写都是通过
Leader 进行,读操作在 Leader 上即可完成,而写操作再由 Leader 复制给 Follower。 大家理解了 Region
之后,应该可以理解下面这张图:

以 Region 为单位做数据的分散和复制,就有了一个分布式的具备一定容灾能力的 KeyValue
系统,不用再担心数据存不下,或者是磁盘故障丢失数据的问题。
MVCC
很多数据库都会实现多版本并发控制(MVCC),TiKV 也不例外。设想这样的场景,两个客户端同时去修改一个 Key 的
Value,如果没有数据的多版本控制,就需要对数据上锁,在分布式场景下,可能会带来性能以及死锁问题。 TiKV 的 MVCC 实现是通过在 Key
后面添加版本号来实现,简单来说,没有 MVCC 之前,可以把 TiKV 看做这样的:
Key1 -> Value
Key2 -> Value
……
KeyN -> Value
有了 MVCC 之后,TiKV 的 Key 排列是这样的:
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。然后可以直接通过 RocksDB 的 SeekPrefix(Key_Version)
API,定位到第一个大于等于这个 Key_Version 的位置。
分布式 ACID 事务
TiKV 的事务采用的是 Google 在 BigTable
中使用的事务模型:Percolator,TiKV
根据这篇论文实现,并做了大量的优化。这个在后续的章节中会有详细的介绍。
在 TiKV 层的事务 API 的语义类似下面的伪代码:
tx = tikv.Begin()
tx.Set(Key1, Value1)
tx.Set(Key2, Value2)
tx.Set(Key3, Value3)
tx.Commit()
这个事务中包含3条 Set 操作,TiKV 能保证这些操作要么全部成功,要么全部失败,不会出现中间状态或脏数据。 就如前面提到的,TiDB 的 SQL
层会将 SQL 的执行计划转换成多个 KV 操作,对于上层的同一个业务层的 SQL 事务,在底层也是对应一个 KV 层的事务,这是 TiDB 实现
MySQL 的事务语义的关键。
计算
表数据与 Key-Value 的映射关系
本小节介绍 TiDB 中数据到 (Key, Value) 键值对的映射方案。这里的数据主要包括以下两个方面:
- 表中每一行的数据,以下简称表数据
- 表中所有索引的数据,以下简称索引数据
表数据与 Key-Value 的映射关系
在关系型数据库中,一个表可能有很多列。要将一行中各列数据映射成一个 (Key, Value) 键值对,需要考虑如何构造 Key。首先,OLTP
场景下有大量针对单行或者多行的增、删、改、查等操作,要求数据库具备快速读取一行数据的能力。因此,对应的 Key 最好有一个唯一 ID(显示或隐式的
ID),以方便快速定位。其次,很多 OLAP 型查询需要进行全表扫描。如果能够将一个表中所有行的 Key
编码到一个区间内,就可以通过范围查询高效完成全表扫描的任务。
基于上述考虑,TiDB 中的表数据与 Key-Value 的映射关系作了如下设计:
- 为了保证同一个表的数据放在一起,方便查找,TiDB 会为每个表分配一个表 ID,用
TableID 表示。表 ID 是一个整数,在整个集群内唯一。
- TiDB 会为表中每行数据分配一个行 ID,用
RowID 表示。行 ID 也是一个整数,在表内唯一。对于行 ID,TiDB 做了一个小优化,如果某个表有整数型的主键,TiDB 会使用主键的值当做这一行数据的行 ID。
每行数据按照如下规则编码成 (Key, Value) 键值对:
Key: tablePrefix{TableID}_recordPrefixSep{RowID}
Value: [col1, col2, col3, col4]
其中 tablePrefix 和 recordPrefixSep 都是特定的字符串常量,用于在 Key
空间内区分其他数据。其具体值在后面的小结中给出。
索引数据和 Key-Value 的映射关系
TiDB 同时支持主键和二级索引(包括唯一索引和非唯一索引)。与表数据映射方案类似,TiDB 为表中每个索引分配了一个索引 ID,用 IndexID
表示。
对于主键和唯一索引,需要根据键值快速定位到对应的 RowID,因此,按照如下规则编码成 (Key, Value) 键值对:
Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue
Value: RowID
对于不需要满足唯一性约束的普通二级索引,一个键值可能对应多行,需要根据键值范围查询对应的 RowID。因此,按照如下规则编码成 (Key, Value)
键值对:
Key: tablePrefix{TableID}_indexPrefixSep{IndexID}_indexedColumnsValue_{RowID}
Value: null
映射关系小结
上述所有编码规则中的 tablePrefix、recordPrefixSep 和 indexPrefixSep 都是字符串常量,用于在 Key
空间内区分其他数据,定义如下:
tablePrefix = []byte{'t'}
recordPrefixSep = []byte{'r'}
indexPrefixSep = []byte{'i'}
另外请注意,上述方案中,无论是表数据还是索引数据的 Key 编码方案,一个表内所有的行都有相同的 Key
前缀,一个索引的所有数据也都有相同的前缀。这样具有相同的前缀的数据,在 TiKV 的 Key
空间内,是排列在一起的。因此只要小心地设计后缀部分的编码方案,保证编码前和编码后的比较关系不变,就可以将表数据或者索引数据有序地保存在 TiKV
中。采用这种编码后,一个表的所有行数据会按照 RowID 顺序地排列在 TiKV 的 Key
空间中,某一个索引的数据也会按照索引数据的具体的值(编码方案中的 indexedColumnsValue)顺序地排列在 Key 空间内。
Key-Value 映射关系示例
最后通过一个简单的例子,来理解 TiDB 的 Key-Value 映射关系。假设 TiDB 中有如下这个表:
CREATE TABLE User (
ID int,
Name varchar(20),
Role varchar(20),
Age int,
PRIMARY KEY (ID),
KEY idxAge (Age)
);
假设该表中有 3 行数据:
1, "TiDB", "SQL Layer", 10
2, "TiKV", "KV Engine", 20
3, "PD", "Manager", 30
首先每行数据都会映射为一个 (Key, Value) 键值对,同时该表有一个 int 类型的主键,所以 RowID 的值即为该主键的值。假设该表的
TableID 为 10,则其存储在 TiKV 上的表数据为:
t10_r1 --> ["TiDB", "SQL Layer", 10]
t10_r2 --> ["TiKV", "KV Engine", 20]
t10_r3 --> ["PD", "Manager", 30]
除了主键外,该表还有一个非唯一的普通二级索引 idxAge,假设这个索引的 IndexID 为 1,则其存储在 TiKV 上的索引数据为:
t10_i1_10_1 --> null
t10_i1_20_2 --> null
t10_i1_30_3 --> null
以上例子展示了 TiDB 中关系模型到 Key-Value 模型的映射规则,以及选择该方案背后的考量。
元信息管理
TiDB 中每个 Database 和 Table 都有元信息,也就是其定义以及各项属性。这些信息也需要持久化,TiDB 将这些信息也存储在了
TiKV 中。
每个 Database/Table 都被分配了一个唯一的 ID,这个 ID 作为唯一标识,并且在编码为 Key-Value 时,这个 ID
都会编码到 Key 中,再加上 m_ 前缀。这样可以构造出一个 Key,Value 中存储的是序列化后的元信息。
除此之外,TiDB 还用一个专门的 (Key, Value) 键值对存储当前所有表结构信息的最新版本号。这个键值对是全局的,每次 DDL
操作的状态改变时其版本号都会加 1。目前,TiDB 把这个键值对持久化存储在 PD Server 中,其 Key 是
“/tidb/ddl/global_schema_version”,Value 是类型为 int64 的版本号值。TiDB 采用 Online Schema
变更算法,有一个后台线程在不断地检查 PD Server 中存储的表结构信息的版本号是否发生变化,并且保证在一定时间内一定能够获取版本的变化。
SQL 层简介
TiDB 的 SQL 层,即 TiDB Server,负责将 SQL 翻译成 Key-Value 操作,将其转发给共用的分布式 Key-Value 存储层
TiKV,然后组装 TiKV 返回的结果,最终将查询结果返回给客户端。
这一层的节点都是无状态的,节点本身并不存储数据,节点之间完全对等。
SQL 运算
最简单的方案就是通过上一节所述的[表数据与 Key-Value
的映射关系](https://docs.pingcap.com/zh/tidb/stable/tidb-
computing#%E8%A1%A8%E6%95%B0%E6%8D%AE%E4%B8%8E-key-
value-%E7%9A%84%E6%98%A0%E5%B0%84%E5%85%B3%E7%B3%BB)方案,将 SQL 查询映射为对 KV 的查询,再通过
KV 接口获取对应的数据,最后执行各种计算。
比如 select count(*) from user where name = "TiDB" 这样一个 SQL
语句,它需要读取表中所有的数据,然后检查 name 字段是否是 TiDB,如果是的话,则返回这一行。具体流程如下:
- 构造出 Key Range:一个表中所有的
RowID 都在 [0, MaxInt64) 这个范围内,使用 0 和 MaxInt64 根据行数据的 Key 编码规则,就能构造出一个 [StartKey, EndKey)的左闭右开区间。
- 扫描 Key Range:根据上面构造出的 Key Range,读取 TiKV 中的数据。
- 过滤数据:对于读到的每一行数据,计算
name = "TiDB" 这个表达式,如果为真,则向上返回这一行,否则丢弃这一行数据。
- 计算
Count(*):对符合要求的每一行,累计到 Count(*) 的结果上面。
整个流程示意图如下:

这个方案是直观且可行的,但是在分布式数据库的场景下有一些显而易见的问题:
- 在扫描数据的时候,每一行都要通过 KV 操作从 TiKV 中读取出来,至少有一次 RPC 开销,如果需要扫描的数据很多,那么这个开销会非常大。
- 并不是所有的行都满足过滤条件
name = "TiDB",如果不满足条件,其实可以不读取出来。
- 此查询只要求返回符合要求行的数量,不要求返回这些行的值。
分布式 SQL 运算
为了解决上述问题,计算应该需要尽量靠近存储节点,以避免大量的 RPC 调用。首先,SQL 中的谓词条件 name = "TiDB"
应被下推到存储节点进行计算,这样只需要返回有效的行,避免无意义的网络传输。然后,聚合函数 Count(*)
也可以被下推到存储节点,进行预聚合,每个节点只需要返回一个 Count(*) 的结果即可,再由 SQL 层将各个节点返回的 Count(*)
的结果累加求和。
以下是数据逐层返回的示意图:

SQL 层架构
通过上面的例子,希望大家对 SQL 语句的处理有一个基本的了解。实际上 TiDB 的 SQL
层要复杂得多,模块以及层次非常多,下图列出了重要的模块以及调用关系:

用户的 SQL 请求会直接或者通过 Load Balancer 发送到 TiDB Server,TiDB Server 会解析 MySQL Protocol Packet,获取请求内容,对 SQL 进行语法解析和语义分析,制定和优化查询计划,执行查询计划并获取和处理数据。数据全部存储在
TiKV 集群中,所以在这个过程中 TiDB Server 需要和 TiKV 交互,获取数据。最后 TiDB Server 需要将查询结果返回给用户。
TiDB和Mysql的区别
TiDB 作为开源 NewSQL 数据库的典型代表之一,同样支持 SQL,支持事务 ACID 特性。在通讯协议上,TiDB 选择与 MySQL
完全兼容,并尽可能兼容 MySQL 的语法。因此,基于 MySQL 数据库开发的系统,大多数可以平滑迁移至
TiDB,而几乎不用修改代码。对用户来说,迁移成本极低,过渡自然。
然而,仍有一些 MySQL 的特性和行为,TiDB 目前暂时不支持或表现与 MySQL 有差异。除此之外,TiDB
提供了一些扩展语法和功能,为用户提供更多的便利。
TiDB 仍处在快速发展的道路上,对 MySQL 功能和行为的支持方面,正按 [路线图](https://pingcap.com/docs-
cn/stable/roadmap/) 的规划在前行。
兼容策略
先从总体上概括 TiDB 和 MySQL 兼容策略,如下表:
| 通讯协议 |
SQL语法 |
功能和行为 |
| 完全兼容 |
兼容绝大多数 |
兼容大多数 |
截至 4.0 版本,TiDB 与 MySQL 的区别总结如下表:
| MySQL | TiDB
—|—|—
隔离级别 | 支持读未提交、读已提交、可重复读、串行化,默认为可重复读 | 乐观事务支持快照隔离,悲观事务支持快照隔离和读已提交
锁机制 | 悲观锁 | 乐观锁、悲观锁
存储过程 | 支持 | 不支持
触发器 | 支持 | 不支持
事件 | 支持 | 不支持
自定义函数 | 支持 | 不支持
窗口函数 | 支持 | 部分支持
JSON | 支持 | 不支持部分 MySQL 8.0 新增的函数
外键约束 | 支持 | 忽略外键约束
字符集 | | 只支持 ascii、latin1、binary、utf8、utf8mb4
增加/删除主键 | 支持 | 通过 [alter-primary-key](https://pingcap.com/docs-
cn/dev/reference/configuration/tidb-server/configuration-file/#alter-primary-
key) 配置开关提供
CREATE TABLE tblName AS SELECT stmt | 支持 | 不支持
CREATE TEMPORARY TABLE | 支持 | TiDB 忽略 TEMPORARY 关键字,按照普通表创建
DML affected rows | 支持 | 不支持
AutoRandom 列属性 | 不支持 | 支持
Sequence 序列生成器 | 不支持 | 支持
区别点详述及应对方案
字符集支持
TiDB 目前支持以下字符集:
tidb> SHOW CHARACTER SET;
+---------|---------------|-------------------|--------+
| Charset | Description | Default collation | Maxlen |
+---------|---------------|-------------------|--------+
| utf8 | UTF-8 Unicode | utf8_bin | 3 |
| utf8mb4 | UTF-8 Unicode | utf8mb4_bin | 4 |
| ascii | US ASCII | ascii_bin | 1 |
| latin1 | Latin1 | latin1_bin | 1 |
| binary | binary | binary | 1 |
+---------|---------------|-------------------|--------+
5 rows in set (0.00 sec)
注意:TiDB 的默认字符集为 utf8mb4,MySQL 5.7 中为 latin1,MySQL 8.0 中修改为 utf8mb4。
当指定的字符集为 utf8 或 utf8mb4 时,TiDB 仅支持合法的 UTF8 字符。对于不合法的字符,会报错:incorrect utf8 value,该字符合法性检查与 MySQL 8.0 一致。对于 MySQL 5.7 及以下版本,会存在允许插入非法 UTF8 字符,但同步到 TiDB
报错的情况。此时,可以通过 TiDB 配置
[“tidb_skip_utf8_check”](https://pingcap.com/docs/stable/faq/upgrade/#issue-3-error-1366-hy000-incorrect-
utf8-value-f09f8c80-for-column-a) 跳过 UTF8 字符合法性检查强制写入 TiDB。
每一个字符集,都有一个默认的 Collation,例如 utf8 的默认 Collation 为 utf8_bin,TiDB 中字符集的默认
Collation 与 MySQL 不一致,具体如下:
| 字符集 |
TiDB 默认 Collation |
MySQL 5.7 默认 Collation |
MySQL 8.0 默认 Collation |
| utf8 |
utf8_bin |
utf8_general_ci |
utf8_general_ci |
| utf8mb4 |
utf8mb4_bin |
utf8mb4_general_ci |
utf8mb4_0900_ai_ci |
| ascii |
ascii_bin |
ascii_general_ci |
ascii_general_ci |
| latin1 |
latin1_bin |
latin1_swedish_ci |
latin1_swedish_ci |
| binary |
binary |
binary |
binary |
在 4.0 版本之前,TiDB 中可以任意指定字符集对应的所有 Collation,并把它们按照默认 Collation
处理,即以编码字节序为字符定序。同时,并未像 MySQL 一样,在比较前按照 Collation 的 PADDING
属性将字符补齐空格。因此,会造成以下的行为区别:
tidb> create table t(a varchar(20) charset utf8mb4 collate utf8mb4_general_ci primary key);
Query OK, 0 rows affected
tidb> insert into t values ('A');
Query OK, 1 row affected
tidb> insert into t values ('a');
Query OK, 1 row affected // MySQL 中,由于 utf8mb4_general_ci 大小写不敏感,报错 Duplicate entry 'a'.
tidb> insert into t1 values ('a ');
Query OK, 1 row affected // MySQL 中,由于补齐空格比较,报错 Duplicate entry 'a '
TiDB 4.0 新增了完整的 Collation 支持框架,允许实现所有 MySQL 中的 Collation,并新增了配置开关
new_collation_enabled_on_first_boostrap,在集群初次初始化时决定是否启用新 Collation
框架。在该配置开关打开之后初始化集群,可以通过 mysql.tidb 表中的 new_collation_enabled 变量确认新
Collation 是否启用:
tidb> select VARIABLE_VALUE from mysql.tidb where VARIABLE_NAME='new_collation_enabled';
+----------------+
| VARIABLE_VALUE |
+----------------+
| True |
+----------------+
1 row in set (0.00 sec)
在新 Collation 启用后,TiDB 修正了 utf8mb4_general_bin 和 utf8_general_bin 的
PADDING 行为,会将字符串补齐空格后比较;同时支持了 utf8mb4_general_ci 和 utf8_general_ci,这两个
Collation 与 MySQL 保持兼容。
系统时区
在 MySQL 中,系统时区 system_time_zone 在 MySQL 服务启动时通过 环境变量 TZ 或命令行参数
--timezone
指定。
对于 TiDB 而言,作为一个分布式数据库,TiDB 需要保证整个集群的系统时区始终一致。因此 TiDB 的系统时区在集群初始化时,由负责初始化的 TiDB
节点环境变量 TZ 决定。集群初始化后,固定在集群状态表 mysql.tidb 中:
tidb> select VARIABLE_VALUE from mysql.tidb where VARIABLE_NAME='system_tz';
+----------------+
| VARIABLE_VALUE |
+----------------+
| Asia/Shanghai |
+----------------+
1 row in set (0.00 sec)
通过查看 system_time_zone 变量,可以看到该值与状态表中的 system_tz 保持一致:
tidb> select @@system_time_zone;
+--------------------+
| @@system_time_zone |
+--------------------+
| Asia/Shanghai |
+--------------------+
1 row in set (0.00 sec)
请注意,这意味着 TiDB 的系统时区在初始化后不再更改。若需要改变集群的时区,可以显式指定 time_zone 系统变量,例如:
tidb> set @@global.time_zone='UTC';
Query OK, 0 rows affected (0.00 sec)
事务
乐观事务
事务是数据库的基础,提供高效的、支持完整 ACID 的分布式事务更是分布式数据库的立足之本。本章节会首先介绍事务的基本概念,然后介绍 TiDB 基于
Percolator 实现的乐观事务以及在使用上的最佳实践。
隔离级别
对用户来说,最友好的并发事务执行顺序为每个事务独占整个数据库,并发事务执行的结果与一个个串行执行相同,也就是串行化,能够避免所有的异常情况。但在这种隔离级别下,并发执行的事务性能较差,提供更弱保证的隔离级别能够显著提升系统的性能。根据允许出现的异常,SQL-92
标准定义了 4 种隔离级别:读未提交 (READ UNCOMMITTED)、读已提交 (READ COMMITTED)、可重复读 (REPEATABLE
READ)、串行化 (SERIALIZABLE)。详见下表:
| Isolation Level |
Dirty Write |
Dirty Read |
Fuzzy Read |
Phantom |
| READ UNCOMMITTED |
Not Possible |
Possible |
Possible |
Possible |
| READ COMMITTED |
Not Possible |
Not possible |
Possible |
Possible |
| REPEATABLE READ |
Not Possible |
Not possible |
Not possible |
Possible |
| SERIALIZABLE |
Not Possible |
Not possible |
Not possible |
Not possible |
并发控制
数据库有多种并发控制方法,这里只介绍以下两种:
- 乐观并发控制(OCC):在事务提交阶段检测冲突
- 悲观并发控制(PCC):在事务执行阶段检测冲突
乐观并发控制期望事务间数据冲突不多,只在提交阶段检测冲突能够获取更高的性能。悲观并发控制更适合数据冲突较多的场景,能够避免乐观事务在这类场景下事务因冲突而回滚的问题,但相比乐观并发控制,在没有数据冲突的场景下,性能相对要差。
TiDB 乐观事务实现
TiDB 基于 Google [Percolator](https://storage.googleapis.com/pub-tools-public-
publication-data/pdf/36726.pdf) 实现了支持完整 ACID、基于快照隔离级别(Snapshot
Isolation)的分布式乐观事务。TiDB 乐观事务需要将事务的所有修改都保存在内存中,直到提交时才会写入 TiKV 并检测冲突。
Snapshot Isolation
Percolator 使用多版本并发控制(MVCC)来实现快照隔离级别,与可重复读的区别在于整个事务是在一个一致的快照上执行。TiDB 使用
PD 作为全局授时服务(TSO)来提供单调递增的版本号:
- 事务开始时获取 start timestamp,也是快照的版本号;事务提交时获取 commit timestamp,同时也是数据的版本号
- 事务只能读到在事务 start timestamp 之前最新已提交的数据
- 事务在提交时会根据 timestamp 来检测数据冲突
两阶段提交(2PC)
TiDB 使用两阶段提交(Two-Phase Commit)来保证分布式事务的原子性,分为 Prewrite 和 Commit 两个阶段:
- Prewrite:对事务修改的每个 Key 检测冲突并写入 lock 防止其他事务修改。对于每个事务,TiDB 会从涉及到改动的所有 Key 中选中一个作为当前事务的 Primary Key,事务提交或回滚都需要先修改 Primary Key,以它的提交与否作为整个事务执行结果的标识。
- Commit:Prewrite 全部成功后,先同步提交 Primary Key,成功后事务提交成功,其他 Secondary Keys 会异步提交。
Percolator
将事务的所有状态都保存在底层支持高可用、强一致性的存储系统中,从而弱化了传统两阶段提交中协调者(Coordinator)的作用,所有的客户端都可以根据存储系统中的事务状态对事务进行提交或回滚。
两阶段提交过程
事务的两阶段提交过程如下:

客户端开始一个事务。
TiDB 向 PD 获取 tso 作为当前事务的 start timestamp。
客户端发起读或写请求。
客户端发起 Commit。
TiDB 开始 两阶段提交 ,保证分布式事务的原子性,让数据真正落盘。
i. TiDB 从当前要写入的数据中选择一个 Key 作为当前事务的 Primary Key。
ii. TiDB 并发地向所有涉及的 TiKV 发起 Prewrite 请求。TiKV 收到 Prewrite
请求后,检查数据版本信息是否存在冲突,符合条件的数据会被加锁。
iii. TiDB 收到所有 Prewrite 响应且所有 Prewrite 都成功。
iv. TiDB 向 PD 获取第二个全局唯一递增版本号,定义为本次事务的 commit timestamp。
v. TiDB 向 Primary Key 所在 TiKV 发起第二阶段提交。TiKV 收到 Commit 操作后,检查锁是否存在并清理 Prewrite
阶段留下的锁。
TiDB 向客户端返回事务提交成功的信息。
TiDB 异步清理本次事务遗留的锁信息。
悲观事务
乐观事务模型在分布式系统中有着极大的性能优势,但为了让 TiDB 的使用方式更加贴近传统单机数据库,更好的适配用户场景,TiDB v3.0
及之后版本在乐观事务模型的基础上实现了悲观事务模型。本文将介绍 TiDB 悲观事务模型特点。
悲观锁解决的问题
通过支持悲观事务,降低用户修改代码的难度甚至不用修改代码:
- 在 v3.0.8 之前,TiDB 默认使用的乐观事务模式会导致事务提交时因为冲突而失败。为了保证事务的成功率,需要修改应用程序,加上重试的逻辑。
- 乐观事务模型在冲突严重的场景和重试代价大的场景无法满足用户需求,支持悲观事务可以 弥补这方面的缺陷,拓展 TiDB 的应用场景。
以发工资场景为例:对于一个用人单位来说,发工资的过程其实就是从企业账户给多个员工的个人账户转账的过程,一般来说都是批量操作,在一个大的转账事务中可能涉及到成千上万的更新,想象一下如果这个大事务执行的这段时间内,某个个人账户发生了消费(变更),如果这个大事务是乐观事务模型,提交的时候肯定要回滚,涉及上万个个人账户发生消费是大概率事件,如果不做任何处理,最坏的情况是这个大事务永远没办法执行,一直在重试和回滚(饥饿)。
基于 Percolator 的悲观事务
悲观事务在 Percolator 乐观事务基础上实现,在 Prewrite 之前增加了 Acquire Pessimistic Lock 阶段用于避免
Prewrite 时发生冲突:
- 每个 DML 都会加悲观锁,锁写到 TiKV 里,同样会通过 raft 同步。
- 悲观事务在加悲观锁时检查各种约束,如 Write Conflict、key 唯一性约束等。
- 悲观锁不包含数据,只有锁,只用于防止其他事务修改相同的 Key,不会阻塞读,但 Prewrite 后会阻塞读(和 Percolator 相同,但有了大事务支持后将不会阻塞读)。
- 提交时同 Percolator,悲观锁的存在保证了 Prewrite 不会发生 Write Conflict,保证了提交一定成功。

等锁顺序
TiKV 中实现了 Waiter Manager 用于管理等锁的事务,当悲观事务加锁遇到其他事务的锁时,将会进入 Waiter Manager
中等待锁被释放,TiKV 会尽可能按照事务 start timestamp 的顺序来依次获取锁,从而避免事务间无用的竞争。
分布式死锁检测
在 Waiter Manager 中等待锁的事务间可能发生死锁,而且可能发生在不同的机器上,TiDB 采用分布式死锁检测来解决死锁问题:
- 在整个 TiKV 集群中,有一个死锁检测器 leader。
- 当要等锁时,其他节点会发送检测死锁的请求给 leader。

死锁检测器基于 Raft 实现了高可用,等锁事务也会定期发送死锁检测请求给死锁检测器的 leader,从而保证了即使之前 leader
宕机的情况下也能检测到死锁。