☰
MySQL+MyBatis+ShardingSphere+JDBC:数据访问层分库分表实践指南
2026/9/26 6:09:32 网站建设 项目流程

我最早接触这套组合的时候,其实心里是有点犯嘀咕的。MyBatis 和 JDBC 算是老搭档了,中间再塞一个 ShardingSphere,听起来就像给老房子做加固,总担心哪里敲了承重墙。但等真正把“MySQL + MyBatis + ShardingSphere + JDBC”这一整套跑通之后,我才意识到,这根本不是简单的加固,而是给数据访问层上了一套完整的“指挥系统”。项目里遇到的很多让人头疼的问题——SQL 性能调优、分页插件失效、连接池打满、分库分表后的路由混乱——在这套组合下都有了清晰的解法。这篇文章我就把从头梳理的思路、配置过程中的坑、以及实测下来最稳的方案一次性讲清楚,适合那些正在做 Java 后端、想把数据访问层真正吃透的同行参考。

1. 内容整体设计与思路拆解

1.1 技术组合的定位:谁在负责什么

在聊配置之前,得先把这套组合里每个角色的分工理顺。很多人一上来就急着写配置文件,结果遇到问题时连“该查哪一层”都分不清。

  • JDBC(Java Database Connectivity)是 Java 访问数据库的底层标准接口。它负责最基础的连接管理、SQL 语句执行、结果集处理。没有它,上层一切框架都是空中楼阁。它就像水管工手里的扳手,虽然笨重,但所有流量最终都要经过它。
  • MySQL是存储层,负责把数据真正落盘。它处理事务、索引、锁,是最终的数据归宿。
  • MyBatis是基于 JDBC 的持久层框架,它帮你省去了手工写Connection、PreparedStatement、ResultSet的繁琐过程,让你专注于写 SQL 和结果映射。它并不会绕过 JDBC,而是在 JDBC 外面包了一层“减负壳”。
  • ShardingSphere在这个组合里扮演的是“数据网关”的角色。它拦截你发给 MyBatis 的 SQL,根据分片规则重新改写,然后路由到正确的数据库实例或数据表上。实时业务场景中,分库分表最难的不是“拆”,而是“拆完之后查询怎么办”。ShardingSphere 的价值就在于把“拆”这件事对你透明化。

理解了这个分工,你就能明白:为什么说“MyBatis + ShardingSphere + JDBC”不是简单的叠加,而是各司其职的协同。

1.2 为什么需要 ShardingSphere 这一层

很多人会问:单库单表跑得好好的,为什么要引入 ShardingSphere 这么个重家伙?答案是数据量到了一定规模,单表瓶颈绕不过去。

比如你有一张订单表,日增 100 万行,一年下来就是 3.6 亿行。这时候哪怕你加了再多的索引,写入并发和查询性能都会出现断崖式下跌。MySQL 的 B+ 树索引虽然高效,但索引文件过大之后,内存放不下,磁盘 IO 就成了瓶颈。此时你只有两条路:要么升级硬件(贵且不解决问题),要么拆分数据。

ShardingSphere 支持两种模式:分库(库内分表)和分表(跨库分表)。举个例子,订单表可以按用户 ID 取模分到 4 个库,每个库里再按月份分 12 张表,总共 48 张物理表。对应用层来说,你仍然是在操作一张名为t_order的逻辑表,剩下的工作 ShardingSphere 替你完成了。在数据量持续增长的项目里,这套机制能让你把精力留在业务上,而不是天天关心底层数据应该被存放在哪里。

1.3 这套组合的实际收益与代价

这套组合不是银弹,它有收益也有代价。

收益方面,最直观的是开发效率。你用 MyBatis 写 SQL,依然可以享受到动态 SQL 的灵活性;你用 ShardingSphere,可以把分库分表的复杂度从业务代码里剥离。过去你要在 Service 层里手动判断路由到哪个数据源,现在不需要了。

代价方面,主要集中在连接数消耗和SQL 限制上。分库分表后,一次全库查询可能会被拆成多次执行,连接池压力成倍增长。另外,ShardingSphere 对 SQL 语法有限制,比如子查询涉及多分片时改写逻辑会很复杂,OVER开窗函数也可能不支持。所以这套组合适合那种“读多写少、数据量大、业务规则相对明确”的场景,并不适合所有项目。

2. 核心配置与实操要点

2.1 MySQL 版本选择与安装避坑

在配置整套组合之前,MySQL 版本选择这一步一直被很多人忽视。你最好根据自己的 MySQL 服务端版本,匹配合适的 JDBC 驱动版本。标题里提到的组合,行业里最常见的环境是 MySQL 5.7 或 8.0。

如果是 MySQL 5.7,推荐 JDBC 驱动用mysql-connector-java5.1.49 或 8.0.x(8.0 驱动向下兼容 5.7)。如果是 MySQL 8.0,直接用 8.0.33 这类较新版本即可。注意:不要用 8.0 驱动连接 5.6 或更老的 MySQL,因为 8.0 驱动默认启用了 caching_sha2_password 认证插件,老版本 MySQL 根本认不了。

安装环节最容易踩坑的是初始化和密码策略。MySQL 8.0 初始化后会生成临时密码在日志文件里,很多人没注意到--initialize-insecure这个参数,导致 root 密码始终不对。建议测试环境直接用--initialize-insecure,开发环境刷新权限后自定义密码,生产环境强制配置复杂密码。

我之前在 Linux 服务器上部署 MySQL 时,遇到过Can't connect to local MySQL server through socket '/tmp/mysql.sock'这类经典报错。排查思路很简单:先确认 mysqld 进程是否在跑,再确认 socket 文件位置是否和客户端配置一致。用mysqladmin ping验证服务端状态,用ls -l /tmp/mysql.sock查看 socket 文件。避免在 socket 文件都不存在的情况下反复重启客户端程序浪费时间。

2.2 JDBC 连接串的核心参数:useSSL 与 serverTimezone

网上 MySQL JDBC 连接串五花八门,好多人直接复制粘贴,结果环境一换就原地爆炸。这里我给出一个生产环境验证过的配置模板,并逐一解释每个参数的作用:

jdbc:mysql://127.0.0.1:3306/test_db?useSSL=false&useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true&allowMultiQueries=true
  • useSSL=false:除非你的业务真的需要加密传输,否则建议关掉。开启 SSL 后,每次握手都要做一次证书交换,既增加延迟又消耗 CPU。MySQL 8.0 默认连接串不带useSSL=false,很多开发工具连上后一直提示 SSL 警告,原因就在这里。
  • serverTimezone=Asia/Shanghai:这个必须手动指定。如果你的数据库时区是 CST,而应用服务器时区不同,插入的时间字段就会相差 8 小时。两个时区信息出现错乱,恰好是项目中排查时间数据“无端偏移”的关键点。
  • rewriteBatchedStatements=true:批量插入优化。配合 MyBatis 的ExecutorType.BATCH,可以让 JDBC 驱动将多条 INSERT 合并成一条多值 INSERT 发送到数据库,性能提升以倍数计。
  • allowMultiQueries=true:允许一个语句中写多个用分号分隔的 SQL。这个开关只在 MyBatis 执行复杂初始化脚本时用到,要注意它放大了 SQL 注入风险,生产环境非必要不开启。

有一点需要说明,MySQL Connector/J 8.x 中设置useSSL=false已经足够了,不再需要sslmode那样的参数。如果使用 PG 数据库,才会有sslmode=require之类的选项,这里不做展开。

2.3 MyBatis 核心配置:二级缓存与日志打印

MyBatis 的配置,主要关注两点:缓存和 SQL 日志。

缓存方面,MyBatis 默认开启一级缓存(SqlSession 级别),但这个缓存生命周期极其短暂——你的 Service 方法里如果每次操作都新建 SqlSession,一级缓存根本起不了作用。二级缓存(Mapper 级别)才是值得好好调教的,配置方式很简单:

