拓冰建站拓冰建站
首页 / 资讯中心 / 正文

MySQL+MyBatis+ShardingSphere+JDBC分库分表从搭建到排坑实战

最近帮朋友接手了一个数据量增长很快的业务系统底层存储一直是 MySQL但随着业务表越来越大分库分表的需求终于提上日程。在技术选型时他纠结了很久用 MyBatis 的拦截器自己写分表逻辑还是直接引入 ShardingSphere最后我们一起定下了“MySQL MyBatis ShardingSphere JDBC”这条链路。这套组合在 Java 生态里非常典型但真要把它们拧成一股绳配置细节和隐藏坑还真不少。这篇文章就把我从零搭建到线上问题排查的完整过程整理出来希望能帮你少走弯路。1. 项目定位与核心链路拆解先明确这个标题代表的是什么。它不是一个具体项目名称而是一个技术栈的组合描述关系型数据库以 MySQL 为核心存储数据访问通过 JDBC 驱动连接ORM 层使用 MyBatis 管理 SQL 与结果映射再加上 ShardingSphere 做分库分表、读写分离和数据脱敏等中间件能力。放在实际系统里四者是一条完整的请求链路。可以用一个不太严谨但很直观的类比来理解这条链路MySQL 是货物仓库JDBC 是仓库门口的统一送货通道规定了货怎么装、怎么卸MyBatis 是仓库管理员帮你把货物清单翻译成仓库能听懂的语言再把取回来的货整理好ShardingSphere 则是智能货架调度系统——当仓库变成多个、货物分散在不同货架上时它决定你去哪个仓库、哪个货架找货以及在主仓和备份仓之间怎么分配读取压力。这条链路解决的典型问题有三个一是单表数据量过大导致读写性能下降需要通过分库分表把压力拆散二是读多写少需要做读写分离让从库分担查询三是团队的 SQL 大多基于 MyBatis 管理不希望为了分库分表把整个数据访问层推翻重写。ShardingSphere 的贴心之处在于它对业务侧基本透明你写成普通 SQL它负责改写和路由MyBatis 不用大改。不过这套组合并不是万能的。如果你的项目只是删删改改的小应用数据量十万级引入 ShardingSphere 属于过度设计中间件本身的复杂度反而会拖累开发效率。如果业务里有大量跨分片联合查询、复杂子查询、自定义数据库函数使用起来也会有诸多限制。它更适合“数据量明确会持续增长、SQL 模式相对可控”的成长型业务。2. 为什么是这套组合技术选型背后的逻辑2.1 MySQL 与关系型数据库的取舍选 MySQL 不是因为它最强大而是因为它在互联网业务里最“稳妥”。开源、社区活跃、排查资料多运维和研发都熟。对比 PostgreSQLMySQL 在大事上可能不够“学术”但胜在易用性和生态惯性。绝大多数 Java 开发者的数据库启蒙都是 MySQL团队学习和维护成本最低。更重要的是 MySQL 在事务支持、在线 DDL、主从复制这些关键能力上都很成熟加上 InnoDB 引擎的行级锁和 MVCC满足了绝大多数 OLTP 场景。分库分表本质上是分布式系统的妥协方案如果 MySQL 单机能扛住没人愿意做这种拆分。选它做底座意味着你在需求真正爆发之前不需要引入任何“超前”的架构复杂度。2.2 JDBC一切数据库访问的根基JDBC 是 Java 访问数据库的标准接口MyBatis 和 ShardingSphere 底层都是基于它实现的。没有 JDBCJava 程序只能靠数据库厂商的私有协议去拼那才是灾难。JDBC 提供了一套统一 API获取连接、执行 SQL、处理结果集、管理事务所有驱动都遵循这个规范。实际操作中我们往往不直接使用原生 JDBC而是通过连接池HikariCP 或 Druid获取数据库连接。这里有个容易忽略的点JDBC 连接不是普通对象创建和销毁的代价很高必须复用。连接池把 Connection 变成可循环利用的资源线程池里怎么管理线程连接池就怎么管理连接。ShardingSphere 在 JDBC 层做了一层抽象它的数据源实现了DataSource接口本质上是一个“JDBC 代理”。你配置好分片规则后一个普通的DataSource会被包装成 ShardingDataSource应用层拿到的 Connection、Statement、ResultSet 都是经过中间件包装的。这就是为什么 MyBatis 可以不感知分库分表——大家都遵守 JDBC 规范。2.3 MyBatis半自动化 ORM 的灵活性与控制力为什么不用全自动的 Hibernate/JPA核心原因是团队需要对 SQL 有绝对控制权。业务一旦复杂表连接、子查询、查询优化都需要手工调整 SQLMyBatis 在这方面的自由度是碾压级的。它只帮你完成参数绑定和结果映射SQL 本身由你定义执行计划可控调优时直接看 SQL 就行。MyBatis 的另一个优势是动态 SQL 能力if、foreach、choose这些标签能让我们在 XML 里拼出非常多变的条件查询。配合 MyBatis Generator 和 MyBatis Plus开发效率也不低。如果你追求的是“精准控制”而不是“快速生成”MyBatis 是更贴合实际运行的方式。2.4 ShardingSphere分库分表与读写分离的接入成本分库分表方案无非两条路业务代码里自己写路由逻辑或者用中间件透明路由。自己写意味着每个 Mapper 方法都要加一层 if/else 判断分表规则一旦变更所有 SQL 都得改维护成本非常高。ShardingSphere 解决的问题就是把路由规则集中配置让 SQL 自动改写和路由。ShardingSphere 提供一个嵌入式的 JDBC 模式ShardingSphere-JDBC不需要额外部署服务直接依赖数据库连接池。它的核心能力包括分片路由、SQL 改写把逻辑表改写成实际表名、分布式主键生成、读写分离主库写、从库读、数据脱敏等。相比 MyBatis Plus 的分表插件ShardingSphere 的方案更完整、社区更活跃对多分片键、复合分片、Hint 分片支持得也好。需要说明的是ShardingSphere 不是银弹。它虽然路由透明但 SQL 执行计划和跨分片聚合的成本依然存在。过度依赖它会让你忽略底层数据库的真实瓶颈所以在选型时必须搞清楚分库分表是数据量倒逼出来的不是提前消费的好看配置。3. 从零搭建项目核心步骤与配置详解这一节我直接以 Spring Boot 项目为例演示如何把 MySQL MyBatis ShardingSphere JDBC 串起来。版本信息很关键Spring Boot 2.7ShardingSphere 5.1.2MyBatis 3.5.10 mybatis-spring-boot-starter 2.2.2MySQL 驱动 8.0.33连接池用 HikariCPSpring Boot 默认内置。3.1 环境准备与依赖引入假设你已经有了一个 Spring Boot 工程第一步引入 Maven 依赖dependency groupIdorg.apache.shardingsphere/groupId artifactIdshardingsphere-jdbc-core-spring-boot-starter/artifactId version5.1.2/version /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.2.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency这里要提醒一句ShardingSphere 的 starter 会自动引入 HikariCP如果你之前项目里用的是 Druid可能存在冲突。我的做法是在引入 shardingsphere-jdbc-core 时排除掉它的默认数据源依赖再单独引入 Druid 或保持 HikariCP。个人建议小型项目直接用 HikariCP性能好配置少不必额外折腾 Druid 的监控。数据库准备建两个库demo_db和demo_db_2每个库里都有一张逻辑表user_info实际分表为user_info_0、user_info_1分片键是user_id。初始化 SQL 示例CREATE DATABASE IF NOT EXISTS demo_db; CREATE DATABASE IF NOT EXISTS demo_db_2; CREATE TABLE user_info_0 ( user_id BIGINT PRIMARY KEY, user_name VARCHAR(64), create_time DATETIME ); -- user_info_1 结构相同3.2 配置文件逐段拆解我的习惯是把所有配置放到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/demo_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue 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/demo_db_2?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: root这里每个ds代表一个物理数据源连接池。jdbc-url里的参数一个都不能随便删useSSLfalse本地测试和大多数内网环境不需要 SSL。如果不关掉MySQL 8 默认会尝试安全连接可能抛 SSL 警告或握手错误。生产环境如果数据库启用了 SSL需要配置useSSLtrue和对应证书。serverTimezoneAsia/ShanghaiMySQL 8.0 的驱动会强制要求设定时区否则报错 “The server time zone value CST is unrecognized”。allowPublicKeyRetrievaltrue使用密码认证时如果服务器要求 RSA 公钥交换这个参数允许客户端自动获取公钥。不配合useSSLfalse时比较容易出现 “Public Key Retrieval is not allowed” 错误加上它省事。然后配置分片规则rules: sharding: tables: user_info: actual-data-nodes: ds$-{0..1}.user_info_$-{0..1} table-strategy: standard: sharding-column: user_id sharding-algorithm-name: user-table-inline key-generate-strategy: column: user_id key-generator-name: snowflake sharding-algorithms: user-table-inline: type: INLINE props: algorithm-expression: user_info_$-{user_id % 2} key-generators: snowflake: type: SNOWFLAKE这段配置要理解三个关键点第一actual-data-nodes定义了数据节点ds$-{0..1}表示有两个数据源 ds0 和 ds1user_info_$-{0..1}表示每个库下有两张分表。SpEL 表达式$-{}里写范围或表达式。如果分片键不是均匀分布这个写法会把查询路由到所有可能的分表上。第二sharding-algorithm-name指定分片算法。我用的是 INLINE表达式user_id % 2即对 user_id 取模。INLINE 算法只支持单分片键的简单运算。如果你需要多分片键、范围查询、时间分片就得用 STANDARD 类型自定义算法或者用哈希取模。实际业务里取模方案在扩容时要做大量数据迁移我更推荐使用哈希一致性或者“日期 取模”的组合策略。第三key-generate-strategy配置分布式主键。因为分库分表后多个节点的自增 ID 会冲突必须使用全局唯一 ID。ShardingSphere 内置了雪花算法实现type: SNOWFLAKE。除了雪花算法还有 UUID、我更喜欢雪花算法因为它是趋势递增的对数据库索引性能友好。3.3 MyBatis 与 ShardingSphere 的衔接配置MyBatis 的配置不需要为 ShardingSphere 做特殊处理只要让 MyBatis 扫描到 Mapper 接口和 XML 文件就行。我习惯在application.yml加mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl注意spring.shardingsphere.datasource配好之后Spring Boot 会自动创建一个ShardingSphereDataSource它会被 MyBatis 的自动配置识别。所以不要再去定义自己的DataSourceBean否则中间件根本不会生效。这是新手最容易犯的错误自己写个Bean返回 Druid 数据源结果 MyBatis 走的还是裸连接分库分表规则完全没用。MyBatis XML 里的表名写成逻辑表名user_info不要写成物理表名user_info_0ShardingSphere 会在执行时把逻辑表改写分配成物理表。比如insert idinsertUser parameterTypecom.example.demo.entity.UserInfo INSERT INTO user_info (user_id, user_name, create_time) VALUES (#{userId}, #{userName}, #{createTime}) /insert插入时如果不主动给user_idShardingSphere 会根据key-generate-strategy自动生成一个雪花 ID。但注意如果你在 SQL 里显式写了user_id那生成策略就不起作用了。所以要么在代码里赋主键值要么让数据库和 MyBatis 都别去管主键赋值交给 ShardingSphere 完成。4. 分库分表与读写分离的落地实践4.1 分片策略设计分片策略直接决定系统的扩展能力和查询效率。我见过很多团队无脑用user_id % 2或% 10但后续扩容时才发现要搬数据。这里分享一个相对稳妥的选型思路如果你的数据是类似订单、流水这样持续增长且需要按时间维度查询的建议分片键用“日期”比如order_id里带上日期后缀或者直接用create_time做 BOUNDARY 算法这样每天的数据落在某个分片上查询时也方便按时间裁剪。如果你只是想让数据均匀分散用一个业务 ID 做哈希取模也可以但要提前规划好分片数量。比如最终规模预计 1000 万行那分 8 张表就够了槽位定成 8 的倍数以后扩到 16 张表时用虚拟桶方式可以避免迁移。我在 demo 里用user_id % 2只是示意真实场景中我会用HASH_MOD算法它和 INLINE 的区别是它内部会对分片键先做 hash 再取模避免某些规律性 ID 造成的倾斜。配置示例sharding-algorithms: user-table-hash: type: HASH_MOD props: sharding-count: 4sharding-count是分表总数。如果你库里有 2 个库每库 2 张表总数就是 4。HASH_MOD 会把分片键的 hashCode 对 4 取模。要注意 HASH_MOD 和库路由的配合更复杂库表不一致时建议用自定义分片。4.2 关联查询与分布式主键的虐心之处分库分表后最痛的不是普通的点查询而是关联查询和全表排序。ShardingSphere 支持跨分片 join但它在内部会把 SQL 改写成多次查询再在内存里做 merge性能不一定好。实际业务里我更推荐把 join 拆成多次单表查询在应用层做组装。比如查询用户和订单先查订单分片再根据 user_id 批量查用户用内存 map 关联数据库压力小扩展性也更强。分布式主键我们前面提到了雪花算法。ShardingSphere 的雪花算法默认生成的 ID 是 19 位的雪花 ID由时间戳 机器ID 序列号组成。我在使用中遇到过一个问题当系统时间发生跳变比如 NTP 同步同一毫秒内生成的 ID 可能重复。所以生产环境的机器务必配置 NTP 并开启平滑时间同步。另一个问题是雪花 ID 的前缀是时间戳所以它在数据库索引上具有顺序性但如果你是分表存储必须把主键放到分片键里否则查询会走广播路由全表扫描。4.3 读写分离配置ShardingSphere 可以把主库和从库配置成一组数据源然后在规则里设置负载均衡算法。如果不做分库分表只做读写分离直接配置rules: readwrite-splitting: >spring: shardingsphere: props: sql-show: true开启后日志里会出现一堆Actual SQL: ds0 ::: SELECT * FROM user_info_0 WHERE user_id ?这样的信息。注意生产环境不要一直开着sql-show它会记录所有 SQL 语句量大而且可能包含敏感数据只建议本地调试用。MyBatis 的慢 SQL 定位可以在 Mapper XML 里使用interceptors做统计或者用 Druid 的监控。我在这套组合中更喜欢用Mapper AOP 自己记录执行时间但注意 AOP 统计的时间只包含应用层到 MyBatis 执行完的时间中间件改写和路由的开销也包含在内这其实是真实用户体验时间不用剔除。6.4 分片查询性能优化最常见的问题是“分片键没在 WHERE 条件里导致广播查询”。比如用户表基本都带user_id查询但后台管理系统经常按user_name查这时如果不加约束SQL 会发到所有分片。解决思路有两个建立“索引表”维护user_name - user_id的映射关系放在 Redis 或者单独的一张全局表。用 ShardingSphere 的 hint 分片强制路由到指定数据源。不过 hint 会让 SQL 失去通用性我一般不用。容忍广播但给分片键以外的高频查询条件也建好索引使得每个分片上的查询都走索引合并再排序。这个方法对数据量不大每个分片百万级很有效。还有排序和聚合问题。ORDER BY create_time DESC LIMIT 20在每个分片要取 20 条然后 merge 排序实际取的数据是分片数乘以 20分片越多内存消耗越大。优化手段是“二次排序”思路先按分片并发查询每片取足够多数据再在应用层合并。ShardingSphere 自己做了这部分工作但我们在 SQL 里尽量避免ORDER BY非索引字段或者把排序下推到业务层用缓存解决。7. 项目落地经验与个人建议这套组合我从“会用”到“用好”花了不少时间。几个深刻的体会想分享出来。第一ShardingSphere 的版本迭代真快5.x 和之前的 4.x 配置方式差别很大。如果你在网上查资料一定先确认版本。我用的 5.1.2 是 5.x 里面相对稳定的但配置 API 在 5.3 后有微小调整升级时不能只更新 jar 包配置也要跟着改。第二除非有强烈需求不要一开始就上分库分表。你可以先用单库单表把系统跑起来预留分片键字段和主键策略比如统一用雪花 ID等数据量真的增长到需要拆分时再引入 ShardingSphere 把表逻辑一改就行。我在一个项目里就是前期直接用雪花 ID 做主键后来分表时只需要改配置和少量 SQL迁移成本低了非常多。第三MyBatis 的 XML 里尽量用相对简单的 SQL把业务组装放在 Service 层。复杂 SQL 与分片中间件总是相克绕开复杂 SQL 不是技术妥协而是架构上的明智。想清楚“数据库只做存储和简单查询复杂逻辑交给内存”这句话你会省掉无数个加班排查的夜。第四也是最重要的监控一定要提前做。分库分表后数据库层面你能看到的是很多个物理库表如果没有在中间件层做 SQL 监控和链路追踪定位问题会极其困难。我后来整理过一套基于 Prometheus Grafana 的监控模板专门盯 ShardingSphere 的sql-show统计和连接池状态建议你也尽早搭起来不要等线上告警了才被动找日志。最后再分享一个小技巧如果你是接手别人的分库分表项目排查“数据不存在”这类问题时先用SELECT * FROM logic_table WHERE actual_table_name xxx这种带分片键的方式查一次物理表确认数据到底存在哪个分片再回头看中间件的路由配置。很多时候不是 SQL 有问题而是配置里的分片算法没有覆盖你当前的分片键值导致数据被写到另一个表里去了。这条路谈不上轻松但走通之后你会对“数据库访问的每一层到底发生了什么”有很清晰的理解。希望这篇文章能帮你把这几块拼图完整地拼起来少踩一些我踩过的坑。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门