在项目中,有这么一些场景,需要去考虑数据源路由的事情,比如说
你看到了,以上不管是分库、分表、还是主从实现读写分离,都表现出了一个应用,对应多个数据库实例的场景。
那么这个时候,我们就需要去考虑多数据源路由的事情了。在具体实现方案上,有两种比较常见的选择方案
这两种方案,具体该如何去选择呢?如果我们是中小型的团队,应用规模也不是很大,那么适合选择在应用层面实现数据源路由的方案,主要理由
基于以上,反过来我们就可以考虑采用proxy的代理方案了。方便你理解,还是上一个图吧,典型的主从集群,读写分离架构
1.准备环境
通过以上描述,我们知道在实现多数据路由上,有两种可选的方案,本篇文章我们分享方案一的实现springboot多数据源配置,即扩展spring提供的AbstractRoutingDataSource。
首先来准备环境,我在本地准备了两个数据库实例
这两个库虽然本身没有直接的关系,我们假设这是一个报表类应用(聚合多个数据源),且我们的重点是实现在数据源之间的路由,因此不影响。
在本案例的实现中
导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.9</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
2.核心配置
本案例持久层,选择的是mybatis框架,相关核心配置如下
2.1.application.yml
server:
port: 8080
spring:
application:
name: follow-me-springboot-multidatasource
#数据源配置
mysql:
datasource:
cn.edu.anan.entity :
classpath:mybatis/mapper/*Mapper.xml :
classpath:mybatis/sqlMapConfig.xml :
write:
url: jdbc:mysql://127.0.0.1:3320/training?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: admin
com.mysql.cj.jdbc.Driver :
read:
url: jdbc:mysql://127.0.0.1:3310/user-center?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
username: root
password: admin
com.mysql.cj.jdbc.Driver :
2.2.sqlMapConfig.xml
<configuration>
<settings>
<setting name="cacheEnabled" value="true" />
<setting name="lazyLoadingEnabled" value="true" />
<setting name="aggressiveLazyLoading" value="true"/>
<setting name="multipleResultSetsEnabled" value="true" />
<setting name="useColumnLabel" value="true" />
<setting name="useGeneratedKeys" value="false" />
<setting name="autoMappingBehavior" value="PARTIAL" />
<setting name="defaultExecutorType" value="SIMPLE" />
<setting name="defaultStatementTimeout" value="25" />
<setting name="defaultFetchSize" value="100" />
<setting name="safeRowBoundsEnabled" value="false" />
<setting name="mapUnderscoreToCamelCase" value="true" />
<setting name="localCacheScope" value="SESSION" />
<setting name="jdbcTypeForNull" value="NULL" />
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString" />
</settings>
</configuration>
2.3.项目代码结构
3.核心代码
扩展AbstractRoutingDataSource实现多数据源之间路由,核心是
3.1.数据源配置DataSourceConfig
/**
* 数据源配置
*
* @author ThinkPad
* @version 1.0
* @date 2021/9/21 15:55
*/
public class DataSourceConfig {
/**
* 包扫描别名
*/
private String typeAliasesPackage;
/**
*mapper映射文件位置
*/
private String mapperLocation;
/**
*mybatis配置文件位置
*/
private String configLocation;
/**
* 写数据源
* @return
*/
public DataSource writeDataSource() {
return new DruidDataSource();
}
/**
* 读数据源
* @return
*/
public DataSource readDataSource() {
return new DruidDataSource();
}
/**
* 配置sqlSessionFactory
* @return
* @throws Exception
*/
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(routingDataSource());
bean.setTypeAliasesPackage(typeAliasesPackage);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
bean.setMapperLocations(resolver.getResources(mapperLocation));
bean.setConfigLocation(resolver.getResource(configLocation));
return bean.getObject();
}
/**
* 设置数据源路由表
* @return
*/
public AbstractRoutingDataSource routingDataSource() {
MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource();
Map
targetDataSources.put(DbContextHolder.WRITE, writeDataSource());
targetDataSources.put(DbContextHolder.READ, readDataSource());
proxy.setDefaultTargetDataSource(writeDataSource());
proxy.setTargetDataSources(targetDataSources);
return proxy;
}
/**
* 配置事务管理器
* @return
*/
public DataSourceTransactionManager dataSourceTransactionManager() {
return new DataSourceTransactionManager(routingDataSource());
}
}
3.2.数据源线程上下文DbContextHolder
/**
* 数据源上下文环境
*
* @author ThinkPad
* @version 1.0
* @date 2021/9/21 16:02
*/
@Slf4j
public class DbContextHolder {
/**
* 写数据源标识
*/
public static final String WRITE = "write";
/**
* 读数据源标识
*/
public static final String READ = "read";
/**
* 本地线程绑定
*/
private static ThreadLocal contextHolder= new ThreadLocal();
/**
* 设置数据源类型
* @param dbType
*/
public static void setDbType(String dbType) {
if (dbType == null) {
log.error("dbType为空");
throw new NullPointerException();
}
log.info("设置dbType为:{}",dbType);
contextHolder.set(dbType);
}
/**
* 获取数据源类型
* @return
*/
public static String getDbType() {
return contextHolder.get() == null ? WRITE : contextHolder.get();
}
/**
* 清除ThreadLocal
*/
public static void clearDbType() {
contextHolder.remove();
}
}
3.3.扩展数据源路由MyAbstractRoutingDataSource
/**
* 数据源路由,扩展AbstractRoutingDataSource
*
* @author ThinkPad
* @version 1.0
* @date 2021/9/21 16:05
*/
4j
public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource{
/**
* 返回数据源路由key
* @return
*/
protected Object determineCurrentLookupKey() {
String dbKey = DbContextHolder.getDbType();
if (dbKey == DbContextHolder.WRITE) {
log.info("当前更新动作,走主库");
return dbKey;
}
log.info("当前读取操作,走从库");
return DbContextHolder.READ;
}
}
3.4.注解DataSourceSwitcher
(RetentionPolicy.RUNTIME)
(ElementType.METHOD)
public DataSourceSwitcher {
/**
* 默认数据源
* @return
*/
String value() default "write";
/**
* 清除
* @return
*/
boolean clear() default true;
}
3.5.切面ReadOnlyAspect
/**
* 读数据源切面
*
* @author ThinkPad
* @version 1.0
* @date 2021/9/21 17:07
*/
4j
public class ReadOnlyAspect implements Ordered{
/**
* 线程上下文设置读数据源
* @param pjp
* @param read
* @return
* @throws Throwable
*/
"@annotation(read)") (
public Object setRead(ProceedingJoinPoint pjp, DataSourceSwitcher read) throws Throwable{
try{
DbContextHolder.setDbType(DbContextHolder.READ);
return pjp.proceed();
}finally {
DbContextHolder.clearDbType();
log.info("清除threadLocal");
}
}
/**
* 顺序
* @return
*/
public int getOrder() {
return 0;
}
}
4.使用案例
在数据源路由配置中,设置了默认的数据源是:写数据源
如果是写入操作,默认应用代码不需要特殊处理;如果读操作,应用代码方法上,需要加上@DataSourceSwitcher(value=”read”)注解,比如
那么在运行时,通过切面绑定线程上下文数据源信息
准备两个测试案例
/**
* controller
*
* @author ThinkPad
* @version 1.0
* @date 2021/9/21 15:46
*/
public class MultiDataSourceController {
private UserService userService;
private OrderService orderService;
/**
* 写数据源测试:写入一个订单
* @param order
* @return
*/
public Order write( Order order){
orderService.insertOne(order);
return order;
}
/**
* 读数据源测试:查询全部用户列表数据
* @return
*/
public List read( User user){
log.info("查询条件:{}", user);
return userService.selectAll(user);
}
启动应用,分别访问端点
观察控制台输出
案例输出读操作springboot多数据源配置,走从库;写操作,走主库。我们看到已经实现多数据源路由,最后本文源码,请参考:,子模块:follow-me-springboot-multidatasource