首页 / 资讯中心 / 文章详情

SpringCloud Alibaba分库分表实战:ShardingSphere核心设计与避坑指南

SpringCloud Alibaba分库分表实战:ShardingSphere核心设计与避坑指南 ★ FEATURED ARTICLE
1. 分库分表到底在解决什么问题1.1 单库单表的性能天花板在哪里很多团队在项目初期都是单库单表一路跑到底业务量小的时候完全没问题查询快、维护简单、开发也省心。但数据量一旦上来问题就会集中爆发。我经历过的一个典型场景是一张订单表跑到三千万行左右日常的列表查询开始出现明显延迟带分页的复杂条件查询甚至要好几秒。加了索引之后好了一阵但数据继续涨索引本身也变得越来越重写入性能跟着往下掉。这里面的核心矛盾在于单表数据量越大B树索引的层级越深磁盘IO次数越多。同时单库的连接数、CPU、内存、磁盘IO都是有限的所有读写压力都压在一个实例上迟早会触顶。一般来说业内比较公认的经验值是单表行数超过500万到1000万或者单表数据文件超过2GB就要开始认真考虑分库分表了。当然这不是绝对数字具体还要看行的宽度、查询模式、硬件配置。分库分表要解决的就是这个天花板问题。它把原本集中在一个库、一张表里的数据按照某种规则拆散到多个库、多张表里让每个数据节点只承担一部分压力从而把整体的存储容量和吞吐能力横向扩展出去。1.2 垂直拆分与水平拆分的区别和选择分库分表严格来说分两个维度垂直拆分和水平拆分。垂直拆分是按业务或按字段来切。按业务切就是把订单、用户、商品这些不同业务的数据放到不同的库里这其实就是微服务架构下常见的“每个服务一个库”。按字段切就是把一张宽表里不常用的字段、大字段拆到另一张表比如商品表里把详情描述这种大文本单独拆出去。垂直拆分的优点是业务边界清晰、维护简单缺点是它解决不了单表数据量过大的问题——同一张表的数据还是那么多。水平拆分才是真正解决单表数据量问题的方案。它按照某个分片键比如用户ID、订单ID把同一张表的数据分散到多张结构相同的表里这些表可以分布在同一个库也可以分布到不同的库。水平拆分之后每张表只存一部分数据单表体量下来了查询和写入都能保持在一个稳定的水平。实际项目中这两种拆分往往是配合使用的先按业务垂直拆库再对数据量大的核心表做水平拆分。我在做电商类系统的时候通常会把订单、用户、商品拆到不同的库然后对订单表再按用户ID做水平分片这样既保证了业务隔离又解决了单表膨胀。1.3 什么阶段该动手别等到扛不住这里我要强调一个很多人踩过的坑分库分表一定要提前规划不要等到数据库快撑不住了才动手。因为一旦线上数据量已经很大再做拆分就意味着要迁移海量数据、要改大量SQL、要处理数据一致性问题风险极高往往需要停机或者做复杂的双写方案。我的建议是在项目设计阶段就评估未来一到两年的数据增长趋势。如果预判某张核心表会突破千万级别就提前把分片方案设计好哪怕一开始只用一个分片把分片逻辑预留出来后面扩容会从容很多。这个“预留”的成本远比事后改造要低。2. 分片方案的核心设计思路2.1 分片键怎么选才不踩坑分片键的选择直接决定了整个分库分表方案能不能用、好不好用。选分片键的核心原则是尽量让查询能带上分片键避免跨库跨表扫描。以订单场景为例最常见的分片键是用户ID或者订单ID。如果选用户ID那么“查询某个用户的所有订单”这类请求就能精准定位到一个分片效率很高但“按订单号查询”就会变成跨分片查询因为订单号和用户ID不一定在同一个分片规则下。反过来选订单ID也是类似的问题。所以实际中经常采用基因法或者冗余字段来解决。基因法的思路是生成订单ID的时候把用户ID的一部分“基因”编进订单ID里这样按订单ID分片和按用户ID分片能落到同一个分片两种查询都能精准命中。冗余字段则是把常用的查询条件冗余存储或者建立映射表来辅助路由。提示分片键一旦确定后期修改成本极高因为它涉及到所有历史数据的重新分布。选之前一定要把核心查询场景列全逐一验证。2.2 分片算法的取舍取模、范围还是哈希常见的分片算法有三种取模shard id % shardCount。优点是数据分布均匀缺点是扩容时几乎所有数据都要重新分布迁移量巨大。范围按ID区间或时间区间分片。优点是扩容方便新数据自然落到新分片缺点是容易出现热点比如按时间分片时最新分片压力最大。一致性哈希在扩容时只影响相邻节点迁移量小但实现相对复杂且可能分布不够均匀。我在实际项目里用得最多的是取模配合成倍扩容或者范围分片配合预分片。所谓预分片就是一开始就分成比如1024个逻辑分片物理上先放在少数几个库扩容时把逻辑分片整体搬迁到新库这样不用重新计算每条数据的归属迁移成本可控。2.3 分库还是只分表数量怎么定分几个库、几张表需要结合数据总量和未来增长来算。假设一张订单表预计三年后达到3亿行单表控制在1000万行以内那么理论上需要30张表。如果每个库放10张表那就是3个库。但这里还要考虑数据库实例的承载能力。一个库放太多表连接数、文件句柄、元数据管理都会有压力。一般单库表数量控制在几十到一百张以内比较稳妥。同时分片总数最好是2的幂次或者便于后续翻倍的数方便扩容。我一般的做法是先按当前数据量估算一个基础分片数然后预留一倍到两倍的扩容空间。比如现在需要8个分片那就直接规划成16个初期可以只部署8个物理库每个库挂两个逻辑分片将来扩容时把逻辑分片拆出去即可。3. SpringCloud Alibaba 下的落地实现3.1 技术选型ShardingSphere 与 MyCat 的对比在 SpringCloud Alibaba 体系里做分库分表主流方案有两个Apache ShardingSphere和MyCat。ShardingSphere 是JDBC层面的增强以jar包形式嵌入应用对应用透明支持分库分表、读写分离、数据加密等社区活跃和SpringCloud集成非常顺滑。MyCat是代理层方案应用连MyCat就像连一个普通MySQLMyCat再转发到后端真实库。代理层的好处是应用几乎不用改语言无关缺点是多了网络跳转MyCat本身可能成为瓶颈和单点。我个人的选择倾向是新项目、Java技术栈、追求灵活性和性能优先用 ShardingSphere。它没有额外的网络开销配置灵活而且和SpringCloud Alibaba的Nacos、Sentinel等组件配合得很好。MyCat更适合老系统改造、多语言混布的场景。3.2 引入依赖与基础配置在SpringCloud Alibaba项目里集成ShardingSphere首先在业务模块的pom里引入依赖。以ShardingSphere-JDBC为例dependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc-core-spring-boot-starter/artifactId version5.3.2/version /dependency然后在application.yml里配置数据源和分片规则。这里要注意ShardingSphere会接管数据源所以原来的单数据源配置要改成它自己的格式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/order_db0 username: root password: xxx ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/order_db1 username: root password: xxx rules: sharding: tables: t_order: actual-data-nodes: ds$-{0..1}.t_order_$-{0..1} database-strategy: standard: sharding-column: user_id sharding-algorithm-name: db-inline table-strategy: standard: sharding-column: user_id sharding-algorithm-name: table-inline sharding-algorithms: db-inline: type: INLINE props: algorithm-expression: ds$-{user_id % 2} table-inline: type: INLINE props: algorithm-expression: t_order_$-{user_id % 2}这段配置的意思是t_order表被拆成 ds0.t_order_0、ds0.t_order_1、ds1.t_order_0、ds1.t_order_1 四个物理表分片键是user_id库和表都按 user_id % 2 来路由。3.3 分片算法与路由规则详解上面用的是INLINE表达式算法适合简单的取模场景。但生产环境往往需要更复杂的逻辑比如按时间范围分片、按多个字段组合分片。ShardingSphere提供了多种算法类型算法类型适用场景特点INLINE简单取模、简单表达式配置简单性能好INTERVAL按时间区间分片适合日志、流水类数据HASH_MOD哈希取模分布均匀COMPLEX_INLINE多字段组合分片灵活支持多列HINT_INLINE强制路由用于无法从SQL提取分片键的场景如果内置算法不够用还可以实现StandardShardingAlgorithm接口自定义算法。我做过一个按“用户所属区域用户ID”组合分片的场景就是自定义算法实现的路由时先按区域定位到库再按用户ID定位到表。路由规则的核心逻辑是ShardingSphere解析SQL提取分片键的值然后根据算法计算出目标分片改写SQL中的表名为真实物理表名最后分发到对应数据源执行。对于不带分片键的查询它会广播到所有分片再归并结果这也是为什么前面强调分片键要尽量出现在查询条件里。3.4 分布式主键的生成方案分库分表之后原来的自增主键就失效了因为多个表各自自增会产生重复ID。必须换成分布式主键方案。常见的有雪花算法Snowflake生成64位长整型ID包含时间戳、机器ID、序列号趋势递增性能高。ShardingSphere内置了雪花算法配置一下就能用。号段模式从数据库批量取一段ID缓存在内存里用完再取减少数据库交互。Redis自增利用Redis的INCR生成简单但依赖Redis可用性。我在项目里最常用的是雪花算法配置如下spring: shardingsphere: rules: sharding: tables: t_order: key-generate-strategy: column: order_id key-generator-name: snowflake key-generators: snowflake: type: SNOWFLAKE props: worker-id: 1注意雪花算法依赖机器时钟如果服务器时钟回拨可能生成重复ID。生产环境要开启NTP时间同步并选择对时钟回拨有处理的实现版本。4. 实操过程中的关键环节与避坑4.1 数据迁移怎么做才安全存量数据迁移是分库分表里最惊心动魄的一步。我的经验是分三步走全量迁移、增量同步、数据校验。全量迁移用工具把老表数据按新分片规则导入新表可以用ShardingSphere的Scaling组件也可以自己写迁移程序。增量同步是在全量迁移期间老表还在写入需要把这段时间的变更同步到新表通常用binlog订阅的方式。数据校验是迁移完成后对比老表和新表的数据一致性抽样或者全量比对。整个过程建议在业务低峰期做并且准备好回滚方案。我踩过的一个坑是迁移时没考虑大事务一次性导入太多数据导致主从延迟飙升影响了线上读请求。后来改成小批量分批导入每批几千行问题就解决了。4.2 跨分片查询与分页的处理不带分片键的查询会广播到所有分片这在数据量大、分片多的时候性能很差。对于必须支持的跨分片查询有几个应对策略建立异构索引表把常用的非分片键查询条件单独建一张索引表用另一种分片规则存储查询时先查索引表拿到主键再回原表查详情。使用Elasticsearch等搜索引擎把需要多维度检索的数据同步到ES复杂查询走ES明细回数据库。限制查询范围产品层面引导用户加上时间范围等条件减少扫描的分片数。分页查询尤其麻烦因为limit offset, size在每个分片上的语义不一样。ShardingSphere会改写为在每个分片取 offsetsize 条然后在内存里归并排序再截取分片多、翻页深的时候内存消耗很大。所以深分页要尽量避免可以用“上一页最大ID”的游标方式来替代offset分页。4.3 分布式事务如何保证一致性跨库操作就涉及到分布式事务。SpringCloud Alibaba体系里有Seata可以配合ShardingSphere使用。Seata的AT模式对业务侵入小通过undo log实现回滚适合大多数场景。配置上需要在ShardingSphere的数据源之上再包一层Seata的代理数据源并在业务方法上标注GlobalTransactional。不过要注意Seata AT模式在分库分表场景下undo log的存储和性能也需要评估高并发下建议用TCC或者可靠消息最终一致性方案来替代。我的经验是能避免跨库事务就避免。通过合理的分片键设计让一个业务操作尽量落在同一个库内从根上减少分布式事务的需求。比如订单和订单明细用同一个分片键就能保证它们落在同一个库本地事务就够了。4.4 扩容时怎么做到平滑迁移业务增长后需要加库加表这是必然的。如果一开始用的是取模算法扩容时数据要大规模重分布非常痛苦。所以我在设计阶段就会考虑扩容友好性。方案一是成倍扩容从2个库扩到4个库从4个扩到8个。这样原来id % 2的数据在id % 4下只会落到原来的库或者对应的新库迁移量减半。方案二是预分片逻辑分片数固定物理库数量可变扩容时搬迁整个逻辑分片。无论哪种方案扩容都要配合双写或者binlog同步保证迁移期间数据不丢。迁移完成后切流量观察一段时间再下线老库。5. 常见问题排查与经验速查5.1 分片后查询变慢的排查思路分片后查询反而变慢通常有几个原因。第一是广播查询SQL里没带分片键导致扫了所有分片。排查方法是打开ShardingSphere的SQL日志看实际执行了几条SQL、路由到了哪些分片。第二是归并排序开销大尤其是order by加limit的深分页。第三是索引没建对分片后的每张物理表都要单独建索引别以为在老表建了就行。我一般的排查顺序是先看SQL日志确认路由结果再看执行计划确认索引使用最后看归并阶段的内存和耗时。5.2 分布式主键重复或乱序问题主键重复多半是雪花算法的worker-id配置冲突多个实例用了同一个worker-id。解决方法是给每个实例分配唯一的worker-id可以借助Nacos或者Redis来分配。主键乱序则是因为雪花算法生成的ID虽然趋势递增但不同机器生成的ID会有交错如果业务对严格递增有要求就得换号段模式。5.3 事务与连接池的典型故障分库分表后一个逻辑操作可能占用多个数据库连接连接池很容易被打满。我遇到过一次线上故障一个批量操作跨了多个分片每个分片都要拿连接结果连接池耗尽整个服务卡死。后来限制了单次批量操作的分片数量并且调大了连接池才稳定下来。另外Seata的全局事务会持有连接直到事务结束高并发下连接占用时间变长也要相应调整连接池配置。常见问题可能原因解决方向查询变慢广播查询、深分页、索引缺失补分片键、改游标分页、逐表建索引主键重复worker-id冲突唯一分配worker-id连接池耗尽跨分片操作过多限制批量分片数、调大连接池数据不一致迁移遗漏、事务未覆盖数据校验、引入分布式事务扩容困难取模算法、无预分片成倍扩容或预分片设计5.4 几个我踩过的坑和对应技巧第一个坑是分片键类型不一致。有一次分片键在配置里是字符串但实际数据是数字导致路由结果和预期不符数据写到了错误的分片。后来统一了类型问题消失。所以分片键的字段类型一定要和算法表达式匹配。第二个坑是DDL变更。分片后加字段要在所有物理表上执行漏一张就出问题。建议用统一的DDL管理工具或者写脚本遍历所有分片执行。第三个坑是统计类查询。count(*)、sum()这类聚合ShardingSphere会在内存里归并数据量大时很吃内存。对于这类需求我一般会单独建汇总表定时任务预计算而不是实时查分片表。第四个坑是测试环境分片数和生产不一致。测试时用2个分片生产用8个结果路由逻辑在两边表现不同上线才发现问题。所以测试环境的分片配置要尽量贴近生产。6. 一些延伸的思考分库分表不是银弹它带来了扩展性也带来了复杂度。在动手之前其实可以先考虑一些成本更低的方案比如先做读写分离把读压力分摊出去比如优化SQL和索引很多时候慢查询是SQL写得不好比如引入缓存把热点数据挡在数据库前面。这些手段都试过了还不够再上分库分表。另外现在也有一些新的选择比如分布式数据库产品它们在底层帮你做了分片应用层感知不到。但这类方案往往有厂商绑定、成本、运维等考量需要结合团队实际情况评估。我个人在实际操作中的体会是分库分表最难的不是技术实现而是前期的方案设计和后期的运维管理。分片键选错、扩容方案没预留、监控没跟上后面都是无尽的麻烦。所以宁可前期多花时间论证把查询场景、数据增长、扩容路径都想清楚再动手写代码。真正落地的时候先用小流量灰度验证路由、事务、性能都符合预期再全量切换。这套流程走下来基本能避开大部分坑。
阅读完成 · 觉得有帮助?
咨询建站