您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
Mybatis 拦截器实现单数据源内多数据库切换
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
Mybatis 拦截器实现单数据源内多数据库切换
wy****
2023-12-28
IP归属:北京
183浏览
物流的分拣业务在某些分拣场地只有一个数据源,因为数据量比较大,将所有数据存在一张表内查询速度慢,也为了做不同设备数据的分库管理,便在这个数据源内创建了多个不同库名但表完全相同的数据库,如下图所示: ![分库.jpg](https://s3.cn-north-1.jdcloud-oss.com/shendengbucket1/2023-12-11-08-546wuxo0qmeZDdgA54.jpg) 现在需要上线报表服务来查询所有数据库中的数据进行统计,那么现在的问题来了,该如何 **满足在配置一个数据源的情况下来查询该数据源下不同数据库的数据** 呢,借助搜索引擎查到的分库实现大多是借助 Sharding-JDBC 框架,配置多个数据源根据分库算法实现数据源的切换,但是对于只有一个数据源的系统来说,我觉得引入框架再将单个数据源根据不同的库名配置成多个不同的数据源来实现分库查询的逻辑我觉得并不好。 如果我们能在 SQL 执行前将 SQL 中所有的表名前拼接上对应的库名的话,那么就能够实现数据源的切换了,下面我们讲一下使用 [JSqlParser](https://jsqlparser.github.io/JSqlParser/) 和 Mybatis拦截器 实现该逻辑,借助 JSqlParser 主要是为了解析SQL,找到其中所有的表名进行拼接,如果大家有更好的实现方式,该组件并不是必须的。 ### 实现逻辑 `SqlSource` 是读取 XML 中 SQL 内容并将其发送给数据库执行的对象,如果我们在执行前能拦截到该对象,并将其中的 SQL 替换掉便达成了我们的目的。 `SqlSource` 有多种实现,包括常见的`DynamicSqlSource`。其中包含着必要的执行逻辑,我们需要做的工作便是在这些逻辑执行完之后,对 SQL 进行改造,所以这次实现我们使用了 **装饰器模式**,在原来的 `SqlSource` 上套一层,执行完 `SqlSource` 本身的方法之后对其进行增强,代码如下: ```java public abstract class AbstractDBNameInterceptor { /** * SqlSource 的装饰器,作用是增强了 getBoundSql 方法,在基础上增加了动态分库的逻辑 */ static class SqlSourceDecorator implements SqlSource { /** * SQL 字段名称 */ private static final String SQL_FIELD_NAME = "sql"; /** * 原本的 sql source */ private final SqlSource sqlSource; /** * 装饰器进行封装 */ public SqlSourceDecorator(SqlSource sqlSource) { this.sqlSource = sqlSource; } @Override public BoundSql getBoundSql(Object parameterObject) { try { // 先生成出未修改前的 SQL BoundSql boundSql = sqlSource.getBoundSql(parameterObject); // 获取数据库名 String dbName = getSpecificDBName(parameterObject); // 有效才修改 if (isValid(dbName)) { // 生成需要修改完库名的 SQL String targetSQL = getRequiredSqlWithSpecificDBName(boundSql, dbName); // 更新 SQL updateSql(boundSql, targetSQL); } return boundSql; } catch (Exception e) { throw new RuntimeException(e); } } /** * 校验是否为有效库名 */ private boolean isValid(String dbName) { return StringUtils.isNotEmpty(dbName) && !"null".equals(dbName); } /** * 获取到我们想要的库名的 SQL */ private String getRequiredSqlWithSpecificDBName(BoundSql boundSql, String dbName) throws JSQLParserException { String originSql = boundSql.getSql(); // 获取所有的表名 Set<String> tables = TablesNamesFinder.findTables(originSql); for (String table : tables) { originSql = originSql.replaceAll(table, dbName + "." + table); } return originSql; } /** * 修改 SQL */ private void updateSql(BoundSql boundSql, String sql) throws NoSuchFieldException, IllegalAccessException { // 通过反射修改sql语句 Field field = boundSql.getClass().getDeclaredField(SQL_FIELD_NAME); field.setAccessible(true); field.set(boundSql, sql); } } // ... } ``` 定义了 `AbstractDBNameInterceptor` 抽象类是为了实现复用,并将 `SqlSourceDecorator` 装饰器定义为静态内部类,这样的话,将所有逻辑都封装在抽象类内部,之后这部分实现好后研发直接实现抽象类的通用方法即可,不必关注它的内部实现。 结合注释我们解释一下 `SqlSourceDecorator` 的逻辑,其中用到了 Java 反射相关的操作。首先通过反射获取到 SQL,`getSpecificDBName` 方法是需要自定义实现的,其中 `parameterObject` 对象是传到 DAO 层执行查询时的参数,在我们的业务中是能够根据其中的设备相关参数拿到对应的所在库名的,而设备和具体库名的映射关系需要提前初始化好。在获取到具体的库名后执行 `getRequiredSqlWithSpecificDBName` 方法来将其拼接到表名前,在这里我们使用到了 JSqlParser 的工具类,解析出来所有的表名,执行字符串的替换,最后一步同样是使用反射操作将该参数值再写回去,这样便完成了指定库名的任务。 接下来我们需要看下抽象拦截器中供拦截器复用的方法,如下: ```java public abstract class AbstractDBNameInterceptor { /** * SqlSource 字段名称 */ private static final String SQL_SOURCE_FIELD_NAME = "sqlSource"; /** * 执行修改数据库名的逻辑 */ protected Object updateDBName(Invocation invocation) throws Throwable { // 装饰器装饰 SqlSource decorateSqlSource((MappedStatement) invocation.getArgs()[0]); return invocation.proceed(); } /** * 装饰 SqlSource */ private void decorateSqlSource(MappedStatement statement) throws NoSuchFieldException, IllegalAccessException { if (!(statement.getSqlSource() instanceof SqlSourceDecorator)) { Field sqlSource = statement.getClass().getDeclaredField(SQL_SOURCE_FIELD_NAME); sqlSource.setAccessible(true); sqlSource.set(statement, new SqlSourceDecorator(statement.getSqlSource())); } } } ``` 这个还是比较简单的,只是借助反射机制做了一层“装饰”,查询拦截器实现如下: ```java @Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}) }) public class SelectDBNameInterceptor extends AbstractDBNameInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { return updateDBName(invocation); } } ``` 将其配置到 Mybatis 拦截器中,便能实现数据库动态切换了。 ---
上一篇:深入理解经典红黑树
下一篇:Spring 应用合并之路(一):摸石头过河
wy****
文章数
22
阅读量
2476
作者其他文章
01
高性能MySQL实战(一):表结构
最近因需求改动新增了一些数据库表,但是在定义表结构时,具体列属性的选择有些不知其所以然,索引的添加也有遗漏和不规范的地方,所以我打算为创建一个高性能表的过程以实战的形式写一个专题,以此来学习和巩固这些知识。1. 实战我使用的 MySQL 版本是 5.7,建表 DDL 语句如下所示:根据需求创建 接口调用日志 数据库表,请大家浏览具体字段的属性信息,它们有不少能够优化的点。CREATE TABLE
01
分布式服务高可用实现:复制
1. 为什么需要复制我们可以考虑如下问题:当数据量、读取或写入负载已经超过了当前服务器的处理能力,如何实现负载均衡?希望在单台服务器出现故障时仍能继续工作,这该如何实现?当服务的用户遍布全球,并希望他们访问服务时不会有较大的延迟,怎么才能统一用户的交互体验?这些问题其实都能通过 “复制” 来解决:复制,即在不同的节点上保存相同的副本,提供数据冗余。如果一些节点不可用,剩余的节点仍然可以提供数据服务
01
高性能MySQL实战(三):性能优化
这篇主要介绍对慢 SQL 优化的一些手段,而在讲解具体的优化措施之前,我想先对 EXPLAIN 进行介绍,它是我们在分析查询时必要的操作,理解了它输出结果的内容更有利于我们优化 SQL。为了方便大家的阅读,在下文中规定类似 key1 的表示二级索引,key_part1 表示联合索引的第一部分,unique_key1 则表示唯一二级索引,primary_key 表示主键索引。高性能MySQL实战(一
01
从2PC和容错共识算法讨论zookeeper中的Create请求
最近在读《数据密集型应用系统设计》,其中谈到了zookeeper对容错共识算法的应用。这让我想到之前参考的zookeeper学习资料中,误将容错共识算法写成了2PC(两阶段提交协议),所以准备以此文对共识算法和2PC做梳理和区分,也希望它能帮助像我一样对这两者有误解的同学。1. 2PC(两阶段提交协议)两阶段提交 (two-phase commit) 协议是一种用于实现 跨多个节点的原子事务(分布
wy****
文章数
22
阅读量
2476
作者其他文章
01
高性能MySQL实战(一):表结构
01
分布式服务高可用实现:复制
01
高性能MySQL实战(三):性能优化
01
从2PC和容错共识算法讨论zookeeper中的Create请求
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号