物流的分拣业务在某些分拣场地只有一个数据源,因为数据量比较大,将所有数据存在一张表内查询速度慢,也为了做不同设备数据的分库管理,便在这个数据源内创建了多个不同库名但表完全相同的数据库,如下图所示:

现在需要上线报表服务来查询所有数据库中的数据进行统计,那么现在的问题来了,该如何 满足在配置一个数据源的情况下来查询该数据源下不同数据库的数据 呢,借助搜索引擎查到的分库实现大多是借助 Sharding-JDBC 框架,配置多个数据源根据分库算法实现数据源的切换,但是对于只有一个数据源的系统来说,我觉得引入框架再将单个数据源根据不同的库名配置成多个不同的数据源来实现分库查询的逻辑我觉得并不好。

如果我们能在 SQL 执行前将 SQL 中所有的表名前拼接上对应的库名的话,那么就能够实现数据源的切换了,下面我们讲一下使用 JSqlParser 和 Mybatis拦截器 实现该逻辑,借助 JSqlParser 主要是为了解析SQL,找到其中所有的表名进行拼接,如果大家有更好的实现方式,该组件并不是必须的。

实现逻辑

SqlSource 是读取 XML 中 SQL 内容并将其发送给数据库执行的对象,如果我们在执行前能拦截到该对象,并将其中的 SQL 替换掉便达成了我们的目的。 SqlSource 有多种实现,包括常见的DynamicSqlSource。其中包含着必要的执行逻辑,我们需要做的工作便是在这些逻辑执行完之后,对 SQL 进行改造,所以这次实现我们使用了 装饰器模式,在原来的 SqlSource 上套一层,执行完 SqlSource 本身的方法之后对其进行增强,代码如下:

  1. public abstract class AbstractDBNameInterceptor {
  2. /**
  3. * SqlSource 的装饰器,作用是增强了 getBoundSql 方法,在基础上增加了动态分库的逻辑
  4. */
  5. static class SqlSourceDecorator implements SqlSource {
  6. /**
  7. * SQL 字段名称
  8. */
  9. private static final String SQL_FIELD_NAME = "sql";
  10. /**
  11. * 原本的 sql source
  12. */
  13. private final SqlSource sqlSource;
  14. /**
  15. * 装饰器进行封装
  16. */
  17. public SqlSourceDecorator(SqlSource sqlSource) {
  18. this.sqlSource = sqlSource;
  19. }
  20. @Override
  21. public BoundSql getBoundSql(Object parameterObject) {
  22. try {
  23. // 先生成出未修改前的 SQL
  24. BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  25. // 获取数据库名
  26. String dbName = getSpecificDBName(parameterObject);
  27. // 有效才修改
  28. if (isValid(dbName)) {
  29. // 生成需要修改完库名的 SQL
  30. String targetSQL = getRequiredSqlWithSpecificDBName(boundSql, dbName);
  31. // 更新 SQL
  32. updateSql(boundSql, targetSQL);
  33. }
  34. return boundSql;
  35. } catch (Exception e) {
  36. throw new RuntimeException(e);
  37. }
  38. }
  39. /**
  40. * 校验是否为有效库名
  41. */
  42. private boolean isValid(String dbName) {
  43. return StringUtils.isNotEmpty(dbName) !"null".equals(dbName);
  44. }
  45. /**
  46. * 获取到我们想要的库名的 SQL
  47. */
  48. private String getRequiredSqlWithSpecificDBName(BoundSql boundSql, String dbName) throws JSQLParserException {
  49. String originSql = boundSql.getSql();
  50. // 获取所有的表名
  51. Set<String> tables = TablesNamesFinder.findTables(originSql);
  52. for (String table : tables) {
  53. originSql = originSql.replaceAll(table, dbName + "." + table);
  54. }
  55. return originSql;
  56. }
  57. /**
  58. * 修改 SQL
  59. */
  60. private void updateSql(BoundSql boundSql, String sql) throws NoSuchFieldException, IllegalAccessException {
  61. // 通过反射修改sql语句
  62. Field field = boundSql.getClass().getDeclaredField(SQL_FIELD_NAME);
  63. field.setAccessible(true);
  64. field.set(boundSql, sql);
  65. }
  66. }
  67. // ...
  68. }

