资讯详情

资讯详情

ShardingJDBC 5.1.1 按月分表实战:分片算法、读写分离与自动创表

简介本资源是面向Java后端开发者的ShardingJDBC 5.1.1实战示例包聚焦按月分库分表、读写分离与自动创表三大核心场景适合已掌握SpringBoot与MyBatis-Plus、希望深入分片中间件的中高级开发者。压缩包共35个文件约45KB以12个java源码与12个class编译文件为主体配合8个xml配置、2个yml配置文件及gitignore等辅助文件完整呈现工程结构与依赖组织。资源基于SpringBoot、MyBatis-Plus与Druid搭建演示了分片规则定义、主从数据源配置及启动时自动建表等关键环节读者可据此快速理解ShardingJDBC 5.1.1的配置方式与运行机制并对照自身业务调整分片策略。目前已有1369人学习下载可作为分库分表方案落地的参考模板。1. 从一次凌晨扩容说起ShardingJDBC 5.1.1 按月分表到底解决了什么去年冬天某公司订单库单表冲到 8000 万行运维半夜两点打电话说磁盘 IO 打满查询从 200ms 飙到 4s。当时第一反应是加索引但业务方反馈“最近三个月的数据查得慢半年前的几乎没人看”。这句话点醒了我——数据冷热分层才是根因而不是索引。后来我们用 ShardingJDBC 5.1.1 做了一套按月分库分表 读写分离 自动创表的方案把单表拆成按月命名的物理表热数据留在主库冷数据归档到历史库查询耗时回到 150ms 以内。这套 demo 就是当时方案的完整还原包含分片算法、读写分离配置、定时自动建表三块核心内容。适合正在做订单、日志、账单类业务被单表膨胀折磨的后端同学。如果你还在用WHERE create_time BETWEEN硬扛千万级数据这篇可以照着复现。2. 分片规则怎么定按月分表的算法选型与配置落地2.1 为什么选按月而不是按年或按天分片粒度直接决定后续运维成本。按年分单表一年可能还是几千万行没解决根本问题按天分一年 365 张表UNION查询和元数据管理会让人崩溃。按月是折中点单表控制在 300 万到 500 万行一年 12 张表三年 36 张SHOW TABLES还能一眼看完。常见做法是订单、流水类业务按月日志类按周账单类按季度。这个 demo 选按月是因为它覆盖了大多数中小规模业务的舒适区。分片键的选择同样关键。我们用的是order_time字段而不是order_id。原因很简单业务查询 90% 都带时间范围分片键和查询条件对齐才能命中分片裁剪。如果拿order_id做分片键按时间查就得扫全部分片分库分表就白做了。这里有个血泪经验分片键一旦上线后期改造成本极高选之前先拉一份慢查询日志看WHERE后面跟得最多的是哪个字段。2.2 标准分片算法的配置写法ShardingJDBC 5.1.1 支持STANDARD、COMPLEX、HINT、INLINE几种分片算法。按月分表用STANDARD配合自定义PreciseShardingAlgorithm和RangeShardingAlgorithm最稳。下面是我实际项目里抽出来的配置application.yml里直接抄spring: shardingsphere: datasource: names: ds0,ds1 ds0: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3306/order_db0?useSSLfalse username: root password: root ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3306/order_db1?useSSLfalse username: root password: root rules: sharding: tables: t_order: actual-data-nodes: ds$-{0..1}.t_order_$-{2024..2025}$-{01..12} database-strategy: standard: sharding-column: order_time sharding-algorithm-name: db-month table-strategy: standard: sharding-column: order_time sharding-algorithm-name: table-month sharding-algorithms: db-month: type: CLASS_BASED props: strategy: STANDARD algorithmClassName: com.demo.sharding.MonthDbShardingAlgorithm table-month: type: CLASS_BASED props: strategy: STANDARD algorithmClassName: com.demo.sharding.MonthTableShardingAlgorithmactual-data-nodes里的$-{2024..2025}是行表达式表示 2024 到 2025 两年$-{01..12}是月份。注意这里写的是202401到202512的物理表后缀不是2024_01因为 MySQL 表名里下划线多了容易和分片符混淆。database-strategy和table-strategy都指向order_time保证库和表用同一个分片键避免跨库笛卡尔积。2.3 自定义分片算法的实现细节配置文件只是壳真正干活的是MonthTableShardingAlgorithm。下面这段代码处理精确查询和范围查询两种情况public class MonthTableShardingAlgorithm implements PreciseShardingAlgorithmLocalDateTime, RangeShardingAlgorithmLocalDateTime { private static final DateTimeFormatter FORMATTER DateTimeFormatter.ofPattern(yyyyMM); Override public String doSharding(CollectionString availableTargetNames, PreciseShardingValueLocalDateTime shardingValue) { // 精确查询order_time 2024-06-15 10:00:00 String suffix shardingValue.getValue().format(FORMATTER); for (String tableName : availableTargetNames) { if (tableName.endsWith(suffix)) { return tableName; } } throw new IllegalArgumentException(未找到匹配表: suffix); } Override public CollectionString doSharding(CollectionString availableTargetNames, RangeShardingValueLocalDateTime shardingValue) { // 范围查询order_time BETWEEN 2024-01-01 AND 2024-06-30 LocalDateTime lower shardingValue.getValueRange().lowerEndpoint(); LocalDateTime upper shardingValue.getValueRange().upperEndpoint(); ListString result new ArrayList(); LocalDateTime cursor lower.withDayOfMonth(1); while (!cursor.isAfter(upper)) { String suffix cursor.format(FORMATTER); for (String tableName : availableTargetNames) { if (tableName.endsWith(suffix)) { result.add(tableName); } } cursor cursor.plusMonths(1); } return result; } }doSharding的精确版本只返回一张表范围版本返回区间内所有月份的表。这里有个容易翻车的点availableTargetNames是配置里actual-data-nodes展开后的全集如果查询时间超出配置范围比如查 2026 年会直接抛异常。我一般会在算法里加一层兜底超出范围时返回最后一张表并打 warn 日志而不是让请求挂掉。参数lowerEndpoint和upperEndpoint在BETWEEN查询里是闭区间和也走这个分支和会由 ShardingJDBC 自动调整边界不用自己处理。3. 读写分离与自动创表两个容易被低估的工程点3.1 读写分离配置与主从延迟的应对分库分表解决容量读写分离解决并发。ShardingJDBC 5.1.1 的读写分离规则和分片规则可以叠加使用配置顺序是先分片再读写分离。下面是在application.yml里追加的部分spring: shardingsphere: rules: readwrite-splitting: >// 写后立即读强制走主库避免主从延迟导致查不到刚插入的数据 try (HintManager hintManager HintManager.getInstance()) { hintManager.setWriteRouteOnly(); return orderMapper.selectById(orderId); }setWriteRouteOnly()让当前线程的查询走主库用完自动关闭。我一般只在“插入后立刻按 ID 查详情”这种场景用普通列表查询还是走从库不然读写分离就失去意义了。如果业务对延迟极度敏感可以在从库上开semi-sync复制但那是 DBA 的活应用层用 Hint 兜底就够了。3.2 自动创表的定时任务与 DDL 执行按月分表最大的运维痛点是每个月 1 号零点如果新表没建好插入直接报Table doesnt exist。手动建表迟早会忘所以自动创表是刚需。思路很简单定时任务提前一个月创建下个月的表用CREATE TABLE IF NOT EXISTS保证幂等。Component public class AutoCreateTableTask { Autowired private DataSource dataSource; // 每月 25 号凌晨 2 点执行提前为下个月建表 Scheduled(cron 0 0 2 25 * ?) public void createNextMonthTable() { LocalDateTime nextMonth LocalDateTime.now().plusMonths(1); String suffix nextMonth.format(DateTimeFormatter.ofPattern(yyyyMM)); for (int i 0; i 2; i) { String dbName order_db i; String tableName t_order_ suffix; String sql String.format( CREATE TABLE IF NOT EXISTS %s.%s LIKE %s.t_order_template, dbName, tableName, dbName); try (Connection conn dataSource.getConnection(); Statement stmt conn.createStatement()) { stmt.execute(sql); log.info(自动建表成功: {}.{}, dbName, tableName); } catch (SQLException e) { log.error(自动建表失败: {}.{}, dbName, tableName, e); } } } }这段代码有几个细节值得说。第一用LIKE t_order_template复制表结构模板表里预置好索引和字段比手写CREATE TABLE不容易漏。第二CREATE TABLE IF NOT EXISTS保证重复执行不报错定时任务重试也安全。第三dataSource注入的是原生数据源不是 ShardingJDBC 的ShardingSphereDataSource因为 DDL 语句走分片路由会找不到逻辑表。第四cron 表达式选 25 号而不是 1 号留出几天缓冲万一任务失败还有时间人工补。参数plusMonths(1)决定提前量如果业务量增长快可以改成plusMonths(2)一次建两个月。提示自动创表任务建议加监控告警建表失败时发通知。我见过因为数据库连接池耗尽导致建表静默失败月初插入全挂的案例。4. 避坑与排查五个真实翻车现场4.1 分片键不在查询条件里导致全路由现象一条SELECT * FROM t_order WHERE user_id 123执行了 8 秒日志显示扫描了 24 张表。原因user_id不是分片键ShardingJDBC 无法裁剪只能广播到所有分片。解决要么在查询里补上order_time范围要么建一张user_id到order_time的映射表先查映射再查订单。我一般要求所有订单查询必须带时间范围产品经理提需求时就得说清楚。4.2 跨库 JOIN 静默返回错误结果现象t_order和t_user关联查询结果少了三分之一。原因t_order分库分表t_user单表ShardingJDBC 不支持跨库 JOIN默认走笛卡尔积后内存合并数据量大时结果不可预期。解决把t_user冗余到订单库或者拆成两次查询在应用层组装。这个坑最阴险的地方是不报错只是结果不对测试环境数据少时发现不了。4.3 分布式主键冲突现象插入订单时报Duplicate entry 1234567890 for key PRIMARY。原因用了数据库自增主键分库后两个库各自从 1 开始必然冲突。解决换成SNOWFLAKE算法配置里加key-generatorsspring: shardingsphere: rules: sharding: key-generators: snowflake: type: SNOWFLAKE props: worker-id: 1然后在表配置里指定key-generate-strategy: {column: order_id, key-generator-name: snowflake}。worker-id每个实例必须不同集群部署时用机器 IP 末位或注册中心分配。4.4 自动创表时区错乱现象12 月 25 号跑任务建出来的表是t_order_202401但业务期望t_order_202312。原因服务器时区是 UTCLocalDateTime.now()比北京时间晚 8 小时plusMonths(1)跨年计算偏移。解决统一用ZoneId.of(Asia/Shanghai)取时间或者 JVM 启动参数加-Duser.timezoneAsia/Shanghai。这个坑平时不显眼跨年那几天集中爆发。4.5 读写分离下事务内读不到写数据现象Transactional方法里先insert再select查不到刚插入的记录。原因事务未提交从库自然看不到即使提交了主从延迟也可能导致从库还没同步。解决事务内强制走主库用HintManager.setWriteRouteOnly()或者把读写分离的read-data-source-names在事务方法上临时置空。我一般建议事务方法里所有查询都走主库事务外再走从库。5. 进阶技巧分片元数据校验与压测验证配置写完只是开始上线前必须验证分片规则是否按预期工作。我习惯用ShardingSphere的SQLParser做元数据校验写一个单元测试遍历所有分片确认表名和库名映射正确Test public void testShardingRoute() { // 模拟 2024 年 6 月的订单查询 String sql SELECT * FROM t_order WHERE order_time BETWEEN 2024-06-01 AND 2024-06-30; // 通过 ShardingSphere 的 SQLRouteEngine 解析路由结果 // 断言路由到 ds0.t_order_202406 和 ds1.t_order_202406 assertThat(routeUnits).hasSize(2); assertThat(routeUnits).allMatch(u - u.getTableName().endsWith(202406)); }这个测试的价值在于配置里的行表达式写错一个数字启动不会报错但查询会路由到不存在的表。单元测试能在 CI 阶段拦住这类低级错误。参数routeUnits是路由后的数据单元集合数量应该等于分片数乘以命中月份数。压测验证同样不能省。我用 JMeter 模拟 500 并发分别测单月查询、跨月查询、跨库查询三种场景。单月查询 TPS 能到 3000 以上跨月查询因为要合并 3 张表结果TPS 降到 1200 左右跨库查询最差只有 600。这个数据帮我们确定了分片键的边界跨月查询控制在 3 个月内跨库查询尽量避免。如果业务必须跨库就在应用层做异步并行查询用CompletableFuture把多个分片的查询并发执行再合并结果。还有一个容易被忽略的点是分片表的索引。每张月表都要有order_time和order_id的联合索引否则分片裁剪后单表查询还是慢。我一般用pt-online-schema-change给存量表加索引新表直接在模板表里建好。从那以后我每次上线分片方案都强制走一遍“单元测试校验路由 JMeter 压测三种场景 检查每张表索引”的流程再也没出现过月初插入失败或者查询扫全表的事故。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →