13. NewSQL 与分布式数据库

分布式数据库架构: Spanner、TiDB、CockroachDB 的共识算法与强一致性

1. CAP 定理的深层含义

CAP 定理指出分布式系统无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition Tolerance)。

         C(一致性)
          /\
         /  \
        /    \
       / CAP  \
      /  三角  \
     /__________\
 A(可用性)    P(分区容错)

现代工程实践认为 P 是必须接受的——网络分区是物理现实,因此设计决策落在 C 与 A 之间的权衡:

  • CP 系统:分区时拒绝写入,保证数据一致(ZooKeeper、etcd、Consul)。
  • AP 系统:分区时继续服务,允许短暂不一致(Cassandra、DynamoDB)。

PACELC 定理进一步细化:即使无分区,也存在延迟(Latency)与一致性(Consistency)的取舍。

2. BASE 与 ACID 的博弈

ACID 是单机数据库的基石,但分布式环境下全序事务成本极高。BASE 提供另一种思路:

维度ACIDBASE
一致性强一致性最终一致性
隔离事务隔离无全局隔离
可用性事务阻塞优先可用
适用场景金融核心交易电商库存、社交 feed

BASE 并不代表放弃一致性,而是通过异步补偿、幂等重试和版本向量等机制,将一致性收敛到可接受的时间窗口内。

3. Google Spanner

Spanner 是 Google 的全球分布式 NewSQL 数据库,核心突破在于通过 TrueTime API 实现外部一致性(External Consistency),即事务提交顺序严格等同于真实物理时间顺序。

3.1 TrueTime API

TrueTime 时间区间:[earliest, latest]
  earliest = TT.now().earliest
  latest   = TT.now().latest

提交协议:
  1. 获取 commit_timestamp = TT.now().latest
  2. 等待直至 TT.now().earliest > commit_timestamp
  3. 正式提交,保证后续读取必见此事务

TrueTime 将误差界(epsilon)控制在毫秒级,每个时间戳不是单点值而是一个时间区间。提交时通过等待误差界消逝,确保所有节点在提交后的一致性读都能观察到该事务。

3.2 事务架构

  ┌─────────────┐     ┌─────────────┐
  │  Client     │────>│  Spanner    │
  └─────────────┘     │  Gateway    │
                      └──────┬──────┘
                             │
          ┌──────────────────┼──────────────────┐
          │                  │                  │
          ▼                  ▼                  ▼
    ┌──────────┐      ┌──────────┐      ┌──────────┐
    │ Paxos G1 │      │ Paxos G2 │      │ Paxos G3 │
    │ - Shard A│      │ - Shard B│      │ - Shard C│
    │ - Leader │      │ - Leader │      │ - Leader │
    └──────────┘      └──────────┘      └──────────┘

Spanner 以目录(Directory)为单位分片,每个分片对应一个 Paxos 复制组。单分片写事务直接通过 Paxos Leader 提交;跨分片写事务使用两阶段提交(2PC),协调者将事务日志同样写入 Paxos 组保证容错。

4. TiDB 架构解析

TiDB 是 PingCAP 推出的开源分布式 HTAP 数据库,兼容 MySQL 协议,采用计算存储分离架构。

4.1 三层架构

┌─────────────────────────────────────────────┐
│              TiDB Server (SQL 层)            │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  │
│  │   SQL    │  │   DDL    │  │  Parser  │  │
│  │   Plan   │  │  Worker  │  │ Optimizer│  │
│  └──────────┘  └──────────┘  └──────────┘  │
│         无状态,可水平扩展                   │
└────────────────────┬────────────────────────┘
                     │ gRPC / TiKV Client
┌────────────────────┴────────────────────────┐
│            TiKV (分布式事务存储层)            │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  │
│  │  Region  │  │   Raft   │  │ RocksDB  │  │
│  │  Leader  │  │   Log    │  │  Engine  │  │
│  └──────────┘  └──────────┘  └──────────┘  │
│     数据分片为 Region,默认 96MB,Raft 复制   │
└─────────────────────────┬───────────────────┘
                          │
┌─────────────────────────┴───────────────────┐
│         PD (Placement Driver)                │
│  - 全局时间戳分配(TSO)                      │
│  - Region 调度与负载均衡                      │
│  - 元数据管理与高可用切换                     │
└─────────────────────────────────────────────┘

4.2 TiKV 存储引擎

TiKV 将数据组织为 Key-Value 形式,底层使用 RocksDB。每张表的每行数据被编码为形如 t{tableID}_r{rowID} 的 Key,索引则编码为 t{tableID}_i{indexID} 前缀的二分搜索结构。

