上文说到,springboot+mybatis项目配置多个数据源,但是整个dao类下的所有方法(甚至是整个包下)都指向同一个数据源,不够灵活。如果存在这样的场景,项目中有多个数据库,且是读写分离的,在一个dao类中有CRUD方法,CUD方法我们希望操作的是主库,读方法我们希望操作从库,也就是说在方法上指定操作的数据源,这样就需要配置动态数据源。
AbstractRoutingDataSource
Spring boot提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。
AbstractRoutingDataSource的多数据源动态切换的核心逻辑是:在程序运行时,把数据源数据源通过 AbstractRoutingDataSource 动态织入到程序中,灵活的进行数据源切换。
基于AbstractRoutingDataSource的多数据源动态切换,可以实现读写分离,这么做缺点也很明显,无法动态的增加数据源。
实现逻辑:
- 定义DynamicDataSource类继承抽象类AbstractRoutingDataSource,并实现了determineCurrentLookupKey()方法。
- 把配置的多个数据源会放在AbstractRoutingDataSource的 targetDataSources和defaultTargetDataSource中,然后通过afterPropertiesSet()方法将数据源分别进行复制到resolvedDataSources和resolvedDefaultDataSource中。
- 调用AbstractRoutingDataSource的getConnection()的方法的时候,先调用determineTargetDataSource()方法返回DataSource在进行getConnection()。
测试代码
springboot整合mybatis不赘述了,默认搭建好
Demo代码结构一览:
首先application.yml中配置多个数据源的数据
spring:
datasource:
db1:
driverClassName: com.mysql.jdbc.Driver
jdbcUrl: jdbc:mysql://192.168.233.136:3306/mydata?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
username: root
password: 123456
db2:
driverClassName: com.mysql.jdbc.Driver
jdbcUrl: jdbc:mysql://192.168.233.141:33306/today_top_news?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
username: root
password: 123456
启动类也无需特殊处理
@SpringBootApplication
@MapperScan("com.meng.dao")
public class DynamicDbApplication {
public static void main(String[] args) {
SpringApplication.run(DynamicDbApplication.class , args);
}
}
pom.xml
<properties>
<spring-boot.version>2.3.12.RELEASE</spring-boot.version>
</properties>
<dependencies>
<!--注意:由于 spring-boot-starter-web 默认替我们引入了核心启动器 spring-boot-starter,
因此,当 Spring Boot 项目中的 pom.xml 引入了 spring-boot-starter-web 的依赖后,
就无须在引入 spring-boot-starter 核心启动器的依赖了-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
<!--整合mytais-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.20</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
<scope>compile</scope>
</dependency>
</dependencies>
springboot项目要想使用AOP需要加spring-boot-starter-aop、aspectjweaver这两个依赖
新建类DynamicDataSource,继承AbstractRoutingDataSource
/**
* 扩展 Spring 的 AbstractRoutingDataSource 抽象类,重写 determineCurrentLookupKey 方法
* 动态数据源
* determineCurrentLookupKey() 方法决定使用哪个数据源
*/
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。
* 也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】,相当于线程的 private static 类型变量。
*/
public static final ThreadLocal<String> holder = new ThreadLocal<>();
@Override
protected Object determineCurrentLookupKey() {
return getDataSource();
}
public static void setDataSource(String type) {
holder.set(type);
}
public static String getDataSource(){
return holder.get();
}
public static void removeDataSource(){
holder.remove();
}
}
定义一个DataSourceConfig类
@Configuration
public class DataSourceConfig {
/**
* 定义两个数据源,读取application.yml中配置
* @return
*/
@Bean(name = "db1")
@ConfigurationProperties(prefix = "spring.datasource.db1") // application.yml中对应属性的前缀
public DataSource dataSource1() {
return DataSourceBuilder.create().build();
}
@Bean(name = "db2")
@ConfigurationProperties(prefix = "spring.datasource.db2") // application.yml中对应属性的前缀
public DataSource dataSource2() {
return DataSourceBuilder.create().build();
}
/**
* 配置自定义的类DynamicDataSource,这里是把两个(多个)数据源放到DynamicDataSource中,
* 同时设置一个默认的数据源
*/
@Bean("dynamicDataSource")
public DynamicDataSource dynamicDataSource(@Qualifier("db1")DataSource db1,
@Qualifier("db2")DataSource db2){
Map<Object,Object> targetDataSource = new HashMap<>();
targetDataSource.put("db1",db1);
targetDataSource.put("db2",db2);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(targetDataSource);
dynamicDataSource.setDefaultTargetDataSource(db1);
return dynamicDataSource;
}
/**
* 把动态数据源放到SqlSessionFactory
* 同时配置扫描的mapping.xml
*/
@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier("db1")DataSource db1,
@Qualifier("db2")DataSource db2) throws Exception{
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource(db1,db2));
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
return sqlSessionFactoryBean.getObject();
}
这个类中创建了两个数据源,和一个动态数据源,把两个数据源放到动态数据源中,同时指定一个默认的数据源,那么如果要切换数据源,只要通知系统使用DynamicDataSource 中的其他数据源即可,我们可以选择使用AOP实现
自定义注解DynamicDB
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DynamicDB {
String value() default "";
}
写一个切面类DynamicDataSourceAspect,用于拦截方法,切换数据源
@Aspect
@Component
public class DynamicDataSourceAspect {
@Pointcut("execution(* com.meng.dao.*.*(..))")
public void pointcut(){}
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint)throws Throwable{
System.out.println("-------------> selected dataSource aspect ");
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
DynamicDB dynamicDB = method.getAnnotation(DynamicDB.class);
if(dynamicDB != null){
String value = dynamicDB.value();
//切换成指定的数据源
DynamicDataSource.setDataSource(value);
}
try {
return joinPoint.proceed();
}finally {
//操作完成,移除指定数据源,还原默认数据源
DynamicDataSource.removeDataSource();
}
}
将需要切换数据源的方法添加自定义注解
@Repository
public interface ResultDao {
@DynamicDB("db2")
Result selectByPrimaryKey(Long id);
int insertSelective(Result record);
int deleteByPrimaryKey(Long id);
int insert(Result record);
这样就完成了数据源的动态切换
注意,在开发中,这个注解应该要作用在service中的方法上,这里简单测试,就放在dao了
测试类
@SpringBootTest(classes = DynamicDbApplication.class)
@RunWith(SpringRunner.class)
public class TestDemo01 {
@Autowired
private ResultDao dao;
@Test
public void test02(){
Result Result = dao.selectByPrimaryKey(2l);
System.out.println("Result = " + Result);
}
}
关于事务
AbstractRoutingDataSource 只支持单库事务,也就是说切换数据源要在开启事务之前执行。 spring DataSourceTransactionManager进行事务管理,开启事务,会将数据源缓存到DataSourceTransactionObject对象中进行后续的commit rollback等事务操作。
.
.
DynamicDataSourceAspect还可以这样写
@Aspect
@Component
public class DynamicDataSourceAspect {
@Before("@annotation(DynamicDB)")
public void beforeSwitchDS(JoinPoint point){
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DynamicDB dynamicDB = method.getAnnotation(DynamicDB.class);
if(dynamicDB != null){
String value = dynamicDB.value();
//切换成指定的数据源
DynamicDataSource.setDataSource(value);
}
}
@After("@annotation(DynamicDB)")
public void afterSwitchDS(JoinPoint point){
//操作完成,移除指定数据源,还原默认数据源
DynamicDataSource.removeDataSource();
}
@Before(“@annotation(DynamicDB)”),这种写法不需要切入点表达式了,我还真是头一次见,以后可以研究研究