<mapper namespace="com.example.mapper.OrderMapper"> <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/> </mapper>

配置二级缓存之前,请务必确认两个前提:第一,接口方法的返回对象必须实现了Serializable,否则缓存写入时报错;第二,如果涉及多表关联查询,脏数据风险极高,建议只给单表单查询的 Mapper 开二级缓存。我对这块的切身体会是:分布式环境下,MyBatis 二级缓存是个“看起来很美”的功能,在分库分表场景下,一旦某张表的数据被 ShardingSphere 路由到多个库,缓存和实际数据的同步逻辑就变得特别绕。如果你用了 ShardingSphere,尽量直接关闭二级缓存,避免数据不一致问题。

日志打印方面,在 Spring Boot 项目里配置application.yml:

mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

这样会将 SQL 输出到控制台,方便开发环境调试。但注意生产环境千万别开启,不然日志文件增长速度超出你想象。更建议的方式是使用p6spy这类代理工具,打印的 SQL 会带上参数值和执行耗时,排查慢查询时信息更全面。

2.4 分页插件的选型与配置分析

分页插件的用法,是搜索热词里反复出现的话题。MyBatis 的 PageHelper 一直是最常用的分页组件,但这个组件在 ShardingSphere 环境下的兼容性比较微妙。我实测下来的结论是:

  • 基于拦截器实现的 PageHelper,在单库单表场景下很好用。
  • 一旦引入 ShardingSphere,PageHelper 的count(*)查询可能会被改写异常,总数统计失真。

如果你还没引入 ShardingSphere,PageHelper 的经典用法是这样的:

PageHelper.startPage(pageNum, pageSize); List<Order> orders = orderMapper.selectByCondition(condition); PageInfo<Order> pageInfo = new PageInfo<>(orders);

核心原理:PageHelper.startPage()底层是用ThreadLocal保存分页参数,拦截器拦截下一次查询时自动拼接LIMIT语句。这里有个经典坑:如果你先调用startPage(),但后面连续执行了两次数据库查询,第二查也会被强制分页。避免方法:把分页参数绑定在紧随其后的第一条查询语句上。

而在 ShardingSphere 环境下,我建议放弃 PageHelper,改为手动分页。原因很简单——经过分片改写后的 SQL,在多个分片库上分别执行 LIMIT 后再合并,PageHelper 的本地内存分页逻辑会遗漏部分数据。简单说,它帮不上忙,反而会在聚合阶段给你添乱。

3. 实操过程与核心环节实现

3.1 依赖引入:Maven 依赖清单

这里我给出一个在 Spring Boot 2.7.x + MySQL 8.0 环境中实测通过的完整依赖组合:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId> <version>5.3.2</version> </dependency> </dependencies>

版本要特别留意:ShardingSphere 5.x 的groupId是org.apache.shardingsphere,和 4.x 时期的io.shardingsphere完全不同。老项目的依赖如果继续沿用 4.x,API 命名和新版是天壤之别。新版使用ShardingSphereDataSource和规则对象,旧版则是ShardingDataSourceFactory。

3.2 基础数据源配置(多数据源场景)

在引入了 ShardingSphere 之后,你不再直接将DataSource配置为 MySQL 的连接地址,而是配置一个 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://127.0.0.1:3306/db_order_0?useSSL=false&serverTimezone=Asia/Shanghai username: root password: "123456" maximum-pool-size: 20 ds1: type: com.zaxxer.hikari.HikariDataSource driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://127.0.0.1:3306/db_order_1?useSSL=false&serverTimezone=Asia/Shanghai username: root password: "123456" maximum-pool-size: 20

注意这里用的是jdbc-url而不是url,原因是 HikariCP 对 ShardingSphere 的数据源属性名要求是jdbc-url。填错的话,HikariPool 初始化时会报“Cannot resolve dataSource property url”,排查时很容易抓瞎。

