SpringBoot + Mybatis实现动态数据源切换

1.动态数据源介绍

        在开发中会经常遇见多数据源的场景,数据量超过500万行就要考虑分库分表和读写分离,那么我们在正向操作和逆向操作的时候,就需要动态的切换到相应的数据库,进行相关的操作。

解决思路:

现在项目的结构设计基本上是基于MVC的,那么数据库的操作集中在dao层完成,主要业务逻辑在service层处理,controller层处理请求。假设在执行dao层代码之前能够将数据源(DataSource)换成我们想要执行操作的数据源,那么这个问题就解决了。

        Spring内置了一个AbstractRoutingDataSource,它可以把多个数据源配置成一个Map,然后,根据不同的key返回不同的数据源。因为AbstractRoutingDataSource也是一个DataSource接口,因此,应用程序可以先设置好key, 访问数据库的代码就可以从AbstractRoutingDataSource拿到对应的一个真实的数据源,从而访问指定的数据库。

查看AbstractRoutingDataSource类:

/*** Abstract {@link javax.sql.DataSource} implementation that routes {@link #getConnection()} 
* calls to one of various target DataSources based on a lookup key. The latter is usually 
* (but not necessarily) determined through some thread-bound transaction context. *
* @author Juergen Hoeller 
* @since 2.0.1 
* @see #setTargetDataSources 
* @see #setDefaultTargetDataSource 
* @see #determineCurrentLookupKey() 
*///翻译结果如下 
/*** 抽象 {@link javax.sql.DataSource} 路由 {@link #getConnection ()} 的实现 
* 根据查找键调用不同的目标数据之一。后者通常是 
* (但不一定) 通过某些线程绑定事务上下文来确定。
*
* @author 
* @since 2.0。1 
* @see #setTargetDataSources 
* @see #setDefaultTargetDataSource 
* @see #determineCurrentLookupKey () 
*/
public abstract class AbstractRoutingDataSource extends AbstractDataSource
implements InitializingBean {
    ....... 
/*** Specify the map of target DataSources, with the lookup key as key. 
* The mapped value can either be a corresponding {@link javax.sql.DataSource}          * instance or a data source name String (to be resolved via a 
* {@link #setDataSourceLookup DataSourceLookup}). 
* <p>The key can be of arbitrary type; this class implements the 
* generic lookup process only. The concrete key representation will
* be handled by {@link #resolveSpecifiedLookupKey(Object)} and 
* {@link #determineCurrentLookupKey()}. 
*///翻译如下 
/***指定目标数据源的映射,查找键为键。
*映射的值可以是相应的{@link javax.sql.DataSource} 
*实例或数据源名称字符串(要通过 
* {@link #setDataSourceLookup DataSourceLookup})。 
*键可以是任意类型的; 这个类实现了 
*通用查找过程只。 具体的关键表示将 
*由{@link #resolveSpecifiedLookupKey(Object)}和 
* {@link #determineCurrentLookupKey()}。 
*/
public void setTargetDataSources(Map<Object, Object> targetDataSources) {  
            this.targetDataSources = targetDataSources; 
}
......
/*** Determine the current lookup key. This will typically be 
* implemented to check a thread-bound transaction context. 
* <p>Allows for arbitrary keys. The returned key needs 
* to match the stored lookup key type, as resolved by the 
* {@link #resolveSpecifiedLookupKey} method. 
*///翻译如下 
/*** 确定当前的查找键。这通常会 
* 实现以检查线程绑定的事务上下文。 
* <p> 允许任意键。返回的密钥需要 
* 与存储的查找密钥类型匹配, 如 
* {@link #resolveSpecifiedLookupKey} 方法。 
*/
 protected abstract Object determineCurrentLookupKey();
 }

        上面源码中还有另外一个核心的方法 setTargetDataSources(Map<Object, Object> targetDataSources) ,它需要一个Map,在方法注释中我们可以得知,这个Map存储的就是我们配置的多个数据源的键值对。我们整理一下这个类切换数据源的运作方式,这个类在连接数据库之前会执行determineCurrentLookupKey()方法,这个方法返回的数据将作为key去 targetDataSources中查找相应的值,如果查找到相对应的DataSource,那么就使用此DataSource获取数据库连接。

它是一个abstract类,所以我们使用的话,推荐的方式是创建一个类来继承它并且实现它的determineCurrentLookupKey() 方法,这个方法介绍上面也进行了说明,就是通过这个方法进行数据源的切换。

2. 配置数据源

第一步:配置多数据源

首先,我们在application.yml中配置两个数据源

# 数据源配置
spring:
  druid:
    datasource:
      master:
        driver-class-name: com.mysql.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/test_master
        username: root
        password: admin
      slave:
        driver-class-name: com.mysql.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/test_slave
        username: root
        password: admin

在SpringBoot的配置代码中,我们初始化两个数据源:

@Slf4j
@Configuration
public class MyDataSourceConfiguration {

    /**
     * Master data source.
     */
    @Bean("masterDataSource")
    @ConfigurationProperties(prefix = "spring.druid.datasource.master")
    DataSource masterDataSource() {
        log.info("create master datasource...");
        return DataSourceBuilder.create().build();
    }
    /**
     * Slave data source.
     */
    @Bean("slaveDataSource")
    @ConfigurationProperties(prefix = "spring.druid.datasource.slave")
    DataSource slaveDataSource() {
        log.info("create slave datasource...");
        return DataSourceBuilder.create().build();
    }


    /**
     * 将数据源信息注入到RoutingDataSource中,方便后续determineCurrentLookupKey()方法根据key获取数据源
     * @param masterDataSource
     * @param slaveDataSource
     * @return
     */
    @Bean
    @Primary
    DataSource primaryDataSource(
            @Autowired @Qualifier("masterDataSource") DataSource
                    masterDataSource,
            @Autowired @Qualifier("slaveDataSource") DataSource
                    slaveDataSource
    ) {
        Map<Object, Object> map = new HashMap<>();
        map.put("masterDataSource", masterDataSource);
        map.put("slaveDataSource", slaveDataSource);
        RoutingDataSource routing = new RoutingDataSource();
        routing.setTargetDataSources(map);
        //设置默认的数据源为masterDataSource
        routing.setDefaultTargetDataSource(masterDataSource);
        return routing;
    }
}

第二步:编写一个RoutingDataSourceContext ,来设置并动态存储key

         在Servlet的线程模型中,使用ThreadLocal存储key最合适。

public class RoutingDataSourceContext {
    // holds data source key in thread local:
    static final ThreadLocal<String> threadLocalDataSourceKey = new
            ThreadLocal<>();

    public static String getDataSourceRoutingKey() {
        String key = threadLocalDataSourceKey.get();
        return key == null ? "masterDataSource" : key;
    }

    public RoutingDataSourceContext(String key) {
        threadLocalDataSourceKey.set(key);
    }

    public void close() {
        threadLocalDataSourceKey.remove();
    }
}

第三步:编写RoutingDataSource

public class RoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        /**
         * 在连接数据库之前会执行determineCurrentLookupKey()方法
         * 这个方法返回的数据将作为key去targetDataSources中查找相应的值,
         * 如果查找到相对应的DataSource,那么就使用此DataSource获取数据库连接
         * RoutingDataSourceContext.getDataSourceRoutingKey()获取想要请求目标数据源的key
         */
        return RoutingDataSourceContext.getDataSourceRoutingKey();
    }
}

第四步:使用AOP实现切换数据源

        我们仔细想想,Spring提供的声明式事务管理,就只需要一个 @Transactional() 注解,放在某个Java方法上,这个方法就自动具有了事务。

        我们也可以编写一个类似的 @RoutingWith("slaveDataSource") 注解,放到某个Controller的方法上,这个方法内部就自动选择了对应的数据源。或者直接拦截那些包或者类,让他进行相应的数据源切换。

编写对应注解

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

    String value() default "master";
}

编写对应的切面类

@Aspect
@Component
public class RoutingAspect {

    /**
     * 标示使用了@RoutingWith()的方法就会执行此方法
     * @param joinPoint
     * @param routingWith
     * @return
     * @throws Throwable
     */
    @Around("@annotation(routingWith)")
    public Object routingWithDataSource(ProceedingJoinPoint joinPoint,
                                        RoutingWith routingWith) throws Throwable {
        //RoutingWith是Spring传入的注解实例,我们根据注解的value()获取配置的key。
        String key = routingWith.value();
        //向ThreadLocal中写入数据源对应的key值
        RoutingDataSourceContext ctx = new RoutingDataSourceContext(key);
        return joinPoint.proceed();
    }
}

第五步:编写demo测试动态数据源的切换

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private IUserService userService;

    @RoutingWith("masterDataSource")
    @RequestMapping("/getMasterAll")
    public List<User> getMasterAll(){
        return userService.list();
    }

    @RoutingWith("slaveDataSource")
    @RequestMapping("/getSlaveAll")
    public List<User> getSlaveAll(){
        return userService.list();
    }
}

首先我们来看一下,两个库中对应表的数据。

master库中表的数据

 slave库中表的数据

 分别调用不同的接口查看是否实现动态选择数据源的功能

访问http://127.0.0.1:8080/user/getMasterAll看是否获取master库中的数据

 访问http://127.0.0.1:8080/user/getSlaveAll看是否获取slave库中的数据

从测试结果上来看,通过调用不同的接口实现了访问不同数据源,现实了简单的动态数据源的切换。

当然也可以将数据源信息保存到数据库表中,从而实现动态数据源的切换。

猜你喜欢

转载自blog.csdn.net/xiaozhang_man/article/details/121724822
今日推荐