定义了 AbstractDBNameInterceptor 抽象类是为了实现复用,并将 SqlSourceDecorator 装饰器定义为静态内部类,这样的话,将所有逻辑都封装在抽象类内部,之后这部分实现好后研发直接实现抽象类的通用方法即可,不必关注它的内部实现。

结合注释我们解释一下 SqlSourceDecorator 的逻辑,其中用到了 Java 反射相关的操作。首先通过反射获取到 SQL,getSpecificDBName 方法是需要自定义实现的,其中 parameterObject 对象是传到 DAO 层执行查询时的参数,在我们的业务中是能够根据其中的设备相关参数拿到对应的所在库名的,而设备和具体库名的映射关系需要提前初始化好。在获取到具体的库名后执行 getRequiredSqlWithSpecificDBName 方法来将其拼接到表名前,在这里我们使用到了 JSqlParser 的工具类,解析出来所有的表名,执行字符串的替换,最后一步同样是使用反射操作将该参数值再写回去,这样便完成了指定库名的任务。

接下来我们需要看下抽象拦截器中供拦截器复用的方法,如下:

  1. public abstract class AbstractDBNameInterceptor {
  2. /**
  3. * SqlSource 字段名称
  4. */
  5. private static final String SQL_SOURCE_FIELD_NAME = "sqlSource";
  6. /**
  7. * 执行修改数据库名的逻辑
  8. */
  9. protected Object updateDBName(Invocation invocation) throws Throwable {
  10. // 装饰器装饰 SqlSource
  11. decorateSqlSource((MappedStatement) invocation.getArgs()[0]);
  12. return invocation.proceed();
  13. }
  14. /**
  15. * 装饰 SqlSource
  16. */
  17. private void decorateSqlSource(MappedStatement statement) throws NoSuchFieldException, IllegalAccessException {
  18. if (!(statement.getSqlSource() instanceof SqlSourceDecorator)) {
  19. Field sqlSource = statement.getClass().getDeclaredField(SQL_SOURCE_FIELD_NAME);
  20. sqlSource.setAccessible(true);
  21. sqlSource.set(statement, new SqlSourceDecorator(statement.getSqlSource()));
  22. }
  23. }
  24. }

这个还是比较简单的,只是借助反射机制做了一层“装饰”,查询拦截器实现如下:

  1. @Intercepts({
  2. @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
  3. @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})
  4. })
  5. public class SelectDBNameInterceptor extends AbstractDBNameInterceptor implements Interceptor {
  6. @Override
  7. public Object intercept(Invocation invocation) throws Throwable {
  8. return updateDBName(invocation);
  9. }
  10. }

将其配置到 Mybatis 拦截器中,便能实现数据库动态切换了。

作者:京东物流 王奕龙

来源:京东云开发者社区 自猿其说 Tech 转载请注明来源

