上一篇博客,我们简单介绍了怎么在Mac OS系统上面使用docker compose搭建一个包含3个pd,3个tikv,1个tidb的TIDB集群。本文,我们会详细介绍TIDB的一些基本概念和整体架构。
TIDB 简介
TIDB是pingcap公司开源的一款分布式数据库,结合了RDBMS和NOSQL两者的特性,支持对业务无感知的水平扩容,具备数据强一致性和高可用性。TIDB具备以下特性对某些业务应该有很大的吸引力。
兼容MYSQL协议:由于TIDB兼容了MYSQL协议,业务几乎可以零成本的从MYSQL迁移到TIDB。
水平弹性扩展:mysql虽然也可以分库分表水平扩展,但是如果需要继续水平扩容,就需要dba迁移数据,客户端可能还要修改路由规则。TIDB可以按需水平扩展吞吐或存储,数据自动迁移。
分布式事物:TIDB支持分布式事物。
高可用数据强一致:TIDB基于raft的多数派选举保证了数据的强一致性,并且在不丢失大部分副本的情况下,可以自动failover。
TiDB 整体架构
之前我们搭建了包含3个pd,3个tikv,1个tidb的TIDB集群,从这里可以看出TIDB其实主要包含了以下3个模块:
tidb server,tikv server,pd servertidb server:tidb server负责和客户端进行交互。tidb server接受客户端的sql语句进行解析,通过PD获取sql关联的数据对应的tikv server对应的地址,然后访问相应的tikv server,获取数据(或者修改数据),然后返回结果给客户端。tidb server本身不存储任何数据,只提供计算能力,本身是无状态的,可以根据需要水平扩展。
pd server:pd server是整个TIDB的管理模块。主要负责以下工作:存储集群的元信息(某个key存储在哪个tikv上);对tikv集群进行调度和负载均衡(数据迁移,raft group leader的迁移);分配全局唯一且递增的事物id;pd本身通过raft协议选举leader保证高可用性,建议线上至少部署3个pd节点。
tikv server:tikv server负责存储数据。tikv server使用rocksdb,提供了分布式key-value的存储引擎。数据存储的基本单位是region,每个tikv负责多个region,每个region包含了一个存储range,从start到end的一个范围。tikv使用raft协议做复制,保持数据的强一致和容灾。副本以region为单位进行管理,不同tikv节点上的leader和副本region构成一个raft group,互为副本。由pd来统一负责region在tikv上的负载均衡。(这里可以类比于kafka架构,region类似于partition,副本类似于replica)
总结下:TIDB由tidb,tikv,pd三个模块组成,tidb负责与客户端通信,提供了计算能力,tikv负责存储真正的数据,提供存储能力,pd作为管理模块,存储集群的一些元信息以及负责对整个集群进行管理调度。可以看到TIDB不同于MYSQL和其他Nosql存储,它是计算和存储分开的,各个模块都可以通过无线的水平扩展保证高可用性。
TiDB 的最佳适用场景
简单来说,TiDB 适合具备下面这些特点的场景:
- 数据量大,单机保存不下
- 不希望做 Sharding 或者懒得做 Sharding
- 访问模式上没有明显的热点
- 需要事务、需要强一致
SQL到KV的映射
通过上面介绍,我们知道了tikv基于rocksdb存储引擎以key-value的形式保存数据,这个和其他RDBMS基于table rows来保存数据不同。但是tidb又兼容了MYSQL协议,这是什么意思呢?就是我们完全可以像使用mysql一样使用tidb,我们可以发送一条sql命令给tidb
select * from user
正因为tidb兼容了mysql协议,所以业务可以几乎零成本的迁移到tidb。下面我们来看下tidb是怎么完成从SQL到KV的转换的。
假设我们有以下一张表:
CREATE TABLE User {
ID int,
Name varchar(20),
Age int,
PRIMARY KEY (ID),
Key idxAge (age)
};
对于一个table来说,需要存储的数据包括以下三个部分:
- 表的元信息
- Table中的row
- 索引数据
对于tidb来说,必须能把以上数据通过某种算法转换成key-value形式发送给tikv。因为我们要像多数RDBMS一样支持大象OLTP业务,需要支持快速读取,保存,修改,删除数据,所以这个转换算法的设计就尤为关键了。
我们可以把tikv想象成一个包含很多key-value键值对的map,可以类比JAVA的TreeMap,将key按照一定的规则进行排序。这样tikv其实就是一个全局有序的分布式key-value引擎,这点很重要,能保证我们根据某个条件快速检索出需要的key。下面我们看下,tidb是怎么讲table转换为key-value的。
- TiDB 对每个表分配一个 TableID,每一个索引都会分配一个 IndexID,每一行分配一个 RowID(如果表有整数型的 Primary Key,那么会用 Primary Key 的值当做 RowID),其中 TableID 在整个集群内唯一,IndexID/RowID 在表内唯一,这些 ID 都是 int64 类型。 每行数据按照如下规则进行编码成 Key-Value pair:
Key: tablePrefix_rowPrefix_tableID_rowID Value: [col1, col2, col3, col4]
- 其中 Key 的 tablePrefix/rowPrefix 都是特定的字符串常量,用于在 KV 空间内区分其他数据。 对于 Index 数据,根据Index是UK和非UK分别按照以下不同规则编码成Key-Value
Unique Index:因为indexColumnsValue是唯一的,所以下面构造的key也是全局唯一的Key: tablePrefix_idxPrefix_tableID_indexID_indexColumnsValue Value: rowID
- 非Unique Index:因为indexColumnsValue不是唯一的,所以不能按照上述规则来构造key,我们可以按照下面规则
Key: tablePrefix_idxPrefix_tableID_indexID_ColumnsValue_rowID Value:null
说明:对于上述key中出现的tablePrefix,rowPrefix,idxPrefix都是一些常量。另外tableID式全局唯一的,rowID和indexID是表内唯一的。这样一个 Table 内部所有的 Row 都有相同的前缀,一个 Index 的数据也都有相同的前缀。这样一个表的所有 Row 数据就会按照 RowID 的顺序排列在 TiKV 的 Key 空间中,某一个 Index 的数据也会按照 Index 的 ColumnValue 顺序排列在 Key 空间内。显然,这种构造key的方式对于点查和范围查询都比较友好。
tablePrefix,rowPrefix,idxPrefix对应常量
var(
tablePrefix = []byte{'t'}
recordPrefixSep = []byte("_r")
indexPrefixSep = []byte("_i")
)
下面举个例子来说明上述构造规则,我们还是以最开始的User表来构造以下3条数据:
id name age
1 'zhangsan' 20
2 'lisi' 18
3 'wangwu' 22
User表存在Primary Key(id),假设User表tableId 为1,所以上述3条数据可以转换为以下key-value pair
key value
t_r_1_1 ['zhangsan', 20]
t_r_1_2 ['lisi', 18]
t_r_1_3 ['wangwu', 22]
另外该表在age上还有一个Index,假设indexID为1,对应key-value pair如下
key value
t_i_1_1_20_1 null
t_i_1_1_18_2 null
t_i_1_1_22_3 null
MVCC
很多数据库都会实现多版本控制(MVCC),TiKV 也不例外。 TiKV 的 MVCC 实现是通过在 Key 后面添加 Version 来实现,简单来说,没有 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 + Version 来获取 Value 的时候,可以将 Key 和 Version 构造出 MVCC 的 Key,也就是 Key-Version。然后可以直接 Seek(Key-Version),定位到第一个大于等于这个 Key-Version 的位置。