分库分表之策略

大家好,我是工藤学编程 一个正在努力学习的小博主,期待你的关注
实战代码系列最新文章 C++实现图书管理系统(Qt C++ GUI界面版)
SpringBoot实战系列 【SpringBoot实战系列】Sharding-Jdbc实现分库分表到分布式ID生成器Snowflake自定义wrokId实战
环境搭建大集合 环境搭建大集合(持续更新)
分库分表 分库分表之数据库分片分类

前情摘要:

1、数据库性能优化
2、分库分表之优缺点分析
3、分库分表之数据库分片分类

MySQL水平分库分表策略:RANGE范围拆分详解

一、RANGE范围拆分的核心逻辑

RANGE拆分是按数据主键的取值范围将数据划分到不同分表/分库中,本质是通过区间分段实现数据分片。其核心规则是定义连续的区间段,每个区间对应一个分片,典型应用场景包括:

  • 日志类业务(如交易日志、操作日志):数据按时间顺序产生,且历史数据查询频率低,新数据访问频繁。
  • 时效性强的业务(如订单、活动记录):需优先保证近期数据的查询性能,历史数据可归档。
  • 数据增长可预测的场景:如按月/年生成的数据报表,可预先规划分库分表区间。
  • 空间维度业务(如多地域数据、云服务器节点数据):需按物理位置或服务节点划分数据,实现本地化访问与故障隔离。

二、RANGE拆分的具体实现方案

1. 自增ID范围分表案例

以订单表为例,基于自增IDorder_id进行RANGE拆分:

分片规则:
table_1: order_id ∈ [1, 1000000)
table_2: order_id ∈ [1000000, 2000000)
table_3: order_id ∈ [2000000, 3000000)
...
table_n: order_id ∈ [(n-1)*1000000, n*1000000)

建表示例

CREATE TABLE `order_01` (
  `order_id` bigint(20) NOT NULL COMMENT '订单ID(1-100万)',
  `user_id` bigint(20) NOT NULL,
  `create_time` datetime NOT NULL,
  -- 其他字段
  PRIMARY KEY (`order_id`)
) ENGINE=InnoDB CHARSET=utf8;

CREATE TABLE `order_02` (
  `order_id` bigint(20) NOT NULL COMMENT '订单ID(100万-200万)',
  `user_id` bigint(20) NOT NULL,
  `create_time` datetime NOT NULL,
  -- 其他字段
  PRIMARY KEY (`order_id`)
) ENGINE=InnoDB CHARSET=utf8;
2. 时间范围分库方案

按时间粒度(年/月/日)拆分数据库,适用于日志、交易等有时间序列特征的数据:

分库规则:
log_2024: 存储2024年全年日志
log_2023: 存储2023年全年日志
log_2022: 存储2022年全年日志

核心SQL路由逻辑

// 根据日志时间确定分库
String year = dateFormat.format(logTime);
String dbName = "log_" + year;
3. 空间范围分库分表方案

按物理位置、云服务器节点或地域属性拆分,实现数据本地化存储:

分库规则(按地域):
db_beijing: 华北地区数据(order_id ∈ [1, 1000万))
db_shanghai: 华东地区数据(order_id ∈ [1000万, 2000万))
db_guangzhou: 华南地区数据(order_id ∈ [2000万, 3000万))

分表规则(按服务器节点):
table_node1: 服务器A数据(order_id % 3 = 0)
table_node2: 服务器B数据(order_id % 3 = 1)
table_node3: 服务器C数据(order_id % 3 = 2)

核心路由逻辑(地域+ID复合)

// 1. 按地域划分大区间
String region = getRegionByUserIp(userIp);
int regionBaseId = regionMap.get(region); // 华北=0,华东=1000万,华南=2000万

// 2. 按ID范围分库
long orderId = ...;
int dbIndex = (int)((orderId - regionBaseId) / 10000000);
String dbName = "db_" + region + "_" + dbIndex;

三、RANGE拆分的三大核心优势

  1. 扩容无数据迁移成本

    • 新增分片只需定义新区间(如table_n+1对应[n*100万, (n+1)*100万)),无需移动历史数据。
    • 案例:当table_10写满后,直接创建table_11并配置区间,新数据自动写入新表。
  2. 规则简单易维护

    • 无需复杂算法,仅需维护区间映射关系,开发与运维成本低。
    • 适用场景:中小团队或初期项目,快速落地分库分表。
  3. 历史数据归档便捷

    • 可按时间范围将冷数据(如3年前日志)迁移至低成本存储,热数据(近1年)保留在高性能集群。
    • 空间场景延伸:按地域归档数据,如将海外用户数据迁移至本地合规存储节点。

四、不可忽视的四大应用痛点

问题类型 具体表现 影响程度
数据热点集中 最新区间(如table_n)或热点地域(如北上广分库)承担90%以上读写请求,导致单表/单库IO瓶颈 ★★★★★
资源利用率低 历史区间(如table_1)或非热点地域分库数据极少被访问,硬件资源浪费 ★★★☆☆
跨分片查询复杂 范围查询(如order_id > 500万)或跨地域统计需扫描多个分片,性能随分片数增加而下降 ★★★☆☆
区间边界设计困难 区间大小难以预估:过小导致分片数激增,过大导致单表数据量再次超限;空间维度中地域流量不均衡可能导致分库负载失衡 ★★★★☆

Hash取模分库分表策略:均匀分布与扩容挑战详解

一、Hash取模的核心原理与实现逻辑

