Files
cs-note/hzh/MS/03-数据一致性/01-数据库拆分.md
T
2026-05-24 11:42:38 +08:00

5.9 KiB

tags, create time
tags create time
microservice
database
sharding
replication
2026-05-05

数据库拆分

概述

微服务的核心设计原则是 "每个服务拥有独立数据库",这意味着每个服务的表结构、数据存储、甚至数据库类型都可以不同。但当单表数据量持续增长时,就面临拆分的需求。

graph LR
    S1[订单服务 DB]
    S2[库存服务 DB]
    
    subgraph BAD["反模式:共享数据库"]
        S1 --- SHARED[(共享 DB)]
        S2 --- SHARED
    end
    
    style SHARED fill:#ffebee,stroke:#ef5350

[!failure] 反模式警告 如果两个服务连接同一个数据库的同一张表,它们就不再是独立的微服务——你得到的是分布式单体。服务可以随意互相查询彼此的数据,失去了边界和自治性。

垂直拆分 vs 水平拆分

垂直拆分(按业务域)

按 微服务边界 拆库——这是微服务的标配。

graph TB
    subgraph "DB-Order"
        orders[orders]
        order_items[order_items]
        order_status[order_status]
    end
    
    subgraph "DB-User"
        users[users]
        user_profiles[user_profiles]
        user_addresses[user_addresses]
    end
    
    subgraph "DB-Product"
        products[products]
        categories[categories]
        product_images[product_images]
    end
特点 说明
每个服务独占一个数据库 物理隔离,互不干扰
可异构选型 订单用 MySQL,用户用 PostgreSQL,搜索用 Elasticsearch
天然解耦 服务间不能直接查对方库

水平拆分(分库分表)

当单个表的记录量达到千万级以上,需要进一步拆分。

graph TB
    subgraph "分库策略"
        DB1[(DB-01)]
        DB2[(DB-02)]
        DB3[(DB-03)]
    end
    
    subgraph "order_0001 表"
        Row1[userId=1 → order_0001]
        Row2[userId=4 → order_0001]
    end
    
    subgraph "order_0002 表"
        Row3[userId=2 → order_0002]
        Row4[userId=5 → order_0002]
    end
    
    userId_mod["ORDER BY user_id % 2"] --> DB1
    userId_mod --> DB2

常见分片策略

策略 哈希公式 优点 缺点
Hash Mod user_id % N 简单高效,路由确定 扩缩容困难,数据迁移成本高
Range user_id BETWEEN x AND y 范围查询友好 热点用户集中到单分片
Time-based year_month 按生命周期管理 新分片写入压力大
Geo-based 按地域分片 本地化访问,延迟低 跨区域操作复杂

ShardingSphere / MyCat

[!tip] 推荐中间件

Apache ShardingSphere 是国内使用最广泛的分库分表方案:

  • 支持 JDBC / Proxy / Sidecar 三种部署模式
  • 内置分片算法:Mod、Range、Hash、Tag
  • 分布式主键生成器(Snowflake)原生集成
  • 读写分离、强制路由、广播表等高级特性

ShardingSphere 配置示例

# sharding-jdbc 配置
sharding jdbc:
  data-sources:
    ds0: { type: HikariCP, ... }
    ds1: { type: HikariCP, ... }
  
  sharding:
    tables:
      orders:
        actual-data-nodes: ds$->{0..1}.orders$->{0..1}
        table-strategy:
          standard:
            sharding-column: user_id
            sharding-algorithm-name: user-id-mod
        key-generate-strategy:
          column: order_id
          key-generator-name: snowflake
    
    sharding-algorithms:
      user-id-mod:
        type: MOD
        props:
          sharding-count: 2

跨库查询方案

[!question] 经典难题 订单服务需要展示用户的姓名和手机号来做收货地址。但用户信息在用户库,订单数据在订单库——怎么办?

方案对比

方案 描述 性能 复杂度 适用场景
冗余字段 订单表存用户名字段 ⭐⭐⭐⭐⭐ 低 只读字段,变更频率低
接口组装 先查订单,再调用户服务补全 ⭐⭐⭐ 中 偶尔需要关联的场景
CQRS / 宽表 异步同步一份宽表用于查询 ⭐⭐⭐⭐ 高 高频关联查询
搜索引擎 ES/Kibana 做关联查询 ⭐⭐⭐⭐ 中高 复杂搜索 + 聚合

冗余字段实践

-- 订单表冗余关键字段
CREATE TABLE orders (
    id         BIGINT PRIMARY KEY,
    user_id    BIGINT NOT NULL,
    username   VARCHAR(64),     -- 冗余用户名(快照,不参与编辑)
    phone      VARCHAR(20),     -- 冗余手机号(脱敏存储)
    created_at TIMESTAMP DEFAULT NOW(),
    INDEX idx_user (user_id)
);

[!note] 冗余数据的维护

用户改名字了怎么办?不改订单表。订单上的 username 是该时刻的"快照"——它反映的是下单时的状态,不是当前状态。这符合业务语义。

如果需要批量更新冗余字段(如用户头像),通过消息队列异步通知订单服务更新。

数据库迁移工具

graph LR
    Dev["开发环境"] -->|"flyway migrate"| Stage["Staging"]
    Stage -->|"人工审批"| Prod["Production"]
    Prod -->|"flyway validate"| Check["校验版本一致性"]
工具 语言 特点
Flyway Java 基于文件命名,SQL 脚本方式,简单直观
Liquibase Java XML/YAML/JSON 格式,支持回滚生成
golang-migrate Go CLI 工具,轻量,适合 Go 项目

Flyway 迁移流程

db/migration/
├── V1__create_users_table.sql
├── V2__add_user_phone.sql
├── V3__create_orders_table.sql
└── V4__add_order_status_enum.sql

执行顺序:V1 → V2 → V3 → V4。Flyway 内部维护 _schema_version 表追踪已执行的迁移。

关联笔记