TiDB 提供完整的分布式事务,事务模型是在 Google Percolator 的基础上做了一些优化。具体的实现可以参考《Percolator 和 TiDB 事务算法》这篇文章。本文档只讨论以下几点:
乐观锁
TiDB 的乐观事务模型,只有在真正提交的时候,才会做冲突检测。如果有冲突,则需要重试。这种模型在冲突严重的场景下,会比较低效,因为重试之前的操作都是无效的,需要重复做。举一个比较极端的例子,就是把数据库当做计数器用,如果访问的并发度比较高,那么一定会有严重的冲突,导致大量的重试甚至是超时。但是如果访问冲突并不十分严重,那么乐观锁模型具备较高的效率。在冲突严重的场景下,推荐使用悲观锁,或在系统架构层面解决问题,比如将计数器放在 Redis 中。
悲观锁
TiDB 的悲观事务模式,悲观事务的行为和 MySQL 基本一致,在执行阶段就会上锁,先到先得,避免冲突情况下的重试,可以保证有较多冲突的事务的成功率。悲观锁同时解决了希望通过
select for update对数据提前锁定的场景。但如果业务场景本身冲突较少,乐观锁的性能会更有优势。事务大小限制
由于分布式事务要做两阶段提交,并且底层还需要做 Raft 复制,如果一个事务非常大,会使得提交过程非常慢,并且会卡住下面的 Raft 复制流程。为了避免系统出现被卡住的情况,我们对事务的大小做了限制:
单个事务包含的 SQL 语句不超过 5000 条(默认)
- 单条 KV entry 不超过 6MB(默认) - KV entry 的总大小不超过 10G在 Google 的 Cloud Spanner 上面,也有类似的限制。
数据分片
TiKV 自动将底层数据按照 Key 的 Range 进行分片。每个 Region 是一个 Key 的范围,从 StartKey 到 EndKey 的左闭右开区间。Region 中的 Key-Value 总量超过一定值,就会自动分裂。这部分用户不需要担心。
负载均衡
PD 会根据整个 TiKV 集群的状态,对集群的负载进行调度。调度是以 Region 为单位,以 PD 配置的策略为调度逻辑,自动完成。
SQL on KV
TiDB 自动将 SQL 结构映射为 KV 结构。具体的可以参考《三篇文章了解 TiDB 技术内幕 - 说计算》这篇文档。简单来说,TiDB 执行了以下操作:
- 一行数据映射为一个 KV,Key 以
TableID构造前缀,以行 ID 为后缀 - 一条索引映射为一个 KV,Key 以
TableID+IndexID构造前缀,以索引值构造后缀
可以看到,对于一个表中的数据或者索引,会具有相同的前缀,这样在 TiKV 的 Key 空间内,这些 Key-Value 会在相邻的位置。那么当写入量很大,并且集中在一个表上面时,就会造成写入的热点,特别是连续写入的数据中某些索引值也是连续的(比如 update time 这种按时间递增的字段),会在很少的几个 Region 上形成写入热点,成为整个系统的瓶颈。同样,如果所有的数据读取操作也都集中在很小的一个范围内(比如在连续的几万或者十几万行数据上),那么可能造成数据的访问热点。
二级索引
TiDB 支持完整的二级索引,并且是全局索引,很多查询可以通过索引来优化。如果利用好二级索引,对业务非常重要,很多 MySQL 上的经验在 TiDB 这里依然适用,不过 TiDB 还有一些自己的特点,需要注意,这一节主要讨论在 TiDB 上使用二级索引的一些注意事项。
二级索引是否越多越好
二级索引能加速查询,但是要注意新增一个索引是有副作用的。上一节介绍了索引的存储模型,那么每增加一个索引,在插入一条数据的时候,就要新增一个 Key-Value,所以索引越多,写入越慢,并且空间占用越大。另外过多的索引也会影响优化器运行时间,并且不合适的索引会误导优化器。所以索引并不是越多越好。
对哪些列建索引比较合适
上文提到,索引很重要但不是越多越好,因此需要根据具体的业务特点创建合适的索引。原则上需要对查询中需要用到的列创建索引,目的是提高性能。下面几种情况适合创建索引:
区分度比较大的列,通过索引能显著地减少过滤后的行数
有多个查询条件时,可以选择组合索引,注意需要把等值条件的列放在组合索引的前面
这里举一个例子,假设常用的查询是
select * from t where c1 = 10 and c2 = 100 and c3 > 10, 那么可以考虑建立组合索引Index cidx (c1, c2, c3),这样可以用查询条件构造出一个索引前缀进行 Scan。
通过索引查询和直接扫描 Table 的区别
TiDB 实现了全局索引,所以索引和 Table 中的数据并不一定在一个数据分片上。通过索引查询的时候,需要先扫描索引,得到对应的行 ID,然后通过行 ID 去取数据,所以可能会涉及到两次网络请求,会有一定的性能开销。
如果查询涉及到大量的行,那么扫描索引是并发进行,只要第一批结果已经返回,就可以开始去取 Table 的数据,所以这里是一个并行 + Pipeline 的模式,虽然有两次访问的开销,但是延迟并不会很大。
以下情况不会涉及到两次访问的问题:
- 索引中的列已经满足了查询需求。比如 Table
t上面的列c有索引,查询是select c from t where c > 10;,这个时候,只需要访问索引,就可以拿到所需要的全部数据。这种情况称之为覆盖索引 (Covering Index)。所以如果很关注查询性能,可以将部分不需要过滤但是需要在查询结果中返回的列放入索引中,构造成组合索引,比如这个例子:select c1, c2 from t where c1 > 10;,要优化这个查询可以创建组合索引Index c12 (c1, c2)。 - 表的 Primary Key 是整数类型。在这种情况下,TiDB 会将 Primary Key 的值当做行 ID,所以如果查询条件是在 PK 上面,那么可以直接构造出行 ID 的范围,直接扫描 Table 数据,获取结果。
- 索引中的列已经满足了查询需求。比如 Table
查询并发度
数据分散在很多 Region 上,所以 TiDB 在做查询的时候会并发进行,默认的并发度比较保守,因为过高的并发度会消耗大量的系统资源,且对于 OLTP 类型的查询,往往不会涉及到大量的数据,较低的并发度已经可以满足需求。对于 OLAP 类型的 Query,往往需要较高的并发度。所以 TiDB 支持通过 System Variable 来调整查询并发度。
在进行扫描数据的时候的并发度,这里包括扫描 Table 以及索引数据。
如果是需要访问索引获取行 ID 之后再访问 Table 数据,那么每次会把一批行 ID 作为一次请求去访问 Table 数据,这个参数可以设置 Batch 的大小,较大的 Batch 会使得延迟增加,较小的 Batch 可能会造成更多的查询次数。这个参数的合适大小与查询涉及的数据量有关。一般不需要调整。
如果是需要访问索引获取行 ID 之后再访问 Table 数据,每次通过行 ID 获取数据时候的并发度通过这个参数调节。
通过索引保证结果顺序
索引除了可以用来过滤数据之外,还能用来对数据排序,首先按照索引的顺序获取行 ID,然后再按照行 ID 的返回顺序返回行的内容,这样可以保证返回结果按照索引列有序。前面提到了扫索引和获取 Row 之间是并行 + Pipeline 模式,如果要求按照索引的顺序返回 Row,那么这两次查询之间的并发度设置的太高并不会降低延迟,所以默认的并发度比较保守。可以通过 tidb_index_serial_scan_concurrency 变量进行并发度调整。
逆序索引
目前 TiDB 支持对索引进行逆序 Scan,目前速度比顺序 Scan 慢一些,通常情况下慢 20%,在数据频繁修改造成版本较多的情况下,会慢的更多。如果可能,建议避免对索引的逆序 Scan。




