跳转到内容
123xiao | 无名键客

《分布式架构下的服务拆分与数据库拆分实战:从单体系统平滑演进到高可用微服务》

字数: 0 阅读时长: 1 分钟

分布式架构下的服务拆分与数据库拆分实战:从单体系统平滑演进到高可用微服务

很多团队第一次做微服务,不是败在“不会拆”,而是败在“拆得太急”。
系统明明还在跑,业务也还在增长,但因为一次激进拆分,结果把发布链路、数据一致性、排障复杂度全拉满,最后团队天天救火。

我见过不少单体系统演进项目,表面目标是“上微服务”,本质目标其实只有两个:

  1. 让系统扛住增长
  2. 在不影响业务的前提下逐步演进

所以这篇文章我不打算只讲理论,而是从一个更实战的角度来拆:服务怎么拆、数据库怎么拆、迁移怎么做才能平滑,以及拆完后怎么避免把复杂度引爆。


背景与问题

单体系统在早期通常有几个天然优势:

  • 开发快
  • 部署简单
  • 事务一致性容易保证
  • 调试路径短

但业务一旦做起来,问题就会越来越明显:

  • 一个模块改动要整体发布
  • 热点功能拖垮整个应用
  • 单库单表数据量膨胀
  • 团队协作冲突严重
  • 某些高频接口需要独立扩容,但单体做不到

典型场景比如一个电商单体应用,里面混着:

  • 用户模块
  • 商品模块
  • 订单模块
  • 支付模块
  • 库存模块
  • 营销模块

如果订单流量暴涨,按理只该扩订单相关能力,但在单体里你往往只能整体扩容。
更麻烦的是数据库,所有业务共享一个库,最终会出现:

  • 大事务多
  • 锁冲突严重
  • 慢 SQL 难定位
  • 表之间强耦合
  • 迁移风险高

所以,服务拆分和数据库拆分,本质上是在用“边界清晰 + 独立伸缩 + 降低耦合”的方式,对抗规模增长带来的复杂度。


先说结论:不要一上来就“全面微服务化”

我更推荐这样的演进顺序:

  1. 先识别业务边界
  2. 优先拆高变化、高流量、高资源消耗模块
  3. 服务边界稳定后,再做数据库边界收缩
  4. 通过灰度、双写、回放、对账实现平滑迁移
  5. 最后再考虑更细颗粒度的拆分

一句话概括:先拆“责任”,再拆“部署”,最后拆“数据”。


核心原理

1. 服务拆分的核心不是“按代码包拆”,而是“按业务边界拆”

很多项目会按技术分层来拆:

  • user-service
  • order-service
  • dao-service
  • common-service

这通常不是微服务,是把单体的包结构搬到了网络上,复杂度更高,收益更低。

更合理的方式是按领域拆分,比如:

  • 用户服务:注册、登录、账户资料
  • 商品服务:商品信息、类目、价格展示
  • 订单服务:下单、订单状态流转
  • 库存服务:锁库存、扣库存、回补库存
  • 支付服务:支付单、回调、退款

判断一个边界是否合理,可以看三个问题:

  • 这个能力是否有独立的业务目标?
  • 是否有独立的扩容诉求?
  • 是否能拥有自己的数据主权?

2. 数据库拆分不是目的,数据自治才是目的

数据库拆分常见有两种:

垂直拆分

按业务域拆库,比如:

  • user_db
  • product_db
  • order_db
  • payment_db

优点:

  • 业务边界清晰
  • 权限控制更容易
  • 独立扩容、独立维护

缺点:

  • 跨库关联变复杂
  • 分布式事务问题暴露出来

水平拆分

同一个业务库或表,按规则切成多份,比如订单表按用户 ID 或订单 ID 分片:

  • order_00
  • order_01
  • order_02
  • order_63

优点:

  • 提升单表容量上限
  • 分散写入压力

缺点:

  • 路由复杂
  • 聚合查询复杂
  • 全局唯一 ID、分页、排序都要重做

3. 演进的关键是“绞杀者模式”

与其停机重构,不如让新系统逐步接管旧系统能力。
这就是经典的 Strangler Pattern(绞杀者模式):像藤蔓一样慢慢包裹旧系统,直到旧功能被替代。

flowchart LR
    A[客户端请求] --> B[网关/API层]
    B --> C{新服务是否已接管?}
    C -- 是 --> D[新微服务]
    C -- 否 --> E[单体系统]
    D --> F[新数据库]
    E --> G[旧数据库]

