TiDB 基础详解

一、TiDB 简介

TiDB 是一款定位于 OLTP (在线事务处理) / OLAP (在线分析处理) 的融合型数据库,结合了传统的 RDBMS 和 NoSQL,实现了一键水平伸缩,强一致性的多副本数据安全,分布式事务,实时 OLAP 等重要特性。同时兼容 MySQL 协议和生态,迁移便捷,运维成本极低。

TiDB 对业务没有任何侵入性,能优雅的替换传统数据库中间件、数据库分库分表 Sharding 方案,同时也让运维人员不用关注数据库 scale 的细节问题,专注于业务开发,提高工作效率。

二、为什么是 TiDB

以 MySQL 为例,一般情况下 MySQL 单表数据较大时,数据库性能、可维护性都会下降,这时候需要对 MySQL 进行分库分表。将单张大表拆分成小表,部署到多台服务器,提升整体性能。

由此可以想到 NoSQL数据库,如 HBase、Redis,这类数据库解决了 RDBMS 伸缩性差的问题,集群扩容方便许多,但由于存储方式多为 key-value 结构,提倡非关系型数据存储,牺牲复杂 SQL 的支持 和 ACID 事务,换取弹性扩展能力,通常不保证强一致性(支持最终一致)。

单机 RDBMS 无法满足性能问题,单机 + 中间件 在中间层有负责的分布式事务、高可用问题,NoSQL 不能完全取代 RDBMS。所以需要 NewSQL。

NewSQL 针对 OLTP 的读写,提供与 NoSQL 相同的可扩展性和性能,同时支持 ACID 事务特性。TiDB 就是这样的数据库。

三、TiDB 整体架构

TiDB 的设计目标是 100% 的 OLTP 场景和 80% 的 OLAP 场景,更复杂的 OLAP 分析可以通过 TiSpark 项目来完成。

TiDB 集群主要包括三个核心组件:PD Server、TiDB Server 和 TiKV Server。此外,还有用于解决复杂 OLAP 需求的 TiSpark 组件。

TiDB 架构

调度节点:PD Server

Placement Driver (简称 PD) 是整个集群的管理模块,其主要工作有三个:

  1. 存储集群的元信息 (某个 Key 存储在哪个 TiKV 节点);
  2. 对 TiKV 集群进行调度和负载均衡 (如数据的迁移等);
  3. 分配全局唯一且递增的事务 ID。

PD 是一个集群,需要部署奇数个节点,一般线上推荐至少部署 3 个节点。

计算节点:TiDB Server

TiDB Server 负责接收客户端的 SQL 请求,处理 SQL 相关的逻辑,并通过 PD 中存储的元数据,找到真实数据到底存储在哪个 TiKV 或 TiFlash,再与 TiKV、TiFlash 交互获取数据,最终返回结果。
TiDB Server 是无状态的,其本身并不存储数据,只负责计算,可以无限水平扩展,可以通过负载均衡组件(如LVS)对外提供统一的接入地址。

存储节点:TiKV Server 与 TiFlash

TiKV Server 负责存储数据,是一个分布式的提供事务的 Key-Value 存储引擎。存储数据的基本单位是 Region,每个 Region 负责存储一个 Key Range(从 StartKey 到 EndKey 的左闭右开区间)的数据,每个 TiKV 节点会负责多个 Region。
TiKV 使用 Raft 协议做复制,保持数据的一致性和容灾。副本以 Region 为单位进行管理,不同节点上的多个 Region 构成一个 Raft Group,互为副本。数据在多个 TiKV 之间的负载均衡由 PD 调度,这里也是以 Region 为单位进行调度。

TiFlash 是一类特殊的存储节点。和普通 TiKV 节点不一样的是,在 TiFlash 内部,数据是以列式的形式进行存储,主要的功能是为分析型的场景加速。

TiSpark

TiSpark 作为 TiDB 中解决用户复杂 OLAP 需求的主要组件,将 Spark SQL 直接运行在 TiDB 存储层上,同时融合 TiKV 分布式集群的优势,融入大数据社区生态。至此,TiDB 可以通过一套系统,同时支持 OLTP 与 OLAP。

