作者 | 温安适

来源 | https://my.oschina.net/floor/blog/1632565

最近在学习Spring boot,写了个读写分离。并未照搬网文,而是独立思考后的成果,写完以后发现从零开始写读写分离并不难!

我最初的想法是:读方法走读库,写方法走写库(一般是主库),保证在Spring提交事务之前确定数据源.

微信新增“炸屎”功能,被好友玩坏了。。

保证在Spring提交事务之前确定数据源,这个简单,利用AOP写个切换数据源的切面,让他的优先级高于Spring事务切面的优先级。至于读,写方法的区分可以用2个注解。

但是如何切换数据库呢?我完全不知道!多年经验告诉我

当完全不了解一个技术时,先搜索学习必要知识,之后再动手尝试。

我搜索了一些网文,发现都提到了一个AbstractRoutingDataSource类。查看源码注释如下

/** Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()}
* calls to one of various target DataSources based on a lookup key. The latter is usually
* (but not necessarily) determined through some thread-bound transaction context.
*
* @author Juergen Hoeller
* @since 2.0.1
* @see #setTargetDataSources
* @see #setDefaultTargetDataSource
* @see #determineCurrentLookupKey()
*/

AbstractRoutingDataSource就是DataSource的抽象,基于lookup key的方式在多个数据库中进行切换。 重点关注setTargetDataSources,setDefaultTargetDataSource,determineCurrentLookupKey三个方法。那么AbstractRoutingDataSource就是Spring读写分离的关键了。

仔细阅读了三个方法,基本上跟方法名的意思一致。setTargetDataSources设置备选的数据源集合。setDefaultTargetDataSource设置默认数据源,determineCurrentLookupKey决定当前数据源的对应的key。

但是我很好奇这3个方法都没有包含切换数据库的逻辑啊!我仔细阅读源码发现一个方法,determineTargetDataSource方法,其实它才是获取数据源的实现。源码如下:

    //切换数据库的核心逻辑protected DataSource determineTargetDataSource() {Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");Object lookupKey = determineCurrentLookupKey();DataSource dataSource = this.resolvedDataSources.get(lookupKey);if (dataSource == null && (this.lenientFallback || lookupKey == null)) {dataSource = this.resolvedDefaultDataSource;}if (dataSource == null) {throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");}return dataSource;}//之前的2个核心方法public void setTargetDataSources(Map<Object, Object> targetDataSources) {this.targetDataSources = targetDataSources;}public void setDefaultTargetDataSource(Object defaultTargetDataSource) {this.defaultTargetDataSource = defaultTargetDataSource;}

简单说就是,根据determineCurrentLookupKey获取的key,在resolvedDataSources这个Map中查找对应的datasource!,注意determineTargetDataSource方法竟然不使用的targetDataSources!

那一定存在resolvedDataSources与targetDataSources的对应关系。我接着翻阅代码,发现一个afterPropertiesSet方法(Spring源码中InitializingBean接口中的方法),这个方法将targetDataSources的值赋予了resolvedDataSources。源码如下:

 @Overridepublic void afterPropertiesSet() {if (this.targetDataSources == null) {throw new IllegalArgumentException("Property 'targetDataSources' is required");}this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size());for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) {Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());this.resolvedDataSources.put(lookupKey, dataSource);}if (this.defaultTargetDataSource != null) {this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);}}

afterPropertiesSet 方法,熟悉Spring的都知道,它在bean实例已经创建好,且属性值和依赖的其他bean实例都已经注入以后执行。

也就是说调用,targetDataSources,defaultTargetDataSource的赋值一定要在afterPropertiesSet前边执行。

AbstractRoutingDataSource简单总结:

  1. AbstractRoutingDataSource,内部有一个Map<Object,DataSource>的域resolvedDataSources

  2. determineTargetDataSource方法通过determineCurrentLookupKey方法获得key,进而从map中取得对应的DataSource。

  3. setTargetDataSources 设置 targetDataSources

  4. setDefaultTargetDataSource 设置 defaultTargetDataSource,

  5. targetDataSources和defaultTargetDataSource 在afterPropertiesSet分别转换为resolvedDataSources和resolvedDefaultDataSource。

  6. targetDataSources,defaultTargetDataSource的赋值一定要在afterPropertiesSet前边执行。

