TIDB分布式数据库提交设想

1. 核心基石:全局唯一的时间戳 (TSO)

分布式事务最难的是确定"谁先谁后"。TiDB 不依赖物理时钟(因为服务器时间可能不同步),而是通过 PD (Placement Driver) 组件提供一个逻辑上的、单调递增的全局时间戳 (TSO)。

  • 作用 :每个事务在开始时,都会从 PD 获取一个 start_ts(开始时间戳);在提交时,会获取一个 commit_ts(提交时间戳)。
  • 意义:这保证了整个集群所有事务的串行化顺序是全局一致的(类似 Percolator 模型)。

2. 写流程:两阶段提交 (2PC) + 乐观锁

当你执行一个 UPDATEINSERT 涉及多个节点的数据时,流程如下:

第一阶段:预写入 (Prewrite) - 乐观锁检查
  1. 获取时间戳 :TiDB Server 向 PD 申请 start_ts
  2. 本地缓冲:TiDB Server 将需要修改的数据缓存在本地内存中。
  3. 分发请求 :TiDB Server 根据数据所在的 Region,将写请求并行发送给对应的 TiKV 节点
  4. 冲突检测 (关键点)
    • TiKV 收到请求后,利用 MVCC 机制检查该数据键上是否有其他未提交的事务,或者是否有比当前 start_ts 更新的已提交版本。
    • 如果没有冲突 :TiKV 将数据以"锁定"状态写入(此时数据对其他事务不可见,标记为 Lock),并返回成功。
    • 如果有冲突:TiKV 直接返回错误,TiDB Server 收到后会回滚整个事务(重试或报错给用户)。
    • 注意:这里就是 MVCC 发挥作用的地方,它在存储层直接拦截了冲突,不需要计算节点去"汇总"判断。
第二阶段:提交 (Commit)
  1. 二次获取时间戳 :如果所有涉及的 TiKV 节点都 Prewrite 成功,TiDB Server 再次向 PD 申请 commit_ts
  2. 下发提交指令 :TiDB Server 向所有参与交易的 TiKV 节点发送 Commit 指令,带上 commit_ts
  3. 落盘生效
    • TiKV 收到指令后,将之前的 Lock 状态移除,并将 commit_ts 写入数据版本。
    • 此时,其他事务如果 start_ts 大于这个 commit_ts,就能读到新数据了。
  4. 异步清理:提交成功后,后台会有异步任务清理旧的版本数据(GC)。

3. 读流程:快照读 (Snapshot Read)

这是 MVCC 最典型的体现:

  • 当事务 A 读取数据时,它带着自己的 start_ts 去 TiKV 读。
  • TiKV 会根据 MVCC 规则,只返回 commit_ts <= start_ts未被锁定 的最新版本数据。
  • 结果 :事务 A 永远看不到在它启动之后才提交的事务 B 的修改,从而实现了可重复读 (Repeatable Read) 隔离级别,且读不阻塞写,写不阻塞读

4.锁原理

在写入数据时候会写入lock数据,内部有锁处理并发,如果新事务进入会检查lock.s_ts<new_lock.s_ts直接异常出去。

这个过程就像是一个**"三步验证"** 机制,确保你读到的数据既是最新的 ,又是已提交的 ,且没有被锁住

让我们把这个流程拆解成标准的伪代码逻辑,看看底层到底发生了什么:

假设你要读取 Key = t100_r1,当前事务的读版本号是 read_ts = 500


第一步:检查锁 (Check Lock)

目标:确认这行数据有没有被"正在运行"的事务锁住。

  1. 动作 :去 Lock CF 查找 Key = t100_r1
  2. 判断
    • 情况 A:找到了锁
      • 检查锁的 start_ts
      • 如果锁的 start_ts < read_ts (500):说明有个老事务还没提交,挡路了!
        • 结果读取阻塞 (等待锁释放)或者 报错(取决于隔离级别),无法继续。
      • 如果锁的 start_ts > read_ts (500):说明是个未来的事务(不可能发生,除非时钟乱跳),忽略。
    • 情况 B:没找到锁 (这是正常路径)
      • 结论:这行数据现在是"干净"的,没有并发冲突。
      • 下一步:进入第二步。

关键点 :这一步极快,因为 Lock CF 是独立的,而且通常数据量很小(只有活跃事务才有锁)。


第二步:查找版本指针 (Find Version in Write CF)

目标 :在已提交的历史版本中,找到对于当前读版本号来说,最新且有效的那个版本。

  1. 动作 :去 Write CF 查找 Key = t100_r1
    • 注意:Write CF 里的 Key 格式通常是 t100_r1_w{commit_ts}
    • 我们要找的是:commit_ts <= read_ts (500) 的最大那个版本。
  2. 扫描逻辑 (从大到小扫描):
    • 先看 commit_ts = 600? -> 大于 500,跳过(那是未来的数据,不可见)。
    • 再看 commit_ts = 400? -> 小于 500,命中!
  3. 读取内容
    • 读取这条记录的 Value。
    • Value 里存着最重要的信息:start_ts (比如是 300) 和 操作类型 (Put/Delete)。
    • 含义:"在时间 400 提交了一个事务,这个事务是在时间 300 开始的,它修改了这行数据。"
  4. 特殊情况
    • 如果找到的记录类型是 Delete(删除标记):说明这行数据在这个时间点被删了。
    • 结果 :直接返回"数据不存在",不需要去第三步查 Default CF 了。