四、TiDB 的核心特点

TiDB 具有许多特性:

1、高度兼容 MySQL

无需修改代码即可从 MySQL 迁移到 TiDB,使用时完全无感,只是新的数据库的存储是无限的,不受制于磁盘容量。

2、分布式事务

TiDB 100% 支持 ACID 事务。

3、一站式 HTAP 解决方案

TiDB 即是典型的 OLTP 行存储数据库,同时兼具 OLAP 性能,配合 TiSpark 可以一份数据存储同时处理 OLTP 和 OLAP,无需 ETL 过程。

4、水平弹性扩展

这里的水平扩展包含两方面:计算能力和存储能力。

TiDB Server 负责处理 SQL 请求,随着业务的增长,可以通过添加 TiDB Server 节点提高整体处理能力,提供更高的吞吐量。

TiKV 负责存储数据,随着数据量增长,可以部署更多的 TiKV Server 节点,解决数据伸缩问题。PD 会在 TiKV 节点之间,以 Region 为单位调度,将部分数据迁移到新节点。

5、高可用

TiDB、TiKV、PD 这三个组件都能容忍部分实例失效,而不影响整个集群的可用性。

TiDB 高可用:TIDB 是无状态的,推荐至少部署两个实例,通过负载均衡组件对外提供服务。当单个实例失效时,会影响正在这个实例上进行的 Session,会出现单次请求失败的情况,重新连接后即可继续获得服务。

PD 高可用:PD 是一个集群,单个实例失效时,如果这个实例不是 leader,那么服务完全不受影响;如果这个实例是 leader,会重新选出新 leader,自动恢复服务。PD 在选举的过程中无法对外提供服务 (约3秒)。

TiKV高可用:TiKV 是一个集群,通过 Raft 协议保持数据的一致性(副本数量可配置,默认保存三副本),并通过 PD 做负载均衡调度。单个节点失效时,会影响这个节点上存储的所有 Region。对于 Region 中的 Leader 结点,会中断服务,等待重新选举;对于 Region 中的 Follower 节点,不会影响服务。当某个 TiKV 节点失效,并且在一段时间内(默认 30 分钟)无法恢复,PD 会将其上的数据迁移到其他的 TiKV 节点上。

五、TiDB 安装部署

这里使用 TiUP 来进行初步体验。TiUP 承担着包管理器的角色,管理着 TiDB 生态下众多的组件,如 TiDB、PD、TiKV 等。用户想要运行 TiDB 生态中任何组件时,只需要执行 TiUP 一行命令即可,相比以前,极大地降低了管理难度。

TiDB 是一个分布式系统。最基础的 TiDB 测试集群通常由 2 个 TiDB 实例、3 个 TiKV 实例、3 个 PD 实例和可选的 TiFlash 实例构成。通过 TiUP Playground,可以快速搭建出上述的一套基础测试集群,步骤如下:

1、下载并安装 TiUP:$ curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh

2、执行 $ tiup playground 命令会运行最新版本的 TiDB 集群,其中 TiDB、TiKV、PD 和 TiFlash 实例各 1 个

3、使用 MySQL 客户端连接 TiDB:$ mysql --host 127.0.0.1 --port 4000 -u root

4、通过 http://127.0.0.1:9090 访问 TiDB 的 Prometheus 管理界面。

5、通过 http://127.0.0.1:2379/dashboard 访问 TiDB Dashboard 页面,默认用户名为 root,密码为空。

6、通过 http://127.0.0.1:3000 访问 TiDB 的 Grafana 界面,默认用户名和密码都为 admin。

六、数据存储

TiKV 是 Key-Value 模型,并且提供有序遍历方法。简单来讲,可以将 TiKV 看做一个巨大的 Map,其中 Key 和 Value 都是原始的 Byte 数组,在这个 Map 中,Key 按照 Byte 数组总的原始二进制比特位比较顺序排列。

