写点什么

分片键选错了,你的数据库分片就是"灾难现场"!

  • 2025-08-08
    湖北
  • 本文字数:2242 字

    阅读完需:约 7 分钟

一、开场白:分片键,数据库分片的"命门"

还记得第一次做数据库分片时,我天真地以为随便选个字段当分片键就行了。结果上线后,数据分布严重不均,有的分片撑爆了,有的分片闲得发慌。


今天我们就来聊聊,分片键到底该怎么选?路由规则又该怎么设计?这些坑,我踩过,你也别踩了!



二、分片键选择,真的不是随便选选

1. 什么是分片键?

分片键就是决定数据分配到哪个分片的字段。比如用户表按user_id分片,订单表按order_id分片,这就是分片键。


错误示范:


-- 按创建时间分片,结果数据严重倾斜CREATE TABLE orders (    id BIGINT,    user_id BIGINT,    create_time TIMESTAMP,    -- 其他字段) SHARD BY create_time;  -- 大错特错!
复制代码


正确做法:


-- 按用户ID分片,数据分布相对均匀CREATE TABLE orders (    id BIGINT,    user_id BIGINT,    create_time TIMESTAMP,    -- 其他字段) SHARD BY user_id;  -- 这样才对!
复制代码



三、分片键选择的"黄金法则"

1. 高基数原则:选择值域范围大的字段

为什么?


  • 基数越高,数据分布越均匀

  • 避免数据倾斜,防止单分片过载


好例子:


  • user_id:用户 ID,基数高,分布均匀

  • order_id:订单 ID,基数高,分布均匀

  • device_id:设备 ID,基数高,分布均匀


坏例子:


  • status:状态字段,通常只有几个值,分布极不均匀

  • gender:性别字段,只有 2 个值,分片效果极差

  • create_date:日期字段,容易造成时间热点

2. 业务关联原则:选择查询频繁的字段

为什么?


  • 避免跨分片查询,提升查询性能

  • 减少网络开销,降低延迟


场景分析:


-- 按user_id分片,查询用户订单很快SELECT * FROM orders WHERE user_id = 123;
-- 按order_id分片,查询用户订单需要跨分片SELECT * FROM orders WHERE user_id = 123; -- 慢!
复制代码

3. 稳定性原则:选择变化频率低的字段

为什么?


  • 避免频繁的数据迁移

  • 减少分片维护成本


好例子:


  • user_id:用户 ID,一旦分配很少变化

  • device_id:设备 ID,相对稳定


坏例子:


  • last_login_time:最后登录时间,频繁变化

  • status:状态字段,经常变化



四、分片键选择的"翻车现场"

场景 1:按时间分片,结果数据严重倾斜

某电商平台按create_time分片,结果:


  • 最近 3 个月的数据占 90%

  • 历史数据分片几乎空着

  • 查询最近订单时,单分片压力爆表


解决方案:


-- 改为按user_id分片SHARD BY user_id;
-- 或者使用复合分片键SHARD BY (user_id, create_time);
复制代码

场景 2:按状态分片,查询性能极差

某订单系统按order_status分片:


  • 待支付订单:分片 1

  • 已支付订单:分片 2

  • 已完成订单:分片 3


结果查询某个用户的全部订单需要跨 3 个分片,性能极差。


解决方案:


-- 改为按user_id分片SHARD BY user_id;
-- 或者使用复合分片键SHARD BY (user_id, order_status);
复制代码



五、路由规则设计,这些坑你一定要避开

1. 哈希路由:最常用的方案

原理:


// 简单的哈希路由int shardIndex = Math.abs(userId.hashCode()) % shardCount;
复制代码


优点:


  • 数据分布相对均匀

  • 实现简单,性能好


缺点:


  • 无法支持范围查询

  • 分片数量变化时,数据迁移量大

2. 范围路由:适合有序数据

原理:


// 范围路由示例if (userId >= 1 && userId <= 1000000) {    return shard0;} else if (userId > 1000000 && userId <= 2000000) {    return shard1;}// ...
复制代码


优点:


  • 支持范围查询

  • 数据迁移量小


缺点:


  • 容易造成数据倾斜

  • 需要预估数据分布

3. 列表路由:适合枚举值

原理:


// 列表路由示例Map<String, Integer> statusShardMap = new HashMap<>();statusShardMap.put("pending", 0);statusShardMap.put("paid", 1);statusShardMap.put("completed", 2);
复制代码


优点:


  • 实现简单

  • 适合状态类字段


缺点:


  • 数据分布可能不均匀

  • 扩展性差



六、实战案例:电商订单系统分片设计

需求分析:

  • 订单表数据量大,需要分片

  • 主要查询:按用户查询订单

  • 次要查询:按订单 ID 查询

  • 需要支持范围查询(时间范围)

分片方案设计:

方案 1:按 user_id 分片(推荐)


CREATE TABLE orders (    id BIGINT,    user_id BIGINT,    order_no VARCHAR(32),    create_time TIMESTAMP,    status VARCHAR(20),    -- 其他字段) SHARD BY user_id;
复制代码


优点:


  • 用户查询性能极佳

  • 数据分布均匀

  • 支持用户维度的事务


缺点:


  • 按订单 ID 查询需要广播


方案 2:复合分片键


-- 按(user_id, create_time)分片SHARD BY (user_id, create_time);
复制代码


优点:


  • 支持时间范围查询

  • 数据分布更均匀


缺点:


  • 实现复杂

  • 路由计算开销大



七、分片键选择的"终极指南"

1. 选择顺序:

  1. 优先选择查询条件中的字段

  2. 选择基数高的字段

  3. 选择变化频率低的字段

  4. 考虑业务增长趋势

2. 常见场景推荐:

用户相关表:


  • 分片键:user_id

  • 原因:查询频繁,基数高,稳定


订单相关表:


  • 分片键:user_id(user_id, create_time)

  • 原因:按用户查询为主,支持时间范围


商品相关表:


  • 分片键:category_idbrand_id

  • 原因:按分类查询,数据分布相对均匀


日志相关表:


  • 分片键:(user_id, create_time)device_id

  • 原因:支持时间范围查询,数据量大

3. 避坑指南:

❌ 不要这样做:


  • 按时间字段单独分片

  • 按状态字段分片

  • 选择基数很低的字段

  • 选择频繁变化的字段


✅ 要这样做:


  • 选择业务主键作为分片键

  • 考虑查询模式

  • 预估数据增长趋势

  • 设计合理的路由规则



八、总结

分片键选择是数据库分片设计的核心,选错了就是"灾难现场"。


记住这三点:


  1. 高基数 + 业务关联 + 稳定性 = 好的分片键

  2. 路由规则要简单高效,避免过度设计

  3. 分片设计要考虑未来 3-5 年的业务增长


最后提醒:分片键一旦选定,修改成本极高。设计时一定要深思熟虑,宁可多花时间设计,也不要上线后再改!




关注服务端技术精选,获取更多后端实战干货!


你在分片键选择上踩过哪些坑?欢迎在评论区分享你的故事!

用户头像

个人博客: http://jiangyi.cool 2019-03-10 加入

公众号:服务端技术精选 欢迎大家关注!

评论

发布
暂无评论
分片键选错了,你的数据库分片就是"灾难现场"!_数据库_我爱娃哈哈😍_InfoQ写作社区