进一步了解理论后,读写分离的方式则基本上出现在眼前了。(“下列方法不唯一”)

先写一个类继承AbstractRoutingDataSource,实现determineCurrentLookupKey方法,和afterPropertiesSet方法。afterPropertiesSet方法中调用setDefaultTargetDataSource和setTargetDataSources方法之后调用super.afterPropertiesSet。

之后定义一个切面在事务切面之前执行,确定真实数据源对应的key。但是这又出现了一个问题,如何线程安全的情况下传递每个线程独立的key呢?没错使用ThreadLocal传递真实数据源对应的key

ThreadLocal,Thread的局部变量,确保每一个线程都维护变量的一个副本

到这里基本逻辑就想通了,之后就是写了。

DataSourceContextHolder 使用ThreadLocal存储真实数据源对应的key

public class DataSourceContextHolder {  private static Logger log = LoggerFactory.getLogger(DataSourceContextHolder.class);//线程本地环境  private static final ThreadLocal<String> local = new ThreadLocal<String>();   public static void setRead() {  local.set(DataSourceType.read.name());  log.info("数据库切换到读库...");  }  public static void setWrite() {  local.set(DataSourceType.write.name());  log.info("数据库切换到写库...");  }  public static String getReadOrWrite() {  return local.get();  }
}

DataSourceAopAspect 切面切换真实数据源对应的key,并设置优先级保证高于事务切面

@Aspect
@EnableAspectJAutoProxy(exposeProxy=true,proxyTargetClass=true)
@Component
public class DataSourceAopAspect implements PriorityOrdered{@Before("execution(* com.springboot.demo.mybatis.service.readorwrite..*.*(..)) "  + " and @annotation(com.springboot.demo.mybatis.readorwrite.annatation.ReadDataSource) ")  public void setReadDataSourceType() {  //如果已经开启写事务了,那之后的所有读都从写库读  DataSourceContextHolder.setRead();    }  @Before("execution(* com.springboot.demo.mybatis.service.readorwrite..*.*(..)) "  + " and @annotation(com.springboot.demo.mybatis.readorwrite.annatation.WriteDataSource) ")  public void setWriteDataSourceType() {  DataSourceContextHolder.setWrite();  }  @Overridepublic int getOrder() {/** * 值越小,越优先执行 要优于事务的执行 * 在启动类中加上了@EnableTransactionManagement(order = 10)  */  return 1;}
}

RoutingDataSouceImpl实现AbstractRoutingDataSource的逻辑

@Component
public class RoutingDataSouceImpl extends AbstractRoutingDataSource {@Overridepublic void afterPropertiesSet() {//初始化bean的时候执行,可以针对某个具体的bean进行配置//afterPropertiesSet 早于init-method//将datasource注入到targetDataSources中,可以为后续路由用到的keythis.setDefaultTargetDataSource(writeDataSource);Map<Object,Object>targetDataSources=new HashMap<Object,Object>();targetDataSources.put( DataSourceType.write.name(), writeDataSource);targetDataSources.put( DataSourceType.read.name(),  readDataSource);this.setTargetDataSources(targetDataSources);//执行原有afterPropertiesSet逻辑,//即将targetDataSources中的DataSource加载到resolvedDataSourcessuper.afterPropertiesSet();}@Overrideprotected Object determineCurrentLookupKey() {//这里边就是读写分离逻辑,最后返回的是setTargetDataSources保存的Map对应的keyString typeKey = DataSourceContextHolder.getReadOrWrite();  Assert.notNull(typeKey, "数据库路由发现typeKey is null,无法抉择使用哪个库");log.info("使用"+typeKey+"数据库.............");  return typeKey;}private static Logger log = LoggerFactory.getLogger(RoutingDataSouceImpl.class); @Autowired  @Qualifier("writeDataSource")  private DataSource writeDataSource;  @Autowired  @Qualifier("readDataSource")  private DataSource readDataSource;
}

基本逻辑实现完毕了就进行,通用设置,设置数据源,事务,SqlSessionFactory等

