本页内容受版权保护 · 已添加水印 · 禁止任何形式转载
数据建模与设计(范式 / 主键 / 分库分表) · 分库分表

什么时候需要分库分表?(数据量、QPS、单表过大)

When to shard databases and tables? (High data volume, high QPS, oversized single tables

答案语言

考察要点

这道题看似在问一个时机问题,实际上是在考察你对数据库瓶颈的理解深度,以及作为工程师的架构决策能力。面试官想看你是否懂:

  1. 数据库的极限在哪里:你是否知道单机 MySQL 在数据量、并发量、I/O 和可维护性等方面的具体瓶颈。
  2. 架构演进的思维:你是否理解分库分表是“最后的手段”,在此之前有一系列优化可以做,体现了你解决问题的层次感和成本意识。
  3. 分库分表的本质:你是否能清晰地区分垂直拆分和水平拆分,并理解它们分别解决什么问题。

核心概念

分库分表是一种将数据分散到多个数据库或表中的技术,用以解决单一数据库实例的性能、存储和维护瓶颈。其核心思想是“分而治之”。

1. 垂直拆分 (Vertical Sharding)

垂直拆分是按业务维度或功能模块,将关联性弱的表或字段拆分到不同的数据库或表中。

  • 垂直分表:将一个宽表(字段很多)拆分为多个窄表。

    • 场景:表中存在 TEXT, BLOB 等大字段,或者某些字段访问频率远低于其他字段。
    • 目的:减少单行数据大小,提高核心字段的查询性能,让数据页能容纳更多行,提升 Buffer Pool 效率。
    • 示例:将 user 表中的个人简介 profile (TEXT类型) 拆分到 user_profile 表。
      sql
      1-- 原始表
      2CREATE TABLE `users_before` (
      3 `id` BIGINT NOT NULL AUTO_INCREMENT,
      4 `name` VARCHAR(50) NOT NULL,
      5 `email` VARCHAR(100) NOT NULL,
      6 `profile` TEXT, -- 大字段,不常用
      7 PRIMARY KEY (`id`)
      8);
      9
      10-- 垂直分表后
      11CREATE TABLE `users_core` (
      12 `id` BIGINT NOT NULL AUTO_INCREMENT,
      13 `name` VARCHAR(50) NOT NULL,
      14 `email` VARCHAR(100) NOT NULL,
      15 PRIMARY KEY (`id`)
      16);
      17CREATE TABLE `users_profile` (
      18 `user_id` BIGINT NOT NULL,
      19 `profile` TEXT,
      20 PRIMARY KEY (`user_id`)
      21);
  • 垂直分库:将不同业务模块的表划分到不同的数据库实例中。

    • 场景:微服务架构,不同服务有独立的业务数据。
    • 目的:隔离业务,实现故障隔离,分散 I/O 和连接压力。
    • 示例:电商系统拆分为用户库、订单库、商品库。

2. 水平拆分 (Horizontal Sharding)

水平拆分是按照某种规则(如 HASH, RANGE),将一个大表中的数据行分散到多个结构相同的表中(这些表可以位于同一个库,也可以在不同库)。

  • 场景:单表数据量过大,导致增删改查性能严重下降。
  • 目的:将单表的压力均分到多个子表中,突破单表的数据量和性能瓶颈。
  • 示例:orders 表根据 user_id 进行哈希取模,分到 16 个库 32 张表中。
    原始数据 orders->水平拆分后
    (id, user_id, ...)orders_00 (user_id % 32 == 0)
    (1, 101, ...)orders_01 (user_id % 32 == 1)
    (2, 102, ...)...
    (3, 133, ...)orders_31 (user_id % 32 == 31)

对比总结

特性垂直拆分水平拆分
解决问题业务耦合、大字段性能、不同业务资源竞争单表数据量过大、单表读写性能瓶颈
拆分对象表的列、不同业务的表表的行
拆分后表结构不同相同
实现复杂度较低,主要在业务代码层面调整极高,引入分片键、路由规则、分布式事务、跨库查询等复杂问题
对应用影响较小,影响范围可控巨大,对现有代码有侵入性,需要中间件支持

常见低分回答

  1. “表超过 500 万行就该分表”:这是最经典也最错误的“一刀切”回答。没有考虑单行数据大小、索引情况、硬件配置和业务特性。一个只有 INT 字段的表存 1 亿行可能都没问题,一个有大 TEXT 字段的表存 100 万行就可能很慢。
  2. “为了提高性能就分库分表”:过于笼统。没有说明是哪方面的性能(读/写/并发),也没有说明是在什么瓶颈下。分库分表本身会带来新的性能开销(如网络、中间件),甚至可能在错误场景下导致性能更差。
  3. 混淆垂直和水平拆分:将“把不常用字段拆出去”说成是水平拆分,或者反之。这表明对核心概念理解不清。
  4. 只谈好处,不谈成本:分库分表是“伤敌一千,自损八百”的方案。它会引入分布式事务、跨库 JOIN、数据扩容、数据倾斜等一系列复杂问题。只说其优点,说明工程经验不足。
  5. 忽略前置优化:在提到分库分表之前,没有先提及索引优化、SQL 调优、读写分离、缓存等成本更低的方案,会给面试官留下“喜欢用大炮打蚊子”的印象。

工程实践与优化

在生产环境中,决定是否分库分表是一个循序渐进、基于数据驱动的决策过程,而不是一个拍脑袋的决定。

决策流程:

  1. 监控与预警:首先,必须建立完善的数据库监控体系(如 Prometheus + Grafana),关注以下核心指标:

    • 性能指标:QPS, TPS, SQL 平均响应时间, 慢查询数量。
    • 资源指标:CPU 使用率, 内存使用率, I/O 吞吐与延迟, 网络带宽。
    • 容量指标:单表行数、表和索引的磁盘占用空间 (information_schema.TABLES)。
  2. 单机优化(成本最低):在触及硬件极限前,穷尽单机优化的所有可能性。

    • SQL 与索引:这是最高频的优化项。通过 EXPLAIN 分析慢查询,创建或修改索引,避免索引失效,使用覆盖索引。
    • 缓存:引入 Redis 等缓存,大幅降低对数据库的读压力。
    • 读写分离:对于读多写少的场景,使用主从架构,将读请求分发到从库,分散主库压力。
    • 配置优化:调整 innodb_buffer_pool_size, innodb_flush_log_at_trx_commit 等 MySQL 关键参数。
    • 硬件升级:如果预算允许,升级 CPU、内存、使用更高性能的 SSD 硬盘是见效最快的方式。
  3. 垂直拆分(中等成本):当业务发展清晰,不同模块间耦合度低时,可以进行垂直拆分。

    • 垂直分表:当 SHOW TABLE STATUS 显示 Avg_row_length 过大,或存在明显冷热字段时。
    • 垂直分库:当不同业务线的 QPS 互相影响,或希望做更彻底的资源隔离时。
  4. 水平拆分(最高成本):当上述手段都已实施,但依然存在瓶颈时,才考虑水平拆分。触发水平拆分的量化指标参考:

    • 数据量:单表行数超过 2000 万 ~ 5000 万,或单表数据大小超过 50GB ~ 100GB。这个数字的背后逻辑是 B+ 树的层级。一般 InnoDB 的 B+ 树在 3-4 层时性能最佳,数据量过大会导致层级加深,I/O 次数增加。
    • 并发/QPS:单库的连接数和 QPS 达到瓶颈。通常,OLTP 场景下单机 MySQL 的写 QPS 极限在 1000-3000,读 QPS 在 5000-10000(有缓存和从库的情况下可能更高)。当业务 QPS 持续逼近或超过这个阈值时,就需要水平分库来分散请求。
    • 可维护性:ALTER TABLE、备份、恢复等 DDL 操作时间长到无法接受(例如,超过业务允许的维护窗口)。对一个 100GB 的大表执行 ADD COLUMN 可能会锁表数小时,这是不可容忍的。

常见追问与延伸题目

  1. 追问:分库分表后,会带来哪些新的问题?如何解决?

    • 回答要点:
      • 分布式事务:跨多个库的写操作无法由单个数据库的 ACID 保证。解决方案有:
        • 二阶段提交 (2PC/XA):性能差,有同步阻塞问题,不常用。
        • TCC (Try-Confirm-Cancel):对业务侵入性强,需要手动实现三个阶段。
        • SAGA:长事务模式,通过正向操作和补偿操作序列来保证最终一致性。
        • 本地消息表/事务消息:利用消息队列实现最终一致性,是业界主流方案。
      • 跨库查询与JOIN:无法直接 JOIN 不同库中的表。解决方案有:
        • 全局表:在每个分片库中都冗余一份数据量小、变动少的表(如配置表)。
        • 字段冗余:在查询表中冗余被关联表的字段,以空间换时间。
        • 应用层组装:分两次或多次查询,在业务代码中进行数据拼装。
        • 数据同步/ETL:将分片数据同步到 Elasticsearch 或一个专门的数据仓库(如 ClickHouse, Doris)中进行复杂查询。
      • 分布式唯一 ID:分片后,不能再依赖数据库的 AUTO_INCREMENT 生成全局唯一主键。解决方案有:
        • UUID:简单但无序,且作为主键时索引性能差。
        • Snowflake 算法:Twitter 开源,生成趋势递增的 64 位 ID,是主流方案。
        • 数据库号段模式:通过一个中心数据库来分配 ID 号段。
  2. 追问:如何选择分片键(Shard Key)?选错了会怎么样?

    • 回答要点:
      • 选择原则:
        • 查询隔离:尽可能让查询落在单个或少数分片上,避免全路由扫描。因此,分片键应是 WHERE 条件中常用的列(如 user_id, order_id)。
        • 数据均衡:分片键的值应能均匀分布,避免数据倾斜导致热点分片。user_id 通常比 city_id 好,因为用户分布相对均匀。
        • 业务关联:考虑业务的关联性,例如订单表和订单详情表都用 order_id 作为分片键,可以保证它们落在同一个分片,便于事务和查询。
      • 选错的后果:
        • 热点问题:如果用时间或某个有偏向性的字段(如 is_vip),会导致大部分请求集中在少数分片上,分片失去了意义。
        • 跨分片查询风暴:如果查询条件不包含分片键,就需要扫描所有分片再聚合结果,性能极差。
  3. 追问:如果数据量持续增长,需要再次扩容(比如从 16 个库扩到 32 个库),该怎么做?

    • 回答要点:
      • 挑战:数据迁移是核心挑战,需要平滑进行,不影响线上业务。
      • 方案 1:停机迁移:简单粗暴,只适用于可接受长时间停服的业务。
      • 方案 2:双写迁移(平滑扩容):这是主流方案。
        1. 准备新库:部署新的 32 个库实例。
        2. 数据同步:使用工具(如 DataX)将老库(16个)的数据按照新的路由规则(如 hash(key) % 32)同步到新库。同时开启双向同步,保证增量数据不丢失。
        3. 流量切换:
          • 双写:修改应用代码或分片中间件配置,写操作同时写入新老两个库。
          • 读切:逐步将读流量从老库切换到新库,验证新库的正确性和稳定性。
          • 写切:读流量完全切换后,将写流量也完全切换到新库。
        4. 下线老库:观察一段时间,确认新库稳定后,停止向老库写入,并最终下线老库资源。
      • 一致性哈希:某些分片策略(如一致性哈希)在设计上可以减少扩容时需要迁移的数据量,但实现更复杂。