Mybatis 拦截器实现单数据源内多数据库切换 | 京东物流技术团队的更多相关文章

  1. 通过spring抽象路由数据源+MyBatis拦截器实现数据库自动读写分离

    前言 之前使用的读写分离的方案是在mybatis中配置两个数据源,然后生成两个不同的SqlSessionTemplate然后手动去识别执行sql语句是操作主库还是从库.如下图所示: 好处是,你可以人为 ...

  2. 基于Spring和Mybatis拦截器实现数据库操作读写分离

    首先需要配置好数据库的主从同步: 上一篇文章中有写到:https://www.cnblogs.com/xuyiqing/p/10647133.html 为什么要进行读写分离呢? 通常的Web应用大多数 ...

  3. 玩转SpringBoot之整合Mybatis拦截器对数据库水平分表

    利用Mybatis拦截器对数据库水平分表 需求描述 当数据量比较多时,放在一个表中的时候会影响查询效率:或者数据的时效性只是当月有效的时候:这时我们就会涉及到数据库的分表操作了.当然,你也可以使用比较 ...

  4. mybatis拦截器 修改mybatis返回结果集中的字段的值

    项目中使用了shardingJDBC,业务库做了分库,公共库没在一起,所以导致做码值转换的时候,需要在实现类里面做转码,重复的代码量大,故考虑用mybatis拦截器,将码值转换后再做返回给实现类.   ...

  5. 解决mybatis拦截器无法注入spring bean的问题

    公司要整合rabbitmq与mybatis拦截器做一个数据同步功能. 整合过程中大部分环节都没什么问题,就是遇到了mybatis拦截器 @Intercepts(@Signature(type = Ex ...

  6. Mybatis拦截器

    Mybatis拦截器

  7. Mybatis拦截器 mysql load data local 内存流处理

    Mybatis 拦截器不做解释了,用过的基本都知道,这里用load data local主要是应对大批量数据的处理,提高性能,也支持事务回滚,且不影响其他的DML操作,当然这个操作不要涉及到当前所lo ...

  8. MyBatis拦截器原理探究

    MyBatis拦截器介绍 MyBatis提供了一种插件(plugin)的功能,虽然叫做插件,但其实这是拦截器功能.那么拦截器拦截MyBatis中的哪些内容呢? 我们进入官网看一看: MyBatis 允 ...

  9. Mybatis拦截器介绍

    拦截器的一个作用就是我们可以拦截某些方法的调用,我们可以选择在这些被拦截的方法执行前后加上某些逻辑,也可以在执行这些被拦截的方法时执行自己的逻辑而不再执行被拦截的方法.Mybatis拦截器设计的一个初 ...

  10. Mybatis拦截器实现分页

    本文介绍使用Mybatis拦截器,实现分页:并且在dao层,直接返回自定义的分页对象. 最终dao层结果: public interface ModelMapper { Page<Model&g ...

随机推荐

  1. APP 备案公钥、签名 MD5、SHA-1、SHA-256获取方法。

    ​ 公钥和 MD5 值可以通过苹果开发工具.Keytool.appuploder 等多种工具获取,最简单的就是以 appuploader为例. 1.下载 appuploader工具 ,点击此处 下载 ...

  2. 火山引擎数智平台最新直播活动:ByteHouse技术架构与最佳实践分享

    数据的时效性,正深刻影响着企业的发展.   以大型半导体制造厂商为例,不同于常规工厂生产流水线,半导体制造通用的无人实验室生产模式高度依赖机械臂作业,且对整个生产调度链路中的精密度要求非常高,这背后主 ...

  3. Solon Aop 特色开发(5)切面与环绕拦截

    Solon,更小.更快.更自由!本系列专门介绍Solon Aop方面的特色: <Solon Aop 特色开发(1)注入或手动获取配置> <Solon Aop 特色开发(2)注入或手动 ...

  4. 【Redis】面试题 GEO地理位置信息

    目录 面试 1 http协议详情,http协议版本,http一些请求头 2 GET请求和POST请求的区别 3 如何实现服务器给客户端发送消息,websocket是什么? 4 悲观锁和乐观锁,如何实现 ...

  5. 认证,权限,频率源码分析 自定义频率类 SimpleRateThrottle缓存频率类 基于APIView编写分页

    目录 昨日回顾 三种位置的token获取 三种权限校验方式 原生django的cookie+session认证底层原理 断点调试使用 认证,权限,频率源码分析(了解) 权限源码分析 认证源码分析 频率 ...

  6. WebRTC SDP 详解和剖析

    WebRTC 是 Web Real-Time Communication,即网页实时通信的缩写,是 RTC 协议的一种 Web 实现,项目由 Google 开源,并和 IETF 和 W3C 制定了行业 ...

  7. 深挖 Python 元组 pt.2

    哈喽大家好,我是咸鱼 在<深挖 Python 元组 pt.1>中我们了解 Python 元组的一些概念(索引和切片等),以及如何创建元组,最重要的是我们还介绍了元组的不可变特性 那么今天我 ...

  8. #2037:今年暑假不AC

    Problem Description "今年暑假不AC?" "是的." "那你干什么呢?" "看世界杯呀,笨蛋!" & ...

  9. vue-router路由复用后页面没有刷新

    vue-router提供了页面路由功能,可以用来构建单页面应用,在使用vue-router的动态路由匹配的时候,遇到了url变化了,但是页面却没有任何动静的问题,记录一下. 动态路由匹配url变化了, ...

  10. Canal使用和安装总结

    转载请注明出处: 1.定义 Canal 组件是一个基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费,支持将增量数据投递到下游消费者(如 Kafka.RocketMQ 等)或者存储(如 El ...