深入淺出SpringBoot系列--多數據源集成

引言

在上一篇文章中已經詳細介紹了與mybatis的集成方案【深入淺出SpringBoot系列–與mybatis集成】,本文主要介紹一些更復雜的數據操作場景,比如動態數據源的切換,分庫,分庫分表等。

其實對於分庫分表這塊的場景,目前市場上有很多成熟的開源中間件,eg:MyCAT,Cobar,sharding-JDBC等。
本文主要是介紹基於springboot的多數據源切換,輕量級的一種集成方案,對於小型的應用可以採用這種方案,我之前在項目中用到是因爲簡單,便於擴展以及優化。

應用場景

假設目前我們有以下幾種數據訪問的場景:
1.一個業務邏輯中對不同的庫進行數據的操作(可能你們系統不存在這種場景,目前都時微服務的架構,每個微服務基本上是對應一個數據庫比較多,其他的需要通過服務來方案。),
2.訪問分庫分表的場景;後面的文章會單獨介紹下分庫分表的集成。

假設這裏,我們以6個庫,每個庫1000張表的例子來介紹),因爲隨着業務量的增長,一個庫很難抗住這麼大的訪問量。比如說訂單表,我們可以根據userid進行分庫分表。
分庫策略:userId%6[表的數量];
分庫分表策略:庫路由[userId/(6*1000)/1000],表路由[ userId/(6*1000)%1000].

集成方案

方案總覽:
方案是基於springjdbc中提供的api實現,看下下面兩段代碼,是我們的切入點,我們都是圍繞着這2個核心方法進行集成的。

第一段代碼是註冊邏輯,會將defaultTargetDataSourcetargetDataSources這兩個數據源對象註冊到resolvedDataSources中。

第二段是具體切換邏輯: 如果數據源爲空,那麼他就會找我們的默認數據源defaultTargetDataSource,如果設置了數據源,那麼他就去讀這個值 Object lookupKey = determineCurrentLookupKey();我們後面會重寫這個方法。

我們會在配置文件中配置主數據源(默認數據源)和其他數據源,然後在應用啓動的時候註冊到spring容器中,分別給defaultTargetDataSource和targetDataSources進行賦值,進而進行Bean註冊。

真正的使用過程中,我們定義註解,通過切面,定位到當前線程是由訪問哪個數據源(維護着一個ThreadLocal的對象),進而調用determineCurrentLookupKey方法,進行數據源的切換。


public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        }
        this.resolvedDataSources = new HashMap<Object, DataSource>(this.targetDataSources.size());
        for (Map.Entry<Object, Object> entry : this.targetDataSources.entrySet()) {
            Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
            DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
            this.resolvedDataSources.put(lookupKey, dataSource);
        }
        if (this.defaultTargetDataSource != null) {
            this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }
    }   

@Override
public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
}

protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = determineCurrentLookupKey();
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        }
        return dataSource;
    }


1.動態數據源註冊 註冊默認數據源以及其他額外的數據源; 這裏只是複製了核心的幾個方法,具體的大家下載git看代碼。

     // 創建DynamicDataSource
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(DyncRouteDataSource.class);
        beanDefinition.setSynthetic(true);
        MutablePropertyValues mpv = beanDefinition.getPropertyValues();
        //默認數據源
        mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource); 
        //其他數據源
        mpv.addPropertyValue("targetDataSources", targetDataSources);
        //註冊到spring bean容器中
        registry.registerBeanDefinition("dataSource", beanDefinition);

2.在Application中加載動態數據源,這樣spring容器啓動的時候就會將數據源加載到內存中。

@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class, MybatisAutoConfiguration.class })
@Import(DyncDataSourceRegister.class)
@ServletComponentScan                                       
@ComponentScan(basePackages = { "com.happy.springboot.api","com.happy.springboot.service"})
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

3.數據源切換核心邏輯:創建一個數據源切換的註解,並且基於該註解實現切面邏輯,這裏我們通過threadLocal來實現線程間的數據源切換;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
    String value();
    // 是否分庫
    boolean isSharding() default false;
    // 獲取分庫策略
    String strategy() default "";
}

// 動態數據源上下文
public class DyncDataSourceContextHolder {

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

    public static List<String> dataSourceNames = new ArrayList<String>();

    public static void setDataSource(String dataSourceName) {
        contextHolder.set(dataSourceName);
    }

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

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

    public static boolean containsDataSource(String dataSourceName) {
        return dataSourceNames.contains(dataSourceName);
    }
}

/**
 * 
 * @author jasoHsu
 * 動態數據源在切換,將數據源設置到ThreadLocal對象中
 */
@Component
@Order(-10)
@Aspect
public class DyncDataSourceInterceptor {

    @Before("@annotation(targetDataSource)")
    public void changeDataSource(JoinPoint point, TargetDataSource targetDataSource) throws Exception {
        String dbIndx = null;

        String targetDataSourceName = targetDataSource.value() + (dbIndx == null ? "" : dbIndx);
        if (DyncDataSourceContextHolder.containsDataSource(targetDataSourceName)) {
            DyncDataSourceContextHolder.setDataSource(targetDataSourceName);
        }
    }

    @After("@annotation(targetDataSource)")
    public void resetDataSource(JoinPoint point, TargetDataSource targetDataSource) {
        DyncDataSourceContextHolder.clearDataSource();
    }
}

//
public class DyncRouteDataSource extends AbstractRoutingDataSource {
@Override
    protected Object determineCurrentLookupKey() {
        return DyncDataSourceContextHolder.getDataSource();
    }
    public DataSource findTargetDataSource() {
        return this.determineTargetDataSource();
    }
}

4.與mybatis集成,其實這一步與前面的基本一致。
大家可以看下這一篇:【深入淺出SpringBoot系列–與mybatis集成
只是將在sessionFactory以及數據管理器中,將動態數據源註冊進去【dynamicRouteDataSource】,而非之前的靜態數據源【getMasterDataSource()】。


    @Resource
    DyncRouteDataSource dynamicRouteDataSource;

    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory getinvestListingSqlSessionFactory() throws Exception {
        String configLocation = "classpath:/conf/mybatis/configuration.xml";
        String mapperLocation = "classpath*:/com/happy/springboot/service/dao/*/*Mapper.xml";
        SqlSessionFactory sqlSessionFactory = createDefaultSqlSessionFactory(dynamicRouteDataSource, configLocation,
                mapperLocation);

        return sqlSessionFactory;
    }


    @Bean(name = "txManager")
    public PlatformTransactionManager txManager() {
        return new DataSourceTransactionManager(dynamicRouteDataSource);
    }

5.核心配置參數:

spring:
  dataSource:
    happyboot:
      driverClassName: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3306/happy_springboot?characterEncoding=utf8&useSSL=true
      username: root
      password: admin
    extdsnames: happyboot01,happyboot02
    happyboot01:
      driverClassName: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3306/happyboot01?characterEncoding=utf8&useSSL=true
      username: root
      password: admin 
    happyboot02:
      driverClassName: com.mysql.jdbc.Driver
      url: jdbc:mysql://localhost:3306/happyboot02?characterEncoding=utf8&useSSL=true
      username: root
      password: admin    

6.應用代碼

@Service
public class UserExtServiceImpl implements IUserExtService {
    @Autowired
    private UserInfoMapper userInfoMapper;
    @TargetDataSource(value="happyboot01")
    @Override
    public UserInfo getUserBy(Long id) {
        return userInfoMapper.selectByPrimaryKey(id);
    }
}

其他:

具體代碼參見:
https://github.com/jasonhsu2017/happy-springboot.git

分庫分表技術方案

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章