需求背景



扩展:当业务也来越复杂,数据量越来越庞大时,就可能会对数据库进行分库分表、读写分离等设计来减轻压力、提高系统性能,那么多数据源动态切换势必是必不可少!

经过了一星期零零碎碎的下班时间,从了解原理、实现、优化的过程,自己终于总算是弄出来了,接下来一起看看!

思考


  1. 如何让Spring知道我们配置了多个数据源?

  2. 配置了多个数据源后,Spring是如何决定使用哪一个数据源?

  3. Spring是如何动态切换数据源?

分析及实现


  1. 配置多数据源信息

spring:
  datasource:
    local:
      database: local
      username: root
      password:
      jdbc-url: jdbc:mysql://ip:port/test_user?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
      driver-class-name: com.mysql.cj.jdbc.Driver
    server:
      database: server
      username: root
      password:
      jdbc-url: jdbc:mysql://ip:port/test_user?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
      driver-class-name: com.mysql.cj.jdbc.Driver

这是我的两个数据库:本地数据库+个人服务器数据库


服务器数据库


实战:Spring AOP实现多数据源动态切换-LMLPHP


本地数据库


实战:Spring AOP实现多数据源动态切换-LMLPHP



  1. Spring如何获取配置好的多个数据源信息?

Spring提供了三种方式进行获取


同事使用的方式是第一种方式,但是我个人觉得这样侵入性较大,每增加一个数据源,就要重新定义变量然后用@Value去重新配置,很麻烦,所以我就选择了第二种方式


通过@ConfigurationProperties注解获取,需要定义前缀,可大批量获取配置信息


@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource")
public class DBProperties {

    private HikariDataSource server;

    private HikariDataSource local;
}

将所有的数据源加载到Spring中,可供其选择使用


@Slf4j
@Configuration
public class DataSourceConfig {

    @Autowired
    private DBProperties dbProperties;

    @Bean(name = "multiDataSource")
    public MultiDataSource multiDataSource(){
        MultiDataSource multiDataSource = new MultiDataSource();
        //1.设置默认数据源
        multiDataSource .setDefaultTargetDataSource(dbProperties.getLocal());
        //2.配置多数据源
        HashMap<Object, Object> dataSourceMap = Maps.newHashMap();

        dataSourceMap.put("local", dbProperties.getLocal());
        dataSourceMap.put("server", dbProperties.getServer());
        //3.存放数据源集
        multiDataSource.setTargetDataSources(dataSourceMap);
        return multiDataSource;
    }
}

如此之后,确实是可以读取YML中的数据源信息,但是总觉得怪怪的。
果然!当我实现了整个功能后,我发现,如果我想要再加一个数据源,我还是得去求改DBProperties和DataSourceConfig这两类的内容,就很烦,我这个人比较懒,所以我就将这部分内容优化了一下:


优化后的YML

spring:
  datasource:
    names:
       - database: dataSource0
         username: root
         password:
         jdbc-url: jdbc:mysql://ip:port/test_user?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
         driver-class-name: com.mysql.cj.jdbc.Driver
       - database: dataSource1
         username: root
         password:
         jdbc-url: jdbc:mysql://ip:port/test_user?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
         driver-class-name: com.mysql.cj.jdbc.Driver

优化后的DBProperties

@Data
@Component
@ConfigurationProperties(prefix = "spring.datasource")
public class DBProperties {

    private List<HikariDataSource> DBNames;

}

优化后的DataSourceConfig


@Slf4j
@Configuration
public class DataSourceConfig {

    @Autowired
    private DBProperties dbProperties;


    @Bean(name = "multiDataSource")
    public MultiDataSource multiDataSource(){
        MultiDataSource multiDataSource = new MultiDataSource();

        List<HikariDataSource> names = dbProperties.getNames();
        if (CollectionUtils.isEmpty(names)){
            throw new RuntimeException(" please configure the data source! ");
        }

        multiDataSource.setDefaultTargetDataSource(names.get(0));

        HashMap<Object, Object> dataSourceMap = Maps.newHashMap();
        int i = 0;
        for (HikariDataSource name : names) {
            dataSourceMap.put("dataSource"+(i++),name);
        }

        multiDataSource.setTargetDataSources(dataSourceMap);
        return multiDataSource;
    }
}

