使用springboot实现读写分离
这也是闲着无聊然后去了解了一下然后写了一个项目,如有不足,欢迎来指出来
因为我们读写分离至少是有两个数据源,有可能是这样的
但是我们理想的状态并不是这样,而是这种状态
这时候保证在Spring提交事务之前确定数据源,利用AOP写个切换数据源的切面,让他的优先级高于Spring事务切面的优先级。
mybatis最终是要通过sqlsessionfactory获取数据连接,创建sqlsession并提交到数据库的。所以我们入手的地方有两点:
- 通过创建多种sqlsessionfactory比如masterFactory,slaverFactory来实现读写分离。
- 让sqlsessionfactory直接可以动态获取到只读或者写的数据源。
但是怎么切换数据源呢,spring中有一个AbstractRoutingDataSourc,这是DataSource的抽象,我们关注其中的四个方法
setTargetDataSources,setDefaultTargetDataSource,determineCurrentLookupKey,determineTargetDataSource,这四个方法:setTargetDataSources设置备选的数据源集合。 setDefaultTargetDataSource设置默认数据源,determineCurrentLookupKey决定当前数据源的对应的key,determineTargetDataSource方法,它才是获取数据源的实现,方法如下。
根据determineCurrentLookupKey获取的key,在resolvedDataSources这个Map中查找对应的datasource!,注意determineTargetDataSource方法竟然不使用的targetDataSources!
那一定存在resolvedDataSources与targetDataSources的对应关系。我接着翻阅代码,发现一个afterPropertiesSet方法(Spring源码中InitializingBean接口中的方法),这个方法将targetDataSources的值赋予了resolvedDataSources。源码如下:
afterPropertiesSet 方法,熟悉Spring的都知道,它在bean实例已经创建好,且属性值和依赖的其他bean实例都已经注入以后执行,也就是说调用,targetDataSources,defaultTargetDataSource的赋值一定要在afterPropertiesSet前边执行
好了,现在开始写,这里只列出部分重要代码,有兴趣可以下载项目
首先定义枚举类,
配置文件也不能少
读取配置文件
这里使用线程控制数据源切换
由于DynamicDataSource是单例的,线程不安全的,所以采用ThreadLocal保证线程安全,由DynamicDataSourceHolder完成
这里继承AbstractRoutingDataSource,并且对读进行简单的负载均衡,如果你是一个读库可以不用,判断返回当前数据源key
这里是mybatis配置,给sessionfactory设置数据源
@Configuration
@ConditionalOnClass({EnableTransactionManagement.class})
@Import({ DataBaseConfiguration.class})
public class MybatisConfiguration {
/** mybatis 配置路径 */
@Value("${spring.mybatis.configLocation:mybatis/mybatis.xml}")
private String MYBATIS_CONFIG;
/** mapper路径 */
@Value("${spring.mybatis.mapperLocations:mapper/*.xml}")
private String MAPPER_LOCATION;
@Value("${spring.datasource.type}")
private Class<? extends DataSource> dataSourceType;
@Value("${spring.datasource.readSize}")
private String dataSourceSize;
@Resource(name = "writeDataSource")
private DataSource dataSource;
@Resource(name = "readDataSources")
private List<DataSource> readDataSources;
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
/** 设置mybatis configuration 扫描路径 */
sqlSessionFactoryBean.setConfigLocation(new ClassPathResource(MYBATIS_CONFIG));
sqlSessionFactoryBean.setDataSource(roundRobinDataSouceProxy());
AbstractRoutingDataSource s= roundRobinDataSouceProxy();
/** 添加mapper 扫描路径 */
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources(MAPPER_LOCATION));
return sqlSessionFactoryBean.getObject();
}
/**
* 有多少个数据源就要配置多少个bean
* @return
*/
@Bean
public AbstractRoutingDataSource roundRobinDataSouceProxy() {
int size = Integer.parseInt(dataSourceSize);
MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource(size);
Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
// DataSource writeDataSource = SpringContextHolder.getBean("writeDataSource");
// 写
targetDataSources.put(DataSourceType.write.getType(),dataSource);
//1个读数据库时
// targetDataSources.put(DataSourceType.read.getType(),readDataSource);
//多个读数据库时
for (int i = 0; i < size; i++) {
targetDataSources.put(i, readDataSources.get(i));
}
proxy.setDefaultTargetDataSource(dataSource);
proxy.setTargetDataSources(targetDataSources);
return proxy;
}
}
``最后使用aop实现拦截,判断是读还是写,当然你也可以使用注解,方式不唯一
@Aspect
@Order(-1)// 保证该AOP在@Transactional之前执行
@Component
public class DataSourceAop {
private static Logger logger = Logger.getLogger(DataSourceAop.class);
@Before("execution(* com.ganinfo.*.mapper..*.select*(..)) || execution(* com.ganinfo.*.mapper..*.get*(..))|| execution(* com.ganinfo.*.mapper..*.query*(..))")
public void setReadDataSourceType() {
DataSourceContextHolder.read();
logger.info("dataSource 切换到:Read");
}
@Before("execution(* com.ganinfo.*.mapper..*.insert*(..)) || execution(* com.ganinfo.*.mapper..*.update*(..))")
public void setWriteDataSourceType() {
DataSourceContextHolder.write();
logger.info("dataSource 切换到:Write");
}
@After("execution(* com.ganinfo.*.mapper..*.*(..))")
public void remove(){
DataSourceContextHolder.clearDB();
logger.info("dataSource clear");
}
}
参考:https://blog.csdn.net/yunzhaji3762/article/details/79750343
这两天如果有需要项目可以评论,过两天也会把项目发出来