Skip to content

海量数据:分库分表与全局 ID【中级】 ​


❓ 面试官:什么时候该分表?什么时候该分库? ​

频率:🔥🔥🔥🔥🔥

💡 一句话总结(先抛结论): 当单表数据量过大(超过千万级)导致 B+ 树变高、查询变慢时,我们需要分表;当单库并发读写太高(CPU、内存、磁盘 IO 遇到瓶颈)、数据库连接数耗尽时,我们需要分库。

📝 详细原理解析:

  1. 垂直拆分(按业务列拆分):

    • 垂直分库:按业务模块,把原本在一个库里的表(如用户表、订单表、商品表)拆分到不同的数据库实例中。这其实就是微服务架构的基础。
    • 垂直分表:把一张字段很多的宽表,按字段的使用频率拆分成主表和扩展表。比如 user_base(存高频访问的 id、name)和 user_ext(存低频访问的个人简介、头像等),减少每次查询读取的数据页大小。
  2. 水平拆分(按行拆分,最核心的技术):

    • 水平分库:把同一张表的数据(比如 1 亿条订单),按一定规则(如 user_id 的哈希)拆分到不同的数据库服务器上。能解决单机存储容量和单机并发瓶颈。
    • 水平分表:把同一张表的数据,按规则拆分到同一个数据库的多个表中(如 order_0、order_1... order_31)。只能解决单表数据量过大导致的 B+ 树查询慢问题,不能解决单机数据库的性能瓶颈。

🌟 面试加分项(实战经验): 面试时我通常会强调:"分库分表是能不搞就不搞的最后手段"。在决定分库分表前,我一定会先尝试:

  1. 优化 SQL 和索引。
  2. 升级硬件(加内存、换 SSD 甚至 NVMe 磁盘)。
  3. 引入 Redis 缓存挡住读请求。
  4. 做冷热数据分离(把半年前的历史数据归档到另一张表或 HBase 中)。 只有当以上方案都无法解决,且评估未来 3 年单表确实会突破 5000 万甚至过亿时,才会慎重启动分库分表(比如采用 ShardingSphere-JDBC 框架进行水平拆分)。

❓ 面试官:分库分表时,Sharding Key(分片键)怎么选?非分片键怎么查询? ​

频率:🔥🔥🔥🔥

💡 一句话总结(先抛结论): 分片键应该选择数据分布均匀且查询最频繁的字段,通常是 user_id。对于非分片键(如 order_id)的查询,通常采用"建立映射关系表"或"基因法(将 user_id 融入 order_id)"或"双写 ES"来解决。

📝 详细原理解析:1. 为什么通常选 user_id 作为分片键? 绝大多数 C 端业务的查询都是围绕用户展开的(如"查询我的订单")。如果我们按 user_id % 32 分库,同一个用户的所有订单都会落到同一个库里,查询时直接定位到一个库,完全不需要跨库跨表,性能极高。

2. 遇到"用订单号查订单(非分片键)"怎么办? 如果只传 order_id,由于不知道对应的 user_id,系统就不知道去哪个库查,只能全库全表路由广播(极其消耗性能)。 解决方案:

  • 方案 A(基因法/ID 融合法,强烈推荐):在生成 order_id 时,把 user_id 的后几位(比如最后 4 位,即路由基因)拼接在 order_id 的末尾。这样拿到 order_id 后,直接截取最后 4 位进行 Hash 取模,就能完美路由到对应的库表!
  • 方案 B(建立映射表):建一张单独的路由表 (order_id, user_id),由于数据量不大,可以不分库或只分少量的库。先查映射表拿到 user_id,再去查真实的订单表。(缺点是多了一次查询)。

3. 遇到"运营后台要根据时间、金额等多维度查订单"怎么办? 方案:双写 Elasticsearch(ES)。将分库分表后的数据通过 Canal + MQ 实时同步到 ES 中。所有复杂的、多维度的、分页的运营侧查询,全部走 ES,查到 ID 后再回源数据库获取详情。


❓ 面试官:分库分表后,自增主键不能用了,你们是怎么生成全局唯一 ID 的? ​

频率:🔥🔥🔥🔥🔥

💡 一句话总结(先抛结论): 分库分表后,各表各自自增会导致 ID 冲突。我们主要使用雪花算法(Snowflake)或者基于 Redis/数据库号段模式(如滴滴的 TinyId、美团的 Leaf)来生成全局唯一的分布式 ID。

📝 详细原理解析:

  1. UUID(极不推荐):

    • 优点:本地生成,性能极高,绝对唯一。
    • 缺点:32 位字符串太长,无序。插入 B+ 树时会引发严重的页分裂和碎片,极大影响插入性能,且无法做范围查询。
  2. 雪花算法 Snowflake(最常用):

    • 原理:生成一个 64 位的 long 型数字。由 1 位符号位 + 41 位时间戳(毫秒级) + 10 位机器 ID(最多 1024 台机器) + 12 位序列号(每台机器每毫秒可生成 4096 个 ID)组成。
    • 优点:本地生成,不依赖中心化组件,趋势递增(对 B+ 树极度友好),性能极高。
    • 缺点:强依赖机器时钟,如果服务器发生时钟回拨,可能会生成重复 ID。目前开源框架(如百度 UidGenerator)基本都解决了时钟回拨问题。
  3. 数据库号段模式(Leaf/TinyId):

    • 原理:业务服务每次向数据库申请一批 ID(比如 1000 个,叫一个号段),缓存在本地内存中。发完这 1000 个再去申请下一批。
    • 优点:生成也是纯内存操作,发号器宕机也不影响短期的 ID 生成,ID 是绝对单调递增的。
    • 缺点:依赖中心化数据库,如果数据库彻底挂了就无法生成新的号段。

🌟 面试加分项(实战经验): 如果在微服务中使用了雪花算法生成了 Long 类型的全局 ID 返回给前端,一定要注意 JS 的精度丢失问题。 Java 的 Long 最大值是 $2^{63}-1$,而 JavaScript 中 Number 类型能安全表示的最大整数是 $2^{53}-1$。雪花算法生成的 ID 往往超出了 JS 的安全范围,传到前端后末尾几位会被直接截断变成 000。 解决方案:在 Spring Boot 中配置全局消息转换器(如 Jackson),在序列化时,只要遇到 Long 类型的全局 ID,统统转成 String 字符串再返回给前端。