ARTICLE DETAIL

资讯详情

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

JDBC与ShardingSphere是什么关系?分库分表实战原理与主从配置解析

JDBC与ShardingSphere是什么关系?分库分表实战原理与主从配置解析 在开始聊 ShardingSphere 之前我建议先搞清楚一件事JDBC 到底是什么。很多人在看 ShardingSphere 文档时第一眼看到的是DataSource、Connection、PreparedStatement这些非常眼熟的 JDBC 面孔于是会产生一个疑问ShardingSphere 到底是替代 JDBC还是在 JDBC 上面做了一层封装这个问题的答案基本决定了你怎么看待 ShardingSphere 的整个设计也直接影响你在 5.x 版本里做主从配置、调连接池参数时的排障思路。这篇文章我会从自己实际跑过的项目出发把 JDBC 规范和 ShardingSphere 的关系讲透顺带把主从配置、Flink JDBC 连接器异常这类实战问题一起拆开聊。如果你正被为什么配了分库分表但代码没变化为什么查询偶尔走错库为什么启动时数据源初始化报错这些问题卡住那这篇文章就是为你准备的。我会尽量少讲空泛的架构图多讲代码和配置背后的原因让小白也能顺着思路走一遍。1. 先搞清楚JDBC 规范到底管了什么1.1 一个接口统一了关系型数据库的方言先别急着背接口用一个生活化的例子切入。JDBC 就像墙上的国标插座MySQL、PostgreSQL、Oracle 的驱动就像各种电器插头。插座定义了电压、孔位、极性至于插头后面是电饭煲还是烤箱插座不关心。JDBC 做的事情就是定义数据库访问插座DriverManager负责找驱动Connection负责建立会话Statement负责执行 SQLResultSet负责读取结果。你的业务代码只要对着这套插座写底层换数据库时Java 代码基本不用动只换驱动和连接串就行。在生产环境里我们一般不用DriverManager.getConnection()而是用DataSource。DataSource是 JDBC 2.0 引入的标准接口它把连接的获取方式抽象出来连接池HikariCP、Druid可以在这里做代理。Spring 管理数据源时也主要面向DataSource。这一点很重要因为 ShardingSphere 正是从DataSource这个入口插进来的。JDBC 版本演进也值得提一句。JDBC 4.2 是 Java 8 时代用得最多的版本支持getObject/setObject映射 SQL 类型之后 JDBC 4.3 虽然随 Java 9 发布但应用层感知不强。真正需要你关心版本的是驱动MySQL 8 的驱动类名从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.DriverURL 里要有serverTimezone参数。很多连接不上的问题其实是驱动版本和连接串不匹配而不是 ShardingSphere 的锅。1.2 JDBC 解决连得上解决不了拆得开JDBC 规范本身解决的是怎么连上一个数据库的问题它不负责告诉你一条 SQL 里有 100 万条订单数据要拆到 4 个库 12 张表里应该先去哪个库哪张表查。如果你自己手写分库分表至少要处理几件事维护分片键和物理表映射关系、改写 SQL、在多节点上执行 SQL、把多个结果集合并排序分页、处理跨库事务。这一套逻辑写进业务代码项目基本就废了。ShardingSphere 解决的就是拆得开和合得拢。它把分片、读写分离、数据加密、影子库这些分布式数据库场景下的通用能力封装在 JDBC API 后面。业务代码写SELECT * FROM t_order WHERE order_id ?它负责把这条 SQL 改写成去ds0.t_order_0还是ds1.t_order_1执行再把结果合并成逻辑上的一个结果集。这个思路就是JDBC 做连接规范ShardingSphere 做数据分布规则的分工基础。2. ShardingSphere 藏在哪个位置——JDBC 视角的入口拦截2.1 从 getConnection() 开始的入口拦截理解 ShardingSphere 的架构最直接的办法是看一段原生 JDBC 代码和一段使用 ShardingSphere 的代码两者几乎一模一样DataSource dataSource new ShardingSphereDataSource( sharding_db, new HashMap(), new ShardingSphereRuleBuilder().build(), new Properties() ); try (Connection connection dataSource.getConnection(); PreparedStatement ps connection.prepareStatement( SELECT * FROM t_order WHERE order_id ?)) { ps.setLong(1, 1001L); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { // 处理结果 } } }和普通 JDBC 唯一的区别是DataSource变成了ShardingSphereDataSource。但getConnection()返回的并不是 MySQL 驱动原生的Connection而是ShardingSphereConnection。这个连接对象内部包了一层逻辑连接管理它知道当前线程应该用哪几个物理连接SQL 经过解析、路由、改写后分别发往哪些物理库执行。对业务代码来说这一切都被 JDBC 接口挡住了。这正是 ShardingSphere-JDBC 最重要的设计它不是让你换一种访问数据库的方式而是让你用原来的方式访问一个假的逻辑库。入口拦截发生在DataSource、Connection、Statement、ResultSet这几个标准接口上所以 Spring、MyBatis、Hibernate 这些依赖 JDBC 的框架全都天然兼容。2.2 一个逻辑库对应多个物理库ShardingSphere 把所有物理数据源整合成一个逻辑数据源。以分片为例逻辑上你连接的是sharding_db实际上 SQL 会被路由到ds0、ds1等多个物理库。这个模型有点像总机和分机的区别你拨总机号码总机根据你的按键转接到具体座机座机之间互相独立你也不需要记住每个座机的分机号。在配置层面物理数据源集合是dataSources每个数据源仍然是一个标准的 JDBCDataSourceHikariDataSource、DruidDataSource 等。ShardingSphere 不重造连接轮子它只负责在逻辑层做决策。因此连接池的调优参数、驱动版本、jdbcUrl写法都还是沿用 JDBC 体系里的那套规则。这也是为什么很多 DBA 第一次接触 ShardingSphere 时不会觉得太陌生因为底层连接串、账号密码、连接池参数都是老熟人。2.3 数据库方言差异由驱动和 ShardingSphere 共同消化有人以为 ShardingSphere 只支持 MySQL其实不对。ShardingSphere 的 SQL 解析层支持 MySQL、PostgreSQL、openGauss、SQLServer、Oracle 等多种方言底层通过 JDBC 驱动真正执行 SQL。也就是说方言解析由 ShardingSphere 完成物理执行由数据库驱动完成两者通过 JDBC 接口衔接。这种设计还带来一个好处如果你从 MySQL 切到 PostgreSQL控制面规则分片、读写分离、加密不用重写只需要换方言解析器和 JDBC 驱动。JDBC 的插座价值在这里体现得很明显只要两端都遵守插座协议中间的替换成本就低。3. 当业务代码遇到 ShardingSphereSQL 解析、路由、改写悄悄做了哪些事3.1 一条 UPDATE 变成两条甚至更多我经常用一条更新语句来演示路由过程。假设分片键是user_id用户执行UPDATE t_order SET status 1 WHERE user_id 123;ShardingSphere 拿到这条 SQL 后会先做词法解析和语法解析生成抽象语法树找到更新对象是t_order、条件里有user_id。接着它根据分片算法算出123应该落在ds0的t_order_1还是ds1的t_order_0把原始 SQL 改写成对应物理表名再通过 JDBC 连接发到对应库上执行。最后它把多个物理连接返回的更新行数累加作为逻辑上的受影响行数返回给业务代码。这一步里最容易踩的坑是分片键没有出现在 WHERE 条件中导致全路由broadcast。全路由不是不能用但意味着所有分片都要执行一遍SQL 性能会明显变差。设计表时尽量保证高频查询都带分片键。还有一个容易被忽略的点如果 UPDATE 语句里把分片键本身也改了ShardingSphere 会出现先删后插的复杂路由这类 SQL 我建议直接禁止业务上几乎没有合理需求会去修改订单的归属用户。3.2 结果归并order by limit 不是最后做这么简单分布式环境下做排序分页比单库复杂得多。拿ORDER BY create_time DESC LIMIT 20 OFFSET 100举例ShardingSphere 不能只让每个分片取 20 条因为各分片各自排完前 120 条后合并起来的前 20 条可能并不是全局前 20 条。所以它会向每个分片要OFFSET LIMIT也就是 120 条然后在内存里做归并排序最终截取需要的 20 条。这个每片多取一些的代价在深分页场景下会非常夸张。比如LIMIT 1000000, 20每个分片都要取 1000020 条网络和内存压力都很大。实际业务里我一般建议用首屏 条件过滤代替深分页或者用唯一排序键配合游标方式往下翻页。如果你非要用 PageHelper 这类分页插件要确认它解析的是不是改写前的逻辑 SQL否则很容易出现分页参数被重复处理的问题。3.3 事务和连接管理本地事务不承诺跨库强一致ShardingSphere 的事务类型默认是本地事务。所谓本地事务是指事务边界内所有操作都通过当前线程持有的同一个Connection完成这在单分片路由时没问题一旦事务涉及多个分片本地事务就无法保证跨库原子性。ShardingSphere 也提供 XA 分布式事务和 Seata 柔性事务的接入但那是另一个层面的选型默认别把本地事务当成分布式事务用。连接管理上ShardingSphere-JDBC 默认把逻辑连接绑定在当前线程上事务状态、路由结果都跟着线程走。这也是为什么在 Spring 里用Transactional时方法内部所有 SQL 会走同一条物理连接链不会因为连接池切换而破坏事务。如果你遇到事务里第一个查询走了从库后面写操作却走了主库导致读到旧数据这类问题先检查是不是没有把整个方法包在Transactional里再检查 ShardingSphere 的事务绑定类型是否被改成了非线程绑定模式。4. 实操对照从 5.x 主从配置看 JDBC 与 ShardingSphere 的协作边界4.1 先准备好两个数据库主从配置是 ShardingSphere 最常见的入门场景。我这里以 MySQL 8.0 为例准备一个主库write_ds、一个从库read_ds。连接参数本质上还是 JDBC URL所以先要确认主从两个实例都能用 MySQL 驱动正常连上。下面是 ShardingSphere 5.x 的 YAML 配置dataSources: write_ds: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver jdbcUrl: jdbc:mysql://127.0.0.1:3306/mall?useSSLfalseserverTimezoneAsia/ShanghairewriteBatchedStatementstrue username: root password: 123456 maximumPoolSize: 20 minimumIdle: 5 read_ds: dataSourceClassName: com.zaxxer.hikari.HikariDataSource driverClassName: com.mysql.cj.jdbc.Driver jdbcUrl: jdbc:mysql://127.0.0.2:3306/mall?useSSLfalseserverTimezoneAsia/ShanghairewriteBatchedStatementstrue username: root password: 123456 maximumPoolSize: 20 minimumIdle: 5 rules: - !READWRITE_SPLITTING dataSources: readwrite_ds: writeDataSourceName: write_ds readDataSourceNames: - read_ds loadBalancerName: round_robin loadBalancers: round_robin: type: ROUND_ROBIN props: sql-show: true注意 5.x 的配置风格和 4.x 差别很大。4.x 里常见!readwrite-splitting小写加横杠的写法5.x 换成了这种用!READWRITE_SPLITTING的规则类型标识。配置项里dataSourceClassName指向连接池实现类而不是直接给一个dataSource工厂这也是 5.x 为了兼容多种连接池做的调整。主从延迟较大的环境里还可以给读库加权重负载均衡type: WEIGHT配合props里的权重值让性能好的从库承担更多读流量。4.2 客户端怎么选主库还是从库配置好之后你会发现在业务代码里写的数据源是readwrite_ds不是write_ds或read_ds。ShardingSphere 在收到 SQL 时会根据是否事务内和SQL 类型决定路由目标事务内查询强制走主库非事务查询按负载均衡算法走从库写操作永远走主库。这里有一个很实际的坑刚写完数据立刻去查如果走了从库可能因为主从同步延迟查不到最新数据。解决方案很简单直接用HintManager强制走主库try (HintManager hintManager HintManager.getInstance()) { hintManager.setWriteRouteOnly(); // 这里执行的查询全部走主库 }或者干脆把这段读逻辑放到同一个Transactional方法里让事务上下文把连接钉在主库上。还要注意HintManager是线程绑定的使用完必须关闭否则在池化线程或异步场景里hint 会漂移到下一个任务造成莫名其妙全部走了主库的诡异现象。4.3 JDBC 连接池与 ShardingSphere 的一层隐藏关系很多人在配置里忽略了一件事ShardingSphere 配置的物理数据源是 HikariCP 或 Druid但应用最终拿到的 DataSource 是 ShardingSphereDataSource。连接池参数比如maximumPoolSize、minimumIdle、connectionTimeout仍然作用于每个物理数据源。如果你的应用写了大量并发查询每个物理库的连接池大小要分别调不能只调一个总池。另外不要试图用DriverManager.getConnection()去连 ShardingSphere 的逻辑库除非你在用 ShardingSphere-Proxy。JDBC 模式下逻辑库不是 MySQL 实例它只活在当前 JVM 里连接必须从ShardingSphereDataSource获取。Spring 项目里最省事的做法是把 ShardingSphereDataSource 作为唯一数据源交给 MyBatis 或 JPA 使用不要在业务层混用多个数据源否则事务和路由边界会被搞乱。提示Spring Boot 接入 ShardingSphere-JDBC 时常见错误是配置了spring.datasource.url但实际生效的却是 ShardingSphere 的逻辑数据源。排查时先在启动日志里看数据源初始化信息确认ShardingSphereDataSource是否正确创建。5. 一次问题排查实录Flink JDBC 连接器异常背后的规律5.1 现象跑批任务偶发连不上 MySQL我前阵子遇到一个挺典型的问题Flink 任务用flink-connector-jdbc直连 MySQL跑批时偶发报错错误信息先是Communications link failure接着是Connection is not available, request timed out。从表面看是连接超时但实际根因在连接池和管理端之间。那套环境里有 ShardingSphere-JDBC 包装的多个应用数据源也有 Flink 直连 MySQL 的独立连接。MySQL 服务端的wait_timeout是 8 小时Flink 连接器里连接的最大存活时间却是默认值 30 分钟理论上不会触发服务端断连。问题出在另一个应用重启时把数据库侧的连接全部重置而 Flink 侧连接池没有及时感知于是一批旧连接被打上半开标记等到下一次请求才发现连不上。这种情况其实是连接池参数与服务端参数没有对齐导致的不是 ShardingSphere 本身的问题。5.2 排查路径先看四个参数遇到这类问题我建议按下面这张表依次排查现象可能原因处理方式偶发Communications link failurewait_timeout大于连接池maxLifetime把maxLifetime调成数据库wait_timeout的 0.8~0.9 倍高并发下连接等待超时物理数据源连接池太小分库分别调大maximumPoolSize不要只调总池空闲连接被服务端回收idleTimeout设置过长让idleTimeout小于服务端空闲回收时间服务重启后第一批请求失败旧连接未被连接池验证开启connectionTestQuery或 Hikari 的isValid校验HikariCP 的maxLifetime建议用一个简单公式估算maxLifetime 数据库 wait_timeout 秒数 * 1000 - 5000。比如 MySQLwait_timeout28800那maxLifetime设 28 分钟左右比服务端短一点让连接在服务端回收之前由客户端主动关闭避免用到失效连接。这个公式是我自己踩坑后总结的不能说对所有数据库都绝对精确但至少能让连接被服务端悄悄杀掉的概率降一个数量级。5.3 开 sql-show让路由过程现场直播排查 ShardingSphere 相关连接问题时我最常用的办法是打开sql-show。配置里props.sql-show: true后日志会打印逻辑 SQL、实际路由到的数据源、改写后的 SQL 和耗时。这时候你能一眼看出这条查询到底走了主库还是从库分片键有没有生效物理 SQL 长什么样。我建议你在排查阶段别关这个开关尤其是刚上主从配置时。打印出来的日志虽然有点吵但比黑盒猜路由高效得多。等稳定了再关掉。如果你用的是 Druid还要注意它的validationQuery默认可能是SELECT 1对 MySQL 没问题但在某些 Oracle 场景下会抛ORA-00923这时候就要换成标准写法。连接池验证语句这种小细节平时不出事一出事就是整批连接不可用。6. 再往深一层为什么说 JDBC 是根基ShardingSphere 是让契约在分布式场景继续成立6.1 从 SPI 机制到方言扩展ShardingSphere 的扩展点很多内部大量使用 SPIService Provider Interface。新增一种数据库的支持主要工作是提供对应的 SQL 方言解析器、数据源类型和必要的类型映射规则引擎本身以 JDBC 标准接口为边界不需要推倒重来。这意味着你在 ShardingSphere 里配置各种规则时底层的连接管理、事务控制、结果集映射仍然在 JDBC 的框架内。JDBC 定义的连接—执行—取结果三步曲在 ShardingSphere 里被扩展成了解析—路由—改写—执行—归并但对外暴露的依然是最原始的 JDBC API。如果你有精力翻源码建议重点看ShardingSphereConnection里createStatement、prepareStatement的实现你会看到它如何把不同规则引擎串到同一条执行链上这种责任链模式比任何架构图都直观。6.2 设计哲学契约与实现分离用一个更容易记的比喻。JDBC 像一套交通规则红灯停、绿灯行、靠右行驶。所有车辆数据库驱动都遵守这套规则。ShardingSphere 则像路口交警负责在高峰期指挥车辆分流、合并车道但车辆最终还是按交通规则在路上跑。你不能说交警替代了交通规则也不能说交通规则替代了交警两者是契约和实现增强的关系。理解了这层关系再看 ShardingSphere-JDBC 和 ShardingSphere-Proxy 的差异就很清晰了。JDBC 模式是把 ShardingSphere 作为驱动嵌入应用应用自身变成一个逻辑数据库客户端Proxy 模式则独立部署一个数据库中间件任何语言的客户端都可以用 MySQL/PostgreSQL 协议连上来。前者对 Java 业务侵入小、性能好适合中大型单体或微服务后者适合多语言团队和集中化管控但要多维护一个中间件节点。6.3 给选型的一句实在话如果你们团队本来就是 Java 技术栈应用里已经用了 MyBatis 或 Spring Data JPAShardingSphere-JDBC 是最顺手的路因为不用额外部署节点JDBC 连接池调优经验可以直接复用。如果团队里还有 Go、Python 服务要访问同一套分片逻辑或者需要给多个业务方提供统一的数据库入口那 ShardingSphere-Proxy 会更合适。无论选哪个JDBC 规范始终是理解数据源、连接池、事务这些概念的起点。我个人最后想提一个很微小的技巧排查 ShardingSphere 问题第一时间开sql-show第二看Connection的实际类型是不是ShardingSphereConnection第三确认连接池参数和数据库服务端参数是否对齐。大部分所谓的JDBC 连接异常其实都绕不过这三点。把这个习惯练成肌肉记忆再看 ShardingSphere 源码和文档会顺畅很多。
返回列表