跨区域事务通过 Percolator 模型实现:预写(Prewrite)阶段在主键上锁并写入数据,提交(Commit)阶段向 PD 申请全局单调递增时间戳并解锁。此模型将分布式事务冲突检测下沉到存储层,使 TiDB Server 保持无状态。

4.3 TSO 时间戳

PD 作为全局时钟源分配时间戳:
  timestamp = (physical_time_ms << 18) | logical_counter

单次申请批量预分配(默认 3000 个),降低网络 RTT 开销,保证事务提交顺序与全局时钟严格递增。

5. CockroachDB 串行化

CockroachDB 是另一款受 Spanner 启发的开源分布式数据库,其核心设计目标为默认串行化隔离级别。

CockroachDB 采用混合逻辑时钟(HLC)替代物理时钟,结合无主多活的共识组设计。事务引擎使用 Serializable Snapshot Isolation(SSI),通过以下机制检测冲突:

  • 读时间戳跟踪:事务读取某 Key 时记录时间戳,提交时校验该 Key 是否在此时间戳之后被修改。
  • 写意图(Write Intents):事务写入时留下意向标记,后续事务遇到意向需等待或重试。
  • 时间戳推送:当检测到低优先级冲突时,系统将事务时间戳向前推进而非直接回滚,减少重试率。
-- CockroachDB 默认即串行化
BEGIN;
SELECT balance FROM accounts WHERE id = 'A';
UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
UPDATE accounts SET balance = balance + 100 WHERE id = 'B';
COMMIT;  -- 若冲突自动重试(由驱动层隐藏)

6. 分布式事务模式

6.1 两阶段提交 (2PC)

Coordinator                 Participant A          Participant B
    │    PREPARE              │                       │
    │ ──────────────────────> │                       │
    │    PREPARE              │                       │
    │ ──────────────────────────────────────────────> │
    │    YES                  │                       │
    │ <────────────────────── │                       │
    │    YES                  │                       │
    │ <────────────────────────────────────────────── │
    │    COMMIT               │                       │
    │ ──────────────────────> │                       │
    │    COMMIT               │                       │
    │ ──────────────────────────────────────────────> │

2PC 保证原子性,但协调者宕机可能导致参与者长时间阻塞。工业界常在协调者日志中加入事务状态以支持恢复。

6.2 Saga 模式

将长事务拆分为一系列本地事务,每个本地事务提交后发送事件触发下一个。失败时执行补偿操作:

T1: 扣减库存  ──success──>  T2: 创建订单  ──success──>  T3: 扣款
              fail <── Compensate T1   <── fail

Saga 适合高并发长流程业务,牺牲瞬时一致性以换取吞吐量和可用性。编排方式分为编排式(Orchestration)和协同式(Choreography)两类。

6.3 TCC 与 AT

TCC(Try-Confirm-Cancel)要求业务方预留资源:

阶段操作
Try检查并预留资源(如冻结金额)
Confirm确认执行,将预留转为正式
Cancel取消预留,回滚到初始状态

AT(Automatic Transaction)由 Seata 等框架实现,通过解析业务 SQL 自动生成反向补偿 SQL,对业务侵入更小。

7. 分片策略

分片(Sharding)将数据水平拆分到多个节点,常见策略如下:

策略路由规则优点缺点
Hashhash(user_id) % N数据均匀、避免热点扩容需全量重分布
Rangeuser_id [0, 1M)范围查询友好、扩容平滑单调写入易形成热点
Listregion IN (‘CN’, ‘US’)按业务语义分组需提前规划枚举值
CompositeHash + Range 分段兼顾均匀与查询路由逻辑复杂

一致性哈希(Consistent Hashing)可显著降低 Hash 分片扩容时的数据迁移量:引入虚拟槽位后,新增节点仅需负责部分槽位的迁移,而非全表重分布。

8. 全局二级索引

分片数据库中,主键索引天然分片,但二级索引若按索引 Key 独立分片则需处理分布式事务一致性问题。工程上通常采用以下方案:

  • 本地索引:二级索引与主表同分片,写入原子但跨分片查询需广播。
  • 全局索引:独立分片存储,查询仅需一次定位,但写入涉及主表与索引表的原子更新(如 TiDB 的 Global Index 通过异步 TTL 校验保证最终一致)。
  • 复制表:小维度表全量复制到所有节点,消除跨分片 JOIN(如 TiDB 的 TiFlash 小表广播)。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「database」更多文章

  1. PostgreSQL 锁与阻塞分析
  2. COPY 与批量数据加载优化
  3. pgvector 向量检索与混合查询