• <menu id="sssag"></menu>
  • <menu id="sssag"></menu>
  • Carson-Zhao
    God helps those who help themselves

    需求背景


    去年底,公司項目有一個需求中有個接口需要用到平臺、算法、大數據等三個不同數據庫的數據進行計算、組裝以及最后的展示,當時這個需求是另一個老同事在做,我只是負責自己的部分。
    直到今年回來了,這個項目也做得差不多了,這會兒才有時間區仔細看同事的代碼,是怎么去實現多數據源動態切換的。


    擴展:當業務也來越復雜,數據量越來越龐大時,就可能會對數據庫進行分庫分表、讀寫分離等設計來減輕壓力、提高系統性能,那么多數據源動態切換勢必是必不可少!

    經過了一星期零零碎碎的下班時間,從了解原理、實現、優化的過程,自己終于總算是弄出來了,接下來一起看看!

    思考


    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
    

    這是我的兩個數據庫:本地數據庫+個人服務器數據庫


    服務器數據庫



    本地數據庫




    1. Spring如何獲取配置好的多個數據源信息?

    Spring提供了三種方式進行獲取

    @Value注解獲?。▽嶓w類需配合@Component),最簡單,但當配置信息較多時,寫起來比較繁瑣

    @ConfigurationProperties注解獲取,需要定義前綴,可大批量獲取配置信息

    @Environment注解從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();
        }
    }
    

    好了!功能已然實現,打完收工!


    。。。。


    如果我工作中也這樣,估計要被測試打死!為了敷衍一下,來進行一下測試


    一套代碼直接打完:

    Controller+Service+Dao


    @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();
    
    }
    

    測試結果:紅框數據來自于服務器數據庫,綠框數據來自于本地數據庫



    遇到的問題

    • 同一個類中,A方法調用B方法用AopContext.currentProxy()報錯問題:在類上加@EnableAspectJAutoProxy(exposeProxy = true)————解決!
    • 配置多數據源時,注意將url修改成jdbc-url
    • 切面時,用JoinPoint獲取方法,判斷是否被注解修飾(雖然純屬多余)結果為false————有待考究!

    結語


    小菜雞的學習成長之路,拒絕無味的CRUD,每過一段時間,就會把工作中用到,或者別人實現的功能解析、實現,并分享!下一篇,Redission實現分布式鎖

    posted on 2022-03-12 22:23  Carson-Zhao  閱讀(22)  評論(0編輯  收藏  舉報

    国产在线码观看超清无码视频,人妻精品动漫H无码,十大看黄台高清视频,国产在线无码视频一区二区三区,国产男女乱婬真视频免费,免费看女人的隐私超爽,狠狠色狠狠色综合久久蜜芽