这里需要对 TiKV 记住两点:

  1. 这是一个巨大的 Map,也就是存储的是 Key-Value;

  2. 这个 Map 中的 Key-Value 按照 Key 的二进制顺序有序,也就是可以 Seek 到某一个 Key 的位置,然后不断的调用 Next 方法以递增的顺序获取比这个 Key 大的 Key-Value。

存储

在这里有一件重要的事情:这里的存储模型和 SQL 中的 Table 无关!

任何持久化的存储引擎,数据终归要保存在磁盘上,TiKV 没有选择直接向磁盘上写数据,而是把数据保存在 RocksDB 中,具体的数据落地由 RocksDB 负责。这里可以简单的认为 RocksDB 是一个单机的 Key-Value Map。

接下来,如何保证单个节点失效的情况下,整个系统的数据不丢失,不出错?简单来说,需要想办法把数据复制到多台机器上,这样一台机器挂了,还有其他的机器上的副本。

Raft

TiKV 利用 Raft 来做数据复制,每个数据变更都会落地为一条 Raft 日志,通过 Raft 的日志复制功能,将数据安全可靠地同步到 Group 的多数节点中。

Raft 数据复制

Region

前面多次提到了 Region,TiKV 将整个 Key-Value 空间分成很多段,每一段是一系列连续的 Key,将每一段叫做一个 Region,并且会尽量保持每个 Region 中保存的数据不超过一定的大小 (默认是 96MB)。每一个 Region 都可以用 StartKey 到 EndKey 这样一个左闭右开区间来描述。

Region

TiKV 与 Raft

TiKV 对 Raft 的实现如图所示。

这里每个 Store 表示一个 TiKV 节点,TiKV 的数据最小存储单位为 Region,将数据按照 key 的范围打散为多个 Region,PD 会尽量保证每个节点上服务的 Region 数量差不多,同时 PD 也会记录 Region 在节点上面的分布情况,也就是通过任意一个 Key 就能查询到这个 Key 在哪个 Region 中,以及这个 Region 目前在哪个节点上。

TiKV 对 Raft 的实现

TiKV 是以 Region 为单位做数据的复制,也就是一个 Region 的数据会保存多个副本 (Replica)。Replica 之间是通过 Raft 来保持数据的一致,一个 Region 的多个 Replica 会保存在不同的节点上,构成一个 Raft Group。其中一个 Replica 会作为这个 Group 的 Leader,其他的 Replica 作为 Follower。所有的读和写都是通过 Leader 进行,再由 Leader 复制给 Follower。

小结

  1. 通过单机的 RocksDB,可以将数据快速地存储在磁盘上;
  2. 通过 Raft,可以将数据复制到多台机器上,以防单机失效;
  3. 数据的写入是通过 Raft 这一层的接口写入,而不是直接写 RocksDB。
  4. 通过实现 Raft,实现了一个分布式的 KV,不用担心某台机器挂掉了。

七、回到过去 — TiDB 的历史读功能

传统的方案会定期备份数据,把数据全量备份。当意外发生的时候,可以用来还原。但是用备份数据还原,代价还是非常大的,所有备份时间点后的数据都会丢失,另外全量备份带来的存储和计算资源的额外开销,也是一笔不小的成本。

TiDB 针对这样的需求和场景支持历史版本的读取,所以可以将错误的版本之前的数据取出来,将损失降到最低。

只需要执行一个 SET 语句:

set @@tidb_snapshot = "2021-09-10 09:30:11.123"

tidb_snapshot 是一个时间的字符串,精确到毫秒,执行了这个语句之后,之后这个客户端发出的所有读请求,读到的都是这个时间点看到的数据,这时是不能进行写操作的,因为历史是无法改变的。如果想退出历史读模式,读取最新数据,只需要再次执行一个 SET 语句:

set @@tidb_snapshot = ""

把 tidb_snapshot 设置成空字符串就可以了。

这种读取历史数据是通过多版本控制(MVCC)来实现的。

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-Version2 -> Value
    Key2-Version1 -> Value
    ……
    KeyN-Version2 -> Value
    KeyN-Version1 -> Value
    ……

