SpringBoot读写分离实现(AbstractRoutingDataSource)
读写分离⼀直都是项⽬的标配,之前项⽬的做法⾮常简单,直接配置两个数据源,⼀个只读,⼀个只写,只读的放到ad,只写的放到
xxx.write包下。Service层调⽤的时候根据操作选择对应的数据源。主要配置:
<bean id="dataSource"class="org.apachemons.dbcp.BasicDataSource"
destroy-method="close">
略...
</bean>
<bean id="sqlSessionFactory"class="batis.spring.SqlSessionFactoryBean">
<property name="dataSource"ref="dataSource" />
<property name="configLocation"value="classpath:config/l" />
<property name="mapperLocations">
<array>
<value>classpath*:xxx/write/resource/*.l</value>
</array>
</property>
</bean>
<bean class="batis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage"value="xxx.write" />
<property name="sqlSessionFactoryBeanName"value="sqlSessionFactory" />
</bean>
<!-- Transaction manager for a single JDBC DataSource -->
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource"ref="dataSource" />
</bean>
以上实现了为只写的数据源配置事务。Mybatis⾃动扫描对应包下的xml⽂件。
这样做优点还是很明显的,简单易懂。以后只要有新功能按照读写分离原则放到指定包下即可。
缺点就是在Service层涉及到读写同时进⾏的时候,需要调⽤对应的Mapper,⽐如:xxxReadMapper,xxxWriteMapper 的⽅法。
如果以后读写分离改成的数据库层处理,那么这⾥的代码就需要合并到⼀起,增加⼯作量。
那有没有更好的⽅法呢?是否可以做到⾃动读写分离呢?
当然是有的,⽽且还有很多种⽅式,⽐如通过数据库代理的⽅式,⽽不是通过代码来实现。或者还有其他开源框架。这⾥介绍下我的实现⽅
式,基于AbstractRoutingDataSource。
该类通过代理的⽅式实现了数据源的动态分配,在使⽤时通过⾃定义的key来选择对应的数据源。它的注释是这么说明的:Abstract javax.sql.DataSource implementation that routes getConnection() calls to one of various target DataSources based on a lookup key. The latter
步骤1:执⾏db⽬录下的springboot.sql⽂件来初始化db,这⾥需要配置两个db,⼀个只读(springboot_r)⼀个写(springboot)。
步骤2:继承⾃AbstractRoutingDataSource,初始化结束时⾃动扫描容器内的数据源,实现⾃动代理
@Component("dynamicDataSource")
@Primary
@ConfigurationProperties(prefix = "dynamicDatasource")
public static class DynamicDataSource extends AbstractRoutingDataSource implements ApplicationContextAware {
public static final Map<String, String> DATASOURCE_STRATEGY = new HashMap<>();
private Map<String, String> strategy = new HashMap<>();
private ApplicationContext applicationContext;
private String defaultDataSource;
@Override
protected Object determineCurrentLookupKey() {
DataSource();
}
@Override
protected Object resolveSpecifiedLookupKey(Object lookupKey) {
solveSpecifiedLookupKey(lookupKey);
}
@Override
public void afterPropertiesSet() {
Map<String, DataSource> dataSources = BeansOfType(DataSource.class);
if (dataSources.size() == 0) {
throw new IllegalStateException("Datasource can not found");
}
// exclude current datasource
Map<Object, Object> targetDataSource = excludeCurrentDataSource(dataSources);
setTargetDataSources(targetDataSource);
// 多数据源⽅法设置
Iterator<String> it = strategy.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
String[] values = (key).split(",");
for (String v : values) {
if (StringUtils.isNotBlank(v)) {
DATASOURCE_STRATEGY.put(v, key);
}
}
}
// 默认数据源设置
(getDefaultDataSource()));
super.afterPropertiesSet();
}
/***
* exclude current Datasource
*
* @param dataSources
* @return
*/
private Map<Object, Object> excludeCurrentDataSource(Map<String, DataSource> dataSources) { Map<Object, Object> targetDataSource = new HashMap<>();
Iterator<String> keys = dataSources.keySet().iterator();
while (keys.hasNext()) {
String key = ();
if (!((key) instanceof DynamicDataSource)) {
targetDataSource.put(key, (key));
}
}
return targetDataSource;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext;
}
public Map<String, String> getStrategy() {
return strategy;
}
public void setStrategy(Map<String, String> strategy) {
this.strategy = strategy;
}
public String getDefaultDataSource() {
return defaultDataSource;
}
public void setDefaultDataSource(String defaultDataSource) {
this.defaultDataSource = defaultDataSource;
}
}
步骤3:配置读和写的数据源
@ConfigurationProperties(prefix = "db.mybatis.jdbc")
@Bean(destroyMethod = "close", name = "write")
public DataSource dataSourceWrite() {
log.info("*************************dataSource***********************");
BasicDataSource dataSource = new BasicDataSource();
dataSource.setRemoveAbandoned(true);
dataSource.setTestWhileIdle(true);
dataSource.setTimeBetweenEvictionRunsMillis(30000);
dataSource.setNumTestsPerEvictionRun(30);
dataSource.setMinEvictableIdleTimeMillis(1800000);
return dataSource;
}
@ConfigurationProperties(prefix = "db.mybatis2.jdbc")
@Bean(destroyMethod = "close", name = "read")
public DataSource dataSourceRead() {
log.info("*************************dataSource***********************");
BasicDataSource dataSource = new BasicDataSource();
dataSource.setRemoveAbandoned(true);
dataSource.setTestWhileIdle(true);
dataSource.setTimeBetweenEvictionRunsMillis(30000);
dataSource.setNumTestsPerEvictionRun(30);
springboot框架的作用
dataSource.setMinEvictableIdleTimeMillis(1800000);
return dataSource;
}
步骤4:为动态数据源配置读写分离策略,这⾥使⽤的是最简单的前缀规则,如果有需要可以⾃⾏改成正则表达式的⽅式,以下配置定义了get,find,select开头的⽅法都使⽤read数据源
ad=get,find,select
dynamicDatasource.strategy.write=insert,update,delete,login
dynamicDatasource.defaultDataSource=write
步骤5:单元测试,在test包下DynamicDataSourceTest类中有两个⽅法,⼀个测试只读⼀个测试写:
@Test
public void testLogin() throws Exception {
User user = new User();
user.setUsername("11111111111");
user.setPassword("123456");
User loginUser = userService.login(user);
System.out.println("登录结果:" + loginUser);
}
@Test
public void testFindUser() throws Exception {
User loginUser = userService.findUserByToken("xxx");
System.out.println("查询⽤户结果:" + loginUser);
}
执⾏testLogin单元测试可以看出这⾥的操作⽤的是写的数据源
ooo Using Connection [jdbc:mysql://localhost/springboot?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull, UserName=root@localhost
执⾏testFindUser可以看出这⾥⽤的是读的数据源
ooo Using Connection [jdbc:mysql://localhost/springboot_r?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull, UserName=root@localhost
这种⽅式优点:不需要再像之前⼀样对读写操作分离了,都可以统⼀到⼀个Mapper上,代码可以统⼀到⼀个包下。程序员甚⾄都不需要意
识到数据库的读写分离。以后替换成db层处理也是⾮常⽅便的。
注意点:
因为事务和动态数据源切换都是基于AOP的,所以顺序⾮常重要。动态切换要在事务之前,如果发现⽆法动态切换数据源那么可以看下他们
之间的顺序。
以上代码已提交⾄SpringBootLearning的DynamicDataSource⼯程。
说明:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论