生产环境还需要额外设置connection-timeout和validation-timeout,避免数据库故障时应用线程被大量阻塞。连接池大小也不是越大越好——MySQL 默认 max_connections 是 151,应用侧如果给每个实例都开 100 个连接,两台实例就把数据库连接挤爆了。合理经验值:单应用实例连接池 20~30 即可。

3.3 分片规则配置(取模分表 + 分库)

ShardingSphere 5.x 使用 YAML 配置分片规则,核心是rules部分。这里给出一个按用户 ID 取模分库、订单号范围分表的片段示例:

spring: shardingsphere: rules: sharding: tables: t_order: actual-data-nodes: ds$->{0..1}.t_order_$->{2023..2024} table-strategy: standard: sharding-column: create_date sharding-algorithm-name: month_range database-strategy: standard: sharding-column: user_id sharding-algorithm-name: user_mod sharding-algorithms: user_mod: type: MOD props: sharding-count: 2 month_range: type: INTERVAL props: datetime-pattern: "yyyy-MM-dd HH:mm:ss" datetime-lower: "2023-01-01 00:00:00" datetime-interval-amount: 1 datetime-interval-unit: MONTHS

分片算法MOD就是按分片键取模,这里按user_id % 2路由到ds0或ds1。时间范围分片,用的是INTERVAL算法,按月切分 2023 到 2024 年的数据到t_order_202301、t_order_202302等物理表。使用时间分片有个重要前提:分片键字段必须上索引,否则每次查询全分片扫描,性能等于全表扫描乘以分片数。

路由策略选standard还是complex也要仔细斟酌。单分片键用standard;多分片键协同路由时用complex,并且需要指定sharding-columns,比如同时按订单创建时间和用户 ID 路由。生产环境优先选用“分库键 + 分表键”联合设计,让大多数查询只命中一个库的一张表,性能才能达到最优。

3.4 MyBatis Mapper 写法与 SQL 优化

分库分表之后,MyBatis 的 Mapper 写法要多留意。逻辑表名保持不变,物理表名交给 ShardingSphere 改写。比如:

@Mapper public interface OrderMapper { @Select("SELECT * FROM t_order WHERE user_id = #{userId} AND create_date >= #{startTime}") List<Order> selectByUserAndTime(@Param("userId") Long userId, @Param("startTime") LocalDateTime startTime); }

这个 SQL 里,t_order是逻辑表,ShardingSphere 会根据user_id和create_date自动匹配物理表。写这类 SQL 时,我有一个特别深的体会:查询条件里必须带上分片键。如果 SQL 里只写了create_date而没有user_id,ShardingSphere 只能走全路由——所有库的所有月份表全扫一遍,性能直接从 10ms 涨到 1s 甚至更糟。

如果有些查询确实没法带分片键,可以考虑使用 ShardingSphere 的broadcast表机制,或者接受全路由的现实并加一层 Redis 缓存兜底。千万不能不设分片键又期望框架能猜到你要查哪台库,这是性能灾难的开端。

3.5 事务问题:分布式事务处理

分库分表后的一个大问题是本地事务失效。你更新了ds0.t_order_202311一条记录,同时又更新了ds1.t_order_202311另一条记录,原先在单库上的@Transactional只能保证其中一个库的一致性,跨库场景要配合其他方案。

ShardingSphere 5.x 提供了两种分布式事务方案:XA 强一致和BASE 最终一致(Seata)。

XA 的配置方式是:

spring: shardingsphere: props: xa-transaction-manager-type: Atomikos

配合代码里的@ShardingSphereTransactionType(TransactionType.XA)和@Transactional使用。注意:XA 事务在数据量小、并发低的场景下很稳定,但在高并发下性能损耗比较大,每个分支事务都要等待全局事务协调,RT 会明显上升。