注意,对于同一个 Key 的多个版本,版本号较大的放在前面,版本号小的放在后面,这样当通过 Key + Version 来获取 Value 的时候,可以直接 Seek (Key-Version),定位到第一个大于等于这个 Key-Version 的位置。

如果所有的版本都保留,数据占用的空间会不会无限膨胀?

TiDB 会定期执行垃圾回收的任务,把过老的旧版本删掉。那么多久以前的老的数据会被删掉呢?这个 GC 的过期时间,是通过配置一个参数来控制的,可以配置成一个小时,一天或永远不回收。

例如,如果需要将 GC 调整为保留最近一天以内的数据,只需执行下列语句即可:

update mysql.tidb set VARIABLE_VALUE="24h" where VARIABLE_NAME="tikv_gc_life_time";

所以,TiDB 的历史读功能是有限制的,只能读取到 GC 过期时间之后的数据,GC 过期时间设置的越久,空间占用的会越大,读性能也会有所下降,这就要看业务的类型和需求了。

八、TiFlash 的数据存储

TiFlash

TiFlash 提供列式存储,它与 TiKV 非常类似,依赖同样的 Multi-Raft 体系,以 Region 为单位进行数据复制和分散。

TiFlash 以低消耗不阻塞 TiKV 写入的方式,实时复制 TiKV 集群中的数据,并同时提供与 TiKV 一样的一致性读取,且可以保证读取到最新的数据。TiFlash 中的 Region 副本与 TiKV 中完全对应,且会跟随 TiKV 中的 Leader 副本同时进行分裂与合并。

TiFlash 接入 TiKV 集群后,默认不会开始同步数据。可通过 MySQL 客户端向 TiDB 发送 DDL 命令来为特定的表建立 TiFlash 副本:ALTER TABLE table_name SET TIFLASH REPLICA count

  • count 表示副本数,0 表示删除。

TiFlash 主要包含三个组件,除了主要的存储引擎组件,另外包含 tiflash proxy 和 pd buddy 组件,其中 tiflash proxy 主要用于处理 Multi-Raft 协议通信的相关工作,pd buddy 负责与 PD 协同工作,将 TiKV 数据按表同步到 TiFlash。

八、TiDB 的数据计算

SQL 和 KV 结构之间存在巨大的区别,那么如何能够方便高效地进行映射,就成为一个很重要的问题。

上面提到了 TiKV 存储的是有序的 key-value,因此对于快速获取一行数据,若能够构造出某一个或者某几个 Key,定位到这一行,就能利用 TiKV 提供的 Seek 方法快速定位到这一行数据所在位置。

再比如对于扫描全表的需求,如果能够映射为一个 Key 的 Range,从 StartKey 扫描到 EndKey,那么就可以简单的通过这种方式获得全表数据。

思路如下:

为了保证同一个表的数据放在一起,方便查找:

  1. TiDB 会为每个表分配一个 TableID;
  2. 每一个索引都会分配一个 IndexID;
  3. 每一行分配一个 RowID(若有整数型的 Primary Key,则用 Primary Key 的值当做 RowID)。

其中 TableID 在整个集群内唯一,IndexID 和 RowID 在表内唯一。

然后每行数据按照如下规则编码成 (Key, Value) 键值对:

Key:   tablePrefix{TableID}_recordPrefixSep{RowID}
Value: [col1, col2, col3, col4]

其中 tablePrefixrecordPrefixSep 都是特定的字符串常量。

对于主键和唯一索引,需要根据键值快速定位到对应的 RowID,因此,按照如下规则编码成 (Key, Value) 键值对:

Key:   tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue
Value: RowID

对于不需要满足唯一性约束的普通二级索引,一个键值可能对应多行,需要根据键值范围查询对应的 RowID。 因此,按照如下规则编码成 (Key, Value) 键值对:

Key:   tablePrefix{TableID}_indexPrefixSep{IndexID}_indexedColumnsValue_{RowID}
Value: null