它的好处是:

  • 迁移可分批
  • 风险可控
  • 可以随时回退
  • 业务不中断

4. 服务拆分后,一致性策略要变

单体里你习惯一个本地事务解决问题。
到了分布式环境,跨服务、跨数据库以后,强一致成本极高,常见做法是:

  • 本地事务 + 事件发布
  • 最终一致性
  • 幂等控制
  • 补偿机制
  • 对账兜底

比如下单流程,不一定非要订单、库存、支付全都在一个强事务里完成。可以设计成:

  1. 创建订单
  2. 发出“订单已创建”事件
  3. 库存服务消费事件并锁库存
  4. 失败则回滚订单状态或标记异常
  5. 定时任务做补偿和对账
sequenceDiagram
    participant U as 用户
    participant O as 订单服务
    participant MQ as 消息队列
    participant S as 库存服务
    participant P as 支付服务

    U->>O: 提交订单
    O->>O: 本地事务创建订单
    O->>MQ: 发布 OrderCreated
    MQ->>S: 消费订单事件
    S->>S: 锁定库存
    S->>MQ: 发布 StockLocked
    MQ->>P: 消费库存锁定事件
    P->>P: 创建支付单

方案对比与取舍分析

方案一:一次性全面拆分

特点:

  • 重新定义所有服务边界
  • 重构代码和数据库
  • 一次性上线

优点:

  • 目标架构清晰
  • 历史包袱处理彻底

缺点:

  • 风险极高
  • 周期长
  • 业务通常等不起

适用场景:

  • 老系统接近废弃
  • 新业务可完全切流
  • 团队有很强的平台能力

方案二:按热点域逐步拆分

特点:

  • 从订单、支付、库存等关键模块开始
  • 保留单体主体
  • 用网关和消息机制衔接

优点:

  • 风险低
  • 可逐步验证
  • 业务连续性更好

缺点:

  • 中间态时间较长
  • 架构会短期内变复杂

这是大多数团队更现实的选择。

方案三:先拆读,再拆写

这是一种非常稳妥的演进方式:

  1. 先把查询接口迁到新服务
  2. 再迁移写接口
  3. 最后收回数据库写权限

优点是风险小,因为“读出错”通常比“写错数据”更容易控制。


一套可落地的演进路径

下面给一套更接地气的步骤,我自己更常用这类节奏。

第一步:识别领域边界

可以先做一个简单的上下文划分:

  • 用户域
  • 商品域
  • 交易域
  • 履约域
  • 财务域

重点看:

  • 调用关系是否过于密集
  • 是否共享大量表
  • 是否存在“谁都能改”的公共模型

如果某张表被五六个模块同时写,那基本说明边界没立住。

第二步:先拆最值得拆的服务

优先拆分具有以下特征的模块:

  • 发布频繁
  • 性能瓶颈明显
  • 业务规则复杂
  • 需要独立扩容
  • 与其他模块耦合相对可控

通常最先拆的是:

  • 订单
  • 支付
  • 库存
  • 搜索
  • 用户认证

而不是报表、后台配置这类收益较低的模块。

第三步:建立服务间通信机制

常见搭配:

  • 同步调用:HTTP/gRPC
  • 异步调用:Kafka/RabbitMQ/RocketMQ
  • 服务治理:注册发现、限流、熔断、重试、超时
  • 可观测性:日志、指标、链路追踪

经验上:

  • 查询类、强实时反馈类适合同步
  • 削峰填谷、解耦、最终一致性类适合异步

第四步:数据库先垂直拆,再考虑水平拆

很多团队还没到千万级数据量,就急着分库分表,这是典型的过度设计。
更合理的路线通常是:

  1. 单库单表
  2. 单库多表
  3. 按业务垂直拆库
  4. 热点表做水平拆分

第五步:迁移数据而不是“直接切库”

比较稳的方式是:

  • 新增目标库表
  • 从旧库全量导入
  • 增量同步
  • 新老双写
  • 灰度读新
  • 全量切换
  • 对账
  • 下线旧链路
flowchart TD
    A[旧单体库] --> B[全量迁移]
    B --> C[新服务库]
    A --> D[增量订阅/CDC]
    D --> C
    E[应用双写] --> A
    E --> C
    F[灰度读新库] --> C
    G[全量切换] --> C
    H[对账与下线旧链路]

实战代码(可运行)

