├── README.md ├── ch02 ├── CAP.png └── cap.md ├── f1 ├── 1.jpg ├── 2.jpg ├── 3.jpg ├── 4.jpg ├── README.md ├── schema-change-implement.md └── schema-change.md ├── hlc └── README.md ├── howto ├── README.md └── 程序媛的分布式数据库开发经验.pdf ├── omid └── README.md ├── percolator ├── README.md └── imgs │ ├── commit.jpg │ ├── get.jpg │ ├── prewrite.jpg │ ├── rollback.jpg │ ├── s_1.jpg │ ├── t_1.jpg │ ├── t_2.jpg │ ├── t_3.jpg │ ├── t_4.jpg │ └── t_5.jpg ├── spanner └── README.md └── tidb ├── architecture.png ├── builtin.md ├── builtin_new.md ├── dist_sql_example.png ├── process_flow.png ├── protocol_layer.png ├── sourcecode.md ├── storage.md ├── tidb-core.png └── tidb_bot.png /README.md: -------------------------------------------------------------------------------- 1 | # 从零开始写分布式数据库 2 | 以 [TiDB](https://github.com/pingcap/tidb) 和 [TiKV](https://github.com/pingcap/tikv) 源码为例 3 | 4 | * 第一章 概论 5 | 6 | * 第二章 基础知识 7 | * [数据库的隔离级别] 8 | * [select for update or not] 9 | * [分布式系统的 CAP 理论] 10 | * [Google Spanner 简介](spanner/README.md) 11 | * [Google F1 简介](f1/README.md) 12 | * [HBase 简介] 13 | * [Google percolator 事务模型](percolator/README.md) 14 | * [Yahoo 的 omid 事务模型](omid/README.md) 15 | * [TiDB 的分布式事务模型] 16 | * [TiDB 的源码介绍](tidb/sourcecode.md) 17 | * [如何参与 TiDB 开源项目](howto/README.md) 18 | * [如何添加新的 key value 存储引擎](tidb/storage.md) 19 | 20 | * 第三章 SQL解析 21 | * [词法分析与 golex 用法] 22 | * [语法分析与 goyacc 用法] 23 | * [通过案例看解决Reduce/Reduce冲突](https://github.com/pingcap/tidb/pull/589/files) 24 | * [通过案例看如何解决Shift/Reduce冲突](https://github.com/pingcap/tidb/pull/128/files) 25 | * [解析整个语句的执行流程] 26 | * [案例:为 TiDB 添加一个新的函数](tidb/builtin.md) 27 | * [案例:为 TiDB 添加一个语句] 28 | * [思考:如何支持 json/protocol buffer] 29 | 30 | * 第四章 MySQL 协议支持 31 | * [协议概述] 32 | * [如何用 wireshark 来辅助调试] 33 | * [Request 格式] 34 | * [Response 格式] 35 | * [Prepare 语句支持] 36 | * [TiDB 代码分析] 37 | 38 | * 第五章 执行计划优化 39 | * [基于规则的优化] 40 | * [基于开销分析的优化] 41 | * [分布式/并行优化] 42 | * [延迟计算] 43 | * [案例分析:FoundationDB, Apache Phoenix] 44 | * [案例分析:Google F1, GreenPlum] 45 | * [TiDB 优化器代码分析] 46 | 47 | * 第六章 分布式 SQL 数据库的异步 Schema 变更 48 | * [深度剖析 Google F1 的 schema 变更算法](f1/schema-change.md) 49 | * [TiDB 的异步 schema 变更实现](f1/schema-change-implement.md) 50 | 51 | * 第七章 高级 52 | * [Hybird logical clocks](hlc/README.md) 53 | * [深入分析 Spanner 那些相关的论文] 54 | * [一些新的论文和技术讨论] 55 | 56 | * 第八章 实现一个分布式 key-value 引擎 57 | * [raft 协议介绍] 58 | * [TiKV 的 raft 实现] 59 | * [聊聊 Google Spanner] 60 | * [TiKV 系统架构] 61 | * [TiKV 实现实现跨数据中心容灾] 62 | * [TiKV 如何实现自动扩容] 63 | * [TiKV 分布式事务实现] 64 | 65 | * 第九章 如何测试分布式系统 66 | * [大话测试] 67 | * [用 Jepsen 模拟网络分区,延迟] 68 | * [用 Namazu 实现 fault injection] 69 | * Fuzz test 70 | -------------------------------------------------------------------------------- /ch02/CAP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/ch02/CAP.png -------------------------------------------------------------------------------- /ch02/cap.md: -------------------------------------------------------------------------------- 1 | # 分布式系统的CAP定理 2 | ## 简介 3 | *CAP定理*: 指的是在一个分布式系统中,一致性(`Consistency`)、可用性 4 | (`Availability`)、分区容错(`Partition tolerance`)这三个要素最多只能同时实现两点, 5 | 不可能三者兼顾。下面是一个更直观的图,图片来源[Distributed Systems for fun and profit](http://book.mixu.net/distsys/abstractions.html) 6 | 7 | ![图2-1](CAP.png) 8 | 9 | 1. *一致性(`C`)*: 这里的一致性不等价于数据库*ACID*中的一致性,也不等价于*Oatmeal*一致 10 | 性,它是针对分布式系统最基本的提升可用性的方式分区复制(副本)。因此这里的一致 11 | 性就是分区副本之间的一致性。此外,这里的一致性指的是强一致性,即所有副本保持 12 | 一致。 13 | 1. *可用性(`A`)*: 在部分节点失败的情况下,系统仍然可用,分区和复制是分布式系统中提升可用性的基本 14 | 方式。 15 | 1. *分区容错(`P`)*: 这里的分区特指网络的分区,与数据的分区不同。来自[CAP定理的含 16 | 义](http://www.ruanyifeng.com/blog/2018/07/cap.html) 的解释比较直观:大多数分 17 | 布式系统都分布在多个子网络,每个子网络就叫做一个区(`Partition`) 区间通信可能 18 | 失败,从而造成网络分区。 而在系统设计的时候必须考虑这种情况。 19 | 20 | ## `C`、`A`、`P` 之间的关系 21 | 理解 CAP 理论的最简单方式是想象两个节点分处分区两侧。允许至少一个节点更新状态会 22 | 导致数据不一致,即丧失了 C 性质。如果为了保证数据一致性,将分区一侧的节点设置为 23 | 不可用,那么又丧失了 A 性质。除非两个节点可以互相通信,才能既保证 C 又保证 A,这 24 | 又会导致丧失 P 性质。来自[CAP 理论十二年回顾:"规则"变了](https://www.infoq.cn/article/cap-twelve-years-later-how-the-rules-have-changed/) 25 | 26 | 1. `C` vs `A`: 一致性和可用性是冲突的,可用性要求容忍部分节点失败,这种情况下继 27 | 续接受写入操作,那么失败节点上的副本无法和接受写入的副本保持一致,降低了一致 28 | 性。 29 | 1. `C` vs `P`: 一致性和分区容错是冲突的,如果要分区容错,必然使得其中一个分区失 30 | 效,这就意味着所有副本不可能达成一致,降低了一致性。 31 | 1. `A` vs `P`: 可用性和分区容错是冲突的,如果要分区容错,那么某一个分区将不能提 32 | 供服务(否则会引入分歧,降低一致性)。那么这会降低系统可用性,因为能够容忍的失 33 | 败的节点更少了,降低了可用性。 34 | 35 | 36 | 1. `CA`系统: 不区分节点失败和网络失败,为了防止多副本之间的分歧,必须停止接受写 37 | 入操作。因为不知道是节点失败还是网络失败,保险的做法就是停止接受写。 38 | 1. `CP`系统: 再发生网络分区的时候,只能保持大多数节点的分区继续工作,少部分节点 39 | 的分区不可用。 40 | 41 | ## `CAP`权衡 42 | 一个全球分布式系统中(全球广域网分布式环境下),网络分区是一个自然的事实(相关参考见[ Fallacies of Distributed Computing 43 | Explained](http://www.rgoarchitects.com/Files/fallacies.pdf))。 因此,`C`、`A`、 44 | `P`三者并不对等:`P`是基础,`CA`之间权衡。但是这并不意味着没有`AP`系统,例如分布 45 | 式缓存系统. 46 | 47 | 然而如果网络分区很少发生(例如Google的Spanner),就没有必要牺牲一致性和可用性。其 48 | 次,一致性有很多级别,可用性显然在0%到100%之间变化,我们可以在三种特性程度上进行 49 | 权衡。 50 | 51 | ## 参考文献 52 | + [CAP原则](https://baike.baidu.com/item/CAP%E5%8E%9F%E5%88%99/5712863?fr=aladdin) 53 | + [CAP理论与分布式系统设计](https://mp.weixin.qq.com/s/gV7DqSgSkz_X56p2X_x_cQ) 54 | + [CAP理论十二年回顾](https://www.infoq.cn/article/cap-twelve-years-later-how-the-rules-have-changed/) 55 | + [Distributed systems for fun and profit](http://book.mixu.net/distsys/abstractions.html) 56 | + [CAP定理的含义](http://www.ruanyifeng.com/blog/2018/07/cap.html) 57 | -------------------------------------------------------------------------------- /f1/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/f1/1.jpg -------------------------------------------------------------------------------- /f1/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/f1/2.jpg -------------------------------------------------------------------------------- /f1/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/f1/3.jpg -------------------------------------------------------------------------------- /f1/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/f1/4.jpg -------------------------------------------------------------------------------- /f1/README.md: -------------------------------------------------------------------------------- 1 | # Google F1 2 | 3 | [参考论文](http://static.googleusercontent.com/media/research.google.com/zh-CN//pubs/archive/41344.pdf) 4 | 5 | F1 的特性 6 | 7 | 8 | 9 | 基本概念和名词解释 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /f1/schema-change-implement.md: -------------------------------------------------------------------------------- 1 | # TiDB 的异步 schema 变更实现 2 | 3 | ## 背景 4 | 现在一般数据库在进行 DDL 操作时都会锁表,导致线上对此表的 DML 操作全部进入等待状态(有些数据支持读操作,但是也以消耗大量内存为代价),即很多涉及此表的业务都处于阻塞状态,表越大,影响时间越久。这使得 DBA 在做此类操作前要做足准备,然后挑个天时地利人和的时间段执行。为此,架构师们在设计整个系统的时候都会很慎重的考虑表结构,希望将来不用再修改。但是未来的业务需求往往是不可预估的,所以 DDL 操作无法完全避免。由此可见原先的机制处理 DDL 操作是令许多人都头疼的事情。本文将会介绍 TiDB 是如何解决此问题的。 5 | 6 | ## 解决方案 7 | 根据 Google F1 的在线异步 schema 变更算法实现,并做了一些简单优化。为了简化设计,整个系统同一时刻只允许一个节点做 schema 变更。这里先说明一下,本文不会讲述 Google F1 schema 算法推导过程,对此算法不了解的可以直接阅读[论文原文](http://static.googleusercontent.com/media/research.google.com/zh-CN//pubs/archive/41376.pdf)或者[本书前一章节](schema-change.md)。 8 | 9 | ## DDL的分类: 10 | 由于 bootstrap 操作时需要修改 DDL,这样就产生了鸡生蛋,蛋生鸡的依赖问题。所以需要将 DDL 分成两类,静态 DDL 和动态 DDL。系统 bootstrap 阶段只使用静态 DDL,同时必须在一个事务内完成,而后续所有的操作只允许使用动态 DDL。 11 | 12 | ## 引入新概念: 13 | * **元数据记录**:为了简化设计,引入 system database 和 system table 来记录异步 schema 变更的过程中的一些元数据。 14 | * **State**:根据 F1 的异步 schema 变更过程,中间引入了一些状态,这些状态要和 column,index, table 以及 database 绑定, state 主要包括 none, delete only, write only, write reorganization, public。前面的顺序是在创建操作的时候的,删除操作的状态与它的顺序相反,write reorganization 改为 delete reorganization,虽然都是 reorganization 状态,但是由于可见级别是有很大区别的,所以将其分为两种状态标记。 15 | * **Lease**:同一时刻系统所有节点中 schema 最多有两个不同的版本,即最多有两种不同状态。正因为如此,一个租期内每个正常的节点都会自动加载 schema 的信息,如果不能在租期内正常加载,此节点会自动退出整个系统。那么要确保整个系统的所有节点都已经从某个状态更新到下个状态需要 2 倍的租期时间。 16 | * **Job**: 每个单独的 DDL 操作可看做一个 job。在一个 DDL 操作开始时,会将此操作封装成一个 job 并存放到 job queue,等此操作完成时,会将此 job 从 job queue 删除,并在存入 history job queue,便于查看历史 job。 17 | * **Worker**:每个节点都有一个 worker 用来处理 job。 18 | * **Owner**:整个系统只有一个节点的 worker 能当选 owner 角色,每个节点都可能当选这个角色,当选 owner 后 worker 才有处理 job 的权利。owner 这个角色是有任期的,owner 的信息会存储在 KV 层中。worker定期获取 KV 层中的 owner 信息,如果其中 ownerID 为空,或者当前的 owner 超过了任期,则 worker 可以尝试更新 KV 层中的 owner 信息(设置 ownerID 为自身的 workerID),如果更新成功,则该 worker 成为 owner。在租期内这个用来确保整个系统同一时间只有一个节点在处理 schema 变更。 19 | * **Background operations**:主要用于 delete reorganization 的优化处理,跟前面的 worker 处理 job 机制很像。所以引入了 background job, background job queue, background job history queue, background worker 和 background owner,它们的功能跟上面提到的角色功能一一对应,这里就不作详细介绍。 20 | 21 | ## 变更流程 22 | 通过上面的章节可以了解异步 schema 变更的基本概念,本章节会将这些基本点串联成具体的变更流程。这里在讲述流程的时候会对 MySQL Client 端, MySQL Protocol 层和 KV 层的操作也会一笔带过,只介绍 TiDB SQL 层中处理异步 schema 变更的流程。 23 | 基本流程如图 1,下面将详细介绍每个模块以及具体流程。 24 | ![图 1 结构流程图](1.jpg) 25 | 26 | 图 1 结构流程图 27 | 28 | #### 模块 29 | * **TiDB Server**:包含了 TiDB 的 MySQL Protocol 层和 TiDB SQL 层,图中主要描述的是 TiDB SQL 层中涉及到异步 schema 变更的基本模块。 30 | * **load schema**:是在每个节点(这个模块跟之前提到的 worker 一样,便于理解可以这样认为)启动时创建的一个 gorountine, 用于在到达每个租期时间后去加载 schema,如果某个节点加载失败 TiDB Server 将会自动挂掉。此处加载失败包括加载超时。 31 | * **start job**:是在 TiDB SQL 层接收到请求后,给 job 分配 ID 并将之存入 KV 层,之后等待 job 处理完成后返回给上层,汇报处理结果。 32 | * **worker**:每个节点起一个处理 job 的 goroutine,它会定期检查是否有待处理的 job。 它在得到本节点上 start job 模块通知后,也会直接去检查是否有待执行的 job 。 33 | * **owner**:可以认为是一个角色,信息存储在 KV 层,其中包括记录当前当选此角色的节点信息。 34 | * **job queue**:是一个存放 job 的队列,存储在 KV 层,逻辑上整个系统只有一个。 35 | * **job history queue**:是一个存放已经处理完成的 job 的队列,存储在 KV 层,逻辑上整个系统只有一个。 36 | 37 | #### 基本流程 38 | 本小节描述的是异步 DDL 变更的整体流程,忽略实现细节。假设系统中只有两个节点,TiDB Server 1 和 TiDB Server 2。其中 TiDB Server 1 是 DDL 操作的接收节点, TiDB Server 2 是 owner。如下图 2 展示的是在 TiDB Server 1 中涉及的流程,图 3 展示的是在 TiDB Server 2 中涉及的流程。 39 | 40 | ![图2 TiDB Server 1流程图](2.jpg) 41 | 42 | 图 2 TiDB Server 1 流程图 43 | 44 | ![图3 TiDB Server 2流程图](3.jpg) 45 | 46 | 图 3 TiDB Server 2 流程图 47 | 48 | 1. MySQL Client 发送给 TiDB Server 一个更改 DDL 的 SQL 语句请求。 49 | 2. 某个 TiDB Server 收到请求(MySQL Protocol 层收到请求进行解析优化),然后到达 TiDB SQL 层进行执行。这步骤主要是在 TiDB SQL 层接到请求后,会起个 start job 的模块根据请求将其封装成特定的 DDL job,然后将此 job 存储到 KV 层, 并通知自己的 worker 有 job 可以执行。 50 | 3. 收到请求的 TiDB Server 的 worker 接收到处理 job 的通知后,判断自身是否处于 owner 的角色,如果处于 owner 角色则直接处理此 job,如果没有处于此角色则退出不做任何处理。图中我们假设没有处于此角色,那么其他的某个 TiDB Server 中肯定有一个处于此角色的,如果那个处于 owner 角色节点的 worker 通过定期检测机制来检查是否有 job 可以被执行时,发现了此 job,那么它就会处理这个 job。 51 | 4. 当 worker 处理完 job 后, 它会将此 job 从 KV 层的 job queue 中移除,并放入 job history queue。 52 | 5. 之前封装 job 的 start job 模块会定期去 job history queue 查看是否有之前放进去的 job 对应 ID 的 job,如果有则整个 DDL 操作结束。 53 | 6. TiDB Server 将 response 返回 MySQL Client。 54 | 55 | #### 详细流程 56 | 本小节以在 Table 中添加 column 为例详细介绍 worker 处理 job 的整个流程,具体流程如图 4 。考虑到与前面章节的连续性,图 4 可以理解为是图 2 和图 3 的展开描绘。 57 | 58 | ![图4 add column 流程图](4.jpg) 59 | 60 | 图 4 add column 流程图 61 | 62 | 便于在之后介绍中获取信息的方式,此处贴出了 job 的结构。 63 | ```go 64 | type Job struct { 65 | ID int64 `json:"id"` 66 | Type ActionType `json:"type"` 67 | SchemaID int64 `json:"schema_id"` 68 | TableID int64 `json:"table_id"` 69 | State JobState `json:"state"` 70 | Error string `json:"err"` 71 | // every time we meet an error when running job, we will increase it 72 | ErrorCount int64 `json:"err_count"` 73 | Args []interface{} `json:"-"` 74 | // we must use json raw message for delay parsing special args. 75 | RawArgs json.RawMessage `json:"raw_args"` 76 | SchemaState SchemaState `json:"schema_state"` 77 | // snapshot version for this job. 78 | SnapshotVer uint64 `json:"snapshot_ver"` 79 | // unix nano seconds 80 | // TODO: use timestamp allocated by TSO 81 | LastUpdateTS int64 `json:"last_update_ts"` 82 | } 83 | ``` 84 | 85 | ###### TiDB Server 1 流程 86 | 1. start job 给 start worker 传递了 job 已经准备完成的信号。 87 | 2. worker 开启一个事务,检查自己是否是 owner 角色,结果发现不是 owner 角色(此处跟先前的章节保持一致,假设此节点 worker 不是 owner 角色),则提交事务退出处理 job 的循环,回到 start worker 等待信号的循环。 88 | 89 | ###### TiDB Server 2 流程 90 | 1. start worker 中的定时器到达时间。 91 | 2. 开启一个事务,检查发现本节点为 owner 角色。 92 | 3. 从 KV 层获取队列中第一个 job(假设就是 TiDB Server 1 之前放入的 job),判断此 job 的类型并对它做相应的处理。 93 | 4. 此处 job 的类型为 add column,然后流程到达图中 get column information 步骤。 94 |   a.取对应 table info(主要通过 job 中 schemaID 和 tableID 获取),然后确定添加的 column 在原先的表中不存在或者为不可见状态。 95 |   b.如果新添加的 column 在原先表中不存在,那么将新 column 信息关联到 table info。 96 |   c.在前面两个步骤中发生某些情况会将此 job 标记为 cancel 状态,并返回 error,到达图中 returns error 流程。比如发现对应的数据库、数据表的状态为不存在或者不可见(即它的状态不为 public),发现此 column 已存在并为可见状态等一些错误,这里就不全部列举了。 97 | 5. schema 版本号加 1。 98 | 6. 将 job 的 schema 状态和 table 中 column 状态标记为 delete only, 更新 table info 到 KV 层。 99 | 7. 因为 job 状态没有 finish(即 done 或者 cancel 状态),所以直接将 job 在上一步更新的信息写入 KV 层。 100 | 8. 在执行前面的操作时消耗了一定的时间,所以这里将更新 owner 的 last update timestamp 为当前时间(防止经常将 owner 角色在不同服务器中切换),并提交事务。 101 | 9. 循环执行步骤 2、 3、 4.a、5、 6、 7 、8,不过将6中的状态由 delete only 改为 write only。 102 | 10. 循环执行步骤 2、 3、 4.a、5、 6、 7 、8,不过将6中的状态由 write only 改为 write reorganization。 103 | 11. 循环执行步骤 2、 3、 4.a、5,获取当前事务的快照版本,然后给新添加的列填写数据。通过应版本下需要得到的表的所有 handle(相当于 rowID),出于内存和性能的综合考量,此处处理为批量获取。然后针对每行新添加的列做数据填充,具体操作如下(下面的操作都会在一个事务中完成): 104 |   a.用先前取到的 handle 确定对应行存在,如果不存在则不对此行做任何操作。 105 |   b.如果存在,通过 handle 和 新添加的 columnID 拼成的 key 获取对应列。获取的值不为空则不对此行做任何操作。 106 |   c.如果值为空,则通过对应的新添加行的信息获取默认值,并存储到 KV 层。 107 |   d.将当前的 handle 信息存储到当前 job reorganization handle 字段,并存储到 KV 层。假如 12 这个步骤执行到一半,由于某些原因要重新执行 write reorganization 状态的操作,那么可以直接从这个 handle 开始操作。 108 | 12. 将调整 table info 中 column 和 index column 中的位置,将 job 的 schema 和 table info 中新添加的 column 的状态设置为设置为public, 更新 table info 到 KV 层。最后将 job 的状态改为 done。 109 | 13. 因为 job 状态已经 finish,将此 job 从 job queue 中移除并放入 job history queue 中。 110 | 14. 执行步骤8,与之前的步骤一样 12, 13, 14 和 15 在一个事务中。 111 | 112 | ###### TiDB Server 1 流程 113 | 1. start job 的定时检查触发后,会检查 job history queue 是否有之前自己放入 job queue 中的 job(通过 jobID)。如果有则此 DDL 操作在 TiDB SQL 完成,上抛到 MySQL Protocol 层,最后返回给 Client, 结束这个操作。 114 | #### 优化 115 | 对删除数据库,删除数据表等减少一个状态,即 2 倍 lease 的等待时间,以及删除数据库或者数据表中大量数据所消耗的时间。原本对于删除操作的状态变化是 public -> write only -> delete only -> delete reorganization -> none,优化的处理是去掉 delete reorganization 状态,并把此状态需要处理的元数据的操作放到 delete only 状态时,把具体删除数据的操作放到后台处理,然后直接把状态标为 none。 116 | 这相对原来设计的主要有两点不同,下面介绍下做如此优化是否对数据完整性和一致性有影响。 117 | * 将删除具体数据这个操作异步处理了。因为在把数据放到后台删除前,此数据表(假设这里执行的是删除表操作,后面提到也这么假设)的元数据已经删除,对外已经不能访问到此表了,那么对于它们下面的具体数据就更不能访问了。这里可能有人会有疑问那异步删除模块是怎么访问的具体数据的,将元数据事先存在 job 信息中,就这么简单。只要保证先删除元数据(保证此事务提交成功)再异步删除具体数据是不会有问题的。 118 | * 去掉了 delete reorganization 状态。本来 delete only 以及之前的状态都没做修改所以必然是没问题的,那么就是考虑 none 这个状态,即当整个系统由于接到变更信息先后不同处于 delete only 以及 none 这两个状态。那么就分析在这个状态下有客户端希望对此表进行一些操作。 119 | * 客户端访问表处于 none 状态的 TiDB Server。这个其实更没有做优化前是一致的,即访问不到此表,这边不过多解释。 120 | * 客户端访问表处于 delete only 状态的 TiDB Server。此时客户端对此表做读写操作会失败,因为 delete only 状态对它们都不可见。 121 | 122 | ###### 实现 123 | 此优化对于原先的代码逻辑基本没有变化,除了对于删除操作(目前还只是删除数据库和表操作)在其处于 delete only 状态时,就会把元数据删除,而对起表中具体数据的删除则推迟到后台运行,然后结束 DDL job。放到后台运行的任务的流程跟之前处理任务的流程类似,详细过程如下: 124 | 125 | 1. 在图 4 中判定 finish 操作为 true 后,判断如果是可以放在后台运行(暂时还只是删除数据库和表的任务),那么将其封装成 background job 放入 background job queue, 并通知本机后台的 worker 将其处理。 126 | 2. 后台 job 也有对应的 owner,假设本机的 backgroundworker 就是 background owner 角色,那么他将从 background job queue 中取出第一个 background job, 然后执行对应类型的操作(删除表中具体的数据)。 127 | 如果执行完成,那么从 background job queue 中将此 job 删除,并放入 background job history queue 中。 128 | 注意步骤2和步骤 3需要在一个事务中执行。 129 | 130 | ## 总结 131 | 以上内容是 TiDB 的异步 schema 变更实现的基本内容介绍,可能有些流程细节没有讲解清晰,如果对本人的描述或者对实现有疑问的欢迎到 [issues](https://github.com/pingcap/tidb/issues) 讨论。 132 | -------------------------------------------------------------------------------- /f1/schema-change.md: -------------------------------------------------------------------------------- 1 | # 异步 schema 变更 2 | 3 | ## 为什么在分布式系统中异步变更 schema 比较困难 4 | 5 | F1 中的 schema 变更是在线的、异步的,schema 变更的过程中所有数据保持可用,保持数据一致性,并最大限度的减小对性能的影响。最大的难点在于所有 F1 服务器的 schema 变更是无法同步的,也就是说不同的 F1 服务器会在不同的时间点切换至新 schema。 6 | 7 | 由于所有的 F1 服务器共享同一个 kv 存储引擎,schema 的异步更新可能造成严重的数据错乱。例如我们发起给一次添加索引的变更,更新后的节点会很负责地在添加一行数据的同时写入一条索引,随后另一个还没来得及更新的节点收到了删除同一行数据的请求,这个节点还完全不知道索引的存在,自然也不会去删除索引了,于是错误的索引就被遗留在数据库中。 8 | 9 | ## 深度剖析 Google F1 的 schema 变更算法 10 | 11 | ### 算法思想 12 | 13 | 为了更好地阐释算法思想,本节我们以一个简化的情境做类比。 14 | 15 | 假想我们有一家跨国公司,公司员工分布在全球各地,员工之间以电子邮件的方式互相通信,同时工作的性质要求员工之间发送的消息不能出现丢失。之后在某一天出现这样的需求:管理层决定把员工通信方式由邮件改为 QQ。 16 | 17 | 对于这样一个跨国公司来说,我们无法瞬间把新的工作方式通知给所有员工。假如先收到通知的员工先改为用 QQ 了,而未收到通知的员工还在用邮件,这样一来自然就会发生大量的消息丢失。 18 | 19 | 那么我们能不能通知员工在未来的某个时刻统一换用 QQ 呢?仔细一想也是不行的。因为每个员工的手表不可能是完全对准的,总是有的快点有的慢点,只要不能保证所有员工的时间完全校准,总有那么一个不一致的时刻。 20 | 21 | 下面让我们来看看 Google 的工程师们是怎么解决这个棘手的问题的。 22 | 23 | #### 在未知中构建已知 24 | 根据上面的讨论,最根本的难点在于无法保证所有员工同时改变通信方式,这基本上是不可能做到的。本来员工都在用邮件,一旦通知发布出去,员工的工作方式就完全是未知的了——在任意一个时刻,任意一个员工都可能收到过通知而换用 QQ,也可能没收到通知而继续用邮件。 25 | 26 | 如果从现实层面来考虑,员工即使是离得远些,一段足够长的时间以后也应该能收到通知。员工的时间即使是没校准,也不至于错得太离谱。所以我们可以认为在足够长的时间过后,所有员工应该都已经换用 QQ 了。 27 | 28 | 我们可以使用一系列方案使“足够长的时间”变成“确定长度的时间”。首先,公司创建一个网站张贴最新的员工手册,其中自然包含员工应使用的通信方式等细则。其次,在员工入职时进行培训,要求员工每隔 30 分钟必须上网查看一下员工手册,并依照最新的手册行事。另外,如果出现网络故障等情况,员工在尝试访问网站 10 分钟后还没有看到新的手册,必须立即停止工作,直到重新看到手册为止。 29 | 30 | 基于这些规定,我们就至少可以知道从通知发布之后的某个时刻开始,所有工作中的员工都已经更新自己的通信方式了。例如我们中午 12:00 在网站上张贴新的手册,员工从 12:00 到 12:30 开始陆续查看手册并换用 QQ,到 12:30 时所有员工都应该尝试过访问网站了,到 12:40 时所有未能看到手册的员工都已经停止了工作,这时我们就可以认为所有工作中的员工都在用 QQ 了。如果再考虑手表时快时慢等特殊情形,我们不妨再多等 20 分钟。到了 13:00 ,我们可以非常自信地说:现在所有员工都换用 QQ 了。 31 | 32 | 在此基础之上,我们再规定两次修改员工手册的时间间隔不能少于 1 小时。例如在 QQ 之后我们还想换用微信,那么最早只能在 13:00 发布新的员工手册。根据前面的讨论,13:00 已经没有员工用邮件了,所以在整个演化过程中,有些时刻邮件和 QQ 同时被使用,有些时刻 QQ 和微信同时被使用,但一定不会发生邮件和微信同时被使用的情况。也就是说,在员工手册不断更新的过程中,最多只有两份手册生效:最新发布的这一份以及上一份。 33 | 34 | #### 在矛盾中构建一致 35 | 上面我们设计了大量的规定和方案,最后只得到了不那么强的结论,看起来对问题的解决并没有什么帮助。不难发现问题的关键在于邮件和 QQ 这两种通信方式是矛盾不兼容的,只要有一个时刻有员工用邮件而有员工用 QQ,那么就很可能会造成消息丢失。 36 | 37 | 那么问题的本质其实是:在通信方式由邮件变成 QQ 的过程中,邮件和 QQ 这两种通信方式不能同时生效。 38 | 39 | 请回想一下上一节中我们得到过的结论,邮件和微信一定不可能同时被使用……你想到了吗? 40 | 41 | BING!没错,只要我们在邮件和 QQ 中间加入一个其他的通信方式 X 作为过渡,因为同时只有两种连续的手册生效,邮件和 QQ 就很自然地被隔离了。很显然通信方式 X 一定不是微信,它一定是同时与邮件和 QQ 兼容的,在这里 X 的定义可以是:同时从邮件和 QQ 查收消息,发送消息时邮件和 QQ 各发送一遍。 42 | 43 | 以上就是 F1 schema 变更的主要思想了。具体在 F1 schema 变更的过程中,由于数据库本身的复杂性,有些变更无法由一个中间状态隔离,我们需要设计多个逐步递进的状态来进行演化。万变不离其宗,只要我们保证任意相邻两个状态是相互兼容的,整个演化的过程就是可依赖的。 44 | 45 | ### F1 中的算法实现 46 | 47 | #### 租约 48 | F1 中 schema 以特殊的 kv 对存储于 Spanner 中,同时每个 F1 服务器在运行过程中自身也维护一份拷贝。为了保证同一时刻最多只有 2 份 schema 生效,F1 约定了长度为数分钟的 schema 租约,所有 F1 服务器在租约到期后都要重新加载 schema 。如果节点无法重新完成续租,它将会自动终止服务并等待被集群管理设施重启。 49 | 50 | #### 中间状态 51 | 前面已经提过,F1 在 schema 变更的过程中,会把一次 schema 的变更拆解为多个逐步递进的中间状态。实际上我们并不需要针对每种 schema 变更单独设计中间状态,总共只需要两种就够了: *delete-only* 和 *write-only* 。 52 | 53 | *delete-only* 指的是 schema 元素的存在性只对删除操作可见。 54 | 55 | 例如当某索引处于 *delete-only* 状态时,F1 服务器中执行对应表的删除一行数据操作时能“看到”该索引,所以会同时删除该行对应的索引,与之相对的,如果是插入一行数据则“看不到”该索引,所以 F1 不会尝试新增该行对应的索引。 56 | 57 | 具体的,如果 schema 元素是 *table* 或 *column* ,该 schema 元素只对 *delete* 语句生效;如果 schema 元素是 *index* ,则只对 *delete* 和 *update* 语句生效,其中 *update* 语句修改 *index* 的过程可以认为是先 *delete* 后再 *insert* ,在 *delete-only* 状态时只处理其中的 *delete* 而忽略掉 *insert* 。 58 | 59 | 总之,只要某 schema 元素处于 *delete-only* 状态,F1 保证该 schema 元素对应的 kv 对总是能够被正确地删除,并且不会为此 schema 元素创建任何新的 kv 对。 60 | 61 | *write-only* 指的是 schema 元素对写操作可见,对读操作不可见。 62 | 63 | 例如当某索引处于 *write-only* 状态时,不论是 *insert* 、 *delete* ,或是 *update* ,F1 都保证正确的更新索引,只是对于查询来说该索引仍是不存在的。 64 | 65 | 简单的归纳下就是 *write-only* 状态的 schema 元素可写不可读。 66 | 67 | #### 示例推演 68 | Google 论文的叙述过程是描述完两种中间状态后就开始了冗长的形式化推导,最后得以证明按照特定的步骤来做 schema 演化是能保证一致性的。这里我想先拿出一个例子把 schema 变更的过程推演一遍,这样形成整体印象后更有助于看懂证明:)我们以添加索引为例,对应的完整 schema 演化过程如下: 69 | 70 | absent --> delete only --> write only --(reorg)--> public 71 | 72 | 其中 *delete-only* 和 *write-only* 是介绍过了的中间状态。 *absent* 指该索引完全不存在,也就是schema变更的初始状态。 *public* 自然对应变更完成后就状态,即索引可读可写,对所有操作可见。 73 | 74 | *reorg* 指 “database reorganization”,不是一种 schema 状态,而是发生在 *write-only* 状态之后的一系列操作。这些操作是为了保证在索引变为 *public* 之前所有旧数据的索引都被正确地生成。 75 | 76 | 根据之前的讨论,F1 中同时最多只可能有两份 schema 生效,我们逐个步骤来分析。 77 | 78 | 先看 *absent* 到 *delete-only* 。很显然这个过程中不会出现与此索引相关的任何数据。 79 | 80 | 再看 *delete-only* 到 *write-only* 。根据 *write-only* 的定义,一旦某节点进入 *write-only* 状态后,任何数据变更都会同时更新索引。当然,不可能所有节点同时进入 *write-only* 状态,但我们至少能保证没有节点还停留在 *absent* 状态, *delete-only* 或 *write-only* 状态的节点都能保证索引被正确清除。于是我们知道:从 *write-only* 状态发布时刻开始,数据库中不会存在多余的索引。 81 | 82 | 接下来是 *reorg* ,我们来考察 *reorg* 开始时数据库的状态。首先因为 *delete-only* 的存在,我们知道此时数据库中不存在多余的索引。另外此时不可能有节点还停留在 *delete-only* 状态,我们又知道从这时起,所有数据的变更都能正确地更新索引。所以 *reorg* 要做的就是取到当前时刻的snapshot,为每条数据补写对应的索引即可。当然 *reorg* 开始之后数据可能发生变更,这种情况下底层Spanner提供的一致性能保证 *reorg* 的写入操作要么失败(说明新数据已提前写入),要么被新数据覆盖。 83 | 84 | 基于前面的讨论,到 *reorg* 完成时,我们的数据不少不多也不错乱,可以放心地改为 *public* 状态了。 85 | 86 | #### 证明过程简介 87 | 这里简单介绍下证明过程,以理解为主,详细情况可自行查阅论文。 88 | 89 | 我们定义**数据库表示**为存储引擎中所有 kv 对的集合。**数据库表示 *d* 对于 schema *S* 是一致的**,当且仅当 90 | 91 | 1. *d* 中不存在多余数据。 92 | 2. *d* 中的数据是完整的。 93 | 94 | 其中不存在多余数据要求: 95 | 96 | 1. *d* 中的列数据或索引必须是 S 中定义过的列或索引。 97 | 2. *d* 中所有索引都指向合法的行。 98 | 3. *d* 中不存在未知数据。 99 | 100 | 数据的完整性要求: 101 | 102 | 1. *public* 状态的行或索引是完整的。 103 | 2. *public* 状态的约束是满足的。 104 | 105 | 我们要求正确实现所有 *delete, update, insert* 或 *query* 操作,保证其对于任何schema *S* ,都不破坏数据库表示的一致性。 106 | 107 | 我们定义**schema *S1* 至 *S2* 的变更过程是保持一致的**,当且仅当 108 | 109 | 1. 任何 *S1* 所定义的操作 *OPs1* 都保持数据库表示 *d* 对于 *S2* 的一致性。 110 | 2. 任何 *S2* 所定义的操作 *OPs2* 都保持数据库表示 *d* 对于 *S1* 的一致性。 111 | 112 | 这里看起来第 2 点是没必要的,但确实是必须的,因为 F1 中在变更发生的过程中 *S1* 和 *S2* 是会同时生效的。我们考虑为 *table* 添加一列 *optional* 列 *C* 的变更( *optional* 表示允许该列在数据库表示 *d* 中不存在,对应于 SQL 中未定义 *NOT NULL* 或有 *DEFAULT* 的情况)。首先, *S2* 定义的 *insert* 操作会写入 *C* 列的数据,其产生的数据库表示 *d'* 对 *S2* 是一致的,但对 *S1* 是不一致的(有多余数据)。现在如果发起由 *S1* 定义的 *delete* 操作, *C* 列对应的数据就不能被正确删除了。 113 | 114 | 显然根据定义,我们有如下推论:**schema *S1* 至 *S2* 的变更过程是保持一致的,当且仅当 *S2* 至 *S1* 的变更过程也是保持一致的。** 115 | 116 | 接下来我们可以得出如下结论:**任何从 schema *S1* 至 *S2* 的变更,如果其添加或删除了一项 *public* schema 元素 *E* ,那么此变更不能保持一致性。** 117 | 118 | 我们先考虑添加 *E* 的情况。不论 *E* 是 *table, column* 或 *index* ,由 *S2* 定义的 *insert* 都将插入 *S1* 未定义的数据,所以 *S1* 至 *S2* 的变更不能保持一致性。根据上面的“可逆”推论,删除的情况也得证。 119 | 120 | 接下来我们要证明:**任何从 schema *S1* 至 *S2* 的变更,如果其添加了一项 *delete-only* schema 元素 *E* ,那么此变更过程保持一致。** 121 | 122 | 因为 *S1* 和 *S2* 上定义的任何操作都不会为 *E* 创建数据,显然不会产生多余数据。又因为 *E* 不处于 *public* 状态,自然也不会破坏数据完整性。所以该变更保持一致性。 123 | 124 | 接下来我们要证明:**任何从 schema *S1* 至 *S2* 的变更,如果其将一项 *delete-only* 状态的 schema *optional* 元素 *E* 置为 *public* ,那么此变更过程保持一致。** 125 | 126 | 因为 *S1* 和 *S2* 上定义的 *delete* 操作都能正确地删除 *E* 所对应的 kv 对,不会产生多余数据。由于 *S1* 中 *E* 是 *delete-only* , *S1* 所定义的 *insert* 不会为 *E* 写入对应的数据,但是因为 *E* 是 *optional* 的,数据的缺失最终不会影响一致性。所以该变更过程保持一致。 127 | 128 | 到这里,我们就有了针对添加 *optional* schema 元素的完整变更方案: 129 | 130 | absent --> delete-only --> public 131 | 132 | 删除 schema 元素以及添加删除 *required* 元素的情况都是类似的推导过程,这里就不再赘述了,具体可参考论文。 133 | 134 | ## TiDB 的异步 schema 变更实现 135 | 136 | (待续) 137 | -------------------------------------------------------------------------------- /hlc/README.md: -------------------------------------------------------------------------------- 1 | # Hybrid Logical Clock(HLC) 2 | 3 | [参考论文](http://www.cse.buffalo.edu/tech-reports/2014-04.pdf) 4 | 5 | 如何解决分布式系统一致性快照(snapshot)的问题 6 | 7 | 8 | 9 | 基本概念和名词解释 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /howto/README.md: -------------------------------------------------------------------------------- 1 | # 如何参与 TiDB 开源项目 2 | 3 | [TiDB 简介及参与流程](程序媛的分布式数据库开发经验.pdf) 4 | -------------------------------------------------------------------------------- /howto/程序媛的分布式数据库开发经验.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/howto/程序媛的分布式数据库开发经验.pdf -------------------------------------------------------------------------------- /omid/README.md: -------------------------------------------------------------------------------- 1 | # Yahoo 的 omid 事务模型 2 | 3 | [系统架构](https://github.com/yahoo/omid/wiki/images/architecture.png) 4 | 5 | 基本概念和名词解释 6 | 7 | Timestamp Oracle (TO) 8 | 9 | The Server Oracle (TSO) 10 | 11 | Commit Table (CT) 12 | 13 | Shadow Cells (SCs) 14 | 15 | Transactional Clients (TCs) 16 | 17 | [操作流程](http://36.media.tumblr.com/3be4620a079c9733bba39d5d23774398/tumblr_inline_nxf4c9gjly1t17fny_500.png) 18 | 19 | 20 | 具体提交流程: 21 | 22 | ```java 23 | public long handleCommit(long startTimestamp, Iterable writeSet, boolean isRetry, Channel c) { 24 | boolean committed = false; 25 | long commitTimestamp = 0L; 26 | 27 | int numCellsInWriteset = 0; 28 | // 0. check if it should abort 29 | if (startTimestamp <= lowWatermark) { 30 | committed = false; 31 | } else { 32 | // 1. check the write-write conflicts 33 | committed = true; 34 | for (long cellId : writeSet) { 35 | long value = hashmap.getLatestWriteForCell(cellId); 36 | if (value != 0 && value >= startTimestamp) { 37 | committed = false; 38 | break; 39 | } 40 | numCellsInWriteset++; 41 | } 42 | } 43 | 44 | if (committed) { 45 | // 2. commit 46 | try { 47 | commitTimestamp = timestampOracle.next(); 48 | 49 | if (numCellsInWriteset > 0) { 50 | long newLowWatermark = lowWatermark; 51 | 52 | for (long r : writeSet) { 53 | long removed = hashmap.putLatestWriteForCell(r, commitTimestamp); 54 | newLowWatermark = Math.max(removed, newLowWatermark); 55 | } 56 | 57 | lowWatermark = newLowWatermark; 58 | LOG.trace("Setting new low Watermark to {}", newLowWatermark); 59 | persistProc.persistLowWatermark(newLowWatermark); 60 | } 61 | persistProc.persistCommit(startTimestamp, commitTimestamp, c); 62 | } catch (IOException e) { 63 | LOG.error("Error committing", e); 64 | } 65 | } else { // add it to the aborted list 66 | persistProc.persistAbort(startTimestamp, isRetry, c); 67 | } 68 | 69 | return commitTimestamp; 70 | } 71 | ``` 72 | 73 | 优化细节: 74 | 通过记录 key 的 hash 而不是 key 自身来减少存储空间 75 | 具体的 hash 算法是murmur3,见代码实现: 76 | 77 | ```java 78 | public long getCellId() { 79 | return Hashing.murmur3_128().newHasher() 80 | .putBytes(table.getTableName()) 81 | .putBytes(row) 82 | .putBytes(family) 83 | .putBytes(qualifier) 84 | .hash().asLong(); 85 | } 86 | ``` 87 | 88 | -------------------------------------------------------------------------------- /percolator/README.md: -------------------------------------------------------------------------------- 1 | # Google Percolator 的事务模型 2 | 3 | [参考论文](https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Peng.pdf) 4 | 5 | ## Percolator 简介 6 | 7 | Percolator是由Google公司开发的、为大数据集群进行增量处理更新的系统,主要用于google网页搜索索引服务。使用基于Percolator的增量处理系统代替原有的批处理索引系统后,Google在处理同样数据量的文档时,将文档的平均搜索延时降低了50%。 8 | 9 | Percolator的特点如下 10 | 11 | * 为增量处理定制 12 | * 处理结果强一致 13 | * 针对大数据量(小数据量用传统的数据库即可) 14 | 15 | 16 | 17 | Percolator为可扩展的增量处理提供了两个主要抽象: 18 | 19 | * 基于随机存取库的ACID事务 20 | * 观察者(observers)--一种用于处理增量计算的方式 21 | 22 | 23 | ## 事务 24 | 25 | Percolator 提供了跨行、跨表的、基于快照隔离的ACID事务。 26 | 27 | ### Snapshop isolation 28 | 29 | Percolator 使用Bigtable的时间戳记维度实现了数据的多版本化。多版本化保证了快照隔离`snapshot isolation`级别,优点如下: 30 | 31 | * 对读操作:使得每个读操作都能够从一个带时间戳的稳定快照获取。 32 | * 对写操作,能很好的应对写写冲突:若事务A和B并发去写一个同一个元素,最多只有一个会提交成功。 33 | 34 | ![s_1.jpg](imgs/s_1.jpg) 35 | 36 | 如图,基于快照隔离的事务,开始于一个开始时间戳`a start timestamp`(图内为小空格),结束于一个提交时间戳`a commit timestamp`(图内为小黑球)。本例包含以下信息: 37 | 38 | * 由于`Transaction 2`的开始时间戳`start timestamp`小于`Transaction 1`的提交时间戳`commit timestamp`,所以`Transaction 2` 不能看到 `Transaction 1` 的提交信息。 39 | * `Transaction 3` 可以看到`Transaction 2` 和 `Transaction 1` 的提交信息。 40 | * `Transaction 1` 和 `Transaction 2` 并发执行:如果它们对同一项进行写入,至少有一个会失败。 41 | 42 | 43 | #### Lock 44 | 45 | 46 | Percolator 的请求会直接反映到对Bigtable的修改上,由于Bigtable没有提供便捷的冲突解决和锁管理,所以Percolator需要独立实现一套锁管理机制。锁的管理必须满足以下条件: 47 | 48 | * 能直面机器故障:若一个锁在两阶段提交时消失,系统可能将两个有冲突的事务都提交。 49 | * 高吞吐量:上千台机器会同时请求获取锁。 50 | * 低延时:每个`Get()`操作都需要读取一次锁 51 | 52 | 故其锁服务在实现时,需要做到: 53 | 54 | * 多副本 ==> 应对故障 55 | * distributed&balance ==> handle load 56 | * 写入持久化存储系统 57 | 58 | BigTable能够满足以上所有要求。所以Percolator在实现时,将实际的数据存于Bigtable中。 59 | 60 | 61 | ### Columns in Bigtable 62 | 63 | Percolator在BigTable上抽象了五个`Columns`,其中三个跟事务相关,其定义如下 64 | 65 | #### Lock 66 | 67 | 事务产生的锁,未提交的事务会写本项,会包含`primary lock`的位置。其映射关系为 68 | 69 | ${key,start\_ts}=>{primary\_key,lock\_type,..etc}$ 70 | 71 | * ${key}$ 数据的key 72 | * ${start\_ts}$ 事务开始时间 73 | * ${primary\_key}$ 该事务的`primary`的引用. `primary`是在事务执行时,从待修改的`keys`中选择一个作为`primary`,其余的则作为`secondary`. 74 | 75 | 76 | #### Write 77 | 78 | 已提交的数据信息,存储数据所对应的时间戳。其映射关系为 79 | 80 | ${key,commit\_ts}=>{start\_ts}$ 81 | 82 | * ${key}$ 数据的key 83 | * ${commit\_ts}$ 事务的提交时间 84 | * ${start\_ts}$ 该事务的开始时间,指向该数据在`data`中的实际存储位置。 85 | 86 | 87 | #### Data 88 | 89 | 具体存储数据集,映射关系为 90 | 91 | ${key,start\_ts} => {value}$ 92 | 93 | * ${key}$ 真实的key 94 | * ${start\_ts}$ 对应事务的开始时间 95 | * ${value}$ 真实的数据值 96 | 97 | 98 | 99 | ### 案例 100 | 101 | 银行转账,Bob 向 Joe 转账7元。该事务于`start timestamp =7` 开始,`commit timestamp=8` 结束。具体过程如下: 102 | 103 | 1. 初始状态下,Bob的帐户下有10(首先查询`column write`获取最新时间戳数据,获取到`data@5`,然后从`column data`里面获取时间戳为`5`的数据,即`$10`),Joe的帐户下有2。![t_1.jpg](imgs/t_1.jpg) 104 | 2. 转账开始,使用`stat timestamp=7`作为当前事务的开始时间戳,将Bob选为本事务的`primary`,通过写入`Column Lock`锁定Bob的帐户,同时将数据`7:$3`写入到`Column,data`列。![t_2.jpg](imgs/t_2.jpg) 105 | 3. 同样的,使用`stat timestamp=7`,锁定Joe的帐户,并将Joe改变后的余额写入到`Column,data`,当前锁作为`secondary`并存储一个指向`primary`的引用(当失败时,能够快速定位到`primary`锁,并根据其状态异步清理)![t_3.jpg](imgs/t_3.jpg) 106 | 4. 事务带着当前时间戳`commit timestamp=8`进入commit阶段:删除primary所在的lock,并在write列中写入从提交时间戳指向数据存储的一个指针`commit_ts=>data @7`。至此,读请求过来时将看到Bob的余额为3。![t_4.jpg](imgs/t_4.jpg) 107 | 5. 依次在`secondary`项中写入`write`并清理锁,整个事务提交结束。在本例中,只有一个`secondary:Joe.`![t_5.jpg](imgs/t_5.jpg) 108 | 109 | 110 | ## 事务处理流程 111 | 112 | ### Prewrite 113 | 114 | ![prewrite.jpg](imgs/prewrite.jpg) 115 | 116 | Prewrite是事务两阶段提交的第一步,其从Oracle获取代表当前物理时间的全局唯一时间戳作为当前事务的start_ts,尝试对所有被写的元素加锁(为应对客户端故障,percolactor为所有需要写的key选出一个作为`primary`,其余的作为`secondary`.),将实际数据存入Bigtable。其中每个key的处理过程如下,中间出现失败,则整个`prewrite`失败: 117 | 118 | 1. 检查`write-write`冲突:从`BigTable`的write列中获取当前`key`的最新数据,若其`commit_ts`大于等于`start_ts`,说明存在更新版本的已提交事务,返回`WriteConflict`错误,结束。 119 | 2. 检查`key`是否已被锁上,如果`key`的锁已存在,返回`KeyIsLock`的错误,结束 120 | 5. 往BigTable的`lock`列写入`lock(start_ts,key,primary)`为当前key加锁,若当前key被选为`primary`,则标记为`primary`,若为`secondary`,则标明指向`primary`的信息 121 | 6. 往BigTable的`data`列写入的数据`data(key,start_ts,value)`。 122 | 123 | 124 | ### Commit 125 | 126 | ![commit.jpg](imgs/commit.jpg) 127 | 128 | `Prewrite`成功后,该事务可能会被提交从而进入第二阶段--`Commit` 129 | 同样的,在事务提交的开始阶段,将从Oracle获取时间戳作为`commit_ts`代表事务的真正提交时间。然后,将依次对各个key进行提交(第一个为parimary),释放锁,打上提交标识。每个key的提交过程具体如下: 130 | 131 | 1. 从Bigtable获取key的lock,检查其合法性,若非法,则返回失败 132 | 2. 将`commit_ts`,当前数据在`data`列中的位置,等其它相关信息写入`write`列。 133 | 3. 将当前key所在的`lock` 从 Bigtable的`lock`列删除,至此,该key的锁被释放。 134 | 135 | `write` 记录着key的提交记录,当客户端读取一个`key`时,会从write表中读取`key`所对应数据在`data`列中的存储位置,然后从`data`列中读取真正的数据。同时,一旦`primary` 被提交成功后,整个事务对外就算提交成功了。 136 | 137 | 138 | ### Get 139 | 140 | ![get.jpg](imgs/get.jpg) 141 | 142 | 1. `Get` 操作首先检查[0,start_ts]时间区间内`Lock`是否存在,若存在,则返回错误 143 | 2. 如果不存在有冲突的`Lock`,则获取在write中合法的最新提交记录指向的在data中的位置 144 | 3. 根据步骤2获取的内容,从`data`中获取到相应的数据并返回。 145 | 146 | 147 | ### 异常处理(异步清理锁) 148 | 149 | 若客户端在`Commit`一个事务时,出现了异常,`Prepare` 时产生的锁会被留下。为避免将新事务`hang`住,Percolator必须清理这些锁。 150 | 151 | Percolator用`lazy`方式处理这些锁:当事务A在执行时,发现事务B造成的锁冲突,事务A将决定事务B是否失败,以及清理事务B的那些锁。 152 | 对事务A而言,能准确地判断事务B是否成功是关键。Percolator为每个事务设计了一个元素`cell`作为事务是否成功的同步标准,该元素产生的`lock`即为`primary lock`。A和B事务都能确定事务B的`primary lock`(因为这个`primary lock`被写入了B事务其它所有涉及元素的`lock`里面)。执行一个清理`clean up`或者提交`commit`操作需要修改该`primary lock`,由于这些修改是基于Bigtable去做,所以只有一个清理或提交会成功。注意: 153 | 154 | * 在B提交`commit`之前,它会先确保其`primary lock`被`write record`所替代(即往`primary`的`write`写提交数据,并删除对应的`lock`)。 155 | * 在A清理B的锁之前,A必须检查B的`primary`以确保B未被提交,如果B的`primary`存在,则B的锁可以被安全的清理掉。 156 | 157 | ![rollback.jpg](imgs/rollback.jpg) 158 | 159 | 当客户端在执行两阶段提交的`commit`阶段失败时,事务依旧会留下一个提交点`commit point`(至少一条记录会被写入`write`中),但可能会留下一些`lock`未被处理掉。一个事务能够从其`primary lock`中获取到执行情况: 160 | 161 | * 如果`primary lock` 已被`write`所替代,也就是说该事务已被提交,事务需要`roll forword`,也就是对所有涉及到的、未完成提交的数据,用`write`记录替代标准的锁`standard lock`。 162 | * 如果`primary lock`存在,事务被`roll back`(因为我们总是最先提交`primary`,所以`primary`未被提交时,可以安全地执行回滚) 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /percolator/imgs/commit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/percolator/imgs/commit.jpg -------------------------------------------------------------------------------- /percolator/imgs/get.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/percolator/imgs/get.jpg -------------------------------------------------------------------------------- /percolator/imgs/prewrite.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/percolator/imgs/prewrite.jpg -------------------------------------------------------------------------------- /percolator/imgs/rollback.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/percolator/imgs/rollback.jpg -------------------------------------------------------------------------------- /percolator/imgs/s_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/percolator/imgs/s_1.jpg -------------------------------------------------------------------------------- /percolator/imgs/t_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/percolator/imgs/t_1.jpg -------------------------------------------------------------------------------- /percolator/imgs/t_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/percolator/imgs/t_2.jpg -------------------------------------------------------------------------------- /percolator/imgs/t_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/percolator/imgs/t_3.jpg -------------------------------------------------------------------------------- /percolator/imgs/t_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/percolator/imgs/t_4.jpg -------------------------------------------------------------------------------- /percolator/imgs/t_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/percolator/imgs/t_5.jpg -------------------------------------------------------------------------------- /spanner/README.md: -------------------------------------------------------------------------------- 1 | # Google spanner 2 | 3 | [参考论文](http://static.googleusercontent.com/media/research.google.com/zh-CN//archive/spanner-osdi2012.pdf) 4 | 5 | Spanner 的特性 6 | 7 | 8 | 9 | 基本概念和名词解释 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tidb/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/tidb/architecture.png -------------------------------------------------------------------------------- /tidb/builtin.md: -------------------------------------------------------------------------------- 1 | 本文档用于描述如何为 TiDB 新增 builtin 函数。首先介绍一些必需的背景知识,然后介绍增加 builtin 函数的流程,最后会以一个函数作为示例。 2 | 3 | ### **背景知识** 4 | 5 | SQL 语句在 TiDB 中是如何执行的? 6 | 7 | SQL 语句首先会经过 parser,从文本 parse 成为 AST(抽象语法树),通过 optimizer 生成执行计划,得到一个可以执行的 plan,通过执行这个 plan 即可得到结果,这期间会涉及到如何获取 table 中的数据,如何对数据进行过滤、计算、排序、聚合、滤重等操作。对于一个 builtin 函数,比较重要的是 parse 和如何求值。这里着重说这两部分。 8 | 9 | #### Parse 10 | 11 | TiDB 语法解析的代码在 parser 目录下,主要涉及 misc.go 和 parser.y 两个文件。在 TiDB 项目中运行 make parser 会通过 goyacc 将 parser.y 其转换为 parser.go 代码文件。转换后的 go 代码,可以被其他的 go 代码调用,执行 parse 操作。 12 | 13 | 将 sql 语句从文本 parse 成结构化 AST 有如下过程。首先是通过 scanner 将文本切分为 tokens,每个 token 会有 name 和 value,其中 name 在 parser 中用于匹配预定义的规则(规则在 parser.y 中定义)。匹配规则时,parser 不断的从 scanner 中获取 token (通过调用 `Scanner.Lex` 方法),这一过程称为**移进**(shift);当 parser 发现能完整匹配上一条规则时,会将匹配上的 tokens 替换为一个新的变量,这一过程称为**归约**(reduce)。同时,在每条规则匹配成功后,可以用 tokens 的 value,构造ast 中的节点或者是 subtree。对于 builtin 函数来说,一般的形式为 name(args),scanner 中要识别 function 的 name、括号、参数等元素;对于匹配预定义规则输入,parser 会构造出一个 ast 的 node,这个 node 中包含函数参数、函数求值的方法,用于后续的求值。 14 | 15 | #### 求值 16 | 17 | 求值过程是根据输入的参数,以及运行时环境,求出函数或者表达式的值。求值的控制逻辑 expression 包中。对于大部分 builtin 函数,在 parse 过程中被解析为 `ast.FuncCallExpr`,求值时首先将 `FuncCallExpr` 转换成 `expression.ScalarFunction`,这时会调用 `NewFunction` 方法 (expression/scalar_function.go),通过 `FuncCallExpr.FnName` 在 `funcs` 表(expression/builtin.go)中找到对应的函数实现类(`functionClass`),并通过 `functionClass.getFunction` 获得函数实现(`builtinFunc`),最后在对 `ScalarFunction` 求值时会调用 `builtinFunc.eval` 完成函数求值。 18 | 19 | ### **添加 builtin 函数整体流程** 20 | 21 | 1. 修改 parser/misc.go ,在 `tokenMap` 中添加函数名到 token code 的映射,其中 token code 是生成的变量,在 parser/parser.y 中通过 %token 声明,由 goyacc 自动生成; 22 | 2. 修改 parser/parser.y : 23 | * 用 %token 声明函数 24 | * 根据函数名性质 (请查阅 [mysql 文档](https://dev.mysql.com/doc/refman/5.7/en/keywords.html)),将其添加到 UnReservedKeyword 或 ReservedKeyword 或 NotKeywordToken 规则中 25 | * 在合适位置添加函数解析规则 26 | 3. 在 parser_test.go 中,添加对应函数的单元测试; 27 | 4. 修改 ast/functions.go ,定义相应函数名常量,供后续代码引用; 28 | 5. 在 expression 包中实现函数,注意函数实现按函数类别分了几个文件,比如时间相关的函数在 expression/builtin\_time.go,函数实现简要说明如下: 29 | * 定义相应函数类(`functionClass`),实现 `getFunction` 方法 30 | * 定义函数签名,其应实现 `builtinFunc` 接口,通过实现 `eval` 方法来完成函数逻辑 31 | * 在 expression/builtin_xxx_test.go 中添加对应函数的单元测试 32 | * 将函数名及其实现类注册到 `builtin.funcs` 中,这里函数名用到了第4步定义的常量 33 | 6. 在 typeinferer 中添加类型推导信息,请保持函数结果类型和 MySQL 的结果一致,全部类型定义参见 [MySQL Const](https://github.com/pingcap/tidb/blob/master/mysql/type.go#L17) 34 | * 在 plan/typeinferer.go 中的 `handleFuncCallExpr` 里面添加这个函数的返回结果类型 35 | * 在 plan/typeinferer_test.go 中添加相应测试 36 | 7. 运行 `make dev`,确保所有的 test case 都能跑过 37 | 38 | ### **示例** 39 | 40 | 这里以新增 [UTC\_TIMESTAMP](https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_utc-timestamp) 支持的 [PR](https://github.com/pingcap/tidb/pull/2592) 为例,进行详细说明 41 | 42 | 1. 首先看 [parser/misc.go](https://github.com/pingcap/tidb/pull/2592/files#diff-2680bef19a08b7dc1c3a74194be5d0f6): 43 | 44 | 在 `tokenMap` 中添加一个 entry 45 | ```go 46 | var tokenMap = map[string]int{ 47 | ... 48 | "UTC_TIMESTAMP": utcTimestamp, 49 | ... 50 | } 51 | ``` 52 | 53 | 这里是定义了一个从文本 'UTC\_TIMESTAMP' 到 token code 的映射关系,token code 的由常量 `utcTimestamp` 指定,其值是 goyacc 自动生成的。SQL 对大小不敏感,`tokenMap` 里面统一用大写。 54 | 55 | 对于 `tokenMap` 这张表里面的文本,不要被当作 identifier,而是作为一个特别的token。接下来在 parser 规则中,需要对这个 token 进行特殊处理。 56 | 57 | 2. 看 [parser/parser.y](https://github.com/pingcap/tidb/pull/2592/files#diff-5dbf5cc474f4f1eed5fa9e9796760002): 58 | 59 | ``` 60 | %token 61 | ... 62 | utcTimestamp "UTC_TIMESTAMP" 63 | ... 64 | ``` 65 | 66 | 这行的意思是,声明一个叫 `utcTimestamp` 的 token,在 parser 调用 lexer 的 `Lex` 方法时,可能会返回这个 token code,供 parser 识别。此外,我们还给他起了个别名叫 "UTC\_TIMESTAMP"。有 yacc/bison 使用经验的同学可能会好奇为何 `utcTimestamp` 后还跟了字符串,这是 goyacc 支持的语法,名为 literal string,可以在后续规则中替代 `utcTimestamp` 使用,具体说明可以参见[该文档](https://godoc.org/github.com/cznic/y#hdr-LiteralString_field)。 67 | 68 | 这里的 `utcTimestamp` 就是 `tokenMap` 里面的那个 `utcTimestamp`,当 parser.y 生成 parser.go 的时,将会生成一个名为 被赋予一个名为 `utcTimestamp` 的 int 常量,即 token code。 69 | 70 | 在查阅[文档](https://dev.mysql.com/doc/refman/5.7/en/keywords.html)后得知 UTC_TIMESTAMP 是 MySQL 的保留字,因此我们将其加到 `ReservedKeyword` 规则下。最后,添加该函数解析规则,由于其不是关键字,因此在 `FunctionCallNonKeyword` 下添加如下规则: 71 | ``` 72 | | "UTC_TIMESTAMP" FuncDatetimePrec 73 | { 74 | args := []ast.ExprNode{} 75 | if $2 != nil { 76 | args = append(args, $2.(ast.ExprNode)) 77 | } 78 | $$ = &ast.FuncCallExpr{FnName: model.NewCIStr($1), Args: args} 79 | } 80 | ``` 81 | 82 | 这里的意思是,当 scanner 输出的 token 序列满足这种 pattern 时,我们将这些 tokens 规约为一个新的变量,叫 `FunctionCallNonKeyword` (通过给$$变量赋值,即可给 `FunctionCallNonKeyword` 赋值),也就是一个 AST 中的 node,类型为 *ast.FuncCallExpr。其成员变量 FnName 的值为 `model.NewCIStr($1)`,其中 `$1` 在生成代码中将被替换成第一个 token (也就是 `utcTimestamp`)的 value。值得一提的是,这里使用了 utcTimestamp 这个 token 的 literal string,倒不是说 token 的 value 就是 "UTC_TIMESTAMP" 这个字符串,其真实的字由 lexer 决定,保存在 `ident string` 这个 union 字段下(因为我们之前声明 `utcTimestamp` 的‘类型’为 ``)。 83 | 84 | 至此我们的parser已经能成功的将文本 "utc\_timestamp()" 转换成为一个 AST node,其成员 `FnName` 记录了函数名 "utc\_timestamp",我们可以通过这个函数名在后面的 `funcs` 找到函数具体的实现类。 85 | 86 | 最后在补充一下yacc规则的基础知识:如果想要在规则处理代码中引用这个规则中某个 token 的值,可以用 $x 这种方式,其中 x 为 token 在规则中的位置,如上面的规则中,$1 为 utcTimestamp,$2 为 FuncDatetimePrec 。$2.(ast.ExprNode) 的意思是引用第2个位置上的 token 的值,并断言其值为 `ast.ExprNode` 类型。 87 | 88 | 3. 在 [parser/parser_test.go](https://github.com/pingcap/tidb/pull/2592/files#diff-27c45ca411f005e1b8796b12fb53e26c) 添加测试代码 89 | 90 | 以上步骤完成后,如果没有文法冲突,你应该可以通过 `make parser` 生成 parser.go 了,快进入 parser 目录执行 `go test` 测试你的成果吧! 91 | 92 | 4. 参考 [ast/functions.go](https://github.com/pingcap/tidb/pull/2592/files#diff-ade136ede78b393a9e9538c6b7008e02) 为函数名定义一个常量 93 | 94 | 5. 在 expression 完成对函数逻辑的代码实现: 95 | 96 | 在 [expression/builtin_time.go](https://github.com/pingcap/tidb/pull/2592/files#diff-d61eef12d314ca7514bc1960312ba5e4) 中实现函数相关类型,实现代码大致如下: 97 | 98 | ```go 99 | type utcTimestampFunctionClass struct { 100 | baseFunctionClass 101 | } 102 | 103 | func (c *utcTimestampFunctionClass) getFunction(args []Expression, ctx context.Context) (builtinFunc, error) { 104 | return &builtinUTCTimestampSig{newBaseBuiltinFunc(args, ctx)}, errors.Trace(c.verifyArgs(args)) 105 | } 106 | 107 | type builtinUTCTimestampSig struct { 108 | baseBuiltinFunc 109 | } 110 | 111 | // See https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_utc-timestamp 112 | func (b *builtinUTCTimestampSig) eval(row []types.Datum) (d types.Datum, err error) { 113 | args, err := b.evalArgs(row) 114 | if err != nil { 115 | return types.Datum{}, errors.Trace(err) 116 | } 117 | 118 | fsp := 0 119 | sc := b.ctx.GetSessionVars().StmtCtx 120 | if len(args) == 1 && !args[0].IsNull() { 121 | if fsp, err = checkFsp(sc, args[0]); err != nil { 122 | return d, errors.Trace(err) 123 | } 124 | } 125 | 126 | t, err := convertTimeToMysqlTime(time.Now().UTC(), fsp) 127 | if err != nil { 128 | return d, errors.Trace(err) 129 | } 130 | 131 | d.SetMysqlTime(t) 132 | return d, nil 133 | } 134 | ``` 135 | 136 | 其中,`utcTimestampFunctionClass` 实现了 `functionClass` 接口,`builtinUTCTimestampSig` 实现了 `builtinFunc` 接口,其求值过程在上文背景知识中已经介绍过了,在此不再赘述。 137 | 138 | 实现了函数后,需要将其注册到 [expression/builtin.go](https://github.com/pingcap/tidb/pull/2592/files#diff-cdbd511e3e5f2bbe0a3c5c173d3938c2) 中的 `funcs` 表里,代码如下: 139 | ```go 140 | ast.UTCTimestamp: &utcTimestampFunctionClass{baseFunctionClass{ast.UnixTimestamp, 0, 1}}, 141 | ``` 142 | 143 | 意思是,我们可以通过 `ast.UTCTimestamp` 这个函数名找到函数的具体实现,其实现是一个 `functionClass`,也就是我们刚才实现的。构造这个 `functionClass` 时,我们传入了一函数基类,其参数含义是:这个函数的函数名是 `ast.UTCTimestamp`,它接受0到1个参数。我们也可能需要实现一个能接受任意多个参数的函数,此时可将 `baseFunctionClass` 最后一个字段设为-1。 144 | 145 | 测试很重要,赶快在 [expression/builtin_time_test.go](https://github.com/pingcap/tidb/pull/2592/files#diff-9efa9861dfe0962aeeda87c63deb0f0f) 添加测试吧! 146 | 147 | 6. 在 [plan/typeinferer.go](https://github.com/pingcap/tidb/pull/2592/files#diff-686e374c1af6ac094dcaed1db4f0fd44) 实现对函数结果的类型推导,代码如下: 148 | 149 | ```go 150 | case "now", "sysdate", "current_timestamp", "utc_timestamp": 151 | tp = types.NewFieldType(mysql.TypeDatetime) 152 | tp.Decimal = v.getFsp(x) 153 | ``` 154 | 155 | 意思是这个函数将返回一个 'DATETIME' 类型的结果。如果不确定函数返回类型,一个小窍门/笨办法是:通过 mysql workbench 连接 mysql 数据库,执行 `select utc_timestamp` 看看 :) 156 | 157 | 目前,函数类型推导和函数实现还是分开的,略有不便,关于这点 pingcap 的小伙伴正在积极改善中,敬请期待!不过当务之急,还是先在添加 [plan/typeinferer_test.go](https://github.com/pingcap/tidb/pull/2592/files#diff-6425b785337ec89d2604eb16f63caac6) 添加测试吧! 158 | 159 | 7. 至此,一个 builtin 函数已经大功告成,运行 `make dev` 通过所以测试,就可以向 [TiDB](https://github.com/pingcap/tidb) 提 PR 了! 160 | -------------------------------------------------------------------------------- /tidb/builtin_new.md: -------------------------------------------------------------------------------- 1 | # **十分钟成为 TiDB Contributor 系列 | 添加內建函数** 2 | 3 | 最近我们对 TiDB 代码做了些改进,大幅度简化了添加內建函数的流程,这篇教程描述如何为 TiDB 新增 builtin 函数。首先介绍一些必需的背景知识,然后介绍增加 builtin 函数的流程,最后会以一个函数作为示例。 4 | 5 | ### **背景知识** 6 | 7 | SQL 语句发送到 TiDB 后首先会经过 parser,从文本 parse 成为 AST(抽象语法树),通过 Query Optimizer 生成执行计划,得到一个可以执行的 plan,通过执行这个 plan 即可得到结果,这期间会涉及到如何获取 table 中的数据,如何对数据进行过滤、计算、排序、聚合、滤重以及如何对表达式进行求值。 8 | 对于一个 builtin 函数,比较重要的是进行语法解析以及如何求值。其中语法解析部分需要了解如何写 yacc 以及如何修改 TiDB 的词法解析器,较为繁琐,我们已经将这部分工作提前做好,大多数 builtin 函数的语法解析工作已经做完。 9 | 对 builtin 函数的求值需要在 TiDB 的表达式求值框架下完成,每个 builtin 函数被认为是一个表达式,用一个 ScalarFunction 来表示,每个 builtin 函数通过其函数名以及参数,获取对应的函数类型以及函数签名,然后通过函数签名进行求值。 10 | 总体而言,上述流程对于不熟悉 TiDB 的朋友而言比较复杂,我们对这部分做了些工作,将一些流程性、较为繁琐的工作做了统一处理,目前已经将大多数未实现的 buitlin 函数的语法解析以及寻找函数签名的工作完成,但是函数实现部分留空。***换句话说,只要找到留空的函数实现,将其补充完整,即可作为一个 PR。*** 11 | 12 | ### **添加 builtin 函数整体流程** 13 | 14 | * 找到未实现的函数 15 | 在 TiDB 源码中的 expression 目录下搜索 `errFunctionNotExists`,即可找到所有未实现的函数,从中选择一个感兴趣的函数,比如 SHA2 函数: 16 | ``` 17 | func (b *builtinSHA2Sig) eval(row []types.Datum) (d types.Datum, err error) { 18 | return d, errFunctionNotExists.GenByArgs("SHA2") 19 | } 20 | ``` 21 | 22 | * 实现函数签名 23 | 接下来要做的事情就是实现 eval 方法,函数的功能请参考 MySQL 文档,具体的实现方法可以参考目前已经实现函数。 24 | 25 | * 在 typeinferer 中添加类型推导信息 26 | 在 plan/typeinferer.go 中的 handleFuncCallExpr() 里面添加这个函数的返回结果类型,请保持和 MySQL 的结果一致。全部类型定义参见 [MySQL Const](https://github.com/pingcap/tidb/blob/master/mysql/type.go#L17)。 27 | ``` 28 | * 注意大多数函数除了需要填写返回值类型之外,还需要获取返回值的长度。 29 | ``` 30 | 31 | * 写单元测试 32 | 在 expression 目录下,为函数的实现增加单元测试,同时也要在 plan/typeinferer_test.go 文件中添加 typeinferer 的单元测试 33 | 34 | * 运行 make dev,确保所有的 test case 都能跑过 35 | 36 | ### **示例** 37 | 38 | 这里以[新增 SHA1() 函数的 PR](https://github.com/pingcap/tidb/pull/2781/files) 为例,进行详细说明 39 | 首先看 `expression/builtin_encryption.go`: 40 | 将 SHA1() 的求值方法补充完整 41 | ``` 42 | func (b *builtinSHA1Sig) eval(row []types.Datum) (d types.Datum, err error) { 43 | // 首先对参数进行求值,这块一般不用修改 44 | args, err := b.evalArgs(row) 45 | if err != nil { 46 | return types.Datum{}, errors.Trace(err) 47 | } 48 | // 每个参数的意义请参考 MySQL 文档 49 | // SHA/SHA1 function only accept 1 parameter 50 | arg := args[0] 51 | if arg.IsNull() { 52 | return d, nil 53 | } 54 | // 这里对参数值做了一个类型转换,函数的实现请参考 util/types/datum.go 55 | bin, err := arg.ToBytes() 56 | if err != nil { 57 | return d, errors.Trace(err) 58 | } 59 | hasher := sha1.New() 60 | hasher.Write(bin) 61 | data := fmt.Sprintf("%x", hasher.Sum(nil)) 62 | // 设置返回值 63 | d.SetString(data) 64 | return d, nil 65 | } 66 | ``` 67 | 接下来给函数实现添加单元测试,参见 `expression/builtin_encryption_test.go`: 68 | ``` 69 | var shaCases = []struct { 70 | origin interface{} 71 | crypt string 72 | }{ 73 | {"test", "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"}, 74 | {"c4pt0r", "034923dcabf099fc4c8917c0ab91ffcd4c2578a6"}, 75 | {"pingcap", "73bf9ef43a44f42e2ea2894d62f0917af149a006"}, 76 | {"foobar", "8843d7f92416211de9ebb963ff4ce28125932878"}, 77 | {1024, "128351137a9c47206c4507dcf2e6fbeeca3a9079"}, 78 | {123.45, "22f8b438ad7e89300b51d88684f3f0b9fa1d7a32"}, 79 | } 80 | 81 | func (s *testEvaluatorSuite) TestShaEncrypt(c *C) { 82 | defer testleak.AfterTest(c)() // 监测 goroutine 泄漏的工具,可以直接照搬 83 | fc := funcs[ast.SHA] 84 | for _, test := range shaCases { 85 | in := types.NewDatum(test.origin) 86 | f, _ := fc.getFunction(datumsToConstants([]types.Datum{in}), s.ctx) 87 | crypt, err := f.eval(nil) 88 | c.Assert(err, IsNil) 89 | res, err := crypt.ToString() 90 | c.Assert(err, IsNil) 91 | c.Assert(res, Equals, test.crypt) 92 | } 93 | // test NULL input for sha 94 | var argNull types.Datum 95 | f, _ := fc.getFunction(datumsToConstants([]types.Datum{argNull}), s.ctx) 96 | crypt, err := f.eval(nil) 97 | c.Assert(err, IsNil) 98 | c.Assert(crypt.IsNull(), IsTrue) 99 | } 100 | * 注意,除了正常 case 之外,最好能添加一些异常的case,如输入值为 nil,或者是多种类型的参数 101 | ``` 102 | 最后还需要添加类型推导信息以及 test case,参见 `plan/typeinferer.go`,`plan/typeinferer_test.go`: 103 | ``` 104 | case ast.SHA, ast.SHA1: 105 | tp = types.NewFieldType(mysql.TypeVarString) 106 | chs = v.defaultCharset 107 | tp.Flen = 40 108 | ``` 109 | ``` 110 | {`sha1(123)`, mysql.TypeVarString, "utf8"}, 111 | {`sha(123)`, mysql.TypeVarString, "utf8"}, 112 | ``` 113 | 114 | 115 | 编辑按:添加 TiDB Robot 微信,加入 TiDB Contributor Club,无门槛参与开源项目,改变世界从这里开始吧(萌萌哒)。 116 | 117 | ![TiDB Robot](tidb_bot.png) -------------------------------------------------------------------------------- /tidb/dist_sql_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/tidb/dist_sql_example.png -------------------------------------------------------------------------------- /tidb/process_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/tidb/process_flow.png -------------------------------------------------------------------------------- /tidb/protocol_layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/tidb/protocol_layer.png -------------------------------------------------------------------------------- /tidb/sourcecode.md: -------------------------------------------------------------------------------- 1 | # TiDB 源码介绍 2 | 3 | 这篇文章是一篇入门文档,难度系数比较低,其中部分内容可能大家在其他渠道已经看过,不过为了内容完整性,我还是会放在这里。 4 | 5 | ## TiDB 架构 6 | ![](architecture.png "TiDB Architecture") 7 | 8 | 本次 TiDB 源码之旅从这幅简单的架构图开始,这幅图很多人都看过,我们可以用一句话来描述这个图:『TiDB 是一个支持 MySQL 协议,以某种支持事务的分布式 KV 存储引擎为底层存储的 SQL 引擎』。从这句话可以看出有三个重要的事情,第一是如何支持 MySQL 协议,与 Client 交互,第二是如何与底层的存储引擎打交道,存取数据,第三是如何实现 SQL 的功能。本篇文章会先介绍一些 TiDB 有哪些模块及其功能简要介绍,然后以这三点为线索,将这些模块串联起来。 9 | 10 | ## 代码简介 11 | TiDB 源码完全托管在 Github 上,从[项目主页](https://github.com/pingcap/tidb "项目主页")可以看到所有信息。整个项目使用 Go 语言开发,按照功能模块分了很多 Package,通过一些依赖分析工具,可以看到项目内部包之间的依赖关系。 12 | 大部分包都以接口的形式对外提供服务,大部分功能也都集中在某个包中,不过有一些包提供了非常基础的功能,会被很多包依赖,这些包需要特别注意。 13 | 项目的 main 文件在 tidb-server/main.go,这里面定义了服务如何启动。整个项目的 Build 方法可以在 [Makefile](https://github.com/pingcap/tidb/blob/source-code/Makefile#L140) 中找到。 14 | 除了代码之外,还有很多测试用例,可以在 xx\_test.go 中找到。另外 `cmd` 目录下面还有几个工具包,用来做性能测试或者是构造测试数据。 15 | 16 | ## 模块介绍 17 | TiDB 的模块非常多,这里做一个整体介绍,大家可以看到每个模块大致是做什么用的,想看相关功能的代码是,可以直接找到对应的模块。 18 | 19 | | Package | Introduction | 20 | | ------------- | :-------------: | 21 | | ast | 抽象语法树的数据结构定义,例如 `SelectStmt` 定义了一条 Select 语句被解析成什么样的数据结构 | 22 | | cmd/benchdb | 简单的 benchmark 工具,用于性能优化 | 23 | | cmd/benchfilesort | 简单的 benchmark 工具,用于性能优化 | 24 | | cmd/benchkv | Transactional KV API benchmark 工具,也可以看做 KV 接口的使用样例 | 25 | | cmd/benchraw | Raw KV API benchmark 工具,也可以看做不带事务的 KV 接口的使用样例 | 26 | | cmd/importer | 根据表结构以及统计信息伪造数据的工具,用于构造测试数据 | 27 | | config | 配置文件相关逻辑 | 28 | | context | 主要包括 Context 接口,提供一些基本的功能抽象,很多包以及函数都会依赖于这个接口,把这些功能抽象为接口是为了解决包之间的依赖关系 | 29 | | ddl | DDL 的执行逻辑 | 30 | | distsql | 对分布式计算接口的抽象,通过这个包把 Executor 和 TiKV Client 之间的逻辑做隔离 | 31 | | domain | domain 可以认为是一个存储空间的抽象,可以在其中创建数据库、创建表,不同的 domain 之间,可以存在相同名称的数据库,有点像 Name Space。一般来说单个 TiDB 实例只会创建一个 Domain 实例,其中会持有 information schema 信息、统计信息等。 | 32 | | executor | 执行器相关逻辑,可以认为大部分语句的执行逻辑都在这里,比较杂,后面会专门介绍 | 33 | | expression | 表达式相关逻辑,包括各种运算符、内建函数 | 34 | | expression/aggregation | 聚合表达式相关的逻辑,比如 Sum、Count 等函数 | 35 | | infoschema | SQL 元信息管理模块,另外对于 Information Schema 的操作,都会访问这里 | 36 | | kv | KV 引擎接口以及一些公用方法,底层的存储引擎需要实现这个包中定义的接口 | 37 | | meta | 利用 structure 包提供的功能,管理存储引擎中存储的 SQL 元信息,infoschema/DDL 利用这个模块访问或者修改 SQL 元信息 | 38 | | meta/autoid | 用于生成全局唯一自增 ID 的模块,除了用于给每个表的自增 ID 之外,还用于生成 | 39 | | metrics | Metrics 相关信息,所有的模块的 Metrics 信息都在这里 | 40 | | model | SQL 元信息数据结构,包括 DBInfo / TableInfo / ColumnInfo / IndexInfo 等 | 41 | | mysql | MySQL 相关的常量定义 | 42 | | owner | TiDB 集群中的一些任务只能有一个实例执行,比如异步 Schema 变更,这个模块用于多个 tidb-server 之间协调产生一个任务执行者。每种任务都会产生自己的执行者。 | 43 | | parser | 语法解析模块,主要包括词法解析 (lexer.go) 和语法解析 (parser.y),这个包对外的主要接口是 Parse(),用于将 SQL 文本解析成 AST | 44 | | parser/goyacc | 对 GoYacc 的包装 | 45 | | parser/opcode | 关于操作符的一些常量定义 | 46 | | perfschema | Performance Schema 相关的功能,默认不会启用 | 47 | | plan | 查询优化相关的逻辑 | 48 | | privilege | 用户权限管理接口| 49 | | privilege/privileges | 用户权限管理功能实现 | 50 | | server | MySQL 协议以及 Session 管理相关逻辑 | 51 | | sessionctx/binloginfo | 向 Binlog 模块输出 Binlog 信息 | 52 | | sessionctx/stmtctx | Session 中的语句运行时所需要的信息,比较杂 | 53 | | sessionctx/variable | System Variable 相关代码 | 54 | | statistics | 统计信息模块 | 55 | | store | 储存引擎相关逻辑,这里是存储引擎和 SQL 层之间的交互逻辑 | 56 | | store/mockoracle | 模拟 TSO 组件 | 57 | | store/mockstore | 实例化一个 Mock TiKV 的逻辑,主要方法是 NewMockTikvStore,把这部分逻辑从 mocktikv 中抽出来是避免循环依赖 | 58 | | store/mockstore/mocktikv | 在单机存储引擎上模拟 TiKV 的一些行为,主要作用是本地调试、构造单元测试以及指导 TiKV 开发 Coprocessor 相关逻辑 | 59 | | store/tikv | TiKV 的 Go 语言 Client | 60 | | store/tikv/gcworker | TiKV GC 相关逻辑,tidb-server 会根据配置的策略向 TiKV 发送 GC 命令 | 61 | | store/tikv/oracle | TSO 服务接口 | 62 | | store/tikv/oracle/oracles | TSO 服务的 Client | 63 | | store/tikv/tikvrpc | TiKV API 的一些常量定义 | 64 | | structure | 在 Transactional KV API 上定义的一层结构化 API,提供 List/Queue/HashMap 等结构 | 65 | | table | 对 SQL 的 Table 的抽象| 66 | | table/tables | 对 table 包中定义的接口的实现 | 67 | | tablecodec | SQL 到 Key-Value 的编解码,每种数据类型的具体编解码方案见 `codec` 包 | 68 | | terror | TiDB 的 error 封装 | 69 | | tidb-server | 服务的 main 方法 | 70 | | types | 所有和类型相关的逻辑,包括一些类型的定义、对类型的操作等 | 71 | | types/json | json 类型相关的逻辑 | 72 | | util | 一些实用工具,这个目录下面包很多,这里只会介绍几个重要的包 | 73 | | util/admin | TiDB 的管理语句( `Admin` 语句)用到的一些方法 | 74 | | util/charset | 字符集相关逻辑 | 75 | | util/chunk | Chunk 是 TiDB 1.1 版本引入的一种数据表示结构。一个 Chunk 中存储了若干行数据,在进行 SQL 计算时,数据是以 Chunk 为单位在各个模块之间流动 | 76 | | util/codec | 各种数据类型的编解码 | 77 | | x-server | X-Protocol 实现| 78 | ## 从哪里入手 79 | 粗看一下 TiDB 有 80 个包,让人觉得无从下手,不过并不是所有的包都很重要,另外一些功能只会涉及到少量包,从哪里入手去看源码取决于看源码的目的。 80 | 如果是想了解某一个具体的功能的实现细节,那么可以参考上面的模块简介,找到对应的模块即可。 81 | 如果是相对源码有全面的了解,那么可以从 tidb-server/main.go 入手,看 tidb-server 是如何启动,如何等待并处理用户请求。再跟着代码一直走,看 SQL 的具体执行过程。另外一些重要的模块,需要看一下,知道是如何实现的。辅助性的模块,可以选择性的看一下,有大致的印象即可。 82 | 83 | ## 重要模块 84 | 在全部 80 个模块中,下面几个模块是最重要的,希望大家能仔细阅读,针对这些模块,我们也会用专门的文章来讲解,等所有的文章都 Ready 后,我将下面的表格中的 TODO 换成对应的文章连链接。 85 | 86 | | Package | Related Articles | 87 | | ------------- | ------------- | 88 | | plan | TODO | 89 | | expression | TODO | 90 | | executor | TODO | 91 | | distsql | TODO | 92 | | store/tikv | TODO | 93 | | ddl | TODO | 94 | | tablecodec | TODO | 95 | | server | TODO | 96 | | types | TODO | 97 | | kv | TODO | 98 | | tidb | TODO | 99 | 100 | ## 辅助模块 101 | 除了重要的模块之外,余下的是辅助模块,但并不是说这些模块不重要,只是锁这些模块并不在 SQL 执行的关键路径上,我们也会用一定的篇幅描述其中的大部分包。 102 | 103 | ## SQL 层架构 104 | ![](tidb-core.png "TiDB SQL Layer") 105 | 这幅图比上一幅图详细很多,大体描述了 SQL 核心模块,大家可以从左边开始,顺着箭头的方向看。 106 | 107 | ### Protocol Layer 108 | 最左边是 TiDB 的 Protocol Layer,这里是与 Client 交互的接口,目前 TiDB 只支持 MySQL 协议,相关的代码都在 `server` 包中。 109 | 这一层的主要功能是管理客户端 connection,解析 MySQL 命令并返回执行结果。具体的实现是按照 MySQL 协议实现,具体的协议可以参考 [MySQL 协议文档](https://dev.mysql.com/doc/internals/en/client-server-protocol.html)。这个模块我们认为是当前实现最好的一个 MySQL 协议组件,如果大家的项目中需要用到 MySQL 协议解析、处理的功能,可以参考或引用这个模块。 110 | 111 | 连接建立的逻辑在 server.go 的 [Run()](https://github.com/pingcap/tidb/blob/source-code/server/server.go#L236) 方法中,主要是下面两行: 112 | > 236: conn, err := s.listener.Accept() 113 | > 258: go s.onConn(conn) 114 | 115 | 单个 Session 处理命令的入口方法是调用 clientConn 类的 [dispatch 方法](https://github.com/pingcap/tidb/blob/source-code/server/conn.go#L465),这里会解析协议并转给不同的处理函数。 116 | 117 | ### SQL Layer 118 | 大体上讲,一条 SQL 语句需要经过,语法解析--\>合法性验证--\>制定查询计划--\>优化查询计划--\>根据计划生成查询器--\>执行并返回结果 等一系列流程。这个主干对应于 TiDB 的下列包: 119 | 120 | | Package | 作用 | 121 | | ------------- | ------------- | 122 | | tidb | Protocol 层和 SQL 层之间的接口| 123 | | parser | 语法解析 | 124 | | plan | 合法性验证 + 制定查询计划 + 优化查询计划 | 125 | | executor | 执行器生成以及执行 | 126 | | distsql | 通过 TiKV Client 向 TiKV 发送以及汇总返回结果 | 127 | | store/tikv | TiKV Client | 128 | 129 | ### KV API Layer 130 | TiDB 依赖于底层的存储引擎提供数据的存取功能,但是并不是依赖于特定的存储引擎(比如 TiKV),而是对存储引擎提出一些要求,满足这些要求的引擎都能使用(其中 TiKV 是最合适的一款)。 131 | 最基本的要求是『带事务的 Key-Value 引擎,且提供 Go 语言的 Driver』,再高级一点的要求是『支持分布式计算接口』,这样 TiDB 可以把一些计算请求下推到 存储引擎上进行。 132 | 这些要求都可以在 `kv` 这个包的[接口](https://github.com/pingcap/tidb/blob/source-code/kv/kv.go)中找到,存储引擎需要提供实现了这些接口的 Go 语言 Driver,然后 TiDB 利用这些接口操作底层数据。 133 | 对于最基本的要求,可以重点看这几个接口: 134 | * [Transaction](https://github.com/pingcap/tidb/blob/source-code/kv/kv.go#L121):事务基本操作 135 | * [Retriever ](https://github.com/pingcap/tidb/blob/source-code/kv/kv.go#L75):读取数据的接口 136 | * [Mutator](https://github.com/pingcap/tidb/blob/source-code/kv/kv.go#L91):修改数据的接口 137 | * [Storage](https://github.com/pingcap/tidb/blob/source-code/kv/kv.go#L229):Driver 提供的基本功能 138 | * [Snapshot](https://github.com/pingcap/tidb/blob/source-code/kv/kv.go#L214):在数据 Snapshot 上面的操作 139 | * [Iterator](https://github.com/pingcap/tidb/blob/source-code/kv/kv.go#L255):`Seek` 返回的结果,可以用于遍历数据 140 | 141 | 有了上面这些接口,可以对数据做各种所需要的操作,完成全部 SQL 功能,但是为了更高效的进行运算,我们还定义了一个高级计算接口,可以关注这三个 Interfce/struct : 142 | * [Client](https://github.com/pingcap/tidb/blob/source-code/kv/kv.go#L150):向下层发送请求以及获取下层存储引擎的计算能力 143 | * [Request](https://github.com/pingcap/tidb/blob/source-code/kv/kv.go#L176): 请求的内容 144 | * [Response](https://github.com/pingcap/tidb/blob/source-code/kv/kv.go#L204): 返回结果的抽象 145 | 146 | ### 小结 147 | 至此,读者已经来了解了 TiDB 的源码结构以及三个主要部分的架构,更详细的内容会在后面的章节中详细描述。 148 | 149 | -------------------------------------------------------------------------------- /tidb/storage.md: -------------------------------------------------------------------------------- 1 | # TiDB 存储引擎接入指南 2 | 3 | ## 简介 4 | 5 | TiDB 在架构上被设计成分层的结构,SQL 逻辑层和 KV 存储层被划分得比较清楚。而且 SQL 层只依赖于抽象的接口而不依赖于存储引擎的具体实现,这保证了我们可以比较方便地为 TiDB 接入新的存储引擎,当然存储引擎需要满足一定的条件并正确实现相应的接口。 6 | 7 | 在 TiDB 的当前版本中,我们实现了 `localstore` 和 `hbase` 两套存储引擎。`kv/kv.go` 文件定义了 Store 所需要实现的抽象接口;`store/localstore` 实现了一份单机存储引擎;`store/hbase` 则依托于 hbase 实现了一份分布式存储引擎。 8 | 9 | ## 接入步骤 10 | 11 | ### 1. 实现 Driver 12 | 13 | `kv/kv.go` 中定义的 Driver 接口如下: 14 | ```go 15 | // Driver is the interface that must be implemented by a KV storage. 16 | type Driver interface { 17 | // Open returns a new Storage. 18 | // The path is the string for storage specific format. 19 | Open(path string) (Storage, error) 20 | } 21 | ``` 22 | 新引擎需要实现一份 Driver 并通过 `tidb.RegisterStore()` 进行注册,注册之后就可以通过 `tidb.NewStore()` 或 `tidb.Open()` 创建存储引擎或数据库连接了。 23 | 24 | path 约定使用形如 `engine://path[?param=value&...]` 的 URL。`Driver.Open()` 的实现负责解析 path 并创建对应的存储引擎实例,可参考 `store/hbase/kv.go` 中的实现。 25 | 26 | ### 2. 实现 Storage 27 | 28 | `kv/kv.go` 中定义的 Storage 原型如下: 29 | ```go 30 | // Storage defines the interface for storage. 31 | // Isolation should be at least SI(SNAPSHOT ISOLATION) 32 | type Storage interface { 33 | // Begin transaction 34 | Begin() (Transaction, error) 35 | // GetSnapshot gets a snapshot that is able to read any data which data is <= ver. 36 | // if ver is MaxVersion or > current max committed version, we will use current version for this snapshot. 37 | GetSnapshot(ver Version) (Snapshot, error) 38 | // Close store 39 | Close() error 40 | // Storage's unique ID 41 | UUID() string 42 | // CurrentVersion returns current max committed version. 43 | CurrentVersion() (Version, error) 44 | } 45 | ``` 46 | 47 | 注意 TiDB 要求 Storage 的隔离性应当至少是 [SI(Snapshot Isolation)](https://en.wikipedia.org/wiki/Snapshot_isolation) 级别的,这意味着所有的读写操作的结果与事务开始时刻息息相关。 48 | 49 | TiDB 中我们用严格递增的版本 `Version` 来标识不同事务的时序,在 hbase store 的实现里使用了全局时间戳服务来保证这一点,细节可参考 [github.com/ngaut/tso](https://github.com/ngaut/tso)。 50 | 51 | 具体的,`Begin()` 用于创建一次数据库读写事务(事务使用数据库当前版本标识时序),`GetSnapshot()` 创建指定版本的只读快照,`UUID()` 返回能唯一标识数据库的 string,`CurrentVersion()` 返回数据库当前时刻的版本。 52 | 53 | ## 3. 实现 Snapshot 54 | 55 | Snapshot 用于处理只读事务,`kv/kv.go` 中定义了相关接口如下: 56 | 57 | ```go 58 | type Key []byte 59 | 60 | // Retriever is the interface wraps the basic Get and Seek methods. 61 | type Retriever interface { 62 | // Get gets the value for key k from kv store. 63 | // If corresponding kv pair does not exist, it returns nil and ErrNotExist. 64 | Get(k Key) ([]byte, error) 65 | // Seek creates an Iterator positioned on the first entry that k <= entry's key. 66 | // If such entry is not found, it returns an invalid Iterator with no error. 67 | // The Iterator must be Closed after use. 68 | Seek(k Key) (Iterator, error) 69 | } 70 | 71 | // Iterator is the interface for a iterator on KV store. 72 | type Iterator interface { 73 | Valid() bool 74 | Key() Key 75 | Value() []byte 76 | Next() error 77 | Close() 78 | } 79 | 80 | // Snapshot defines the interface for the snapshot fetched from KV store. 81 | type Snapshot interface { 82 | Retriever 83 | // BatchGet gets a batch of values from snapshot. 84 | BatchGet(keys []Key) (map[string][]byte, error) 85 | // Release releases the snapshot to store. 86 | Release() 87 | } 88 | ``` 89 | 90 | 其中 `Get()` 和 `Seek()` 提供了最基本的 KV 读取操作,`Iterator` 用于遍历,`BatchGet()` 用于批量读取。如果对应的引擎没有针对批量读操作的优化,`BatchGet()` 接口可以简单地实现为多次 `Get()` 操作。 91 | 92 | ## 4. 实现 Transaction 93 | 94 | Transaction 用于处理读写事务,`kv/kv.go` 中定义了相关接口如下: 95 | 96 | ```go 97 | // Mutator is the interface wraps the basic Set and Delete methods. 98 | type Mutator interface { 99 | // Set sets the value for key k as v into kv store. 100 | // v must NOT be nil or empty, otherwise it returns ErrCannotSetNilValue. 101 | Set(k Key, v []byte) error 102 | // Delete removes the entry for key k from kv store. 103 | Delete(k Key) error 104 | } 105 | 106 | // Transaction defines the interface for operations inside a Transaction. 107 | // This is not thread safe. 108 | type Transaction interface { 109 | RetrieverMutator 110 | // Commit commits the transaction operations to KV store. 111 | Commit() error 112 | // Rollback undoes the transaction operations to KV store. 113 | Rollback() error 114 | // String implements fmt.Stringer interface. 115 | String() string 116 | // LockKeys tries to lock the entries with the keys in KV store. 117 | LockKeys(keys ...Key) error 118 | // SetOption sets an option with a value, when val is nil, uses the default 119 | // value of this option. 120 | SetOption(opt Option, val interface{}) 121 | // DelOption deletes an option. 122 | DelOption(opt Option) 123 | } 124 | ``` 125 | 126 | 其中 `Set()` 和 `Delete()` 定义了基本的 KV 写操作;`Commit()` 和 `Rollback()` 用于事务的提交和回滚;`String()` 返回事务的唯一标识,已有实现中我们直接使用了事务起始版本号;`LockKeys()` 用于加锁指定的 Key,以保证当前事务提交之前不会有其他事务修改相关数据;`SetOption()` 和 `DelOption()` 用于设置一些性能优化选项,存储引擎可根据需要实现。 127 | 128 | TiDB 中 `store/localstore` 和 `store/hbase` 的实现都使用了“乐观锁”,即大部分情况下冲突的检测在事务提交阶段进行,如果提交时发生可重试的事务冲突,SQL 层会根据情况选择进行重试。`kv/union_store.go` 中的 `UnionStore` 提供了与之相关的写缓冲、条件检测等相关组件。 129 | 130 | ## 测试 131 | 132 | 使用 interpreter 做简单测试。`tidb/interpreter` 目录中有一份简单的交互式 SQL 环境,编译后运行 `./interpreter -store myengine -path mypath`,即可进行简单的 SQL 语句测试,注意需要略微修改代码保证自定义 Driver 被成功注册。 133 | 134 | 使用 tidb-server 测试。`tidb/tidb-server` 目录中实现了 MYSQL 兼容的服务器,编译后运行 `./tidb-server -store myengine -path mypath -L error`,启动成功后可使用 MYSQL 客户端进行测试,注意需要略微修改代码保证自定义 Driver 被成功注册。 135 | 136 | 使用 Driver 测试。在新的 Go 工程中使用 `tidb.Open()` 创建数据库连接后进行 SQL 操作。 137 | 138 | 运行 TiDB 提供的测试用例。`store/store_test.go` 文件中有一些针对 Storage 功能性及一致性的测试,可使用 `go test -teststore myengine -testpath mypath` 进行测试,注意需要略微修改代码保证自定义 Driver 被成功注册。 -------------------------------------------------------------------------------- /tidb/tidb-core.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/tidb/tidb-core.png -------------------------------------------------------------------------------- /tidb/tidb_bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ngaut/builddatabase/ab1d6b174035c88c8fb9a524349d031221564125/tidb/tidb_bot.png --------------------------------------------------------------------------------