最近在学习 Spring boot,写了个读写分离。并未照搬网文,而是独立思考后的成果,写完以后发现从零开始写读写分离并不难!
我最初的想法是:读方法走读库,写方法走写库(一般是主库),保证在 Spring 提交事务之前确定数据源.
保证在 Spring 提交事务之前确定数据源,这个简单,利用 AOP 写个切换数据源的切面,让<typo id="typo-168" data-origin="他" ignoretag="true">他</typo>的优先级高于 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。源码如下:
@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 简单总结:
- 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();
}
@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;
}
其他代码,就不在这里赘述了,有兴趣可以移步完整代码。
gitee.com/WLjava/spri…
使用 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 前边执行。