从零开始写简易读写分离,不难嘛!

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

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

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

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

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

我搜索了一些网文,发现都提到了一个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。源码如下:

	@Override
	public 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();  
    }  
	@Override
	public int getOrder() {
		/** 
         * 值越小,越优先执行 要优于事务的执行 
         * 在启动类中加上了@EnableTransactionManagement(order = 10)  
         */  
		return 1;
	}
}

RoutingDataSouceImpl实现AbstractRoutingDataSource的逻辑

@Component
public 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;
	}

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

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

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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏枕边书

在Spring-Boot中实现通用Auth认证的几种方式

最近一直被无尽的业务需求淹没,没时间喘息,终于接到一个能让我突破代码舒适区的活儿,解决它的过程非常曲折,一度让我怀疑人生,不过收获也很大,代码方面不明显,但感觉...

10000
来自专栏Albert陈凯

Spark详解05架构Architecture架构

架构 前三章从 job 的角度介绍了用户写的 program 如何一步步地被分解和执行。这一章主要从架构的角度来讨论 master,worker,driver ...

33380
来自专栏猿湿Xoong

Android SystemUI(二):启动流程和初始化

32840
来自专栏老码农专栏

TodoBackend展示应用以及ActFramework的实现

13950
来自专栏吉浦迅科技

DAY71:阅读Device-side Launch from PTX

我们正带领大家开始阅读英文的《CUDA C Programming Guide》,今天是第71天,我们正在讲解CUDA 动态并行,希望在接下来的30天里,您可以...

12920
来自专栏coolblog.xyz技术专栏

Spring AOP 源码分析系列文章导读

前一段时间,我学习了 Spring IOC 容器方面的源码,并写了数篇文章对此进行讲解。在写完 Spring IOC 容器源码分析系列文章中的最后一篇后,没敢懈...

11530
来自专栏林冠宏的技术文章

关于Android中为什么主线程不会因为Looper.loop()里的死循环卡死?引发的思考,事实可能不是一个 epoll 那么 简单。

( 转载请务必标明出处:https://cloud.tencent.com/developer/user/1148436/activities) 前序 本文将...

34750
来自专栏coolblog.xyz技术专栏

Spring IOC 容器源码分析系列文章导读

Spring 是一个轻量级的企业级应用开发框架,于 2004 年由 Rod Johnson 发布了 1.0 版本。经过十几年的迭代,现在的 Spring 框架已...

300100
来自专栏架构师之旅

《Spring敲门砖之基础教程第一季》 第二章(1) Spring框架之IOC首例-HelloWorld

回顾 上一章我们主要学习了Spring的一些理论知识,对Spring框架有了一个总体的概括,大家应该在头脑里形成一个初步的印象,接下来我们就会针对Spring框...

205100
来自专栏Kubernetes

深度解析Kubernetes Local Persistent Volume(二)

摘要:上一篇博客”深度解析Kubernetes Local Persistent Volume(一)“对local volume的基本原理和注意事项进行了分析,...

1.7K30

扫码关注云+社区

领取腾讯云代金券