这样子,我之后无论配置了多少个数据源信息,我都不需要再去修改配置代码



  1. Spring如何选择使用数据源?

选择一个数据源


通过继承AbstractRoutingDataSource接口,重写determineCurrentLookupKey方法,选择具体的数据源


@Slf4j
public class MultiDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {

        return MultiDataSourceHolder.getDatasource();

    }

}

利用ThreadLocal实现数据源线程隔离


public class MultiDataSourceHolder {

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

    public static void setDatasource(String datasource){
        threadLocal.set(datasource);
    }

    public static String getDatasource(){
        return threadLocal.get();
    }

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

}


利用AOP切面+自定义注解


自定义注解


@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MultiDataSource {

    String DBName();

}

AOP切面


@Slf4j
@Aspect
@Component
public class DataSourceAspect {

    @Pointcut(value = "@within(com.xiaozhao.base.aop.annotation.MultiDataSource) || @annotation(com.xiaozhao.base.aop.annotation.MultiDataSource)")
    public void dataSourcePointCut(){}


    @Before("dataSourcePointCut() && @annotation(multiDataSource)")
    public void before(MultiDataSource multiDataSource){

        String dbName = multiDataSource.DBName();

        if (StringUtils.hasLength(dbName)){

            MultiDataSourceHolder.setDatasource(multiDataSource.DBName());
            log.info("current dataSourceName ====== "+dbName);

        }else {

            log.info("switch datasource fail, use default, or please configure the data source for the annotations,");

        }
    }


    @After("dataSourcePointCut()")
    public void after(){
        MultiDataSourceHolder.clearDataSource();
    }
}

好了!功能已然实现,打完收工!


实战:Spring AOP实现多数据源动态切换-LMLPHP

。。。。


如果我工作中也这样,估计要被测试打死!为了敷衍一下,来进行一下测试


一套代码直接打完:


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

    @Autowired
    private UserService userService;



    @GetMapping("/info")
    public UserVO getUser(){
        return userService.creatUser();
    }
}




public interface UserService {
    UserVO creatUser();

    UserVO setUserInfo(String phone);
}




@Service
@EnableAspectJAutoProxy(exposeProxy = true)
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private InfoMapper infoMapper;


    @Override
    public UserVO creatUser() {
        UserVO userVO = userMapper.getUserInfoMapper();

        return ((UserService) AopContext.currentProxy()).setUserInfo(userVO.getPhone());
    }

    @MultiDataSource(DBName = "dataSource1")
    public UserVO setUserInfo(String phone) {

        UserVO userInfo = infoMapper.getUserInfo();

        UserVO user = new UserVO();
        user.setUserName(userInfo.getUserName());
        user.setPassword(userInfo.getPassword());
        user.setAddress(userInfo.getAddress());
        user.setPhone(phone);
        return user;
    }
}




@Mapper
public interface InfoMapper {

    @Select("select id,user_name as userName,password,phone,address from test_user")
    UserVO getUserInfo();
}



@Mapper
public interface UserMapper {

    @Select("select id,user_name as userName,password,phone from user")
    UserVO getUserInfoMapper();

}

测试结果:红框数据来自于服务器数据库,绿框数据来自于本地数据库


实战:Spring AOP实现多数据源动态切换-LMLPHP


遇到的问题

  • 同一个类中,A方法调用B方法用AopContext.currentProxy()报错问题:在类上加@EnableAspectJAutoProxy(exposeProxy = true)————解决!
  • 配置多数据源时,注意将url修改成jdbc-url
  • 切面时,用JoinPoint获取方法,判断是否被注解修饰(虽然纯属多余)结果为false————有待考究!

结语


小菜鸡的学习成长之路,拒绝无味的CRUD,每过一段时间,就会把工作中用到,或者别人实现的功能解析、实现,并分享!下一篇,Redission实现分布式锁

本文来自博客园,作者:Carson-Zhao,转载请注明原文链接:https://www.cnblogs.com/zhaorongbiao/p/15998940.html

03-13 00:24