 @Primary@Bean(name = "writeDataSource", destroyMethod = "close")@ConfigurationProperties(prefix = "test_write")public DataSource writeDataSource() {return new DruidDataSource();}@Bean(name = "readDataSource", destroyMethod = "close")@ConfigurationProperties(prefix = "test_read")public DataSource readDataSource() {return new DruidDataSource();}@Bean(name = "writeOrReadsqlSessionFactory")public SqlSessionFactory sqlSessionFactorys(RoutingDataSouceImpl roundRobinDataSouceProxy) throws Exception {try {SqlSessionFactoryBean bean = new SqlSessionFactoryBean();bean.setDataSource(roundRobinDataSouceProxy);ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();// 实体类对应的位置bean.setTypeAliasesPackage("com.springboot.demo.mybatis.model");// mybatis的XML的配置bean.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));return bean.getObject();} catch (IOException e) {log.error("" + e);return null;} catch (Exception e) {log.error("" + e);return null;}}@Bean(name = "writeOrReadTransactionManager")public DataSourceTransactionManager transactionManager(RoutingDataSouceImpl roundRobinDataSouceProxy) {//Spring 的jdbc事务管理器DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(roundRobinDataSouceProxy);return transactionManager;}

其他代码,就不在这里赘述了,有兴趣可以移步完整代码。

使用Spring写读写分离,其核心就是AbstractRoutingDataSource,源码不难,读懂之后,写个读写分离就简单了!。

AbstractRoutingDataSource重点回顾:

  1. AbstractRoutingDataSource,内部有一个Map<Object,DataSource>的域resolvedDataSources

  2. determineTargetDataSource方法通过determineCurrentLookupKey方法获得key,进而从map中取得对应的DataSource。

  3. setTargetDataSources 设置 targetDataSources

  4. setDefaultTargetDataSource 设置 defaultTargetDataSource,

  5. targetDataSources和defaultTargetDataSource 在afterPropertiesSet分别转换为resolvedDataSources和resolvedDefaultDataSource。

  6. targetDataSources,defaultTargetDataSource的赋值一定要在afterPropertiesSet前边执行。

这周确实有点忙,周五花费了些时间不过总算实现了自己的诺言。

完成承诺不容易,喜欢您就点个赞!

往期推荐

微信新增“炸屎”功能,被好友玩坏了。。

存在多个不同注册中心的时候,如何平滑的统一注册中心?

你向 MySQL 数据库插入 100w 条数据用了多久?

Spring Boot 2.5.0 重新设计的spring.sql.init 配置有啥用?

Spring Boot 中关于 %2e 的 Trick

如果你喜欢本文,欢迎转发并关注我,订阅更多精彩内容

关注我回复「加群」,加入Spring技术交流群

从零实现 SpringBoot 简易读写分离,也不难嘛!相关推荐

  1. 从零实现SpringBoot简易读写分离,也不难嘛!

    作者:温安适 my.oschina.net/floor/blog/1632565 最近在学习Spring boot,写了个读写分离.并未照搬网文,而是独立思考后的成果,写完以后发现从零开始写读写分离并 ...

  2. SpringBoot实现读写分离

    SpringBoot实现读写分离 数据库读写分离配置:linux环境数据库读写分离 gitee地址:SpringBoot实现读写分离 1.目录结构 2.maven依赖 <parent>&l ...

  3. SpringBoot配置读写分离

    SpringBoot配置读写分离 1 概述 本文讲述了如何使用MyBatisPlus+ShardingSphereJDBC进行读写分离,以及利用MySQL进行一主一从的主从复制. 具体步骤包括: My ...

  4. 开源框架springboot-mybatis-wr-separation实现springboot+mybatis读写分离

    最近做springboot+mybatis的项目想要用到读写分离,查了一圈发现大家都是自己写的,没用通用现成的读写分离架构,因此就写了一个比较简单好用的小插件,来帮助大家简单的实现读写分离的功能,项目 ...

  5. 从零开始实现 Spring Boot 简易读写分离,其实也不难嘛!

    >>号外:关注"Java精选"公众号,菜单栏->聚合->干货分享,回复关键词领取视频资料.开源项目. 最近在学习Spring boot,写了个读写分离.并未 ...

  6. SpringBoot Mybatis 读写分离配置(山东数漫江湖)

    为什么需要读写分离 当项目越来越大和并发越来大的情况下,单个数据库服务器的压力肯定也是越来越大,最终演变成数据库成为性能的瓶颈,而且当数据越来越多时,查询也更加耗费时间,当然数据库数据过大时,可以采用 ...

  7. SpringBoot Mybatis 读写分离配置

    为什么需要读写分离 当项目越来越大和并发越来大的情况下,单个数据库服务器的压力肯定也是越来越大,最终演变成数据库成为性能的瓶颈,而且当数据越来越多时,查询也更加耗费时间,当然数据库数据过大时,可以采用 ...

  8. SpringBoot 整合 MyCat 实现读写分离

    作者:颜不喜 cnblogs.com/muycode/p/12603037.html MyCat一个彻底开源的,面向企业应用开发的大数据库集群.基于阿里开源的Cobar产品而研发.能满足数据库数据大量 ...

  9. SpringBoot下MySQL的读写分离

    首页 博客 专栏·视频 下载 论坛 问答 代码 直播 能力认证 高校 会员中心 收藏 动态 消息 创作中心 02-下篇-SpringBoot下MySQL的读写分离 dusuanyun 2018-07- ...

最新文章

  1. Xtrabackup对mysql全备以及增量备份实施
  2. linux安装nginx源码,CentOS7源码编译安装Nginx
  3. C++标准输出流对象
  4. 复习笔记(四)——C++继承
  5. 隐私策略-今日头条(纯净版)
  6. while、do while练习——7月24日
  7. python实现TCP客户端从服务器下载文件
  8. libyuv 海思平台编译测试
  9. 宿舍计算机管理制度,【宿舍门禁系统能统计夜不归宿吗】_学生宿舍门禁系统使用管理规定(试行)...
  10. 举办计算机知识竞赛的意义,计算机专业成功举办“计算机基础知识竞赛”
  11. DevOps元素周期表——1号元素 Gitlab
  12. 七天免登陆有效期 java_JWT过期刷新问题,实现十五天免登陆
  13. vscode Trace/breakpoint trap 问题
  14. php网站开发期末大作业,网页设计期末大作业报告..doc
  15. vue后台系统管理项目-openlayers地图定位、港口数据标记功能
  16. 获取、设置响应头、设置缓冲区
  17. 解决Error:All flavors must now belong to a named flavor dimension. Learn more at https://d.android.com
  18. 讲给后台程序员看的前端系列教程(38)——事件处理
  19. pycharm 添加远程项目interpreter 报 the authenticity of host can‘t be established 解决方法
  20. 程序员的算法趣题Q56: 鬼脚图中的横线(思路2的Python题解)

热门文章

  1. python3 判断ip有效性 是否是内网ip
  2. telegram bot 机器人 发送 加粗 斜体 字体
  3. android 修改编译内核源码 对抗反调试
  4. 签名工具 signtool.exe 参数简介
  5. python3 redis 设置连接超时
  6. linux redis 三种启动方式
  7. Android开发--多媒体应用开发(二)--SoundPool的使用
  8. Shell脚本个例二
  9. 华为备忘录导入印记云笔记_原来华为手机自带会议神器,开会不用手写,这个功能就能搞定...
  10. php facade模,PHP 设计模式之外观模式 Facade