Mybatis 拦截器实现单数据源内多数据库切换 | 京东物流技术团队
物流的分拣业务在某些分拣场地只有一个数据源,因为数据量比较大,将所有数据存在一张表内查询速度慢,也为了做不同设备数据的分库管理,便在这个数据源内创建了多个不同库名但表完全相同的数据库,如下图所示:

现在需要上线报表服务来查询所有数据库中的数据进行统计,那么现在的问题来了,该如何 满足在配置一个数据源的情况下来查询该数据源下不同数据库的数据 呢,借助搜索引擎查到的分库实现大多是借助 Sharding-JDBC 框架,配置多个数据源根据分库算法实现数据源的切换,但是对于只有一个数据源的系统来说,我觉得引入框架再将单个数据源根据不同的库名配置成多个不同的数据源来实现分库查询的逻辑我觉得并不好。
如果我们能在 SQL 执行前将 SQL 中所有的表名前拼接上对应的库名的话,那么就能够实现数据源的切换了,下面我们讲一下使用 JSqlParser 和 Mybatis拦截器 实现该逻辑,借助 JSqlParser 主要是为了解析SQL,找到其中所有的表名进行拼接,如果大家有更好的实现方式,该组件并不是必须的。
实现逻辑
SqlSource 是读取 XML 中 SQL 内容并将其发送给数据库执行的对象,如果我们在执行前能拦截到该对象,并将其中的 SQL 替换掉便达成了我们的目的。 SqlSource 有多种实现,包括常见的DynamicSqlSource。其中包含着必要的执行逻辑,我们需要做的工作便是在这些逻辑执行完之后,对 SQL 进行改造,所以这次实现我们使用了 装饰器模式,在原来的 SqlSource 上套一层,执行完 SqlSource 本身的方法之后对其进行增强,代码如下:
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 的工具类,解析出来所有的表名,执行字符串的替换,最后一步同样是使用反射操作将该参数值再写回去,这样便完成了指定库名的任务。
接下来我们需要看下抽象拦截器中供拦截器复用的方法,如下:
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()));
}
}
}
这个还是比较简单的,只是借助反射机制做了一层“装饰”,查询拦截器实现如下:
@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 拦截器中,便能实现数据库动态切换了。
作者:京东物流 王奕龙
来源:京东云开发者社区 自猿其说 Tech 转载请注明来源
Mybatis 拦截器实现单数据源内多数据库切换 | 京东物流技术团队的更多相关文章
- 通过spring抽象路由数据源+MyBatis拦截器实现数据库自动读写分离
前言 之前使用的读写分离的方案是在mybatis中配置两个数据源,然后生成两个不同的SqlSessionTemplate然后手动去识别执行sql语句是操作主库还是从库.如下图所示: 好处是,你可以人为 ...
- 基于Spring和Mybatis拦截器实现数据库操作读写分离
首先需要配置好数据库的主从同步: 上一篇文章中有写到:https://www.cnblogs.com/xuyiqing/p/10647133.html 为什么要进行读写分离呢? 通常的Web应用大多数 ...
- 玩转SpringBoot之整合Mybatis拦截器对数据库水平分表
利用Mybatis拦截器对数据库水平分表 需求描述 当数据量比较多时,放在一个表中的时候会影响查询效率:或者数据的时效性只是当月有效的时候:这时我们就会涉及到数据库的分表操作了.当然,你也可以使用比较 ...
- mybatis拦截器 修改mybatis返回结果集中的字段的值
项目中使用了shardingJDBC,业务库做了分库,公共库没在一起,所以导致做码值转换的时候,需要在实现类里面做转码,重复的代码量大,故考虑用mybatis拦截器,将码值转换后再做返回给实现类. ...
- 解决mybatis拦截器无法注入spring bean的问题
公司要整合rabbitmq与mybatis拦截器做一个数据同步功能. 整合过程中大部分环节都没什么问题,就是遇到了mybatis拦截器 @Intercepts(@Signature(type = Ex ...
- Mybatis拦截器
Mybatis拦截器
- Mybatis拦截器 mysql load data local 内存流处理
Mybatis 拦截器不做解释了,用过的基本都知道,这里用load data local主要是应对大批量数据的处理,提高性能,也支持事务回滚,且不影响其他的DML操作,当然这个操作不要涉及到当前所lo ...
- MyBatis拦截器原理探究
MyBatis拦截器介绍 MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能.那么拦截器拦截MyBatis中的哪些内容呢? 我们进入官网看一看: MyBatis 允 ...
- Mybatis拦截器介绍
拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法.Mybatis拦截器设计的一个初 ...
- Mybatis拦截器实现分页
本文介绍使用Mybatis拦截器,实现分页:并且在dao层,直接返回自定义的分页对象. 最终dao层结果: public interface ModelMapper { Page<Model&g ...
随机推荐
- 【CVPR2022】用于域适应语义分割的域无关先验
摘要:本文给大家分享一篇我们在CVPR 2022 上发表的paper:Domain-Agnostic Prior for Transfer Semantic Segmentation.文章提出了一种图 ...
- 火山引擎 DataTester 升级:降低产品上线风险,助力产品敏捷迭代
更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,并进入官方交流群 在企业竞争加剧的今天,精益开发和敏捷迭代已成为产品重要的竞争力.如何保障每一次 Feature 高效迭代与安全,如何快速实 ...
- JQuery 修改用户信息
JQuery 修改用户信息,多项选择,赋值,框架: https://www.h-ui.net/v3.shtml $(data.data.roleList).each(function (i, val) ...
- Docker 安装 Elasticsearch、Kibana
为了Skywalking 准备 elasticsearch 至少 需要2G内存 docker pull elasticsearch:7.9.3 docker run --name elasticsea ...
- 数组递增的判断【python实现】
有时候需要对某一组数组的数据进行判断是否 递增 的场景,比如我在开发一些体育动作场景下,某些肢体动作是需要持续朝着垂直方向向上变化,那么z轴的值是会累增的.同理,逆向考虑,递减就是它的对立面. 下面是 ...
- 用户 IP,里面藏了多少秘密?
大家都知道,要邮寄一封信给正确的收件人,需要提供准确而精细的地址,这个地址需要从国家和城市精确到邮政编码,街道和门牌号码.只有这样,邮局的工作人员才能知道将信送到那里. Internet 上也是如此, ...
- [啊哈!算法] 零基础彻底弄懂"并查集"
今天是算法数据结构专题的第5篇文章,我们一起来学习一下「并查集」. 并查集被很多ACMer认为是最简洁而优雅的数据结构之一,主要用于解决一些元素分组的问题.并支持两种操作: 合并(Union):把两个 ...
- 【教程】步兵 cocos2dx 加密和混淆
文章目录 摘要 引言 正文代码加密具体步骤代码加密具体步骤测试和配置阶段IPA 重签名操作步骤 总结 参考资料 摘要 本篇博客介绍了针对 iOS 应用中的 Lua 代码进行加密和混淆的相关技术.通过对 ...
- 三、mycat实验数据
系列导航 一.Mycat实战---为什么要用mycat 二.Mycat安装 三.mycat实验数据 四.mycat垂直分库 五.mycat水平分库 六.mycat全局自增 七.mycat-ER分片 最 ...
- fusionpbx简介
概述 fusionpbx是以freeswitch作为底层框架开发而成的开源PBX,在freeswitch的基础上,优化了GUI的易用性. fusionpbx可用作高可用性的单租户或基于域的多租户 PB ...