Skip to content

分库分表

单机数据库的性能瓶颈是必然的。随着数据量增长,查询变慢、磁盘空间不足、备份恢复耗时增加。分库分表是突破单机限制的主流方案,通过将数据分散到多个数据库实例,实现水平扩展。

垂直与水平

垂直分库

垂直分库按业务功能拆分,将不同业务的表分散到不同的数据库中。例如将用户表、订单表、商品表分别放在独立的数据库中。这种方式的优势是业务隔离、职责清晰、便于独立扩展和迁移。劣势是跨库查询和事务变得复杂,原本的单库 JOIN 需要在应用层处理。

垂直分库的判断标准是业务耦合度。如果一组表经常一起查询、一起修改,应该放在同一个库中。如果表之间几乎没有关联查询,可以拆分到不同库。垂直分库的粒度没有标准答案,取决于业务复杂度和团队规模,常见的粒度是按微服务的边界划分。

水平分表

水平分表将同一张表的数据按照某种规则分散到多个表或库中。例如按用户 ID 取模,将用户数据分散到 16 个表中。这种方式的优势是突破单表数据量限制,查询和写入分散到多个库,性能线性扩展。劣势是路由逻辑复杂、跨分片查询和事务需要特殊处理。

水平分表的判断标准是数据量。单表数据量超过 1000 万行或单表数据量超过 100GB,就应该考虑水平分表。分表数量不是越多越好,过多的分片会增加管理复杂度和跨片查询的概率。16、32、64 是常见的分片数量,可以根据业务增长逐步扩展。

分库与分表

分库还是分表取决于瓶颈是单机资源还是单表数据量。如果瓶颈是 CPU、内存、磁盘 I/O,需要分库,每个库部署在不同的机器上。如果瓶颈是单表数据量导致的查询慢,可以只分表不分库,多个表在同一个库中。分库比分表复杂度更高,需要考虑分布式事务、数据一致性等问题。

分片策略

取模分片

取模分片是最简单的分片策略,计算分片键的哈希值对分片数取模,决定数据属于哪个分片。例如 shard_id = hash(user_id) % 16。这种方式数据分布均匀,扩展性差。增加分片需要数据迁移,大部分数据需要重新分配。

取模分片适合分片数量固定的场景,或者可以接受数据迁移的业务。考虑到迁移成本,初期可以规划足够的分片数量,例如 32 或 64 个分片,避免短期内需要扩展。分片数量最好是 2 的幂次,方便路由计算和后续扩容。

范围分片

范围分片按照分片键的值范围分配分片。例如按日期范围,每个月的数据放在一个分片;按 ID 范围,0-1000 万在第一个分片,1000-2000 万在第二个分片。范围分片的优点是范围查询效率高,只需要查询相关分片。缺点是数据分布不均,可能导致某些分片数据量远大于其他分片。

范围分片适合有明显时间或顺序特征的场景,例如日志、订单、交易记录。这些场景的查询通常是按时间范围查询,范围分片可以减少跨片查询。数据不均衡的问题可以通过定期分裂大分片、合并小分片来解决。

一致性哈希

一致性哈希将分片键和分片节点都映射到哈希环上,数据属于环上顺时针最近的节点。这种方式解决了取模分片扩展时需要大量数据迁移的问题,增加或删除节点只影响相邻节点的数据。

一致性哈希的实现方式包括虚拟节点、哈希环分段。虚拟节点是每个物理节点映射多个虚拟节点,增加虚拟节点数量可以让数据更均匀分布。哈希环分段是将环分成多个段,每个段由一个节点负责。

地理位置

地理位置分片按照用户的地理位置分配分片,例如华北、华东、华南的用户数据分别放在对应地区的数据中心。这种方式的优势是数据就近访问,延迟低,符合数据合规要求。劣势是用户跨地区访问时需要跨片查询,数据分布受地理位置限制。

地理位置分片适合有明确地区业务、对延迟敏感的应用。例如电商、内容分发、社交网络,根据用户的 IP 或注册地选择分片。

路由算法

客户端路由

客户端路由是应用层根据分片键计算目标分片,然后直接连接对应数据库。这种方式实现简单,无中间层性能损耗。劣势是路由逻辑分散在各个应用中,修改分片规则需要更新所有应用。

客户端路由的典型实现是 ShardingSphere-JDBC、MyCat 客户端模式。应用配置分片规则,由中间件库拦截 SQL 并路由到对应分片。这种方式适合简单场景,如果分片规则复杂、有大量应用接入,建议使用代理路由。

代理路由

代理路由部署独立的中介服务,应用连接代理,代理根据 SQL 和分片规则路由到后端数据库。这种方式的优势是路由逻辑集中,修改分片规则只需要更新代理配置。劣势是增加了一层网络跳转,有性能损耗。

代理路由的典型实现是 MyCat、ShardingSphere-Proxy、Vitess。代理解析 SQL,提取分片键,计算目标分片,重写 SQL,发送到对应数据库,最后合并结果返回给应用。代理路由适合复杂场景和多应用接入。

路由算法实现