下面用一个简化示例演示:
我们把单体中的“订单模块”拆成独立服务,并通过事件驱动库存锁定。代码使用 Python + Flask,便于本地快速运行理解思路。

说明:示例重点是演示拆分与迁移思路,不是生产级框架选型。


示例一:订单服务

功能:

  • 创建订单
  • 本地保存订单
  • 提交事件给消息队列(这里用内存队列模拟)
from flask import Flask, request, jsonify
import sqlite3
import uuid
import json
import queue
import threading
import time

app = Flask(__name__)
event_bus = queue.Queue()

DB_FILE = "order.db"

def init_db():
    conn = sqlite3.connect(DB_FILE)
    cur = conn.cursor()
    cur.execute("""
    CREATE TABLE IF NOT EXISTS orders (
        id TEXT PRIMARY KEY,
        user_id TEXT NOT NULL,
        product_id TEXT NOT NULL,
        amount INTEGER NOT NULL,
        status TEXT NOT NULL
    )
    """)
    conn.commit()
    conn.close()

def save_order(order_id, user_id, product_id, amount, status):
    conn = sqlite3.connect(DB_FILE)
    cur = conn.cursor()
    cur.execute(
        "INSERT INTO orders (id, user_id, product_id, amount, status) VALUES (?, ?, ?, ?, ?)",
        (order_id, user_id, product_id, amount, status)
    )
    conn.commit()
    conn.close()

def update_order_status(order_id, status):
    conn = sqlite3.connect(DB_FILE)
    cur = conn.cursor()
    cur.execute("UPDATE orders SET status = ? WHERE id = ?", (status, order_id))
    conn.commit()
    conn.close()

def get_order(order_id):
    conn = sqlite3.connect(DB_FILE)
    cur = conn.cursor()
    cur.execute("SELECT id, user_id, product_id, amount, status FROM orders WHERE id = ?", (order_id,))
    row = cur.fetchone()
    conn.close()
    if not row:
        return None
    return {
        "id": row[0],
        "user_id": row[1],
        "product_id": row[2],
        "amount": row[3],
        "status": row[4]
    }

@app.route("/orders", methods=["POST"])
def create_order():
    data = request.json
    user_id = data["user_id"]
    product_id = data["product_id"]
    amount = int(data["amount"])

    order_id = str(uuid.uuid4())
    save_order(order_id, user_id, product_id, amount, "CREATED")

    event = {
        "type": "OrderCreated",
        "payload": {
            "order_id": order_id,
            "user_id": user_id,
            "product_id": product_id,
            "amount": amount
        }
    }
    event_bus.put(json.dumps(event))
    return jsonify({"order_id": order_id, "status": "CREATED"}), 201

@app.route("/orders/<order_id>", methods=["GET"])
def query_order(order_id):
    order = get_order(order_id)
    if not order:
        return jsonify({"error": "not found"}), 404
    return jsonify(order)

def inventory_consumer():
    inventory = {"p1": 10, "p2": 5}

    while True:
        try:
            msg = event_bus.get(timeout=1)
        except queue.Empty:
            continue

        event = json.loads(msg)
        if event["type"] != "OrderCreated":
            continue

        payload = event["payload"]
        order_id = payload["order_id"]
        product_id = payload["product_id"]
        amount = payload["amount"]

        stock = inventory.get(product_id, 0)
        if stock >= amount:
            inventory[product_id] -= amount
            update_order_status(order_id, "STOCK_LOCKED")
            print(f"[inventory] stock locked, order={order_id}, remain={inventory[product_id]}")
        else:
            update_order_status(order_id, "FAILED_NO_STOCK")
            print(f"[inventory] no stock, order={order_id}")

if __name__ == "__main__":
    init_db()
    t = threading.Thread(target=inventory_consumer, daemon=True)
    t.start()
    app.run(port=5001, debug=True)

运行方式

先安装依赖:

pip install flask

启动服务:

python app.py

创建订单:

curl -X POST http://127.0.0.1:5001/orders \
  -H "Content-Type: application/json" \
  -d '{"user_id":"u1001","product_id":"p1","amount":2}'

查询订单状态:

curl http://127.0.0.1:5001/orders/<你的订单ID>

这个例子展示了一个很关键的思想:

  • 订单创建是本地事务
  • 库存处理异步完成
  • 订单状态通过事件推进

也就是说,我们不再追求一口气把所有事情做完,而是把流程拆成可恢复、可观测、可补偿的状态机。


示例二:数据库拆分后的简单路由