采用这种编码后,一个表的所有行数据会按照 RowID 顺序地排列在 TiKV 的 Key 空间中,某一个索引的数据也会按照索引数据的具体的值(编码方案中的 indexedColumnsValue)顺序地排列在 Key 空间内。

通过一个简单的例子,来说明 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

九、PD 的数据调度

调度的基本操作指的是为了满足调度的策略。可整理为以下三个操作:
● 增加一个副本
● 删除一个副本
● 将 Leader 角色在一个 Raft Group 的不同副本之间 transfer(迁移)。
刚好 Raft 协议通过 AddReplica、RemoveReplica、TransferLeader 这三个命令,可以支撑上述三种基本操作。

调度依赖于整个集群信息的收集,简单来说,调度需要知道每个 TiKV 节点的状态以及每个 Region 的状态。TiKV 集群会向 PD 汇报两类消息,TiKV 节点信息和 Region 信息:

每个 TiKV 节点会定期向 PD 汇报节点的状态信息
TiKV 节点与 PD 之间存在心跳包,一方面 PD 通过心跳包检测每个 TiKV 是否存活,以及是否有新加入的 TiKV;另一方面,心跳包中也会携带这个 TiKV 的状态信息,如:总磁盘容量、可用磁盘容量、承载的 Region 数量、数据写入/读取速度等。

每个 Raft Group 的 Leader 会定期向 PD 汇报 Region 的状态信息
每个 Raft Group 的 Leader 和 PD 之间存在心跳包,用于汇报这个 Region 的状态,如:Leader 的位置、Followers 的位置、掉线副本的个数、数据写入/读取的速度等。

PD 不断地通过 TiKV 或者 Raft Leader 的心跳包收集整个集群信息,并且根据这些信息以及调度策略生成调度操作序列。每次收到 Region Leader 发来的心跳包时,PD 都会检查这个 Region 是否有待进行的操作,然后通过心跳包的回复消息,将需要进行的操作返回给 Region Leader,并在后面的心跳包中监测执行结果。

十、事务

TiDB 支持分布式事务,提供 乐观事务 与 悲观事务 两种事务模型。TiDB 默认采用悲观事务模型。

TiDB 支持分布式事务,提供乐观事务与悲观事务两种事务模式,默认采用悲观事务模式。

乐观锁与悲观锁模式的主要差异在于:到底是事务提交阶段加锁来检测事务冲突?还是在事务开始阶段就直接对数据加锁来阻塞其他写操作?

乐观事务

乐观事务

TiDB 在处理一个事务时,处理流程如下:

  1. 客户端开启事务后,tidb 向 pd 获取一个 tso 作为事务的start_ts, 理解为 start timestamp 即可,以下同理。
  2. tidb 从 pd 获取读取数据的路由(即数据存在哪些 tikv 节点上),然后从 tikv 读取数据,读取小于此 start_ms 的最新数据版本。
  3. TiDB 校验写入数据是否符合约束(如数据类型是否正确、是否符合非空约束等)。校验通过的数据将存放在 TiDB 中该事务的私有内存里。
  4. 客户端发起 commit 操作,接下来就是 TiDB 的两阶段提交流程:
    a. 第一阶段(prewrite阶段,也可以叫做加锁阶段):
    b. tidb 将要写入的数据按 key 分类,然后从 pd 获取数据的写入路由(即数据应该写入哪些 tikv 节点)。
    c. tidb 并发的向所有涉及的 tikv 发起请求,tikv 收到请求后检查对应记录是否过期或者存在版本冲突,正常的话会加锁。
    d. tidb 收到所有 prewrite 成功的响应,至此第一阶段完成。
    e. 第二阶段(正式提交阶段)
    f. tidb 向 pd 获取一个 tso 作为 commit_ts
    g. tidb 向 tikv 发起第二阶段的提交请求,tikv 进行数据写入,然后清理第一阶段的锁。
    h. tidb 收到两阶段提交成功的信息,客户端收到 tidb 反馈的事务成功的信息。
    i. 最后 tidb 异步的清理本次事务遗留的锁信息。

悲观事务

悲观事务