关键点Write CF 充当了索引的角色。它不存真实数据,只存"版本地图"。


第三步:获取真实数据 (Fetch Data from Default CF)

目标 :拿着第二步找到的"钥匙"(start_ts),去仓库里取货。

  1. 动作 :去 Default CF 查找 Key = t100_r1
    • 注意:Default CF 里的 Key 格式通常是 t100_r1_{start_ts}
    • 我们要精确匹配第二步拿到的 start_ts (即 300)。
  2. 查找
    • 查找 Key = t100_r1_300
  3. 结果
    • 找到 :读取 Value,这就是你要的真实数据(比如 {status: 1, name: "Alice"})。
    • 没找到:这就出大问题了!说明数据不一致(写了锁和提交记录,但没写数据),通常意味着底层存储损坏或严重的 Bug。

流程图总结
复制代码
用户发起读请求 (read_ts = 500)
       ↓
[1. 查 Lock CF]
   Key: t100_r1
   ├── 有锁? ──→ 阻塞/报错 (等待事务结束)
   └── 无锁? ──→ 继续 ↓
       ↓
[2. 查 Write CF]
   寻找:commit_ts <= 500 的最大版本
   扫描:... w600(跳过), w400(命中!)
   读取 Value: { start_ts: 300, type: Put }
   ├── type 是 Delete? ──→ 返回 NULL (数据已删)
   └── type 是 Put? ──→ 拿着 start_ts=300 继续 ↓
       ↓
[3. 查 Default CF]
   查找:Key = t100_r1_300
   读取 Value: { status: 1, ... }
       ↓
[4. 返回结果给用户]
   "status: 1"
为什么要这么麻烦分三步?

你可能会问:"为什么不直接把数据存在 Write CF 里,省得再查一次 Default CF?"

原因有二:

  1. 性能分离 (Read Amplification)

    • Write CF 只需要存很小的元数据(几个字节的时间戳和类型)。
    • Default CF 存的是巨大的业务数据(可能几 KB 甚至几 MB)。
    • 如果把大数据都塞进 Write CF,那么第二步扫描版本历史时会变得巨慢无比(因为要读很多大字段)。
    • 分开存,第二步扫描只需读几十字节,速度极快。只有确定要哪个版本了,才去读一次大数据。
  2. 存储优化 (Compaction)

    • 历史版本(旧数据)在 Default CF 中可以更容易地被 GC 清理。
    • Write CF 保留更长的历史记录用于一致性读,而 Default CF 可以更早地清理掉没人用的大对象,节省空间。

总结图示

复制代码
[客户端] 
   |
   v
[TiDB Server (计算节点)] <--- 1. 申请 start_ts (从 PD)
   |                        <--- 2. 解析 SQL, 规划路由
   | (并行发送)
   +-----> [TiKV Node A] --- 3. 检查冲突 (MVCC), 写 Lock (Prewrite)
   +-----> [TiKV Node B] --- 3. 检查冲突 (MVCC), 写 Lock (Prewrite)
   |                        <--- 4. 返回结果给 TiDB
   | (若全部成功)
   |                        <--- 5. 申请 commit_ts (从 PD)
   +-----> [TiKV Node A] --- 6. 提交事务, 移除 Lock, 写入 commit_ts
   +-----> [TiKV Node B] --- 6. 提交事务, 移除 Lock, 写入 commit_ts
相关推荐
ZhengEnCi3 小时前
J7A-已有数据表如何安全添加新字段 🛡️
数据库
2401_833197733 小时前
用Python制作一个文字冒险游戏
jvm·数据库·python
一叶飘零_sweeeet3 小时前
数据库连接池天花板之争:HikariCP 与 Druid 底层原理 + 高并发调优全拆解
数据库·hikaricp·数据库连接池·druid
GoodStudyAndDayDayUp3 小时前
RUO-VUE-PRO权限关联sql
java·数据库·sql
@insist1233 小时前
数据库系统工程师-SQL 数据定义语言(DDL)核心知识点与软考实战指南
数据库·oracle·软考·数据库系统工程师·软件水平考试
专利观察员3 小时前
情报升维,决策降本:2026年专利数据库和专利检索实践的演进逻辑和实测
数据库
次旅行的库3 小时前
【问渠哪得清如许-数据分析】学习笔记-下
数据库·笔记·sql·学习
万粉变现经纪人3 小时前
如何解决 pip install cx_Oracle 报错 未找到 Oracle Instant Client 问题
数据库·python·mysql·oracle·pycharm·bug·pip