如果订单表需要水平拆分,可以先用最简单的一致性路由策略。下面示例按用户 ID 做分片。

import hashlib

SHARD_COUNT = 4

def shard_index(user_id: str) -> int:
    h = hashlib.md5(user_id.encode("utf-8")).hexdigest()
    return int(h, 16) % SHARD_COUNT

def order_table(user_id: str) -> str:
    idx = shard_index(user_id)
    return f"orders_{idx}"

if __name__ == "__main__":
    for user_id in ["u1001", "u1002", "u1003", "u1004"]:
        print(user_id, "=>", order_table(user_id))

输出类似:

u1001 => orders_2
u1002 => orders_0
u1003 => orders_1
u1004 => orders_3

如果你后续要扩分片数,就不能直接改 % 4% 8,否则历史数据路由全乱。
这也是为什么很多分库分表方案会引入一致性哈希或中间件层,而不是把路由规则写死在业务代码里。


示例三:迁移期间双写与兜底

双写是迁移里经常用的策略,但一定要有失败处理。下面给一个简化示例:

import sqlite3

OLD_DB = "legacy.db"
NEW_DB = "order_new.db"

def init_db():
    for db in [OLD_DB, NEW_DB]:
        conn = sqlite3.connect(db)
        cur = conn.cursor()
        cur.execute("""
        CREATE TABLE IF NOT EXISTS orders (
            id TEXT PRIMARY KEY,
            user_id TEXT,
            product_id TEXT,
            amount INTEGER,
            status TEXT
        )
        """)
        conn.commit()
        conn.close()

def write_db(db, order):
    conn = sqlite3.connect(db)
    cur = conn.cursor()
    cur.execute(
        "INSERT INTO orders (id, user_id, product_id, amount, status) VALUES (?, ?, ?, ?, ?)",
        (order["id"], order["user_id"], order["product_id"], order["amount"], order["status"])
    )
    conn.commit()
    conn.close()

def dual_write(order):
    write_db(OLD_DB, order)
    try:
        write_db(NEW_DB, order)
    except Exception as e:
        # 生产上这里应写入重试队列或补偿日志
        print("write new db failed:", e)
        return False
    return True

if __name__ == "__main__":
    init_db()
    order = {
        "id": "o1001",
        "user_id": "u1001",
        "product_id": "p1",
        "amount": 2,
        "status": "CREATED"
    }
    ok = dual_write(order)
    print("dual write result:", ok)

这个示例虽然简单,但表达了一个实战上非常重要的原则:

  • 双写不是“同时写两次”这么简单
  • 必须考虑:
    • 写旧成功、写新失败怎么办
    • 重试是否幂等
    • 补偿如何落地
    • 对账如何执行

常见坑与排查

这一部分是我觉得最有必要提前讲的,因为很多问题上线后才暴露,而且很难排。

坑一:服务拆完了,但数据库还是“共享大泥球”

现象:

  • 服务已经独立部署
  • 但多个服务还在共用同一个库,甚至共写同一张表

后果:

  • 数据主权不清
  • 任一服务改表结构都可能影响全局
  • 最终还是“假微服务”

排查方法:

  • 盘点每个服务写入的表
  • 统计同一张表的写入来源
  • 明确唯一 owner 服务

建议:

  • 一张核心业务表,尽量只允许一个服务写
  • 其他服务通过接口或事件获取数据

坑二:同步调用链过长,导致雪崩

现象:

  • 用户请求进来后
  • A 调 B,B 调 C,C 调 D
  • 任意一个节点慢,整条链路超时

排查方法:

  • 看链路追踪
  • 统计调用深度、平均 RT、超时率
  • 检查是否有无超时配置的 HTTP 调用

建议:

  • 核心链路调用深度尽量控制
  • 设置超时、重试、熔断
  • 非关键逻辑异步化

坑三:分布式事务执念太重

现象:

  • 一定要像单体一样“所有步骤同时成功”
  • 到处引入复杂事务框架

问题:

  • 成本高
  • 性能差
  • 排障难

更务实的做法:

  • 接受最终一致性
  • 做好状态流转、幂等、补偿、对账

坑四:分库分表后查询能力退化

现象:

  • 管理后台查订单变慢
  • 跨分片分页非常痛苦
  • count(*) 成本高

排查方法:

  • 区分交易查询与分析查询
  • 看是否把运营报表需求直接压在交易库上