TiDB 悲观锁复用了乐观锁的两阶段提交逻辑,重点在 DML 执行时做了改造。
对比乐观锁的实现流程,悲观锁只有一个地方有区别,即上图红色框内,在 TiDB 收到写入请求后,TiDB 按照如下方式开始加锁:

  1. 从 PD 获取当前 ts 作为当前锁的 for_update_ts
  2. TiDB 将写入信息写入 TiDB 的内存中(与乐观锁相同)
  3. 使用 for_update_ts 并发地对所有涉及到的 Key 发起加悲观锁(acquire pessimistic lock)请求,
  4. 如果加锁成功,TiDB 向客户端返回写成功的请求
  5. 如果加锁失败
    如果遇到 Write Conflict, 重新回到步骤 1 直到加锁成功。
    如果超时或其他异常,返回客户端异常信息

其中 TiKV 中 acquire pessimistic lock 接口的具体处理逻辑,具体步骤如下:

  1. 检查 TiKV 中锁情况,如果发现有锁
    ○ 不是当前同一事务的锁,返回 KeyIsLocked Error
    ○ 锁的类型不是悲观锁,返回锁类型不匹配(意味该请求已经超时)
    ○ 如果发现 TiKV 里锁的 for_update_ts 小于当前请求的 for_update_ts (同一个事务重复更新), 使用当前请求的 for_update_ts 更新该锁
    ○ 其他情况,为重复请求,直接返回成功
  2. 检查是否存在更新的写入版本,如果有写入记录
    ○ 若已提交的 commit_ts 比当前的 for_update_ts 更新,说明存在冲突,返回 WriteConflict Error
    ○ 如果已提交的数据是当前事务的 Rollback 记录,返回 PessimisticLockRollbacked 错误
    ○ 若已提交的 commit_ts 比当前事务的 start_ts 更新,说明在当前事务 begin 后有其他事务提交过
    ○ 检查历史版本,如果发现当前请求的事务有没有被 Rollback 过,返回 PessimisticLockRollbacked 错误
  3. 给当前请求 key 加上悲观锁,并返回成功

从上面这个过程可以看到, TiDB 事务存在以下优点:

  1. 简单,好理解。
  2. 基于单实例事务实现了跨节点事务。
  3. 去中心化的锁管理。

缺点如下:

  1. 两阶段提交,网络交互多。
  2. 需要一个中心化的版本管理服务。
  3. 事务在 commit 之前,数据写在内存里,数据过大内存就会暴涨。

十一、TiDB 和 MySQL InnoDB 的差异

1、TiDB 当前不支持 Gap Locking(间隙锁)

BEGIN /*T! PESSIMISTIC */; 
SELECT * FROM t1 WHERE id BETWEEN 1 AND 10 FOR UPDATE;
BEGIN /*T! PESSIMISTIC */; 
INSERT INTO t1 (id) VALUES (6); -- 仅 MySQL 中出现阻塞。 
UPDATE t1 SET pad1='new value' WHERE id = 5; -- MySQL 和 TiDB 处于等待阻塞状态。

2、TiDB 不支持 SELECT LOCK IN SHARE MODE。使用这个语句执行的时候,效果和没有加锁是一样的,不会阻塞其他事务的读写。
3、autocommit 事务优先采用乐观事务提交。使用悲观事务模式时,autocommit 事务首先尝试使用开销更小的乐观事务模式提交。如果发生了写冲突,重试时才会使用悲观事务提交。所以 tidb_retry_limit = 0 时,autocommit 事务遇到写冲突仍会报 Write Conflict 错误。自动提交的 SELECT FOR UPDATE 语句不会等锁。
4、TiDB 在悲观事务模式下支持 2 种隔离级别:

  1. 默认使用与 MySQL 行为相同的可重复读隔离级别 (Repeatable Read)。注意在这种隔离级别下,DML 操作会基于已提交的最新数据来执行,行为与 MySQL 相同。
  2. 使用 SET TRANSACTION 语句可将隔离级别设置为读已提交隔离级别 (Read Committed)。

你可能感兴趣的:(TiDB 基础详解)