更推荐的是 Seata AT 模式的最终一致性方案。它不锁数据库资源,而是通过全局锁和 before-image 快照,实现分布式事务的回滚。适合电商下单、库存扣减之类可接受短暂数据不一致、最终必须一致的场景。我项目里用的是 Seata 1.6.1 + ShardingSphere 5.x,跑了大半年,稳定性靠谱。

3.6 批量插入性能提升实测

前面提到了rewriteBatchedStatements=true,这里说一个具体案例。我们项目里有个定时任务,每分钟要从消息队列拉取 3 万条支付流水,写入到t_pay_log表。最初用 MyBatis 默认 Executor,逐条 INSERT,耗时 25 秒。

调整方案分两步:

第一步,在 JDBC 连接串加入rewriteBatchedStatements=true。

第二步,在 MyBatis 配置中开启批量执行器:

SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH, false); try { PayLogMapper mapper = sqlSession.getMapper(PayLogMapper.class); for (PayLog log : logs) { mapper.insert(log); } sqlSession.commit(); } finally { sqlSession.close(); }

调整后耗时从 25 秒降到 4 秒左右,近 6 倍的提升。原理就是驱动把多条 INSERT 语句拼接成INSERT INTO ... VALUES (...),(...),(...)一次性发给 MySQL,大幅减少网络往返次数。MySQL 单次允许的最大包大小默认是 4MB(max_allowed_packet),如果一次性插入 5 万行导致包超大,需要同时调大这个参数。

4. 常见问题与排查技巧实录

4.1 驱动版本不兼容引发的一连串问题

热词里有个典型错误提示,大意是“This version of the JDBC driver is only compatible with Elasticsearch version...”。这其实是连错了目标——你用 MySQL 的 JDBC 驱动去连接 Elasticsearch,自然会版本不匹配。排查思路很简单:先确认spring.datasource.driver-class-name配的是com.mysql.cj.jdbc.Driver,然后确认jdbc.url前缀是不是jdbc:mysql://。如果这些都没错,再检查依赖中是否不小心引入了多个版本的 JDBC 驱动,Maven 依赖树里出现两个版本时,用mvn dependency:tree查看,排除脏依赖。

还有一个常见的报错:com.mysql.cj.exceptions.InvalidConnectionAttributeException: The server time zone value 'Öйú±ê׼ʱ¼ä'。这个乱码是 MySQL 服务端的 timezone 使用了 CST,而 Java 无法识别。最简单的修复方式就是在 JDBC 连接串后面加上serverTimezone=Asia/Shanghai。

4.2 MySQL 2002 报错排查实录

热词里有一条ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock',这个问题在本地开发环境特别常见。大概有三层原因:

  1. 服务没启动。macOS 上brew services start mysql后,确认进程运行状态再用客户端连接。
  2. socket 文件路径不一致。客户端默认找/tmp/mysql.sock,而服务端配置socket=/var/lib/mysql/mysql.sock,两边不在同一处。查看服务端实际 socket 路径:mysql -uroot -p -h127.0.0.1 -P3306(强制 TCP 连接,避免走 socket 文件),进去执行show variables like 'socket';,或者检查/etc/my.cnf、/etc/mysql/my.cnf中的配置。
  3. 访问权限受限。连接串里使用用户root@localhost但只有root@127.0.0.1的授权时,需要用 TCP 协议配合-h指定主机再去验证权限。