建议:

  • 交易库服务在线请求
  • 分析场景走 ES、OLAP、数仓
  • 不要试图让分片交易库承担所有查询职责

坑五:ID 生成不稳定

分布式环境下如果还依赖数据库自增主键,会很快遇到瓶颈。
建议使用:

  • 雪花算法
  • 号段模式
  • 数据库发号服务

否则会出现:

  • 主键冲突
  • 路由键缺失
  • 数据迁移困难

安全/性能最佳实践

1. 服务安全边界要前置

服务拆开以后,攻击面实际上扩大了。至少要做:

  • 服务间鉴权
  • 内外网隔离
  • 敏感字段脱敏
  • 接口限流
  • 审计日志

尤其是内部服务,不要觉得“内网就安全”。很多事故都不是外部攻击,而是内部误调用或配置失误。

2. API 要保证幂等

比如创建订单、支付回调、库存扣减,都必须幂等。
常见做法:

  • 请求幂等键
  • 唯一索引约束
  • 消息消费去重表
  • 状态机校验

一个常见 SQL 例子:

CREATE TABLE payment_callback_log (
    callback_id VARCHAR(64) PRIMARY KEY,
    order_id VARCHAR(64) NOT NULL,
    status VARCHAR(32) NOT NULL,
    created_at DATETIME NOT NULL
);

收到重复回调时,利用 callback_id 去重,而不是重复处理。

3. 读写分离和缓存要谨慎使用

缓存可以大幅降低数据库压力,但也会带来一致性问题。
建议:

  • 先优化 SQL 和索引
  • 再做缓存
  • 缓存更新策略要清晰:旁路缓存、失效通知、TTL
  • 绝不要把缓存当数据库

4. 消息队列不是银弹

消息队列能解耦,但也会带来:

  • 消息重复
  • 消息乱序
  • 积压
  • 死信
  • 消费失败重试风暴

上线前至少要回答这些问题:

  • 消息是否可重复消费?
  • 消费失败怎么补偿?
  • 顺序是否真的重要?
  • 如何监控积压?

5. 容量估算要做在拆分前

很多架构问题,本质是容量没有算清楚。至少估这几项:

  • 峰值 QPS
  • 日订单量
  • 单表增长速度
  • 热点用户/热点商品比例
  • 库存扣减峰值
  • 消息峰值堆积时长

一个简单估算例子:

假设:

  • 日订单量 500 万
  • 峰值是均值的 8 倍
  • 单订单写入 2 KB

那么峰值写入吞吐、索引增长、日志增长都要提前考虑。
如果连这些量级都没概念,就很容易“还没到该拆的时候却拆了”,或者“真该拆的时候已经晚了”。


一个推荐的落地原则清单

如果你现在正准备从单体往微服务演进,我建议先守住下面这些原则:

  1. 优先拆业务边界清晰的模块
  2. 一个服务尽量拥有自己的数据主权
  3. 能本地事务解决的,不要强行分布式事务
  4. 先垂直拆库,再考虑水平分片
  5. 双写必须配套幂等、补偿、对账
  6. 核心链路少同步、多异步解耦
  7. 把监控、日志、链路追踪当基础设施,不是附属品
  8. 迁移用灰度,切换要可回滚
  9. 不要为了“微服务而微服务”
  10. 复杂度预算要和团队能力匹配

总结

从单体系统演进到高可用微服务,真正困难的地方从来不是“把代码拆开”,而是:

  • 怎么定义稳定的边界
  • 怎么处理数据一致性
  • 怎么在迁移期间不影响业务
  • 怎么控制拆分后新增的系统复杂度

如果让我给一个最实用的建议,那就是:

把微服务拆分看成一次长期架构治理,而不是一次性技术改造。

更具体一点:

  • 先拆最痛的点,不要全面开花
  • 先把服务边界立住,再谈数据库自治
  • 先解决可观测性,再追求更细粒度拆分
  • 对一致性问题保持务实,接受最终一致性并设计好补偿
  • 每一次切换都要有回滚方案和对账机制

最后强调一句边界条件:
如果你的系统规模、团队协作复杂度、性能瓶颈都还没有明显到一定程度,单体未必是问题。
好的架构不是“最先进”,而是在当前阶段最合适、最可控、最能支撑业务增长


分享到:

上一篇
《微服务架构中的服务拆分与边界治理:从领域建模到生产环境落地实践》
下一篇
《微服务架构中分布式事务的实战治理:基于 Saga、消息最终一致性与补偿机制的落地方案》