从零实现 SpringBoot 简易读写分离,也不难嘛!
作者 | 温安适
来源 | 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简单总结:
AbstractRoutingDataSource,内部有一个Map<Object,DataSource>的域resolvedDataSources
determineTargetDataSource方法通过determineCurrentLookupKey方法获得key,进而从map中取得对应的DataSource。
setTargetDataSources 设置 targetDataSources
setDefaultTargetDataSource 设置 defaultTargetDataSource,
targetDataSources和defaultTargetDataSource 在afterPropertiesSet分别转换为resolvedDataSources和resolvedDefaultDataSource。
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重点回顾:
AbstractRoutingDataSource,内部有一个Map<Object,DataSource>的域resolvedDataSources
determineTargetDataSource方法通过determineCurrentLookupKey方法获得key,进而从map中取得对应的DataSource。
setTargetDataSources 设置 targetDataSources
setDefaultTargetDataSource 设置 defaultTargetDataSource,
targetDataSources和defaultTargetDataSource 在afterPropertiesSet分别转换为resolvedDataSources和resolvedDefaultDataSource。
targetDataSources,defaultTargetDataSource的赋值一定要在afterPropertiesSet前边执行。
这周确实有点忙,周五花费了些时间不过总算实现了自己的诺言。
完成承诺不容易,喜欢您就点个赞!
往期推荐
微信新增“炸屎”功能,被好友玩坏了。。
存在多个不同注册中心的时候,如何平滑的统一注册中心?
你向 MySQL 数据库插入 100w 条数据用了多久?
Spring Boot 2.5.0 重新设计的spring.sql.init 配置有啥用?
Spring Boot 中关于 %2e 的 Trick
如果你喜欢本文,欢迎转发并关注我,订阅更多精彩内容
关注我回复「加群」,加入Spring技术交流群
从零实现 SpringBoot 简易读写分离,也不难嘛!相关推荐
- 从零实现SpringBoot简易读写分离,也不难嘛!
作者:温安适 my.oschina.net/floor/blog/1632565 最近在学习Spring boot,写了个读写分离.并未照搬网文,而是独立思考后的成果,写完以后发现从零开始写读写分离并 ...
- SpringBoot实现读写分离
SpringBoot实现读写分离 数据库读写分离配置:linux环境数据库读写分离 gitee地址:SpringBoot实现读写分离 1.目录结构 2.maven依赖 <parent>&l ...
- SpringBoot配置读写分离
SpringBoot配置读写分离 1 概述 本文讲述了如何使用MyBatisPlus+ShardingSphereJDBC进行读写分离,以及利用MySQL进行一主一从的主从复制. 具体步骤包括: My ...
- 开源框架springboot-mybatis-wr-separation实现springboot+mybatis读写分离
最近做springboot+mybatis的项目想要用到读写分离,查了一圈发现大家都是自己写的,没用通用现成的读写分离架构,因此就写了一个比较简单好用的小插件,来帮助大家简单的实现读写分离的功能,项目 ...
- 从零开始实现 Spring Boot 简易读写分离,其实也不难嘛!
>>号外:关注"Java精选"公众号,菜单栏->聚合->干货分享,回复关键词领取视频资料.开源项目. 最近在学习Spring boot,写了个读写分离.并未 ...
- SpringBoot Mybatis 读写分离配置(山东数漫江湖)
为什么需要读写分离 当项目越来越大和并发越来大的情况下,单个数据库服务器的压力肯定也是越来越大,最终演变成数据库成为性能的瓶颈,而且当数据越来越多时,查询也更加耗费时间,当然数据库数据过大时,可以采用 ...
- SpringBoot Mybatis 读写分离配置
为什么需要读写分离 当项目越来越大和并发越来大的情况下,单个数据库服务器的压力肯定也是越来越大,最终演变成数据库成为性能的瓶颈,而且当数据越来越多时,查询也更加耗费时间,当然数据库数据过大时,可以采用 ...
- SpringBoot 整合 MyCat 实现读写分离
作者:颜不喜 cnblogs.com/muycode/p/12603037.html MyCat一个彻底开源的,面向企业应用开发的大数据库集群.基于阿里开源的Cobar产品而研发.能满足数据库数据大量 ...
- SpringBoot下MySQL的读写分离
首页 博客 专栏·视频 下载 论坛 问答 代码 直播 能力认证 高校 会员中心 收藏 动态 消息 创作中心 02-下篇-SpringBoot下MySQL的读写分离 dusuanyun 2018-07- ...
最新文章
- Xtrabackup对mysql全备以及增量备份实施
- linux安装nginx源码,CentOS7源码编译安装Nginx
- C++标准输出流对象
- 复习笔记(四)——C++继承
- 隐私策略-今日头条(纯净版)
- while、do while练习——7月24日
- python实现TCP客户端从服务器下载文件
- libyuv 海思平台编译测试
- 宿舍计算机管理制度,【宿舍门禁系统能统计夜不归宿吗】_学生宿舍门禁系统使用管理规定(试行)...
- 举办计算机知识竞赛的意义,计算机专业成功举办“计算机基础知识竞赛”
- DevOps元素周期表——1号元素 Gitlab
- 七天免登陆有效期 java_JWT过期刷新问题,实现十五天免登陆
- vscode Trace/breakpoint trap 问题
- php网站开发期末大作业,网页设计期末大作业报告..doc
- vue后台系统管理项目-openlayers地图定位、港口数据标记功能
- 获取、设置响应头、设置缓冲区
- 解决Error:All flavors must now belong to a named flavor dimension. Learn more at https://d.android.com
- 讲给后台程序员看的前端系列教程(38)——事件处理
- pycharm 添加远程项目interpreter 报 the authenticity of host can‘t be established 解决方法
- 程序员的算法趣题Q56: 鬼脚图中的横线(思路2的Python题解)
热门文章
- python3 判断ip有效性 是否是内网ip
- telegram bot 机器人 发送 加粗 斜体 字体
- android 修改编译内核源码 对抗反调试
- 签名工具 signtool.exe 参数简介
- python3 redis 设置连接超时
- linux redis 三种启动方式
- Android开发--多媒体应用开发(二)--SoundPool的使用
- Shell脚本个例二
- 华为备忘录导入印记云笔记_原来华为手机自带会议神器,开会不用手写,这个功能就能搞定...
- php facade模,PHP 设计模式之外观模式 Facade