Mybatis系列:数据源灵活切换

在业务场景中,随着数据量迅速增长,一个库一个表已经满足不了我们的需求的时候,我们就会考虑分库分表的操作,本文主要介绍 Mybatis 动态数据源切换,可用于读写分离或多库存储。

一、开始项目

1.1 数据库准备

我这里操作两个数据库(master/slave),首先都在两个库中插入同一个表,sql如下:

DROP TABLE IF EXISTS `user_info`;
CREATE TABLE `user_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_name` varchar(10) NOT NULL COMMENT '用户名',
  `pass_word` varchar(10) NOT NULL COMMENT '密码',
  `user_sex` varchar(10) DEFAULT NULL COMMENT '性别',
  `nick_name` varchar(10) DEFAULT NULL COMMENT '昵称',
  `gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

为了区分,master 库和 slave 分别插入一条不同的数据:

  • master
INSERT INTO `user_info` VALUES (1, 'master', 'password', '男', '主库', '2019-04-10 16:24:14');
  • slave
INSERT INTO `user_info` VALUES (2, 'slave', 'password', '女', '从库', '2019-04-10 16:24:14');

1.2 pom.xml

通过AOP注解切换数据源,需要AOP包,使用druid连接池,mysql + mybatis

<dependencies>
    <!-- aop 依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <!-- test -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.2</version>
    </dependency>
    <!-- druid连接池-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>1.8.4</scope>
    </dependency>
</dependencies>

1.3 application.yml

配置了 masterslave 两个库,这里也可以使用两种不同类型的数据库,如mysql + postgrapsql,连接池使用 druid

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 主数据源
    master:
      url: jdbc:mysql://47.98.178.84:3306/master
      username: master
      password: password
    # 其他数据源(可多个)
    slave:
      url: jdbc:mysql://47.98.178.84:3306/slave
      username: slave
      password: password
# mybatis sql 日志
logging:
  level:
    cn:
      van:
        annotation:
          multipleDataSource:
            mapper: debug

二、多数据源配置

2.1 数据源枚举

我这里有两个数据源,我们用枚举类来代替,方便我们后续扩展和管理。

public enum DynamicDSEnum {
    MASTER(1,"master"),
    SLAVE(2,"slave"),
    
    ;

    DynamicDSEnum(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }
    private Integer code;

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    private String desc;
    
}

2.2 数据源配置

这里配置了两个数据源,主库:masterDS 和 从库:slaveDS

@Configuration
public class DBConfig {

    /**
     * 主库
     * @return
     */
    @ConfigurationProperties(prefix = "spring.datasource.master")
    @Bean("masterDS")
    public DataSource masterDS() {
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 从库
     * @return
     */
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    @Bean("slaveDS")
    @Primary
    public DataSource slaveDS() {
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 主从动态配置
     */
    @Bean
    public DynamicDataSource dynamicDS(@Qualifier("masterDS") DataSource masterDataSource,
                                       @Autowired(required = false) @Qualifier("slaveDS") DataSource slaveDataSource) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
        targetDataSources.put(DynamicDSEnum.MASTER.getDesc(), masterDataSource);
        if (slaveDataSource != null) {
            targetDataSources.put(DynamicDSEnum.SLAVE.getDesc(), slaveDataSource);
        }
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        return dynamicDataSource;
    }

    @Bean
    public SqlSessionFactory sessionFactory(@Qualifier("dynamicDS") DataSource dynamicDataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*Mapper.xml"));
        bean.setDataSource(dynamicDataSource);
        return bean.getObject();
    }

    @Bean
    public SqlSessionTemplate sqlTemplate(@Qualifier("sessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean(name = "dataSourceTx")
    public DataSourceTransactionManager dataSourceTx(@Qualifier("dynamicDS") DataSource dynamicDataSource) {
        DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dynamicDataSource);
        return dataSourceTransactionManager;
    }
}

2.3 设置路由

用于记录当前线程使用的数据源的key是什么,以及记录所有注册成功的数据源的key的集合。用 ThreadLocal 保存数据源的信息到每个线程中,方便我们需要时获取。

public class DataSourceContextHolder {
    private static final ThreadLocal<String> DYNAMIC_DATASOURCE_CONTEXT = new ThreadLocal();

    public static void set(String datasourceType) {
        DYNAMIC_DATASOURCE_CONTEXT.set(datasourceType);
    }

    public static String get() {
        return DYNAMIC_DATASOURCE_CONTEXT.get();
    }

    public static void clear() {
        DYNAMIC_DATASOURCE_CONTEXT.remove();
    }
}

2.4 获取路由

Spring提供一个接口,名为AbstractRoutingDataSource的抽象类,我们只需要重写determineCurrentLookupKey方法就可以通知Springkey获取当前的数据源。

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.get();
    }
}

三、通过AOP注解实现数据源动态切换

3.1 切换数据源的注解

为了可以方便切换数据源,我们可以写一个注解,注解中包含数据源对应的枚举值,默认是主库,

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DS {
    /**
     * 如果没设置数据源,默认 用 master 库
     * @return
     */
    DynamicDSEnum value() default DynamicDSEnum.MASTER;

    boolean clear() default true;
}

3.2 切换数据源切面

对有注解的方法做切换数据源的操作。

@Aspect
@Component
@Slf4j
public class DataSourceAspect {

    @Pointcut("@annotation(cn.van.multipledata.demo.annotation.DS)")
    public void dataSourceAspect() {
    }

    @Around("dataSourceAspect()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        boolean clear = true;
        try {
            Method method = this.getMethod(pjp);
            DS DS = method.getAnnotation(DS.class);
            clear = DS.clear();
            DataSourceContextHolder.set(DS.value().getDesc());
            log.info("========数据源切换至:{}", DS.value().getDesc());
            return pjp.proceed();
        } finally {
            if (clear) {
                DataSourceContextHolder.clear();
            }

        }

    }

    private Method getMethod(JoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        return signature.getMethod();
    }
}

四、测试多数据源配置

UserService.java两个方法selectMaster()selectSlave()及其实现;分别查询 masterslave 库的数据。

@Service("userService")
public class UserServiceImpl implements UserService {

    @Resource
    UserMapper userMapper;

    @Override
    public List<UserInfo> selectMaster() {
        return userMapper.selectAll();
    }

    @DS(value = DynamicDSEnum.SLAVE)
    @Override
    public List<UserInfo> selectSlave() {
        return userMapper.selectAll();
    }
}

4.1 查询 master

@Test
public void selectMaster() {
    List<UserInfo> users = userService.selectMaster();
    log.info("master-users:{}", users);
}
  • 测试结果:
....: master-users:[User{id=1, userName='master', passWord='password', userSex='男', nickName='主库', gmtCreate=2019-04-11T05:24:14}]

4.2 查询 slave

@Test
public void selectSlave() {
    List<UserInfo> users = userService.selectSlave();
    log.info("slave-users:{}", users);
}
  • 测试结果:
...: slave-users:[User{id=2, userName='slave', passWord='password', userSex='女', nickName='从库', gmtCreate=2019-04-11T05:24:14}]

五、总结

相较于另一篇文章【Mybatis系列:配置多数据源最简解决方案】,该方式虽然配置麻烦,但是如果新增数据源,配置更简单。文中代码不全,文章代码,详见

Github 示例代码

5.1 日常求赞

博主祖传秘籍 Spring Boot 葵花宝典 开源中,欢迎前来吐槽,提供线索,告诉博主接下来更新哪方面文章,共同进步!

5.2 文化交流

  1. 风尘博客
  2. 风尘博客-掘金
  3. 风尘博客-博客园
  4. 风尘博客-CSDN
  5. Github

最新文章,欢迎关注:公众号-风尘博客;交流观点,欢迎添加:个人微信

风尘博客个人微信号

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值