一、简介TiDB并不陌生,很多团队都在使用它。我们为什么要使用它,它有什么特点?TiDB是一个开源的分布式关系型数据库,可以同时支持在线事务处理和在线分析处理(HybridTransactionalandAnalyticalProcessing,HTAP)。实时HTAP、云原生分布式数据库、兼容MySQL5.7协议和MySQL生态等重要特性,支持本地和云端部署。可以看出TiDB具有以下特点:同时支持OLTP和OLAP分布式数据库,金融级高可用全面兼容MySQL,无缝切换2.TiDB结构介绍整体架构图(以下各图来自TiDB官方文档&参考知乎)2.1TiDBServerSQL层,暴露MySQL协议的连接端点,负责接受客户端连接,进行鉴权,SQL解析优化,最终生成分布式执行计划。TiDB层本身是无状态的。在实践中,可以启动多个TiDB实例,通过负载均衡组件(如LVS、HAProxy、F5)对外提供统一的访问地址。客户端连接可以均匀分布在多个TiDB实例中。从而达到负载均衡的效果。TiDBServer本身不存储数据,只是解析SQL并将实际的数据读取请求转发给底层存储节点TiKV(或TiFlash)。2.2PD(PlacementDriver)Server整个TiDB集群的元数据管理模块,负责存储各个TiKV节点的实时数据分布和集群的整体拓扑结构,提供TiDBDashboard管理接口,并分配事务ID给分布式事务。PD不仅存储元信息,还会根据TiKV节点上报的实时数据分布情况,向特定的TiKV节点发送数据调度命令,可以说是整个集群的“大脑”。另外PD是无状态的,通过raft共识协议完成数据同步。也是由至少3个节点组成,具备高可用能力。建议部署奇数个PD节点。2.3存储节点(TiKV&TiFLASH)TiKVServer和TiDB的存储方式都是KV(key-value)存储,一切都是KV。负责存储数据。从外部看,TiKV是一个提供事务的分布式Key-Value存储引擎。存储数据的基本单元是Region,每个Region负责存储一个KeyRange(从StartKey到EndKey的左闭右开区间)的数据,每个TiKV节点负责多个Region。TiKV的API在KV键值对层面原生支持分布式事务,默认提供SI(SnapshotIsolation)的隔离级别,这也是TiDB在SQL层面支持分布式事务的核心。TiDB的SQL层完成SQL解析后,会将SQL执行计划转化为对TiKVAPI的实际调用。因此,数据存储在TiKV中。另外,TiKV中的数据会自动维护多份(默认是三份),天然支持高可用和自动故障转移。拷贝质检也通过raft协议保持数据的一致性。TiFlashTiFlash是一类特殊的存储节点。不同于普通的TiKV节点,TiFlash内部数据以列的形式存储,主要作用是加速分析型OLAP场景。一般TiDB作为数据库使用OLTP功能,不需要部署这个节点。TiDB和MySQL的区别从TiDB4.0开始,与MySQL有以下区别(图片引用自知乎)3.TiDB存储介绍作为存储数据的系统,首先要决定的是数据存储模型,也就是说,应该使用什么样的数据形式来保存。TiKV选择的是Key-Value模型,它提供了一种有序的遍历方式。TiKV数据存储的两个关键点:这是一个巨大的Map,里面存储的是Key-ValuePairs(键值对)。这个Map中的Key-Value对是按照Key的二进制顺序排序的,即可以Seek到某个Key的位置,然后不断调用Next方法获取大于的Key-Value此键按递增顺序排列。3.1持久化使用任何RocksDB的持久化存储引擎,数据终究要保存在磁盘上,TiKV也不例外。但TiKV并没有选择直接将数据写入磁盘,而是将数据保存在RocksDB中,由RocksDB负责具体的数据落地。之所以这样选择,是因为开发单机存储引擎需要做大量的工作,尤其是高性能的单机引擎,需要进行各种细致的优化。RocksDB是Facebook开源的一款优秀的单机KV存储引擎。可以满足TiKV对单机引擎的各种需求。这里可以简单的认为RocksDB是一个独立的持久化Key-ValueMap。为了实现Region中存储的水平扩展,数据会分布在多台机器上。对于KV系统,数据跨多机分布有两种典型方案:Hash:根据Key做Hash,根据Hash值选择对应的存储节点。Range:Range按照Key进行划分,在一个存储节点上存储一定的连续Key。TiKV选择了第二种方式,将整个Key-Value空间分成很多段,每个段是一系列连续的Key,每个段称为一个Region,并且会尽量让每个Region中存储的数据保持在一定的大小内,目前TiKV默认是96MB。每个Region都可以用左闭右开区间来描述,例如[StartKey,EndKey)。将数据划分成Region后,TiKV会做两件重要的事情:将数据以Region为单位分散在集群中的所有节点上,并尽量保证每个节点上服务的Region数量大致相同。以Region为单位进行Raft复制和成员管理。TiKV通过以Region为单位进行数据的分布和复制,成为一个具有一定容灾能力的分布式KeyValue系统,你不用担心数据存储或者磁盘故障导致的数据丢失。3.2TiDB索引介绍表数据和key-value映射TiDB存储方式key-value键值对,为了方便查找,在设计上做了如下优化:为同一张表设计一个表id,用TableID表示,integerandGloballyunique为每个表的每一行设计一个rowid,用RowId表示,integer和unique在同一张表内。这里还有一个小的优化。如果这张表有主键,TiDB会使用主键作为RowId,否则会自己分配一个。映射结构示例:Key:tablePrefix{TableID}_recordPrefixSep{RowID}Value:[col1,col2,col3,col4]其中tablePrefix和recordPrefixSep为固定常量,用于区分键空间中的其他数据。索引和键值映射TiDB索引支持主键索引(MySQL主键索引)和二级索引(MySQL平台非聚集索引,分为唯一索引和非唯一索引),类似于表和表之间的映射方式核心价值。还分配了一个全局唯一的整数索引id,由IndexId表示。对于主键索引和唯一索引,由于列数据是唯一的,对应的键值结构如下:最后列的值对应一个RowId。对于非唯一索引,由于列数据不唯一,一个key值可能对应多行,需要根据key值范围查询对应的RowID。因此,按照如下规则编码成(Key,Value)键值对:Key:tablePrefix{TableID}_indexPrefixSep{IndexID}_indexedColumnsValue_{RowID}Value:null这里需要注意的是value是null,不是RowID,而RowID拼接在key的末尾。这里为什么不把value设计成RowID呢?猜测是key不能重复,必须全局唯一,必须均匀分布在TiKV的各个节点上。RowID只能拼接在key的末尾,查询是根据key值RowID前面的范围。索引示例以上所有编码规则中的tablePrefix、recordPrefixSep和indexPrefixSep都是字符串常量,用于区分Key空间中的其他数据,定义如下:tablePrefix=[]byte{'}recordPrefixSep=[]byte{'r'}indexPrefixSep=[]byte{'i'}另外需要注意的是,在上述方案中,无论是表数据还是索引数据的Key编码方案,一个表中的所有行都具有相同的Key前缀,并且索引中的所有行数据也都具有相同的前缀。这些具有相同前缀的数据在TiKV的密钥空间中排列在一起。因此,只要精心设计后缀部分的编码方案,保证编码前后的对比关系不变,表数据或索引数据就可以有序的存储在TiKV中。采用这种编码后,一张表的所有行数据会按照RowID顺序排列在TiKV的Key空间中,某个索引的数据也会按照索引数据的具体值(indexedColumnsValuein密钥空间中的编码方案)。下面通过一个简单的例子来理解TiDB的Key-Value映射关系。假设TiDB中有如下表:CREATETABLEUser(IDint,Namevarchar(20),Rolevarchar(20),Ageint,PRIMARYKEY(ID),KEYidxAge(Age));假设Data表中有4行:1,"TiDB","SQLLayer",102,"TiKV","KVEngine",203,"PD","Manager",30,4,"TiFLASH","STORAGE",30First每一行的数据都会被映射成一个(Key,Value)键值对,而表有一个int类型的主键,所以RowID的值就是主键的值。假设表的TableID为10,TiKV上存储的表数据为:t10_r1-->["TiDB","SQLLayer",10]t10_r2-->["TiKV","KVEngine",20]t10_r3-->["PD","Manager",30]t10_r4-->["TiFLASH","STORAGE",30]表除了主键还有一个非唯一的普通二级索引idxAge,假设这个索引的IndexID为1,那么TiKV上存储的索引数据为:t10_i1_10_1-->nullt10_i1_20_2-->nullt10_i1_30_3-->nullt10_i1_30_4-->null如果查询的是age=30,则前缀为基于t10_i1_30匹配可以匹配到t10_i1_30_3和t10_i1_30_4,最后3和4为RowID4.TiDB执行计划查看4.1概述由于TiDB在协议上兼容MySQL,其余的都有自己独特的实现,特别是核心TiDB的存储实现方式大不相同,因此查看执行计划也完全不同。以一个简单的SQL为例,解释一下TiDB执行计划的含义。SQL:Executionplan(SQL1):Executionplan(SQL3):首先在create_time列添加一个普通的二级索引。先介绍一下每一列的含义:id是算子名,或者是执行SQL语句需要执行的子任务。estRows是TiDB期望处理的行数。估计可能是根据字典信息(比如访问方式是根据主键或唯一键),也可能是根据CMSketch或直方图等统计信息来估计(说了这么多其实都差不多到MySQL执行计划的行)。任务显示执行语句时运算符所在的位置。访问对象显示正在访问的表、分区和索引。显示的索引是部分索引。在上面的例子中,TiDB使用了a列的索引。特别是在复合索引的情况下,该字段显示的信息非常翔实。operatorinfo显示有关访问表、分区和索引的附加信息。4.2详细介绍4.2.1算子算子是为返回查询结果而执行的具体步骤。真正扫描表(读磁盘或者读TiKVBlockCache)的算子有几种类型:TableFullScan:全表扫描。TableRangeScan:带范围的表数据扫描。TableRowIDScan:根据上层传下来的RowID扫描表格数据。通常在索引读取操作之后检索符合条件的行。IndexFullScan:另一种“全表扫描”,扫描索引数据,不扫描表数据。IndexRangeScan:带范围的索引数据扫描操作。TiDB会聚合TiKV/TiFlash上扫描的数据或计算结果。这种“数据聚合”算子目前有几种类型:TableReader:对TiKV上底层扫描算子TableFullScan或TableRangeScan得到的数据进行汇总。IndexReader:汇总TiKV上底层扫描算子IndexFullScan或IndexRangeScan得到的数据。IndexLookUp:首先在Build端汇总TiKV扫描到的RowID,然后根据这些RowID去Probe端准确读取TiKV上的数据。Build端是IndexFullScan或IndexRangeScan类型的算子,Probe端是TableRowIDScan类型的算子。IndexMerge:类似于IndexLookupReader,可以看作是它的扩展,可以同时读取多个索引的数据,有多个Build端和一个Probe端。执行过程也很相似。首先在Build端汇总TiKV扫描到的所有RowID,然后到Probe端根据这些RowID准确读取TiKV上的数据。Build端是IndexFullScan或IndexRangeScan类型的算子,Probe端是TableRowIDScan类型的算子。OperatorExecutionOrder算子结构是树状的,但查询执行时,并不严格要求子节点任务先于父节点完成。TiDB支持在同一查询内并行处理,即子节点“流入”父节点。父节点、子节点和兄弟节点可以并行执行部分查询。SQL1例子中│└─IndexFullScan_15算子扫描idx_create_time(create_time)索引的RowID降序排列,├─Limit_17(Build)算子取出前10个RowID,└─TableRowIDScan_16(Probe))运算符然后取出上面返回的RowID从表中检索数据。Builds总是在Probes之前执行,Builds总是出现在Probes之前。即如果一个算子有多个子节点,子节点ID后面带Build关键字的算子总是先于带Probe关键字的算子执行。TiDB显示执行计划时,总是首先出现Build端,然后是Probe端。在范围查询的WHERE/HAVING/ON条件下,TiDB优化器会分析主键或索引键的查询返回。例如数字和日期类型的比较字符,如大于、小于、等于、大于等于、小于等于、字符类型的LIKE符号等。要使用索引,条件必须是“Sargable”(搜索ARGumentABLE)。例如条件YEAR(date_column)<1992不能使用索引,但是date_column<'1992-01-01可以使用索引。建议使用相同类型的数据和相同类型的字符串和排序规则进行比较,避免引入额外的强制转换操作和无法使用索引。您可以在范围查询条件中使用AND(??交集)和OR(并集)进行组合。对于多维复合索引,您可以在多个列上使用条件。比如对于复合索引(a,b,c):当a为等值查询时,可以继续查找b的查询范围。当b也是一个等价查询时,可以继续寻找c的查询范围。反之,如果a为非等值查询,则只能查找a的范围。4.2.2任务介绍目前,TiDB的计算任务分为两种不同的任务:cop任务和root任务。Coptask是指Coprocessor在TiKV中执行的计算任务,roottask是指在TiDB中执行的计算任务。SQL优化的目标之一是尽可能将计算下推到TiKV。TiKV中的Coprocessor可以支持大部分SQL内置函数(包括聚合函数和标量函数)、SQLLIMIT操作、索引扫描和表扫描。但是,所有Join操作只能作为根任务在TiDB上执行。4.2.3operatorinfo介绍EXPLAIN返回结果中的operatorinfo栏可以显示条件下推等信息。在本文上面的例子中,operatorinfo结果的各个字段解释如下:range:[1,1]表示查询的WHERE子句(a=1)已经下推到TiKV,并且对应的任务是cop[tikv]。keeporder:false表示查询语义不要求TiKV按顺序返回结果。如果查询指定排序(例如SELECT*FROMtWHEREa=1ORDERBYid),则该字段的返回结果为keeporder:true。stats:pseudo表示estRows显示的预估数字可能不准确。TiDB定期在后台更新统计信息。也可以通过执行ANALYZETABLEt手动更新统计信息。EXPLAIN执行后,不同的算子返回不同的信息。您可以使用优化器提示来控制优化器的行为,以控制物理运算符的选择。例如,/*+HASH_JOIN(t1,t2)*/表示优化器将使用HashJoin算法。4.2.4索引汇总SQL1中的执行计划:operator│└─IndexFullScan_15扫描TiKV操作中的索引idx_create_time(create_time)并降序排序(operatorinfo指示)operator├─Limit_17(Build)获取前10个条目(根据operatorinfoOffset:0,count:10)TiKV中数据RowID算子└─TableRowIDScan_16(Probe)根据上面得到的RowID查询TiDB汇总结果表算子IndexLookUp_18中的数据├─Limit_17(Build)RowID,然后使用RowID从算子└─TableRowIDScan_16(Probe)中准确查找数据。实际上,算子IndexLookUp_18的作用就是聚合TableRowIDScan或TableRangeScan和IndexFullScan或IndexRangeScan的数据。SQL3中的执行计划:operator└─IndexFullScan_19从TiKV中的索引create_time中降序读取10个RowID。算子└─IndexReader_21直接返回TiDB中的汇总数据,不需要读表,因为SELECT的字段是索引上的算子Limit_10是根算子,返回给客户端的结果写在最后.上面是执行计划,是一个比较粗略的结果。如果想获得详细的结果(包括性能、执行时间等),需要查看性能分析结果。方法是在SQL语句前加上EXPLAINANALYZE。下面是SQL1性能分析的例子:分析结果:由于execution_info信息比较长,所以分别截取了两次。与执行计划相比,性能分析计划有两个更重要的信息,actRows和execution_info,需要经常关注。actRows是实际扫描的行数,execution_info表示各种执行的详细信息。5.总结TiDB目前应用广泛,在很多知名公司都有非常成功的实践。这篇文章介绍了TiDB的架构、使用场景、与MySQL的兼容性、数据和索引的存储原理、执行计划的查看,基本涵盖了TiDB使用的所有基本东西。当然,TiDB的复杂度远高于文中介绍的,其内部设计和各种实现都相当复杂,值得仔细研究。通过本文的介绍,希望读者对TiDB有一个大概的了解,可以扩展自己的知识点。综上所述,MySQL可以无缝切换到TiDB,但仅用于查询。两者在事务支持上其实有细微差别,不建议从业务角度完全替代。参考资料:TiDB简介|PingCAPDocshttps://docs.pingcap.com/zh/tidb/stable/overviewTiDB简介-知乎https://zhuanlan.zhihu.com/p/494715695
