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

沙海
沙海
沙海
994
文章
2
评论
2021年4月28日12:52:00
评论
2 9537字阅读31分47秒
摘要

速读摘要

速读摘要

并未照搬网文,而是独立思考后的成果,写完以后发现从零开始写读写分离并不难!当完全不了解一个技术时,先搜索学习必要知识,之后再动手尝试。我接着翻阅代码,发现一个afterPropertiesSet方法(Spring源码中InitializingBean接口中的方法),这个方法将targetDataSources的值赋予了resolvedDataSources。afterPropertiesSet方法,熟悉Spring的都知道,它在bean实例已经创建好,且属性值和依赖的其他bean实例都已经注入以后执行。

原文约 2365 | 图片 1 | 建议阅读 5 分钟 | 评价反馈

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

架构师专栏

收录于话题

#SpringBoot20

#架构进阶79

#历史专栏215

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

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

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

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

注 意

文末放有:7701页最新面试题

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

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

我搜索了一些网文,发现都提到了一个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方法中调用setDefaultTargetDataSourcesetTargetDataSources方法之后调用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();      }   @Override public int getOrder() {  /**          * 值越小,越优先执行 要优于事务的执行          * 在启动类中加上了@EnableTransactionManagement(order = 10)           */    return 1; }}

RoutingDataSouceImpl实现AbstractRoutingDataSource的逻辑

@Componentpublic class RoutingDataSouceImpl extends AbstractRoutingDataSource {  @Override public void afterPropertiesSet() {  //初始化bean的时候执行,可以针对某个具体的bean进行配置  //afterPropertiesSet 早于init-method  //将datasource注入到targetDataSources中,可以为后续路由用到的key  this.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加载到resolvedDataSources  super.afterPropertiesSet(); } @Override protected Object determineCurrentLookupKey() {  //这里边就是读写分离逻辑,最后返回的是setTargetDataSources保存的Map对应的key  String 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;}

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

https://gitee.com/WLjava/spring-boot-readorwrite-demo

使用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前边执行。

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

近期技术热文

1、为什么SpringBoot的 jar 可以直接运行?2、Spring解析,加载及实例化Bean的顺序(零配置)3、SpringBoot 的@Value注解,高级特性,真心强大!4、IDEA 2021.1正式发布!新特性,骚操作,不少啊!

第3版:互联网大厂面试题

包括 Java 集合、JVM、多线程、并发编程、设计模式、算法调优、Spring全家桶、Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、MongoDB、Redis、MySQL、RabbitMQ、Kafka、Linux、Netty、Tomcat、Python、HTML、CSS、Vue、React、JavaScript、Android 大数据、阿里巴巴等大厂面试题等、等技术栈!

阅读原文: 高清 7701页大厂面试题  PDF

阅读原文

继续阅读
weinxin
资源分享QQ群
本站是一个IT技术分享社区, 会经常分享资源和教程; 分享的时代, 请别再沉默!
沙海
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: