ARTICLE DETAIL

资讯详情

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

MySQL + ShardingSphere 分库分表实践:原理、配置与踩坑记录

MySQL + ShardingSphere 分库分表实践:原理、配置与踩坑记录 分库分表这四个字听起来挺吓人实际上就是把原本存在一张表里的数据按照某种规则拆到多个库、多张表里去。这几年我接手过的项目里因为数据量暴涨、单表撑不住而被迫搞分库分表的不在少数。用 MySQL 做底层存储再用 ShardingSphere 做中间层来承接分片逻辑是目前 Java 技术栈里比较省心的一套组合。这篇内容就是基于 MySQL ShardingSphere 的分库分表落地记录把核心原理、配置细节、实操步骤和踩过的坑一次讲清楚给打算搞或者正在搞分库分表的同学做个参考。1. 分库分表到底解决什么问题单库单表的容量天花板1.1 单表为什么扛不住从 B 树到锁竞争先聊一个最基础的问题为什么一张表的数据量大了之后读写会变慢MySQL 的 InnoDB 引擎用的是 B 树索引结构数据量越大B 树的层数就越高。三层 B 树能存储的记录数大概也就是百万到千万级别因为普通非叶子节点的索引项占用空间有限。一旦数据量涨到几千万甚至上亿索引深度增加每次查询要走更多磁盘 IO性能自然往下掉。这还只是单次查询的情况。更麻烦的是写入竞争。一张表同一时刻只能有一个写锁或者行锁但范围也会扩大当业务流量大起来insert、update 在这些行锁上排队延迟就会飙升。再加上 binlog 同步、主从复制延迟、备份恢复时间变长等一系列连锁反应单库单表的架构很快就到了瓶颈。很多人会把分区表和分库分表搞混。分区表解决的也是大表问题但它还是在同一个 MySQL 实例里只是把数据按照一定规则放在不同的物理分区文件中。分区的本质依然受限于单机 CPU、内存、磁盘 IO 的上限而且分区键的选择受限、全局唯一索引也不太好处理。分库分表不一样它是把数据物理地分散到多个库、多个表甚至多台服务器上从根本上分摊压力。1.2 分库分表带来哪些能力又引入哪些问题分库分表的核心收益有三点第一单表数据量可控索引层级不再失控查询性能稳定第二写入被分散到多张表、多个库锁竞争大幅降低第三资源可以横向扩展扛不住的时候加机器就行而不是把单机配置拉到顶。但收益和代价是并存的。分库分表之后原本一条 SQL 就能完成的事情现在可能要被拆成多条 SQL 再合并结果。跨库 JOIN 基本废了分布式事务成本变高分页排序要做二次聚合全局主键不能再用数据库自增这些全是新的麻烦。所以我一直有个观点分库分表不是银弹它是在单表确实扛不住的情况下才做的技术决策。如果你的数据量还在百万级别索引建得合理、慢查询都优化过那老老实实先把单表优化做好比盲目上分库分表要靠谱得多。判断是否要分库分表的几个信号我这里整理一下单表行数超过两千万且查询性能明显劣化。写入并发高行锁竞争导致业务超时。数据增长趋势明确未来一年内可能翻倍。单实例的磁盘、CPU、内存已经出现周期性高峰。如果命中两条以上就可以认真考虑分库分表了。如果只是偶尔一条 SQL 慢那大概率是索引问题别把分库分表当退路该优化的先优化。2. ShardingSphere 核心概念与选型思路2.1 两种形态ShardingSphere-JDBC 和 ShardingSphere-Proxy 怎么选ShardingSphere 是 Apache 下的一个开源生态目前最常用的组件是 ShardingSphere-JDBC 和 ShardingSphere-Proxy。ShardingSphere-JDBC 是一个轻量级 Java 框架以 jar 包的形式存在直接在应用层完成分片路由、SQL 改写、结果归并应用通过 DataSource 连接就可以使用代码里基本无感知。因为它在应用进程内执行所以性能损耗比较小部署也简单业务方只需要引入依赖、配置规则、重启应用就能生效。ShardingSphere-Proxy 则是一个独立部署的代理服务对应用来说就是一个普通的 MySQL 数据库。应用不需要改造连上代理就行。Proxy 的优点是支持异构语言、隔离性强、 DBA 可以直接用 SQL 操作适合团队里有多个语言栈、不方便改应用的场景。缺点是多了一层网络转发性能有一些损耗而且需要额外部署和维护一套服务。我个人的经验是如果是 Java 技术栈统一的新项目优先选 ShardingSphere-JDBC因为它跟 Spring Boot 集成很顺滑架构简单直接排查问题也容易。如果你们是多语言混用或者 DBA 想直接上手管理分片那考虑 Proxy但要对性能损耗有心理准备。两种形态的对比列个表格更直观对比项ShardingSphere-JDBCShardingSphere-Proxy部署方式应用内集成 jar独立部署代理服务应用改造需要引入依赖、改数据源配置客户端零改造当 MySQL 直连性能损耗较低进程内计算较高多一层网络转发支持语言Java任意 MySQL 客户端运维复杂度低随应用发布中需要独立运维适合场景新项目、Java 统一技术栈存量系统改造、多语言混用2.2 分片设计逻辑表、数据节点、分片键和分片算法理解 ShardingSphere必须先理解几个核心概念。第一个是逻辑表。比如你定义了t_order作为逻辑表它在真实库里并不存在实际存在的是t_order_0、t_order_1、t_order_2、t_order_3这些物理表。业务 SQL 写的是逻辑表名ShardingSphere 在运行时把 SQL 改写成真实表的 SQL这个过程叫 SQL 改写。第二个是数据节点。数据节点是逻辑表对应真实物理表的位置用表达式描述比如ds$-{0..1}.t_order_$-{0..3}表示分布在ds0、ds1两个库上每个库各 4 张分片表总共 8 张表。这个写法是 ShardingSphere 特有的$-{...}行表达式初始化时会自动展开成实际节点列表。第三个是分片键。分片键用来决定一条记录最终落到哪个物理表比如订单表用order_id做分片键用户表用user_id做分片键。分片键的选择极其重要它直接决定了查询能否精确定位到唯一分片。选分片键的原则是主查询条件里的高频字段而且要保证数据分布均匀。第四个是分片算法。最常见的分片算法有三种取模法order_id % 4简单粗暴但数据规模增大需要扩容时取模的基数一变大量数据要迁移。哈希取模先把分片键做哈希再取模可以让字符串类型的分片键分布更均匀。范围分片比如按时间段、按区域分片适合有明显分区维度的数据但对访问热点不敏感的数据容易歪斜。选哪种算法取决于业务模型。订单表、流水表这类数据增长快、又常按主键查询的用取模或哈希取模很顺手。时间维度明显的日志数据用范围分片更自然。另外还有两个容易被忽略的概念绑定表和广播表。绑定表是指分片规则一致的一组表比如t_order和t_order_detail都按order_id分片关联查询时 ShardingSphere 会把两个表的路由结果绑定到同一分片避免笛卡尔积式的跨库 JOIN。广播表则是所有库都保有一份相同数据的表比如配置表、字典表适合这种全量数据需要随时 JOIN 的场景。3. 实操基于 ShardingSphere 的分库分表落地3.1 环境准备版本匹配是第一道坎先说环境选择。我这次用的是 MySQL 8.0.28、JDK 8、Spring Boot 2.7.x、ShardingSphere-JDBC 5.1.2。这套组合在项目里跑得很稳。如果你用的是 Spring Boot 3.x需要对应选择 ShardingSphere 5.3 版本因为 Spring Boot 3 基于 Jakarta EE老的 starter 可能会不兼容。Maven 依赖长这样dependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc-core-spring-boot-starter/artifactId version5.1.2/version /dependency引入依赖之后还需要注意 JDBC 驱动。MySQL 8 对应的驱动类名已经变成了com.mysql.cj.jdbc.Driver不再是老的com.mysql.jdbc.Driver。如果是从 MySQL 5.x 迁移过来的项目这个坑很容易踩到启动的时候直接报无法加载驱动类。连接 URL 这里有个细节。MySQL 8 默认开启了 SSL 认证如果你本地的 MySQL 服务器没配 SSL 证书连接时会报类似SSL connection error的错误。在开发环境里我在 JDBC URL 上追加了关闭 SSL 的参数来解决jdbc:mysql://localhost:3306/db0?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8注意生产环境建议还是把 SSL 打开别为了省事丢掉安全。开发环境关掉只是图个方便。3.2 一份能跑起来的分库分表配置实例这里结合我实际的项目场景订单系统单表数据增长太快需要把订单表拆成 2 个库每个库 4 张表总共 8 张物理表。YAML 配置如下spring: shardingsphere: datasource: names: ds0,ds1 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/ds0?useSSLfalseserverTimezoneAsia/Shanghai username: root password: root123 ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/ds1?useSSLfalseserverTimezoneAsia/Shanghai username: root password: root123 rules: sharding: tables: t_order: actual-data-nodes: ds$-{0..1}.t_order_$-{0..3} table-strategy: standard: sharding-column: order_id sharding-algorithm-name: order_inline key-generate-strategy: column: order_id key-generator-name: order_snowflake sharding-algorithms: order_inline: type: INLINE props: algorithm-expression: t_order_$-{order_id % 4} key-generators: order_snowflake: type: SNOWFLAKE props: sql-show: true这段配置拆开来看actual-data-nodes定义了物理表的位置ds$-{0..1}展开为ds0、ds1t_order_$-{0..3}展开为t_order_0到t_order_3。table-strategy指定了分片键是order_id分片算法名称指向order_inline。sharding-algorithms里定义的是 INLINE 算法表达式为t_order_$-{order_id % 4}。这里有个容易踩的坑如果我对两个库各 4 张表做表级分片那么每个库内部都是order_id % 4。但如果库级也做了分片表达式还会更复杂后面细说。key-generate-strategy配置了雪花算法生成分布式主键。因为订单表的主键不能再用 MySQL 自增分库分表后各分片表自增 ID 会重复。ShardingSphere 的 SNOWFLAKE 会生成全局唯一主键。库级分片和表级分片要区分清楚。我这里的配置只在表级别做了分片两个库只是水平复制关系所有数据按order_id % 4散落到每个库的 4 张表里。也就是说ds0.t_order_0和ds1.t_order_0里都有数据查询时路由结果分别落到两个库。如果你设计的场景是“库分片 表分片”两层那actual-data-nodes和库级分片策略都要配套定义配置复杂度会翻倍。3.3 分片键和分片算法的选择到底该怎么算清楚分片算法不是随便写的选错了后面扩容、查询全是坑。拿取模法来说核心参数就是取模的基数也就是分片总数。我在上面示例中使用的是order_id % 4意思是按订单 ID 的末两位落到 4 张表里。为什么是 4 而不是 8 或者 16因为当前数据量评估后每张表未来一年的数据量大约是 500 万行这个体量对 MySQL 单表来说是舒服的区间。于是分片总数 预估总数据量 / 单表目标容量 2000 万 / 500 万 4 个分片。这里要注意一个很经典的细节取模分片的下标是从 0 开始的order_id % 4的取值范围是 0、1、2、3对应t_order_0到t_order_3所以分片总数和物理表数量必须严格对应。INLINE 表达式看似简单但数据类型和写法有个隐藏问题。如果分片键是字符串类型直接用%可能报错。比如用手机号分片不要直接写mobile % 4而是先取哈希再取模。ShardingSphere 里的 HASH_MOD 算法就是干这个的sharding-algorithms: user_inline: type: HASH_MOD props: sharding-count: 4HASH_MOD会把分片键先用 MD5 或一致性哈希处理再对sharding-count取模字符串类型的分布效果要好很多。除了分片算法分片键还有一个容易忽略的点业务 SQL 如果不带分片键ShardingSphere 会把这条 SQL 广播到所有数据节点去执行。听起来好像也能查到但数据量一大全表扫描的成本直接爆炸。所以在设计表结构时就要把业务查询路径理清楚让核心查询都强制带上分片键。比如订单表按order_id分片那按用户查订单这种需求就一定要配套维护一个“用户-订单映射表”或者用 ES 这类搜索引擎来承接。3.4 读写分离、分布式 ID 和分布式事务的配套方案分库分表上线一段时间后读压力也会跟着上来这时候通常还要配上读写分离。ShardingSphere 的读写分离配置和分库分表规则是并列的定义在主库和从库的数据源之上指定负载均衡策略即可。一个简化配置的思路如下在datasource里配置write-ds和read-ds两组数据源然后在rules下新增readwrite-splitting规则把write-ds作为主库read-ds作为从库查询请求默认走从库。这个机制能有效分担查询压力但要注意主从同步延迟问题刚写入的数据如果立刻查可能查不到。关键业务场景可以通过hint强制路由主库。分布式 ID 方面ShardingSphere 内置了 SNOWFLAKE 算法生成分布式主键。雪花算法的好处是全局唯一、趋势递增、性能高生成的 ID 是 Long 类型。但有个使用细节雪花算法生成的 ID 末尾有 4 个连续数字位MySQL 的 BIGINT 可以承载但如果你用 JavaScript 处理这类 ID因为 JS 的 Number 精度只有 53 位会丢失精度所以前后端交互要用字符串方式传递 ID。分布式事务是另一个大话题。ShardingSphere 支持三种事务模式Local Transaction默认模式只保证单个分片内的局部事务跨分片不保证原子性。XA 事务基于两阶段提交协议适合跨分片强一致性要求高的场景缺点是性能损耗明显事务时间长。BASE 事务集成 Seata适合追求最终一致性的长事务场景对业务侵入小吞吐量更好。我的建议是核心账务类操作用 XA业务链路过长、有消息补偿机制的用 Seata。不要把分布式事务当万能尽量通过设计避免分布式事务比如把同一用户的多笔操作路由到同一个分片上这样大部分写操作其实都是本地事务根本不需要跨分片协调。4. 常见问题与排查技巧实录4.1 启动报错、驱动不兼容和 SSL 连接的坑先列我踩过的几个启动期问题。第一个是Cannot load driver class: com.mysql.jdbc.Driver。原因就是 MySQL 8 的驱动类名变了ShardingSphere 配置里的driver-class-name必须写com.mysql.cj.jdbc.Driver。这一点看似简单但项目里如果多个数据源混用很容易漏改。第二个是 SSL 连接错误。MySQL 8 默认开了 SSL如果你没有为服务器配置证书连接时报错信息会很长核心是SSL connection error或者Public Key Retrieval is not allowed。MySQL 8 的 caching_sha2_password 认证插件下还会出现Public Key Retrieval is not allowed的报错这也是很多人拿到新 MySQL 8 就连接报错的原因。解决方法是在 JDBC URL 加allowPublicKeyRetrievaltrue。安全起见开发环境可以useSSLfalse测试和生产还是建议正常配置 SSL。第三个是行表达式解析出错。配置里写了ds$-{0..1}但加载时报表达式格式错误。这种问题多半是缩进或引号问题YAML 里$-{...}的表达式不要加多余的引号写完配置后先确认表达式能独立解析再启动。4.2 不带分片键查询导致的全路由风暴这是运行时最高危的问题。某个查询条件没有包含分片键ShardingSphere 只能把 SQL 发给所有分片去执行然后归并结果。在分片少的时候问题不大一旦分片数到了 8 个、16 个一个不带分片键的查询就是一次小型全库扫描而且全部串行或并行执行数据库压力瞬间飙升。我遇到过一例运营后台按订单状态查询订单列表条件里只传了status没传order_id结果每次执行都走全路由直接把从库打到了 100% CPU。排查方法很简单开启sql-show: true在日志里看 SQL 的路由结果发现每条查询都路由到了全部 8 张表。解决办法有几个业务查询必须限定允许不带分片键的场景后台低频率查询可以接受但高频接口绝对不行。为高频的按用户查询场景建立“用户-订单映射表”先把分片键查出来再带分片键去查订单表。使用绑定表或广播表减轻 JOIN 场景下的全局路由压力。另外还可以用 ShardingSphere 的hint强制指定分片这种手动控制路由的方式在特殊业务里很管用。4.3 分页排序聚合查询的坑分页问题在分库分表之后非常典型。假设执行SELECT * FROM t_order ORDER BY create_time LIMIT 100000, 10ShardingSphere 的逻辑是每个分片都取前 100010 条然后在内存里归并排序再取最终的 10 条。分片越多、偏移量越大内存和 CPU 的浪费成倍增长。我踩过最深的一次运营后台翻页到第 5000 页接口直接超时数据库内存撑到接近上限。应对策略有几个限制翻页深度加上截止条件比如只能查最近 30 天、只能翻前 200 页。使用“延迟关联”技巧先查询出主键 ID再用主键回表查询详细信息减少传输量。如果业务有实时翻页需求建议在上层引入搜索引擎或者做宽表冗余不要用 MySQL 分库分表去硬扛深度分页。聚合查询也有类似的问题。COUNT(*)、SUM()这类操作会在每个分片执行一次然后归并汇总。如果分片键字段本身和聚合维度不一致结果可能不准。所以设计分片键时也要把你最核心的聚合统计维度考虑进去尽量让高频统计能在单个分片内完成。4.4 扩容和数据迁移为什么说配分片方案要想清楚后路最后聊一下扩容。这是分库分表最难受的一个环节。如果你用的是取模算法分片总数从 4 扩到 8所有数据的分布位置基本全部变化比如原来order_id % 4落在t_order_1的记录现在对 8 取模可能落在t_order_1、t_order_5等不同的表里。这意味着大量数据需要迁移而且迁移过程中还要保证新旧数据保持一致。可选的迁移方案有几种停机迁移维护时间段内停服把数据按新规则重新灌入简单但影响业务。双写迁移新旧两套库同时写迁移历史数据完成后切读流量过程冗长但不停服。工具迁移用 Canal 监听 binlog把增量数据同步到新库历史数据用批量任务搬完后再校验。不管用哪种方案提前规划分片策略都特别重要。如果业务还处于早期数据量增长可控我建议直接用一致性哈希思路或者至少预留分片数的倍数关系比如从 4 扩到 8 时用 4 的倍数扩展这样老的 4 张表映射到新的 8 张表时可以保持一类映射规律迁移成本会小很多。根据我个人的实际感受分库分表不是一次性的改造而是一个需要持续打磨的技术底座。最早可以先小规模试点比如只拆两个库每库拆两张表把分片键、分布式 ID、事务边界、SQL 规范全跑通积累几个月的真实数据和日志再决定要不要扩大分片规模。我在实际项目里最深的体会是分库分表最大的难点从来不是 ShardingSphere 的配置怎么写而是业务上的分片键设计是否合理。分片键一旦定下来后面对查询路径、扩容策略、数据迁移的影响是深远的。另外sql-show这个配置建议一直开着至少在测试阶段千万不要关它能直观看到每条 SQL 被路由到哪些分片排查全路由、笛卡尔积关联、分布式聚合问题都靠它。最后再分享一个小技巧如果你刚接手一个没做过数据分层的项目又评估下来确实需要分库分表别一上来就追求 16 分片、64 分片这种大规格。先把分片总数设成 4 或者 8物理机不够就用同一实例多个库先顶着把链路、监控、告警、数据校验脚本全部准备好等到业务量真的逼近了再平滑扩容。能稳定跑起来的方案比看起来酷炫的方案要值钱得多。
返回列表