最常用的路由算法是哈希取模,shard_id = hash(key) % N。哈希算法可以使用 CRC32、MurmurHash,保证分布均匀。对于字符串分片键,可以先计算哈希值再取模。对于复合分片键,可以将多个字段组合后计算哈希值。

范围路由使用二分查找,将分片键值与分片范围比较,找到目标分片。需要维护分片范围的有序列表,查询时快速定位。

一致性哈希路由实现复杂,可以使用已有的库如 Ketama。哈希环的节点数量是 2^32,分片键和节点 ID 都映射到环上,顺时针查找最近的节点。

跨分片查询

结果合并

跨分片查询需要从多个分片获取数据并合并结果。查询条件包含分片键时,可以路由到单一分片,性能好。查询条件不包含分片键时,需要广播到所有分片,然后合并结果,性能差。

结果合并分为三种:简单合并、排序合并、聚合合并。简单合并将多个分片的结果直接拼接,例如 SELECT * FROM orders WHERE status = 1,每个分片返回部分结果,应用层合并。排序合并需要先在每个分片排序,然后应用层归并排序,例如 SELECT * FROM orders ORDER BY created_at。聚合合并需要先在每个分片聚合,然后应用层再次聚合,例如 SELECT COUNT(*) FROM orders,先在每个分片计数,然后求和。

聚合函数的处理比较复杂,COUNT、SUM 可以通过二次聚合实现,AVG 需要先求和再除以总数,MAX/MIN 需要比较所有分片的最值。DISTINCT 需要去重,可以使用布隆过滤器减少数据传输。

分页优化

跨分片分页是经典难题。例如查询第 1000000 页,每页 10 条,需要从每个分片查询 10000010 条数据,然后去重、排序、取前 10 条,效率极低。

优化方案有几种:第一种是禁止深分页,限制分页深度。第二种是使用游标分页,记住上一页最后一条记录的位置,查询时从该位置继续,例如 WHERE id > last_id ORDER BY id LIMIT 10。第三种是使用二次查询法,先在每个分片查询前 N 条,合并后取前 M 条,不足再补充。

sql
-- 游标分页示例
SELECT * FROM orders
WHERE shard_key = ? AND id > ?
ORDER BY id
LIMIT 10;

分布式主键

UUID

UUID 是 128 位的唯一标识符,生成简单无需协调。UUID 的优势是全局唯一、无单点、分布式友好。劣势是长度 36 字符,占用存储空间大;无序写入导致索引页分裂,影响插入性能;UUID 包含机器信息,存在隐私问题。

数据库自增

单机数据库的自增 ID 不能直接用于分布式场景,因为不同实例的自增范围会冲突。解决方案是设置不同的自增起始值和步长,例如实例 1 从 1 开始步长 2,实例 2 从 2 开始步长 2。这种方式实现简单,但扩展性差,新增实例需要修改已有实例的配置。

雪花算法

雪花算法是 Twitter 开源的分布式 ID 生成算法,生成 64 位长整型 ID。结构是:1 位符号位 + 41 位时间戳 + 10 位机器 ID + 12 位序列号。时间戳精确到毫秒,机器 ID 支持最多 1024 个实例,序列号每毫秒最多生成 4096 个 ID。

雪花算法的优势是 ID 递增、高性能、无需持久化。劣势是依赖机器时钟,时钟回拨会导致 ID 重复。解决方案是记录上次时间戳,如果检测到时钟回拨,拒绝生成 ID 或等待时钟追上。

Leaf 算法

Leaf 是美团开源的分布式 ID 方案,提供两种模式:Leaf-segment 和 Leaf-snowflake。Leaf-segment 预分配 ID 号段,从数据库获取号段缓存在本地,用完再获取下一个号段。这种方式性能高,但 ID 不连续。Leaf-snowflake 基于 ZK 生成唯一的工作机器 ID,解决时钟回拨问题。

数据迁移

停机迁移

停机迁移是最简单的方式,步骤是:停止应用写入,导出全量数据,按新分片规则拆分数据,导入到新分片,修改应用配置指向新分片,启动应用。这种方式业务中断时间长,适合可以接受短时间停机的场景。

双写迁移

双写迁移步骤:第一步,新分片部署完成,应用同时写入旧分片和新分片。第二步,数据校验,比较新旧分片数据一致性。第三步,逐步切换读流量到新分片。第四步,确认新分片稳定后,停止写入旧分片。这种方式业务无中断,但双写期间逻辑复杂,需要处理新旧分片的不一致。

历史数据迁移

历史数据迁移是指从单库迁移到多库,需要解决三个问题:数据拆分、数据一致性、增量同步。数据拆分按照新分片规则将数据分配到各分片,可以通过脚本或工具实现。数据一致性通过校验机制保证,迁移后对比新旧分片的数据行数、校验和、采样内容。增量同步在迁移过程中继续处理写入,可以通过记录迁移位置、binlog 同步、双写等方式实现。

分库分表是数据库水平扩展的必经之路,但也是复杂度的显著增加。在选择分库分表之前,应该先确认是否可以通过优化 SQL、增加索引、升级硬件等方式解决问题。分库分表是最后的手段,不是首选方案。