Hash取模是通过对分片字段(如用户ID、订单ID)进行哈希计算后取模,将数据均匀分配到不同库表中。其核心逻辑是利用哈希函数的离散性,确保数据在分片节点上的均衡分布,典型应用场景包括:

  • 用户维度强相关业务(如用户中心、订单系统):需保证同一用户数据集中在同一库表;
  • 高并发且数据访问均匀的场景(如社交平台消息表):避免单节点热点集中。

二、Hash取模的具体实现方案

1. 整数型字段直接取模案例

以用户ID分库分表为例,目标是拆分为2个库,每个库包含4张表(共8张表):

分片规则:
库路由:db_idx = userId % 2  
表路由:table_idx = (userId / 2) % 4  
注:为什么table_idx不直接(userId ) % 4 呢?
这种方式会导致每个数据库会存储所有表的一部分数据,并且不同数据库里相同表索引的表所存数据是不一样的。这将可能导致数据不均匀的情况。
示例验证
假设用户 ID 为偶数(如 2、4、6):
策略table_idx =(userId ) % 4:这些 ID 会被分到不同的表中,例如 2 → 表 2,4 → 表 0,6 → 表 2。
策略table_idx = (userId / 2) % 4:这些 ID 会被分到同一张表,例如 2 → 表 1,4 → 表 2,6 → 表 3。
可以看到,我们提出的策略将会使得数据更加均匀
直接取模的缺陷
userId % 4 的结果仅由 ID 末两位决定(二进制角度),当 ID 按奇偶分段(如分 2 库)时,偶数 ID 的末两位可能集中在 00、10 等模式,导致取模结果偏向特定值(如策略一中偶数 ID%4 结果多为 0 或 2)。
先除再取模的原理
将 ID 除以 2(userId / 2)相当于将 ID 范围「压缩」一半,例如:
原 ID 范围:2,4,6,8,10,12 → 压缩后:1,2,3,4,5,6
压缩后的数值对 4 取模,等价于将原 ID 范围按每 8 个 ID(2 库 ×4 表)为一组重新分段,每组内的 ID 会均匀分布到 4 张表:
组 1(ID=2-9):压缩后 1-4 → %4 得 1,2,3,0
组 2(ID=10-17):压缩后 5-8 → %4 得 1,2,3,0

数据路由示例

userId db_idx(库) table_idx(表) 目标库表
1 1%2=1 (1/2)=0 → 0%4=0 db_1.table_0
2 2%2=0 (2/2)=1 → 1%4=1 db_0.table_1
8 8%2=0 (8/2)=4 → 4%4=0 db_0.table_0
9 9%2=1 (9/2)=4 → 4%4=0 db_1.table_0

建表示例

-- 库1(db_1)中的表
CREATE TABLE `user_0` ( ... );  -- table_idx=0
CREATE TABLE `user_1` ( ... );  -- table_idx=1
CREATE TABLE `user_2` ( ... );  -- table_idx=2
CREATE TABLE `user_3` ( ... );  -- table_idx=3

-- 库2(db_2)中的表
CREATE TABLE `user_0` ( ... );  -- table_idx=0
CREATE TABLE `user_1` ( ... );  -- table_idx=1
CREATE TABLE `user_2` ( ... );  -- table_idx=2
CREATE TABLE `user_3` ( ... );  -- table_idx=3
2. 非整数型字段哈希取模方案

当分片字段为字符串(如手机号、邮箱)时,需先通过哈希函数转为整数再取模:

// 手机号分库分表示例
String phone = "13800138000";
int hashCode = phone.hashCode();  // 转为哈希值
int dbIdx = Math.abs(hashCode) % 2;  // 库路由
int tableIdx = Math.abs(hashCode) / 2 % 4;  // 表路由

哈希函数选择建议

  • 轻量级场景:使用Java原生hashCode()或Pythonhash()
  • 高并发场景:采用MurmurHashFNVHash等高性能哈希算法,降低哈希冲突概率。

三、Hash取模的核心优势

  1. 数据均匀分布,避免热点集中

    • 通过哈希取模,数据在各库表间的分布偏差可控制在5%以内。
    • 案例:1000万用户ID取模分8张表,每张表数据量在124万-126万之间,无明显倾斜。
  2. 单用户数据聚合,查询效率高

    • 同一用户的所有数据(如订单、信息)集中在同一库表,避免跨库JOIN。
    • 场景:查询用户订单时,仅需访问单个库表,无需聚合多节点数据。
  3. 规则简单,适合高并发场景

    • 路由逻辑可在应用层快速计算,无需查询配置表,响应时间稳定在1ms以内。

四、不可忽视的扩容痛点与解决方案

问题类型 具体表现 影响程度
扩容需全量数据迁移 从N库扩至2N库时,需重新计算所有数据的%2N,并迁移至新库 ★★★★★
迁移期间服务波动 全量迁移需停机或读写分离,可能导致分钟级至小时级服务不可用 ★★★★☆
哈希冲突风险 不同字段哈希后取模结果相同(如手机号1380013800013900139000哈希值相同) ★★☆☆☆

Hash取模作为最常用的分库分表策略,通过简单规则实现数据均匀分布,尤其适合用户维度强相关的高并发场景。尽管存在扩容成本高的缺点,但通过复合拆分策略与中间件优化,可在数据均衡性与可扩展性之间找到平衡点,是互联网业务落地分库分表的首选方案之一。

你可能感兴趣的:(分库分表,数据库,后端,spring,boot)