下载工作台
Python 数据库实战

读写分离与分库分表入门

试读上半部分 · 解锁后可读全文

第 14 章 · 读写分离与分库分表入门

本章目标:理解 主从复制读写分离 架构;掌握 垂直分库水平分表 概念与路由;在 Python/SQLAlchemy 层实现 读写路由 概念验证;对照 architecture ch10 数据架构决策;为 shop-db 毕业项目标注扩展边界;认识 ProxySQL 与应用双 DSN 两种落地方式。

学时建议:4~5 小时(含 1 小时 architecture ch10 对照阅读)

前置:完成 ch13 EXPLAIN 优化;建议先读 architecture ch10 分库分表与读写分离。


14.1 何时需要扩展读能力

单库 MySQL 在 shop-db 教学规模(万级商品、十万级订单)通常足够。出现以下信号才考虑读写分离或分片:

信号阈值(经验)优先动作
单表行数>3000~5000 万水平分表
单库容量>500GB~1TB垂直分库 / 分片
写 TPS>2000~3000分片或异步化
读 P99索引优化后仍 >100ms读写分离
连接数> max_connections×70%读库分担

决策顺序(与 architecture ch10 一致):

垂直分库 → 读写分离 → 冷数据归档 → 水平分片

本章以 概念 + db-demo 配置 为主,不要求本地搭完整 1 主 2 从集群;Docker Compose 提供 读写双 DSN 模拟 即可。


14.2 主从复制概念

14.2.1 拓扑

                    ┌─────────────┐
         写 ───────►│   Master    │
                    │  (Primary)  │
                    └──────┬──────┘
                           │ binlog 复制
              ┌────────────┼────────────┐
              ▼            ▼            ▼
        ┌─────────┐  ┌─────────┐  ┌─────────┐
        │ Slave 1 │  │ Slave 2 │  │ Slave 3 │
        └─────────┘  └─────────┘  └─────────┘
              ▲            ▲
              └──── 读 ────┘
角色职责
Master唯一写;产生 binlog
SlaveIO 线程拉 binlog → relay log → SQL 线程重放
应用写走 Master;读走 Slave(只读)

14.2.2 复制延迟

写 TPS典型延迟业务风险
2000.3s 内
8002~5s下单后列表可能缺单
2000+10s+必须写后读主

监控指标:Seconds_Behind_Master;>5s 触发告警(architecture ch10)。

14.2.3 与 shop-db 的关系

shop-db 毕业项目默认 单库单表;文档中注明:

生产扩展路径见 architecture ch10;读写分离不改变 slug / is_published / price 分 契约。

14.3 读写分离规则

操作路由说明
INSERT / UPDATE / DELETEMaster所有写
SELECT 列表、统计Slave可接受秒级延迟
事务内 SELECTMaster同事务一致读
写后立即读(下单详情)Master 或 Redis避免「丢单」错觉
DDL / 迁移MasterAlembic 只连写库
用户下单 POST /orders
    │
    ├─► INSERT orders ──► Master
    │
    └─► GET /orders/{id}  ──► Master(写后读)
         GET /orders?page=1 ──► Slave(列表可延迟)

对照 architecture ch10 §10.4:ProxySQL match_pattern 写走 hostgroup 10、读走 20。


14.4 垂直分库

业务域 拆库,与微服务边界对齐:

shop_user     ── users, addresses
shop_catalog  ── products, categories
shop_order    ── orders, order_items
shop_payment  ── payments, refunds
优点缺点
故障隔离跨库 JOIN 不可用
团队自治分布式事务复杂度上升
备份粒度细需应用层聚合

14.4.1 跨库场景正解(architecture ch10)

场景错误正解
订单页显示商品名跨库 JOIN下单时 冗余 product_name 快照
用户订单列表查 catalog 库order 表存 user_id + 必要快照
报表 GMV实时跨 4 库T+1 数仓宽表

14.4.2 shop-db 垂直拆边界(文档级)

毕业项目 ER 仍 单库;在 docs/SCALE.md 标注未来拆法:

## 垂直分库预案
- catalog.products → shop_catalog
- orders.* → shop_order
- 跨库字段:order_items.product_slug, product_name, price_cents(分)

price 分 冗余到订单行,避免拆库后跨库查价。


14.5 水平分表

单表过大时,按 分片键 拆多表或多库。

14.5.1 拓扑示例(architecture ch10)

4 库 × 16 表 = 64 物理表

db_index    = user_id % 4   → shop_order_0 .. shop_order_3
table_index = user_id % 16  → t_order_00 .. t_order_15

14.5.2 路由手算例题

user_id计算落点
1008610086%4=2, %16=6shop_order_2.t_order_06
88888801%4=1, %16=1shop_order_1.t_order_01

14.5.3 分片键选择

策略适用注意
Hash 取模均匀分布扩容需迁移
Range 时间按月订单当月热点
user_id用户订单列表architecture 推荐

为何订单用 user_id 而非 order_id:用户查「我的订单」占查询 95%;按 order_id 分片会导致列表扫全片。

14.5.4 Python 路由函数

def route_order_table(user_id: int) -> tuple[str, str]:
    db_suffix = user_id % 4
    table_suffix = user_id % 16
    return f"shop_order_{db_suffix}", f"t_order_{table_suffix:02d}"


# 示例
assert route_order_table(10086) == ("shop_order_2", "t_order_06")

14.6 全局 ID 与雪花

分片后 自增 ID 冲突;常用 雪花算法(architecture ch10 §10.5):

0 | 41bit 时间 | 10bit 机器 | 12bit 序列  → 64bit 整数
方案评价
雪花推荐,百万+/s
DB 号段 batch=1000简单,中小流量
UUID v4索引碎片大
自增分片后冲突

订单号展示:ORD + yyyyMMddHHmmss + shard(2) + seq(6)(与业务无关,教学虚构)。


14.7 应用层读写路由(db-demo)

14.7.1 双 Engine 配置

.env.example

DATABASE_WRITE_URL=mysql+pymysql://shop:shop@127.0.0.1:3306/shop_db
DATABASE_READ_URL=mysql+pymysql://shop:shop@127.0.0.1:3307/shop_db

db-demo/db/session.py

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session

write_engine = create_engine(
    os.environ["DATABASE_WRITE_URL"],
    pool_pre_ping=True,
)
read_engine = create_engine(
    os.environ["DATABASE_READ_URL"],
    pool_pre_ping=True,
)

WriteSession = sessionmaker(bind=write_engine)
ReadSession = sessionmaker(bind=read_engine)


def get_read_session() -> Session:
    return ReadSession()


def get_write_session() -> Session:
    return WriteSession()

14.7.2 路由装饰器(概念)

from functools import wraps

def use_master_after_write(func):
    """写后读主:线程局部标记,下一次读走 Master"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        # 简化教学:实际可用 contextvars
        _write_flag.set(True)
        try:
            return func(*args, **kwargs)
        finally:
            _write_flag.set(False)
    return wrapper

14.7.3 Repository 示例

class ProductRepository:
    def get_by_slug(self, slug: str, *, master: bool = False):
        session = get_write_session() if master else get_read_session()
        try:
            return session.scalar(
                select(Product).where(Product.slug == slug)
            )
        finally:
            session.close()

    def create(self, data: dict):
        session = get_write_session()
        try:
            p = Product(**data)  # price 分, is_published, slug
            session.add(p)
            session.commit()
            return p
        finally:
            session.close()

创建商品后点查:

repo.create({"slug": "new-item", "price": 9900, "is_published": False})
# 写后读必须 master=True
p = repo.get_by_slug("new-item", master=True)

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。