It is not difficult to write simple read-write separation from scratch!

Recently, I was learning Spring boot and wrote a read-write separation. I did not copy the online text, but the result of independent thinking. After writing it, I found that it is not difficult to separate writing and reading from scratch!

My initial thought was: read method walk-through library, write method walk-through library (usually the main library), and ensure that the data source is determined before Spring commits the transaction.

 

Ensure that the data source is determined before Spring commits the transaction. This is simple. Use AOP to write an aspect that switches the data source, so that its priority is higher than that of the Spring transaction aspect. As for the distinction between read and write methods, two annotations can be used.

But how to switch databases? I have absolutely no idea! Years of experience tell me

When you don't know a technology at all, search and learn the necessary knowledge first, and then try it out.

                                                                                         --Wen Anshi 20180309

I searched some web articles and found that an AbstractRoutingDataSource class is mentioned. View the source code comments as follows

/**
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 is the abstraction of DataSource, which switches between multiple databases based on the lookup key. Focus on the three methods setTargetDataSources, setDefaultTargetDataSource, determineCurrentLookupKey. Then AbstractRoutingDataSource is the key to Spring's read-write separation.

After carefully reading the three methods, basically the meaning of the method name is the same. setTargetDataSources sets the set of alternative data sources. setDefaultTargetDataSource sets the default data source, and determineCurrentLookupKey determines the corresponding key of the current data source.

But I'm curious that none of these 3 methods contain the logic of switching databases ! I carefully read the source code and found a method, the determineTargetDataSource method, which is actually the implementation of obtaining the data source. The source code is as follows:

    //切换数据库的核心逻辑
    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;
	}

Simply put, according to the key obtained by determineCurrentLookupKey, find the corresponding datasource in the resolvedDataSources Map! , notice that the determineTargetDataSource method doesn't even use targetDataSources!

There must be a correspondence between resolvedDataSources and targetDataSources. I went through the code and found an afterPropertiesSet method (a method in the InitializingBean interface in the Spring source code) that assigns the value of targetDataSources to resolvedDataSources. The source code is as follows:

	@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);
		}
	}

The afterPropertiesSet method, as anyone familiar with Spring knows, is executed after the bean instance has been created and the property values ​​and other dependent bean instances have been injected.

That is to say , the assignment of targetDataSources and defaultTargetDataSource must be executed before afterPropertiesSet.

AbstractRoutingDataSource brief summary:

  1. AbstractRoutingDataSource, which has a Map<Object,DataSource> field resolvedDataSources
  2. The determineTargetDataSource method obtains the key through the determineCurrentLookupKey method, and then obtains the corresponding DataSource from the map.
  3. setTargetDataSources set targetDataSources
  4. setDefaultTargetDataSource 设置 defaultTargetDataSource,
  5. targetDataSources and defaultTargetDataSource are converted to resolvedDataSources and resolvedDefaultDataSource respectively in afterPropertiesSet.
  6. The assignment of targetDataSources and defaultTargetDataSource must be executed before afterPropertiesSet.

After further understanding of the theory, the way of separation of reading and writing basically appears in front of us. ("The following methods are not unique")

First write a class that inherits AbstractRoutingDataSource, implements the determineCurrentLookupKey method, and the afterPropertiesSet method. In the afterPropertiesSet method, call super.afterPropertiesSet after calling the setDefaultTargetDataSource and setTargetDataSources methods.

Then define an aspect to execute before the transaction aspect to determine the key corresponding to the real data source. But this raises another question, how to pass each thread's independent key in a thread-safe manner ? Yes , use ThreadLocal to pass the key corresponding to the real data source .

ThreadLocal, the local variable of Thread, ensures that each thread maintains a copy of the variable

At this point, the basic logic is figured out, and then it is written.

DataSourceContextHolder uses ThreadLocal to store the key corresponding to the real data source

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();  
    }  
}

The DataSourceAopAspect aspect switches the key corresponding to the real data source, and sets the priority to ensure that it is higher than the transaction aspect

@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 implements the logic of 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;  
}

After the basic logic is implemented, proceed with general settings, set data sources, transactions, SqlSessionFactory, etc.

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

Other codes will not be repeated here, if you are interested, you can move to the complete code .

Using Spring to separate writing and reading, the core of which is AbstractRoutingDataSource. The source code is not difficult. After reading it, it is simple to write a separation of reading and writing! .

AbstractRoutingDataSource highlights:

  1. AbstractRoutingDataSource, which has a Map<Object,DataSource> field resolvedDataSources
  2. The determineTargetDataSource method obtains the key through the determineCurrentLookupKey method, and then obtains the corresponding DataSource from the map.
  3. setTargetDataSources set targetDataSources
  4. setDefaultTargetDataSource 设置 defaultTargetDataSource,
  5. targetDataSources and defaultTargetDataSource are converted to resolvedDataSources and resolvedDefaultDataSource respectively in afterPropertiesSet.
  6. The assignment of targetDataSources and defaultTargetDataSource must be executed before afterPropertiesSet.

It's been a bit of a busy week, and it took some time on Friday, but it's finally fulfilling its promise.

It's not easy to fulfill the promise, if you like it, please give it a like!

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324387775&siteId=291194637