ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MySQL 分库分表讲透:什么时候该拆、怎么拆、以及那些绕不开的坑

MySQL 分库分表讲透:什么时候该拆、怎么拆、以及那些绕不开的坑 个人主页 for_ever_love__ 欢迎各位大佬莅临其他栏目: 大模型开发从0到1 其他栏目: iOS项目总结大全 其他栏目: 我想学python了 其他栏目: iOS UI 文章目录MySQL 分库分表讲透什么时候该拆、怎么拆、以及那些绕不开的坑一、先别急着拆能不拆就不拆二、什么时候真的该拆三、两种拆法3.1 垂直拆分按字段/业务拆3.2 水平拆分按行拆四、分片键怎么选典型例子订单表解决办法一基因法解决办法二映射表异构索引解决办法三双写 / 异构存储五、分片算法5.1 取模最常用5.2 一致性哈希5.3 范围分片5.4 按时间分片归档场景常用扩容问题的现实解预留分片六、拆完之后这些操作变麻烦了6.1 跨分片查询6.2 跨分片排序 / 分组6.3 跨分片 JOIN6.4 分布式事务6.5 全局唯一 ID七、中间件怎么选八、实施步骤稳妥路线九、小结MySQL 分库分表讲透什么时候该拆、怎么拆、以及那些绕不开的坑分库分表是核武器——威力大代价也大。本篇讲清楚什么信号出现时该考虑拆、垂直拆分和水平拆分的差别、分片键怎么选、分片算法怎么定以及拆完之后那些原本简单现在变得很麻烦的操作该怎么办。一、先别急着拆能不拆就不拆分库分表会引入巨大的复杂度。在拆之前先确认这些手段都用过了优化手段成本能撑到什么量级SQL 优化 索引低—读写分离主从中读压力大幅缓解缓存Redis中挡掉大部分热点读归档历史数据低单表行数降一个数量级升级硬件 / 上 SSD低—分区表Partition低单表千万级查询有规律分库分表高亿级最容易被忽略的一招归档。很多单表 5000 万行的库其实 90% 是半年前的历史数据业务只查最近三个月。把这些冷数据迁到归档表/数据仓库主表立刻降到 500 万行完全不用分库分表。-- 定期归档INSERTINTOorders_archiveSELECT*FROMordersWHEREcreated_at2025-07-01;DELETEFROMordersWHEREcreated_at2025-07-01LIMIT5000;-- 分批删二、什么时候真的该拆硬指标经验值不是绝对值信号阈值单表数据量 2000 万行或 10 GBB 树层级变深IO 增加写压力主库写入 QPS 接近上限主从延迟追不上单库连接数经常接近max_connections磁盘/IO单机 IOPS 打满⚠️ 关键认知单表行数不是唯一标准B 树高度才是本质。InnoDB 默认页 16 KB一个 3 层 B 树大约能存2000 万行。超过之后变成 4 层每次查询多一次 IO性能就有可感知的下降。如果单行数据很大比如带 TEXT 字段这个数字会低很多。三、两种拆法3.1 垂直拆分按字段/业务拆垂直分库按业务把表拆到不同库单库orders / users / products / logs ↓ 订单库orders 用户库users 商品库products 日志库logs垂直分表把大字段拆到单独的详情表-- 主表热字段查询频繁CREATETABLEproduct(idBIGINTPRIMARYKEY,nameVARCHAR(100),priceDECIMAL(10,2),stockINT);-- 详情表大字段访问少CREATETABLEproduct_detail(product_idBIGINTPRIMARYKEY,descriptionTEXT,-- 大字段spec_json JSON);好处主表变窄一个数据页能放更多行缓存命中率上升。垂直拆分优先于水平拆分——它的复杂度低得多而且往往就能解决问题。3.2 水平拆分按行拆把一张表的数据按规则分散到多个库/表orders (5000万行) ↓ orders_0 orders_1 orders_2 orders_3 (每个1250万行)分片键Sharding Key是核心决策后面详说。四、分片键怎么选这是分库分表最重要的一步选错了后面全盘皆输。选择标准查询频率最高大多数查询都带这个条件数据分布均匀避免数据倾斜某个分片特别大不会频繁变更分片键改了要迁移数据典型例子订单表订单的查询场景用户查自己的订单WHERE user_id ?最高频商家查店铺订单WHERE shop_id ?运营按订单号查WHERE order_no ?如果按user_id分片✅ 用户查自己订单只需查 1 个分片❌ 商家查店铺订单要查所有分片再聚合跨片查询❌ 按订单号查不知道在哪个分片也要全扫解决办法一基因法把user_id的基因嵌入order_no这样按订单号查也能路由到正确分片# 生成订单号时把 user_id 的分片信息编进去defgen_order_no(user_id,shards4):sharduser_id%shardsreturnf{shard}{user_id:08d}{int(time.time()*1000)%100000:05d}# ↑ 第一位就是分片号defroute_by_order_no(order_no):returnint(order_no[0])# 直接取第一位解决办法二映射表异构索引-- 维护一张 order_no → user_id 的映射表CREATETABLEorder_route(order_noVARCHAR(32)PRIMARYKEY,user_idBIGINT);按订单号查时先查映射表拿到 user_id再路由。代价是多一次查询但映射表很小可以全缓存。解决办法三双写 / 异构存储同一个数据按不同维度存两份订单库按user_id分片服务 C 端查询ES / 另一套库按shop_id分片服务 B 端查询这是大厂的标准做法代价是要维护数据一致性。五、分片算法5.1 取模最常用sharduser_id%4# 分 4 个库tableuser_id%16# 每库 16 张表✅ 数据分布均匀❌扩容麻烦从 4 个扩到 8 个几乎所有数据要重分布5.2 一致性哈希# 把节点和数据都映射到 0~2^32 的环上# 扩容时只需迁移 1/N 的数据✅ 扩容只需迁移少量数据❌ 需要虚拟节点才能分布均匀5.3 范围分片id 1~1000万 → 分片 0 id 1000万~2000万 → 分片 1✅ 扩容简单新数据放新分片❌容易热点新数据都在最后一个分片5.4 按时间分片归档场景常用orders_202601 orders_202602 orders_202603✅ 天然冷热分离历史数据好归档❌ 跨月查询要 union扩容问题的现实解预留分片一开始就分远超当前需要的分片数现在需要 4 个库 → 一开始就分 16 个库部署在 4 台机器上每台 4 个库 扩容时把部分库迁移到新机器逻辑分片数不变数据不用重分布这是最实用的扩容方案避免了取模扩容的数据迁移噩梦。六、拆完之后这些操作变麻烦了6.1 跨分片查询-- 原本SELECT*FROMordersWHEREstatuspaidORDERBYcreated_atDESCLIMIT20;-- 分库后要在每个分片各查 20 条然后在应用层归并排序归并逻辑defquery_all_shards(sql):results[]forshardinshards:rowsshard.query(sql)# 每个分片都查 LIMIT 20results.extend(rows)results.sort(keylambdar:r[created_at],reverseTrue)returnresults[:20]# 应用层再排序取前 20⚠️注意深分页问题LIMIT 100000, 20在每个分片上都要扫 10 万行聚合起来就是 N × 10 万。分库分表下深分页基本不可用必须改成上一页最大 ID的游标方式。6.2 跨分片排序 / 分组ORDER BY、GROUP BY、COUNT(*)都要在应用层归并。COUNT可以各分片 count 后相加但AVG、DISTINCT就不能简单相加了。6.3 跨分片 JOIN原则能不 JOIN 就不 JOIN。常见做法字段冗余把user_name直接冗余到订单表避免 JOIN users应用层拼装先查订单再批量查用户代码里组装广播表小表广播字典表、配置表在每个分片都存一份全量这样 JOIN 可以在单分片内完成6.4 分布式事务跨分片的数据修改无法用本地事务保证。方案说明最终一致 消息队列最常用业务上接受短暂不一致TCCTry-Confirm-Cancel业务侵入大XA / 2PC数据库支持但性能差很少用SeataAT 模式阿里开源 retrofit 较小现实建议尽量设计成不跨分片的事务。比如把订单和订单明细用同一个分片键让它们在同一个分片里本地事务就够了。6.5 全局唯一 ID分库后自增 ID 会冲突。几种方案方案说明雪花算法Snowflake64 位时间戳 机器ID 序列号推荐UUID简单但太长索引性能差无序数据库号段单独一张表批量分配号段有单点风险Redis INCR简单但依赖 RedisclassSnowflake:简化版雪花算法def__init__(self,machine_id):self.machine_idmachine_id self.seq0self.last_ms0defnext_id(self):msint(time.time()*1000)ifmsself.last_ms:self.seq(self.seq1)0xFFFifself.seq0:# 序列号用尽等下一毫秒whilemsself.last_ms:msint(time.time()*1000)else:self.seq0self.last_msmsreturn(ms22)|(self.machine_id12)|self.seq⚠️ 雪花算法的两个坑时钟回拨会导致 ID 重复要做保护机器 ID 必须全局唯一通常用 ZK/配置中心分配七、中间件怎么选中间件类型特点ShardingSphere-JDBC客户端 Jar无中间件层性能好但只支持 JavaShardingSphere-Proxy独立服务支持多语言多一跳网络MyCat独立服务国产老牌社区活跃度下降TDDL / TDSQL阿里/腾讯云产品开箱即用选型的现实考量纯 Java 技术栈→ ShardingSphere-JDBC性能最好多语言→ ShardingSphere-Proxy用云数据库→ 直接用云厂商的分布式数据库PolarDB、TDSQL它们把分库分表做成了存储层的事业务无感知这是个重要趋势如果用云优先选分布式数据库而不是自己分库分表。PolarDB、TDSQL、OceanBase 这类产品在存储层解决扩展问题业务代码不用改省掉了上面所有的复杂度。八、实施步骤稳妥路线1. 评估确认是否真的需要拆先试归档、索引、缓存 2. 选型分片键 分片算法 中间件 3. 双写新旧库同时写新库用于验证 4. 迁移历史数据分批导入新库 5. 校验对比新旧库数据一致性 6. 灰度小流量切到新库观察 7. 切读全量读切到新库 8. 停双写停止写旧库 9. 下线旧库保留一段时间再删每一步都要能回滚。特别是第 3 步的双写——它是整个迁移的安全垫新库出问题随时切回旧库。九、小结先别急着拆归档历史数据往往就能解决成本低一个数量级硬指标单表 2000 万行 / 10 GB或写 QPS 到上限垂直拆分优先拆业务、拆大字段复杂度远低于水平拆分分片键选最高频的查询条件否则大量跨片查询跨维度查询的解法基因法 / 映射表 / 异构双写扩容最实用的是预留分片逻辑分片数不变只迁移物理位置拆完之后跨片查询要应用层归并、深分页基本不可用、JOIN 靠冗余或广播表、事务尽量设计成不跨片全局 ID 用雪花算法注意时钟回拨用云数据库就别自己拆选 PolarDB / TDSQL 这类分布式数据库到这里 MySQL 六篇就完整了索引、事务隔离、锁与死锁、慢查询、EXPLAIN、主从复制、分库分表。这七块索引是之前那篇覆盖了从单条 SQL 优化到架构扩展的完整链路。
返回列表