SpringBoot AOP方式实现多数据源切换的方法

作者:zx 时间:2023-04-08 20:58:08 

最近在做保证金余额查询优化,在项目启动时候需要把余额全量加载到本地缓存,因为需要全量查询所有骑手的保证金余额,为了不影响主数据库的性能,考虑把这个查询走从库。所以涉及到需要在一个项目中配置多数据源,并且能够动态切换。经过一番摸索,完美实现动态切换,记录一下配置方法供大家参考。

设计总体思路

Spring-Boot+AOP方式实现多数据源切换,继承AbstractRoutingDataSource实现数据源动态的获取,在service层使用注解指定数据源。

步骤

一、多数据源配置

在application.properties中,我们的配置是这样的


#主数据源
druid.master.url=jdbc:mysql://url/masterdb?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull
druid.master.username=xxx
druid.master.password=123
druid.master.driver-class-name=com.mysql.jdbc.Driver
druid.master.max-wait=5000
druid.master.max-active=100
druid.master.test-on-borrow=true
druid.master.validation-query=SELECT 1

#从数据源
druid.slave.url=jdbc:mysql://url/slavedb?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull
druid.slave.username=xxx
druid.slave.password=123
druid.slave.driver-class-name=com.mysql.jdbc.Driver
druid.slave.max-wait=5000
druid.slave.max-active=100
druid.slave.test-on-borrow=true
druid.slave.validation-query=SELECT 1

读取配置


<!-- master数据源 -->
<bean primary="true" id="masterdb" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
 <!-- 基本属性 url、user、password -->
 <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
 <property name="url" value="${druid.master.url}"/>
 <property name="username" value="${druid.master.username}"/>
 <property name="password" value="${druid.master.password}"/>
 <!-- 配置初始化最大 -->
 <property name="maxActive" value="${druid.master.max-active}"/>
 <!-- 配置获取连接等待超时的时间 -->
 <property name="maxWait" value="${druid.master.max-wait}"/>
 <property name="validationQuery" value="${druid.master.validation-query}"/>
 <property name="testOnBorrow" value="${druid.master.test-on-borrow}"/>

</bean>

<!-- slave数据源 -->
<bean primary="true" id="slavedb" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
 <!-- 基本属性 url、user、password -->
 <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
 <property name="url" value="${druid.slave.url}"/>
 <property name="username" value="${druid.slave.username}"/>
 <property name="password" value="${druid.slave.password}"/>

<!-- 配置初始化大小、最小、最大 -->
 <property name="maxActive" value="${druid.slave.max-active}"/>
 <!-- 配置获取连接等待超时的时间 -->
 <property name="maxWait" value="${druid.slave.max-wait}"/>
 <property name="validationQuery" value="${druid.slave.validation-query}"/>
 <property name="testOnBorrow" value="${druid.slave.test-on-borrow}"/>
</bean>

<!-- 动态数据源,根据service接口上的注解来决定取哪个数据源 -->
<bean id="dataSource" class="datasource.DynamicDataSource">
 <property name="targetDataSources">
   <map key-type="java.lang.String">
     <entry key="slave" value-ref="slavedb"/>
     <entry key="master" value-ref="masterdb"/>
   </map>
 </property>
 <property name="defaultTargetDataSource" ref="masterdb"/>
</bean>

<!-- Spring JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
 <property name="dataSource" ref="dataSource" />
</bean>

<!-- Spring事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
 <property name="dataSource" ref="dataSource" />
</bean>

<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
 <property name="transactionManager" ref="transactionManager"/>
</bean>

<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true" order="2" />

<!-- depositdbSqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
 <property name="dataSource" ref="dataSource" />
 <property name="mapperLocations" value="classpath*:mapper-xxdb/*Mapper*.xml" />
</bean>

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
 <property name="basePackage" value="xxdb.mapper"/>
 <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>

二、动态数据源

spring为我们提供了AbstractRoutingDataSource,即带路由的数据源。继承后我们需要实现它的determineCurrentLookupKey(),该方法用于自定义实际数据源名称的路由选择方法,由于我们将信息保存到了ThreadLocal中,所以只需要从中拿出来即可。


public class DynamicDataSource extends AbstractRoutingDataSource {
 private Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
 protected Object determineCurrentLookupKey() {
   String dataSource = JdbcContextHolder.getDataSource();
   logger.info("数据源为{}",dataSource);
   return dataSource;
 }
}

三. 数据源动态切换类

动态数据源切换是基于AOP的,所以我们需要声明一个AOP切面,并在切面前做数据源切换,切面完成后移除数据源名称。