建议统一方案:Java 应用永远通过 TCP 连接(jdbc:mysql://127.0.0.1:3306/...),不要依赖本地 socket 文件。这样可以把本地开发和服务器部署的差异降到最低,减少“本地跑得好好的,上服务器就连不上”的尴尬。

4.3 MyBatis @Update 执行慢的定位思路

热词里有“mybatis @update 执行慢”。看到这种问题,第一步不要怀疑 MyBatis,先拿到真实 SQL 去 MySQL 命令行跑一遍。

常见症结有三个:

  1. 没走索引:UPDATE的 WHERE 条件字段没有索引,MySQL 只能全表扫描定位行。用EXPLAIN SELECT * FROM ... WHERE ...查看 type 字段,如果是ALL,说明妥妥的全表扫描,必须补索引。
  2. 锁等待阻塞:UPDATE语句执行慢,可能是行锁被其他事务占用,在等锁。执行show processlist看是否有Waiting for table metadata lock或Waiting for lock状态,找出占用事务并检查代码里事务提交时机。
  3. 大批量更新导致 undo 膨胀:一次性UPDATE大量行,InnoDB 在事务提交前要保留 undo log 用于回滚,占用大量内存和磁盘,也会拖慢整个操作。

排查工具推荐挂上performance_schema,开启events_statements_history_long,能直接看到等待事件和耗时分布。这套链路配齐后,大部分 SQL 慢的问题都可以在分钟级别定位到人。

4.4 ShardingSphere 路由不生效的解决思路

如果你配置了分片规则,但查询时发现数据不对或直接报错“Cannot find table rule”,注意排查下面几点:

  1. 检查tables下的逻辑表名称是否和 Mapper 里的 SQL 表名大小写一致。MySQL 表名大小写敏感度由lower_case_table_names参数决定,开发机器和 Linux 服务器上行为经常不一致。
  2. 检查 Java 代码里是否通过@TableName注解或 XML 里写死了物理表名。ShardingSphere 只能改写它认识的那些逻辑表,如果你自己在 SQL 里写了t_order_202311,它反而不会去拦截,等于完全绕开了分片。
  3. 使用了 Hint 强制路由时,检查hintShardingAlgorithm是否配置到了对应sharding-algorithms。Hint 路由的方式极容易因为键名拼写不一致而失效。

一个真实的项目教训:我们有一次升级 ShardingSphere 从 4.1.1 到 5.3.2,发现原本能命中分片键的查询全部走了全库路由。原因是 5.x 版本对sharding-columns的驼峰转换更严格,我们把userId写成了userid,导致分片键匹配不上。启用下划线风格命名字段后,路由就恢复精准了。这种问题靠 log 翻半天看不出来,直接在配置文件里把sharding-columns: user_id规范化最有效。

4.5 连接池耗尽问题排查

分库分表后连接池特别容易被打满,原因前面提过:一次跨分片查询会被拆分执行,需要同时从多个数据源获取连接。

  • 症状:日志报HikariPool-1 - Connection is not available, request timed out after 30000ms。
  • 排查:用show processlist看 MySQL 侧连接数是否打满;再用 JVisualVM 或 jstack 看应用侧哪些线程持有连接不放。
  • 解决:第一,把maximum-pool-size调低到合适范围(20~30);第二,精简事务边界,避免在长事务中执行多个跨分片查询;第三,确认没有连接泄漏——比如 MyBatis 批量操作后 SqlSession 没有正确关闭,这种连接泄漏排查起来相当耗时间,务必用try-with-resources或者在 finally 中关闭。

配置一个connection-test-query: SELECT 1虽然简单,但在高并发场景会把数据库负载拉高。Hikari 默认用JDBC4 isValid()做连接活性检测,比 testQuery 高效得多,这个配置不用额外加。

4.6 批量插入与分页在 ShardingSphere 下的特殊现象

补充两个实战中容易被忽略的场景:

批量写入场景:ShardingSphere 遇到批量 INSERT 时,会把一条 SQL 拆分成多次执行,分别路由到不同的分片。此时 JDBC 的rewriteBatchedStatements并不生效,因为 SQL 已经被中间件接管改写。如果想提升批量插入性能,推荐把ExecutorType.BATCH和 ShardingSphere 的max-connections-size-per-query结合调整,同时降低每次提交的批次大小,减少连接占用。

分页深翻页场景:在 ShardingSphere 下做LIMIT 100000, 10,框架会在每个分片上都执行LIMIT 100000, 10,再把结果合并后截取,效率极其低下。建议业务上改成“游标分页”的方式——用WHERE id > #{lastId} ORDER BY id LIMIT 10,每次传上次查询的最后一条 id。实测从深翻页到游标分页后,第 100 页以后的查询耗时从 2 秒降到 50 毫秒,数据量大时效果极为明显。

5. 工具链配置细节与开发调试心得

5.1 开发环境配置快速验证清单

在本地搭这套环境时,我习惯按下面这个顺序依次验证,出现问题就卡在对应环节,不要一次性全部启动再来排查:

  1. 验证 MySQL 启动状态:用命令行客户端连接mysql -uroot -p -h127.0.0.1 -P3306,确认能通过 TCP 连接。
  2. 验证 JDBC 连接:写一个只含 JDBC 驱动的 Java main 方法,用连接串去拿Connection并执行SELECT 1。这里能排除绝大多数 SSL 和时区参数问题。
  3. 验证 MyBatis 单表查询:不引入 ShardingSphere,先只配置 MyBatis,确保 Mapper XML 能正常扫描、SQL 能正常执行。
  4. 引入 ShardingSphere 验证路由:配置分片规则后,执行插入和查询,确认日志中打印了Actual SQL: ds0 ::: SELECT ...,看到实际路由到哪个库哪张表。

这套顺序走一遍,整个链路的故障点就立即清晰了。很多同学一上来就把所有组件全部启动,然后不知从何排查,根源就是没把问题的“分层”思路理清。

5.2 日志输出技巧与调试命令

MyBatis 打印的 SQL 日志是最直观的排查工具,但要想看 ShardingSphere 的“真实路由结果”,光靠 MyBatis 日志还不够。在application.yml中增加:

logging: level: org.apache.shardingsphere: debug

然后执行一条查询,观察日志中类似Actual SQL: ds0 ::: SELECT * FROM t_order_202311 WHERE user_id = 10086的内容。这里能看到逻辑 SQL 和实际 SQL 的对比,是验证分片规则是否生效的关键。

另外一个调试技巧:全局启用log-impl: org.apache.ibatis.logging.stdout.StdOutImpl后,如果 SQL 日志里的占位符?太多,手动转成真实参数很麻烦。这时候用 MyBatis 的配置属性mybatis.configuration.map-underscore-to-camel-case: true简化结果映射的同时,加上shardingSphere的 debug 日志,两边的输出一对,SQL 全貌就齐了。

5.3 常见工具推荐

  • DBeaver:社区版够用,连接 MySQL 时自带 SSL 设置提示,避免踩useSSL参数的坑。查看表结构、执行计划都很顺手。
  • MySQL Workbench:官方工具,用于查看 InnoDB 状态、管理用户权限比较合适。热词里也有提到,大家在 MySQL 安装配置时常遇到字符集不匹配的问题,Workbench 的图形化界面都能直接调整。
  • arthas(阿里开源的 Java 诊断工具):排查应用中“哪个方法慢”特别好使。用trace命令定位到 Mapper 层耗时,再结合 MySQL 慢查询日志判断是应用侧还是数据库侧的问题,效率极高。
  • mysqldumpslow / pt-query-digest:分析 MySQL 慢查询日志的工具。分页插件执行慢、Update 执行慢这类问题的分析,第一步就是让它把 Top 10 慢 SQL 列出来,比盲猜强得多。

6. 从单库迁移到 ShardingSphere 的踩坑复盘

6.1 迁移分片键选择与线上平滑过渡

我之前带过一个金融类项目,订单表数据量大约 2 亿行。当时刚拆库,团队里第一个纠结的问题就是分片键选什么。用户 ID 是天然的业务分片键,用户查询订单时可以精准路由;但运营人员经常按商户号去查订单,这时候没有分片键,就只能全分片扫描了。

最终方案是双分片键:user_id作为主分片键,merchant_id通过冗余表映射路由。也就是说,写操作按user_id均匀分布,读操作如果只带merchant_id,先查询映射表找到目标用户,再回源查询订单表。这样既保证了写入均匀,又满足运营端的高频查询。

过渡期间怕的就是“拆库后性能反而下降”。我们当时做了灰度对比:先把一个月的数据根据规则拆到双库双表,随后把新写入流量切换到 ShardingSphere 数据源,查询流量前一小时按 10% 放量,确认 RT 稳定后逐步切到 100%。整个过程大约用了两个窗口期,数据库连接数、慢查询数、平均耗时都需要监控,一格不能少。

6.2 分布式主键方案对比

分库分表之后,单库的自增 ID 不能再用,否则多个分片产生的 ID 一定重复。业界常用方案有三个,我按实测难度排序:

  1. 雪花算法(Snowflake):ShardingSphere 内置了这个方案。在 YAML 里配置key-generator即可,生成的 ID 是长整型,趋势递增,适合做索引。唯一注意的地方:机器时钟回拨会导致 ID 重复,运算时必须有回拨容忍逻辑。
  2. UUID:最简单,32 位字符串,缺点是不是数字,做索引时容易导致索引碎片,并且 Java 生成的 UUID 是无序随机串,插入性能下降明显。不推荐作为索引列,除非你有类似消息队列 ID 的绑定需求。
  3. Redis 自增:用 Redis 生成递增序列,再结合日期作为前缀,性能好、有序,但引入了额外的 Redis 依赖,需要考虑高可用问题。

我实际项目里选的是雪花算法。原因很简单:不需要额外维护中间件,ShardingSphere 生成全局唯一 ID 已经内置,跟分片键配合得最好。还有一点是它在插入时能保持大致有序,这让索引写入性能远好于 UUID 那种随机分布。

6.3 数据迁移与双写校验

迁移时最怕的是旧数据丢失,所以会用到双写校验。我们的流程大概是:

  1. 存量数据按分片规则,用 DataX 或自研导出程序批量导入到分片表。
  2. 开启双写阶段:旧库写入的同时,通过 MQ 异步转发一份到新分片库,保证增量数据两边都有。
  3. 每天跑一次对账程序,按主键 ID 对比新旧两边的字段值,发现不一致就告警并触发补偿任务。

这个方案在生产跑了两周,对账差异率从 0.5% 一路降到 0.01% 以下,最终才敢把流量完全切到新库。期间排查出的问题包括:批次写入时字段为空、串行读出来处理时乱序、旧库的 DELETE 操作没有转发到新库,每一个都是经验累积。

7. 总结与经验沉淀

说了这么多,回到最初的问题:MySQL + MyBatis + ShardingSphere + JDBC 的价值到底在哪?

在我看来,它最大的价值不是某个单一技术,而是给数据访问层提供了一个可演进的架构底座。单机时代,Mysql + MyBatis 足够支撑几百万到几千万级数据量的业务;数据量突破天花板后,引入 ShardingSphere 扩容分布式能力,而应用代码不需要做翻天覆地的改动。JDBC 是整个链路里最低的通用层,任何上层变化都要尊重它的行为。把这些组件的边界认清楚了,你才真正掌控了你自己项目的数据库访问能力。

我个人在实际操作中最深的体会是:这套组合并不意味着你不再需要了解底层原理,反而倒逼你把 SQL 写出“能被框架可靠路由”的样子——条件里明确带上分片键,事务边界尽量精简,连接参数准确配置。这些基本功在任何一个系统里都是通用的。每当我看到有人又复制了一个连接串、又默认配置缓存、又忘了给分片键做索引,我就知道,他大概率又要踩一遍我当年踩过的坑了。

这套技术栈后续还可以继续扩展的方向,我个人觉得是结合现代可观测性组件,把每个分片上的慢查询、连接池指标、路由命中率都统一收集起来。让数据访问层跟应用层同等透明,是大型系统健康运维的必经之路。趁现在项目复杂度还能掌控,尽早把基础打牢,后面加应用、加流量都不会太慌。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询