1. 为什么读TiDB源码,最先要做的是制定计划
1.1 一上来就翻源码仓库,绝大多数人都会在半路放弃
TiDB源码学习这件事,我身边动过念头的人不少,真正啃下来的却不多。多数人的路径惊人地相似:先找到一个入口,然后随机点开某个文件,读着读着发现依赖链路越来越长,今天追了一晚上只有一个模糊印象,明天接着翻又找不到昨天的状态。这样折腾两三周,别说掌握整体架构,连自己读过什么、还有什么没读都说不清楚,最后只能默默放弃。
出现这个结果,问题不在于智商,也不在于资料太少,而在于一开始缺少一份靠谱的计划。读TiDB源码不是读一本从第1页翻到第300页的技术书,它是一个庞大的、分布式的代码体系,几个核心仓库加起来是百万行甚至几百万行的量级。如果没有计划,你天然会被细节吞没。TiDB的特点决定了阅读路线必须取舍,源码里既有核心的SQL执行、事务处理、节点调度,也有大量的错误处理、测试代码、兼容性逻辑。没有计划的人会把时间花在那些不算关键的分支上,有计划的读者会先走通主线,再往细枝末节延伸。
1.2 一份计划解决的不只是时间问题,更是认知负荷问题
我给自己的定义是:一份源码学习计划,本质上是一条知识地图、一套精力分配规则,以及一组反馈机制。知识地图告诉你模块之间是什么关系,先后长什么顺序;精力分配规则告诉你哪些代码必须细读、哪些扫一眼即可;反馈机制则确保你每周都能看到产出,不会陷入"读了但感觉什么都没得到"的状态。
具体来说,计划能解决四个核心痛点。第一个是迷路问题。TiDB源码中的函数调用链非常深,一条简单的SQL从客户端走到TiKV,中间要穿过多个进程和多层抽象。你跟着代码走一遍,如果没有地图,很容易在某个包里面绕晕。第二个是时间失控问题。学习源码最容易出现的情况是"今天本来只想看一个函数,结果被某个底层细节带走了一整天"。计划里给每个模块限定了时间上限,能有效防止这类时间黑洞。第三个是正反馈缺失问题。读代码本身是输入,如果只有输入没有输出,大脑很快就会倦怠。计划必须给每个阶段配上输出任务,比如画出时序图、写一篇笔记、复现某段逻辑。第四个是深度和广度的取舍问题。TIDB源码那么多包,谁值得逐行读、谁应该跳过,这本身就需要提前决策,不能靠当天心情。
所以说,制定计划不是形式主义。它是整个TiDB源码学习过程中性价比最高的一步,花两三天时间做出来的路线,足够支撑你后面一两个月的阅读不跑偏。
2. 先看清TiDB源码全貌,再动笔写计划
2.1 TiDB不是单仓库:核心组件与仓库对应关系
制定计划之前,第一步是搞清楚你要读的对象到底有哪些。很多初学者以为从TiDB仓库进去就万事大吉了,其实TiDB是一个多仓库组成的分层架构,每个仓库承担完全不同的角色,用的编程语言也不相同。
从整体架构来看,一条查询请求的完整路径要经过以下几类组件。
| 仓库 | 主要语言 | 核心角色 | 最值得关注的内容 |
|---|---|---|---|
| TiDB | Go | SQL层计算引擎 | 解析、优化、执行器、事务入口 |
| TiKV | Rust | 分布式存储与事务执行引擎 | 分布式事务、MVCC、Raft复制、RocksDB集成 |
| PD | Go | 集群控制面与调度中心 | TSO时间戳、Region调度、负载均衡决策 |
| TiFlash | C++ | 分析型列存引擎 | 列式存储、MPP计算、与TiKV的数据同步 |
如果只是入门,我建议先把精力放在前三个仓库上。TiFlash是HTAP方向的列存引擎,属于进阶内容,在初始计划里不必强行加入。
这些仓库之间的关系可以类比成"大脑、身体和指挥中枢":TiDB是大脑,负责思考怎么消费用户请求;TiKV是身体,负责实际存取数据;PD是调度中枢,负责告诉整个集群数据放哪里、怎么切换。实际学习时,很多人习惯从TiDB仓库开始,因为它面向用户、问题具象,比如"为什么这条SQL慢"可以很快沿着日志追到具体模块。TiKV则是更底层的硬骨头,适合在SQL层主线打通之后再深入。
2.2 语言、代码量与难度评估,直接影响时间安排
在制定计划时,你需要对自己的编程语言掌握情况做一次诚实的评估。TiDB主仓库使用Go,整体语法门槛不算高,但对接口、并发、goroutine调度这些概念要有基础了解。TiKV使用Rust,生命周期、所有权、trait约束随处可见,如果你之前没有Rust实践经验,直接把TiKV作为第一站会非常痛苦。
从代码量上看,TiDB源码规模在百行万级别,TiKV也在百万行级别。这里说的百万行不是让你全部读完,而是提醒你必须有取舍。真正需要精读的代码可能只占20%到30%,其余大量代码属于辅助逻辑、边界处理、测试基建。经验法则是:核心链路反复精读,外围功能按需查询,测试代码当文档看,历史兼容逻辑遇到再说。
难度评估也要分层看待。SQL层的代码逻辑相对线性,parser解析、优化器改写、执行器调用,每一步都可以单独拉出来讲清楚,比较适合作为源码阅读的起点。但如果一上来就扎进TiKV的raftstore模块,你要同时面对Raft协议实现、异步批量处理、并发冲突窗口等一系列复杂问题,很容易产生挫败感。所以我的建议是,计划里一定要设计一条先易后难的路线,把最难的TiKV放到SQL层有了一定手感之后。
2.3 不同学习目标,计划切入点差别很大
读TiDB源码不是只有一种读法。同样花一个半月,目标不同,计划内容会完全不同。
如果你的目标是快速理解TiDB整体架构,那么主线应该是"一条SQL的完整生命周期"。具体路径是:客户端发送SQL -> TiDB的session层处理 -> parser生成语法树 -> planner做逻辑优化和物理优化 -> executor执行 -> distsql拆分任务 -> TiKV的coprocessor扫描数据。这条链路走通后,你就掌握了TiDB最重要的模块分工。
如果你的目标是深入SQL优化器,那重点要放在planner包上。这个方向需要对关系代数、代价估算有基础知识,阅读时主要关注的函数是逻辑算子转换、物理算子选择、统计信息利用。这部分单纯看代码不容易理解,最好配合几条典型SQL去对比执行计划的变化。
如果你的目标是分布式事务,切入点应从TiDB session层的事务管理开始,然后顺着两阶段提交走到TiKV底层的transaction模块。重点包括事务状态机、冲突检测、Percolator模型在TiKV里怎么实现、快照隔离级别怎么保证。
如果你的目标是Raft共识或者分布式调度,那就要去TiKV的raftstore和PD的schedule模块了。这类方向更底层,需要的前置知识更多,计划周期也要相应拉长。
把目标写在计划第一页,是一件看似朴素但非常有效的事情。后续所有模块排序、时间分配、输出形式,都由这个目标推导出来。
3. 制定TiDB源码学习计划的五个关键步骤
3.1 明确目标:通读架构还是深挖某条链路
很多人制定计划时最容易犯的错误是目标过于宏大。比如写一句"三个月内精通TiDB源码",这个目标既无法量化,也没有边界。真正可执行的计划必须精确到"我能在哪一周、通过读哪些文件、理解哪个问题"。
我给初学者推荐的目标格式是"链路线"而非"全文线"。例如:"通过30天时间,从TiDB主仓库入口开始,完整追读一条INSERT语句的执行链,并最终梳理成一份包含模块分工和核心数据结构的图文笔记"。这个目标有明确的起始点、终止点、检验指标。相比之下,"把TiDB源码读完"这种目标注定没法检验,也没法指导你每天的阅读内容。
如果你已经有数据库领域经验,目标可以定得更深一些,比如"定位一条查询在优化器中的cost计算过程,并找出三个可优化的点"。这个目标已经带有研究性质,计划周期往往要按两个月到三个月的尺度来设计。总之一句话:目标写不清楚,后面的模块切分基本就是瞎切。
3.2 按依赖关系切分模块,确定阅读顺序
TiDB源码模块之间不是孤立的,阅读顺序应该尽量顺应依赖关系。我是按三层来切分的。
第一层是基础理论层,不需要直接读源码,但必须垫底。包括:Raft共识协议的基本概念、MVCC和快照隔离的基本思路、RocksDB的LSM树存储原理。这一层不读的话,后面看TiKV时很多设计会看不懂原因。
第二层是架构干线层,主要读TiDB和PD的Go代码。可以先读TiDB,因为SQL层有很多概念与普通单机数据库相通,入口多、反馈快。PD的代码相比TiDB更偏控制流,如果你的主线是SQL层,PD可以先跳到与TSO、Region分裂相关的那几个函数。
第三层是深度深潜层,集中攻坚TiKV。TiKV是Rust代码,阅读速度会比Go慢不少,所以计划里要给足缓冲。如果时间有限,可以先重点读两个子模块:一个负责Raft复制,一个负责事务与MVCC。这两块是理解分布式数据库最核心的难度担当。
模块切分完成后,还要给模块排一个依赖拓扑。比如不理解Region的基本概念,就读不懂PD的调度逻辑;不理解MVCC的版本链,就读不懂TiKV事务的快照实现。这份依赖顺序会天然让你形成"先SQL层、再PD调度、最后TiKV深潜"的路径。
3.3 给每个模块设计输出闭环
读源码和刷视频不一样,它很难给你即时的反馈。如果只读不输出,人的记忆曲线会把你两周前读到的细节全部抹平。我在制定计划时,会给每个模块配置至少一类输出。
输出形式可以有三种。第一种是画图,比如画出一次事务提交的时序图,画出优化器从AST生成执行计划的流程图。这种输出强迫你把碎片化的代码调用串成一条有因果关系的链条。第二种是写笔记,但不要写流水账,最好采用"问题-证据-结论"的结构。比如问题是"TiDB什么时候决定把任务下推到TiKV",证据是代码里哪个函数、哪个判断条件,结论是你自己总结的一句话。第三种是做实验验证,比如改一行代码让某个逻辑分支触发,或者通过日志观察到某个行为。
把输出任务写进计划后,你会发现每天阅读的终点变得很明确。我不会在"读完一个函数"就停下来,而是会问自己"这一个函数对整条链路有什么影响,我能不能用自己的话表达出来"。这种带着产出目的的阅读,效率比纯浏览高出很多。
3.4 用两倍原则估算时间,设立里程碑
时间估算是最容易被低估的部分。很多人看代码时会高估自己的速度,特别是读TiKV这种带并发和异步逻辑的代码时,一个小时可能只啃下几十行。
我的经验是使用两倍原则:先按理想状态估算每个模块需要的工作日,再把这个数字乘以2。比如你预计用3天看懂SQL优化器的基本框架,计划里就写6天。这样做的好处是给意外留了缓冲,不会因为某几天卡壳就全盘崩溃。
在时间轴上,我建议设立三个里程碑。第一个里程碑是环境跑通,包括编译成功、本地集群启动、能发起一条SQL并看到日志。第二个里程碑是主线走通,大概读懂一条查询从客户端到存储的完整路径。第三个里程碑是一个模块深潜,能说出某个关键子模块的核心数据结构、主要流程和常见问题排查方式。每个里程碑内部还可以细分周目标。里程碑的意义在于让你能清楚看到自己在向前推进,而不是永远停在"了解中"的模糊地带。
3.5 配置源码阅读环境与工具链
计划里还要包含环境准备,这一步看似基础,实际能卡住很多人。TiDB主仓库用Go开发,官方要求特定版本的Go工具链,直接按仓库里的说明安装即可。TiKV使用Rust,组件多、编译时间也长,建议提前配好rustup工具链和CMake等依赖。如果只是为了读代码,不一定要完整编译TiKV,但至少要用rust-analyzer让IDE能把符号索引起来,不然查找函数跳转会非常痛苦。
本地不用部署完整的生产集群,用tiup playground命令就能拉起一个mini集群,这是官方提供的本地开发环境工具,用来跟踪调试足够方便。想观察一条SQL在TiDB和TiKV中的具体行为,可以在配置里打开日志级别,通过日志里的时间戳与模块名定位调用位置。
调试工具方面,Go代码用dlv调试器比较直接,可以在函数入口下断点,查看变量在不同层的传递过程。TiKV是Rust代码,调试相对重一些,入门阶段主要靠日志和单元测试。IDE选择两个主流方案:一是GoLand加Rust插件,二是VS Code加Go插件和rust-analyzer。我个人偏好VS Code,因为在一个窗口里既能看Go,也能看Rust,切换成本最低。
工具链的目的不是好看,而是放大你的阅读能力。有了全局符号跳转、交叉引用、断点调试,你才能真正做到顺着调用链往下追,而不会被搜索的繁琐拖慢节奏。
4. 实操示例:30天源码入门计划完整拆分
4.1 第1到7天:环境准备与快速跑通
拿一份具体的30天计划来举例,应该可以帮助你更好地理解前面那些原则怎么落地。这个计划面向的目标读者是:有Go基础、能看懂简单Rust代码、想在一个月内建立TiDB源码全局认识的开发者。
第1天到第7天是环境准备期,产出标准是"本地能跑能调,日志能看"。第1天做仓库clone,把TiDB、TiKV、PD三个仓库都拉下来,并且大致浏览每个仓库的README和目录结构,先在脑袋里建立一份目录地图。第2天到第3天只做TiDB和PD的编译,因为Go编译相对快,能建立起正反馈。TiKV先不急着编译,用IDE索引起来即可,避免第一天就陷入Rust编译漫长的泥潭。第4天用tiup playground拉一个本地集群跑起来,然后用mysql客户端连接,随便执行几条SQL。你的目标不是使用数据库,而是找到集群日志,观察一条SQL执行时日志里出现了哪些关键节点。第5天到第7天,我建议做的事是结合日志,把"一条SELECT语句经过TiDB哪些模块、有没有下推到TiKV、TiKV有没有返回结果"这个过程勾出一个粗糙的草图。这个草图不用准,它只是给后续两周的细读提供一个坐标。
4.2 第8到21天:啃下SQL执行与事务提交两条链路
第8天到第21天是主线攻坚期,分两条链路来读。
第一条链路是SQL执行链路,从第8天到第14天。推荐的阅读顺序是:先找到session层处理一条查询的入口函数,然后看parser怎么生成语法树。之后进planner,理解逻辑优化和物理优化大概发生在哪几个函数里,不用抠到每个优化规则,知道CBO的整体流程即可。再进executor,看每个执行算子怎么被构建和启动的。最后看distsql层,理解任务怎么被拆分、下发到TiKV。这条链路走完,你已经能把一条查询的全过程用语言复述出来。
第二条链路是事务提交链路,从第15天到第21天。可以选择一条INSERT语句作为样本,跟踪它在TiDB侧如何开启事务、如何进入两阶段提交的状态、如何与TiKV协调。然后去TiKV侧重点读transaction相关代码,理解每条事务的key在执行时怎么加锁、怎么处理冲突、怎么通过Raft日志同步落盘。读到这里,你会真正理解分布式事务为什么要用两阶段提交,以及网络异常时可能发生什么。
第21天的收尾建议是:整合两条链路,画一张完整的"INSERT原子写流程"时序图,图上的每个节点都要对应到具体的文件和函数名。这张图就是前期计划的第一个核心产出。
4.3 第22到30天:选定一个模块深入阅读并产出一篇分析
最后一周做模块深潜,目的是让你体验一把"从看到一个点,到挖出一条线"的研究过程。
我的建议是选一个你最感兴趣的话题。如果你在之前的阅读里对优化器产生了兴趣,可以选"代价估算函数到底怎么计算选择率";如果你对事务更感兴趣,可以选"锁冲突检测在什么时机触发,睡眠重试多久"。关键在于规模要小,宁可选择单个问题,也不要试图覆盖整个模块。
在这8天里,你可以采用一种"三遍法"的读法。第一遍用半天找到相关函数,画出调用关系,理解输入输出是什么。第二遍花一两天把关键数据结构和注释吃透,运行相关单元测试确认你的理解是对的。第三遍把你在主线阅读中积累的疑问拿出来,对照代码逐条求证,然后写成一篇有结构、有引用、有图的主题笔记。
周末时,把这篇文章作为你这30天学习的交付物。它不一定要提交到任何公开平台,但内容必须达到"你下周重看时不靠代码也能回忆起核心结论"的程度。如果你有精力,还可以把这个话题整理成一个技术分享讲给同事听,讲不出来或者被问住的地方,就是你下次改进的方向。
5. TiDB源码学习计划中的避坑指南
5.1 常见问题速查表
我把自己和周围人踩过的坑整理成了一张速查表,希望你在制定和执行计划时能对照检查。
| 现象 | 深层原因 | 对策 |
|---|---|---|
| 读了几天源码,完全找不到东西南北 | 没有绘制模块地图就开读 | 第1周先产出目录地图和粗糙草图 |
| 某个细节研究了半天,主线被丢掉 | 缺少时间盒约束 | 给每个模块设时间上限,时间到就切换 |
| 编译TiKV花了两天,还没开始读代码 | 目标外成本太高 | 入门阶段只索引TiKV,用日志和单测代替编译 |
| Rust代码看不懂、进度极慢 | 前置语言基础不足 | 先从TiDB和PD入手,TiKV放到第二个计划周期 |
| 读懂了但不会讲,缺乏表达出口 | 一直没有输出任务 | 每个模块完成后写笔记或画图,强制输出 |
| 总是问"这个功能在哪个文件" | 工具链没有充分利用 | 配好IDE符号索引,用全局搜索和调用关系追踪 |
| 笔记变成代码搬运,没有自己的观点 | 写作结构不清晰 | 统一采用"问题-证据-结论"段落结构 |
这张表解决的是执行层面的多数困扰。实际上,源码学习的过程中,多数问题都不是能力问题,而是方法和流程问题,提前做预判能省去很多试错时间。
5.2 几个让我学习效率翻倍的细节习惯
在计划执行过程中,有几个细节习惯我把它们称为"隐形杠杆"。它们看起来不起眼,实际对效率的影响非常大。
第一,读代码之前先读测试。每个模块的测试函数会直接展示出这个功能期望的输入和输出,相当于给了你答案再让你看推理过程。先去test目录抓行为,再回到代码里找实现,理解成本会显著降低。第二,善用git log和代码注释的历史版本。很多设计决策在注释里写得很克制,但在提交信息里反而更直白。看一个关键函数时,用git log找到它最早被加进来的那次提交,通常能看到当时的背景与设计取舍。第三,带着"我要改它"的心理去读代码。阅读者与审阅者的注意力分布完全不同,当你思考"怎么给这段逻辑加一个日志开关"时,你会更容易注意边界状态和条件分支。第四,维护一份疑问清单。遇到看不懂的先记下来,不要当场死磕,隔两天再回来,往往在读了其他模块后豁然开朗。
我个人在实际操作中的体会是,源码学习计划最关键的环节不在于计划有多完美,而在于它能不能让你每周都能前进一步。哪怕某几天进度慢了,只要里程碑还在、输出还在累积,你就不会陷入"白学了"的焦虑。TiDB源码的复杂度是明摆着的,但把地图画好、把顺序排对、把反馈建起来,它就是一本可以慢慢啃的大书。如果你准备开始,这周就把克隆仓库和目录浏览做完,先把第一个草图画出来再说。