@Aspect
@Order(1)  //设置AOP执行顺序(需要在事务之前,否则事务只发生在默认库中)
@Component
public class DataSourceAspect {

private Logger logger = LoggerFactory.getLogger(this.getClass());
 //切点
 @Pointcut("execution(* com.xxx.service.*.*(..))")
 public void aspect() { }

@Before("aspect()")
 private void before(JoinPoint point) {
   Object target = point.getTarget();
   String method = point.getSignature().getName();
   Class<?> classz = target.getClass();// 获取目标类
   Class<?>[] parameterTypes = ((MethodSignature) point.getSignature())
       .getMethod().getParameterTypes();
   try {
     Method m = classz.getMethod(method, parameterTypes);
     if (m != null && m.isAnnotationPresent(MyDataSource.class)) {
       MyDataSource data = m.getAnnotation(MyDataSource.class);
       logger.info("method :{},datasource:{}",m.getName() ,data.value().getName());
       JdbcContextHolder.putDataSource(data.value().getName());// 数据源放到当前线程中
     }
   } catch (Exception e) {
     logger.error("get datasource error ",e);
     //默认选择master
     JdbcContextHolder.putDataSource(DataSourceType.Master.getName());// 数据源放到当前线程中
   }

}

@AfterReturning("aspect()")
 public void after(JoinPoint point) {
   JdbcContextHolder.clearDataSource();
 }
}

四、数据源管理类


public class JdbcContextHolder {

private final static ThreadLocal<String> local = new ThreadLocal<>();

public static void putDataSource(String name) {
   local.set(name);
 }

public static String getDataSource() {
   return local.get();
 }

public static void clearDataSource() {
   local.remove();
 }
}

五、数据源注解和枚举

我们切换数据源时,一般都是在调用具体接口的方法前实现,所以我们定义一个方法注解,当AOP检测到方法上有该注解时,根据注解中value对应的名称进行切换。


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyDataSource {

DataSourceType value();

}
public enum DataSourceType {
 // 主表
 Master("master"),
 // 从表
 Slave("slave");

private String name;

private DataSourceType(String name) {
   this.name = name;
 }

public String getName() {
   return name;
 }

public void setName(String name) {
   this.name = name;
 }
}

六、切点注解

由于我们的动态数据源配置了默认库,所以如果方法是操作默认库的可以不需要注解,如果要操作非默认数据源,我们需要在方法上添加@MyDataSource("数据源名称")注解,这样就可以利用AOP实现动态切换了


@Component
public class xxxServiceImpl {
 @Resource
 private XxxMapperExt xxxMapperExt;

@MyDataSource(value= DataSourceType.Slave)
 public List<Object> getAll(){
   return xxxMapperExt.getAll();
 }
}

来源:http://tech.dianwoda.com/2018/03/28/spring-boot-aopfang-shi-shi-xian-duo-shu-ju-yuan-qie-huan

标签:SpringBoot,AOP
0
投稿

猜你喜欢

  • Android中TextView动态设置缩进距离的方法

    2023-08-07 09:52:00
  • SpringBoot2.x过后static下的静态资源无法访问的问题

    2023-07-07 00:21:09
  • Android 情景模式的设置代码

    2023-09-23 07:26:37
  • jdk自带线程池实例详解

    2022-05-14 02:04:56
  • c# 实现模糊PID控制算法

    2022-12-06 13:06:22
  • JDBC环境设置(中文详解)

    2021-10-16 21:27:16
  • java简单坦克大战制作代码

    2023-02-07 05:08:37
  • Flutter手机权限检查与申请实现方法详解

    2022-01-15 23:52:38
  • 关于@ConditionalOnProperty的作用及用法说明

    2023-11-24 02:39:19
  • Android中WebView图片实现自适应的方法

    2021-11-15 06:37:32
  • C#实现带阴历显示的日期代码

    2023-12-11 02:19:04
  • Android图片加载库Glide用法

    2023-08-11 10:12:01
  • SpringBoot中的PUT和Delete请求使用

    2022-01-22 19:33:32
  • 一文详解Java中的Stream的汇总和分组操作

    2021-12-24 17:32:54
  • WindowsForm实现TextBox占位符Placeholder提示功能

    2023-03-18 12:57:44
  • C#中Hashtable和Dictionary的区别与用法示例

    2023-10-05 03:22:59
  • SpringSecurity Jwt Token 自动刷新的实现

    2022-04-28 18:49:45
  • SpringBoot项目速度提升之延迟初始化(Lazy Initialization)详解

    2021-11-27 06:37:22
  • Android 关闭多个Activity的实现方法

    2022-02-24 21:21:19
  • Flutter 使用fluro的转场动画进行页面切换

    2023-06-17 11:49:26
  • asp之家 软件编程 m.aspxhome.com