閱讀446 返回首頁    go 阿裏雲 go 技術社區[雲棲]


spring-boot+aop實現多數據源切換

spring-boot+aop實現多數據源切換

當對同一個請求的QPS達到一定程度時,係統的響應會出現瓶頸,一般都是在數據庫上,這個時候數據庫一般會采取各種措施,例如主從服務,分表分庫,讀寫分離,緩存技術等等。一旦這幾種出現,我們在技術上也要做相應的變通。大多數情況是從原始的單庫單表變成了多庫多表

例如:我們有一個表user_info,我們護綠其他字段,裏麵有兩個字段id、remarks,當數據量達到一定程序後,係統做了分表分庫(你也可以當成是主從)

假設我們有一個庫test和test2
test庫中數據如下:

id remarks
1 test1
2 test1

test2庫中數據如下:

id remarks
1 test2
2 test2

最初單庫單表時,我們可能直接使用spring-boot自動配置的方式,係統沒有任何問題。如果我們現在要從兩個表中查詢數據原來的自動單數據源的方式就不再適用了,這個時候可能就涉及到了多數據源的程序了。多數據源有多種方式,接下來我們介紹采用spring-boot+AOP方式實現多數據源切換。

首先由於使用了spring-boot,我們還是讓程序繼承spring-boot-starter-parent,這樣可以少管理一些版本。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.2.RELEASE</version>
</parent>

其次,我們引入相關依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
</dependency>
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.0</version>
    <exclusions>
        <exclusion>
            <artifactId>spring-boot-starter</artifactId>
            <groupId>org.springframework.boot</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

以上依賴,為了引入了spring-web、mybatis、AOP、jdbc、mysql驅動、HikariCP連接池。

接下來我們開始實現動態數據源主要代碼功能。

1、創建線程共享工具

由於我們的數據源信息要保證在同一線程下切換後不要被其他線程修改,所以我們將數據源信息保存在ThreadLocal共享中。

/**
 * 動態數據源持有者,負責利用ThreadLocal存取數據源名稱
 */
public class DynamicDataSourceHolder {
    /**
     * 本地線程共享對象
     */
    private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

    public static void putDataSource(String name) {
        THREAD_LOCAL.set(name);
    }

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

    public static void removeDataSource() {
        THREAD_LOCAL.remove();
    }
}

2、實現動態數據源AbstractRoutingDataSource

spring為我們提供了AbstractRoutingDataSource,即帶路由的數據源。繼承後我們需要實現它的determineCurrentLookupKey(),該方法用於自定義實際數據源名稱的路由選擇方法,由於我們將信息保存到了ThreadLocal中,所以隻需要從中拿出來即可。

/**
 * 動態數據源實現類
 */
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource{
    //數據源路由,此方用於產生要選取的數據源邏輯名稱
    @Override
    protected Object determineCurrentLookupKey() {
        //從共享線程中獲取數據源名稱
        return DynamicDataSourceHolder.getDataSource();
    }
}

3、創建數據源切換方法注解

我們切換數據源時,一般都是在調用mapper接口的方法前實現,所以我們定義一個方法注解,當AOP檢測到方法上有該注解時,根據注解中value對應的名稱進行切換。

/**
 * 目標數據源注解,注解在方法上指定數據源的名稱
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TargetDataSource {
    String value();//此處接收的是數據源的名稱
}

4、定義處理AOP切麵

動態數據源切換是基於AOP的,所以我們需要聲明一個AOP切麵,並在切麵前做數據源切換,切麵完成後移除數據源名稱。

/**
 * 數據源AOP切麵定義
 */
@Component
@Aspect
@Slf4j
public class DataSourceAspect {
    //切換放在mapper接口的方法上,所以這裏要配置AOP切麵的切入點
    @Pointcut("execution( * com.comven.example.mapper.*.*(..))")
    public void dataSourcePointCut() {
    }

    @Before("dataSourcePointCut()")
    public void before(JoinPoint joinPoint) {
        Object target = joinPoint.getTarget();
        String method = joinPoint.getSignature().getName();
        Class<?>[] clazz = target.getClass().getInterfaces();
        Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
        try {
            Method m = clazz[0].getMethod(method, parameterTypes);
            //如果方法上存在切換數據源的注解,則根據注解內容進行數據源切換
            if (m != null && m.isAnnotationPresent(TargetDataSource.class)) {
                TargetDataSource data = m.getAnnotation(TargetDataSource.class);
                String dataSourceName = data.value();
                DynamicDataSourceHolder.putDataSource(dataSourceName);
                log.debug("current thread " + Thread.currentThread().getName() + " add " + dataSourceName + " to ThreadLocal");
            } else {
                log.debug("switch datasource fail,use default");
            }
        } catch (Exception e) {
            log.error("current thread " + Thread.currentThread().getName() + " add data to ThreadLocal error", e);
        }
    }

    //執行完切麵後,將線程共享中的數據源名稱清空
    @After("dataSourcePointCut()")
    public void after(JoinPoint joinPoint){
        DynamicDataSourceHolder.removeDataSource();
    }
}

5、定義多個數據源

之前我們假設中訪問兩個庫兩個表,假設test庫數據源我們命名為test1,test2庫數據源我們命名為test2。

我們先定義一個實際數據源配置類

/**
 * 實際數據源配置
 */
@Component
@Data
@ConfigurationProperties(prefix = "hikari")
public class DBProperties {
    private HikariDataSource test1;
    private HikariDataSource test2;
}

在application.properties中,我們的配置是這樣的

#test1數據源配置
hikari.test1.jdbc-url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false
hikari.test1.username=root
hikari.test1.password=123456
hikari.test1.maximum-pool-size=10
#test2數據源配置
hikari.test2.jdbc-url=jdbc:mysql://127.0.0.1:3306/test2?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false
hikari.test2.username=root
hikari.test2.password=123456
hikari.test2.maximum-pool-size=10

接下來我們采用@Bean注解完成動態數據源對象的申明

/**
 * 數據源配置
 */
@Configuration
@EnableScheduling
@Slf4j
public class DataSourceConfig {

    @Autowired
    private DBProperties properties;

    @Bean(name = "dataSource")
    public DataSource dataSource() {
        //按照目標數據源名稱和目標數據源對象的映射存放在Map中
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("test1", properties.getTest1());
        targetDataSources.put("test2", properties.getTest2());
        //采用是想AbstractRoutingDataSource的對象包裝多數據源
        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSources);
        //設置默認的數據源,當拿不到數據源時,使用此配置
        dataSource.setDefaultTargetDataSource(properties.getTest1());
        return dataSource;
    }

    @Bean
    public PlatformTransactionManager txManager() {
        return new DataSourceTransactionManager(dataSource());
    }

}

6、在mapper接口方法上做切換

由於我們的動態數據源配置了默認庫,所以如果mapper方法是操作默認庫的可以不需要注解,如果要操作非默認數據源,我們需要在方法上添加@TargetDataSource("數據源名稱")注解。兩個方法selectByOddUserId我們定義為奇數Id從test1庫獲取數據,selectByEvenUserId定義為偶數Id從test2庫獲取數據,

public interface UserInfoMapper {
    /**
     * 從test1數據源中獲取用戶信息
     */
    UserInfo selectByOddUserId(Integer id);
    /**
     * 從test2數據源中獲取用戶信息
     */
    @TargetDataSource("test2")
    UserInfo selectByEvenUserId(Integer id);
}

完成以上6個步驟,還不行,因為使用了spring-boot會自Autoconfiguration,所以我們需要在啟動類注解上作如下修改,不讓spring-boot給我們自動配置。

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

好了,我們采用Junit進行代碼測試

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = App.class)
@Slf4j
public class AppTest {
    @Autowired
    private UserInfoMapper userInfoMapper;

    @Test
    public void testDynamicDatasource() {
        UserInfo userInfo;
        for (int i = 1; i <= 1; i++) {
            //i為奇數時調用selectByOddUserId方法獲取,i為偶數時調用selectByEvenUserId方法獲取
            userInfo = i % 2 == 1 ? userInfoMapper.selectByOddUserId(i) : userInfoMapper.selectByEvenUserId(i);
            log.info("{}->={}", userInfo.getId(),userInfo.getRemarks());
        }
    }
}

運行testDynamicDatasource()方法,按照我的的思路應該分別打印出1->test1和2->test2,如下圖運行效果

[2017-09-01 21:29:47,558][INFO ] c.c.exmaple.AppTest[29] - 1->=test1
[2017-09-01 21:29:47,598][INFO ] c.c.exmaple.AppTest[29] - 2->=test2

可以看到,完成了我們想要的執行結果,說明我們的處理成功了

總結

利用AbstractRoutingDataSource以及AOP,我們實現了多數據源的切換,可以滿足我們想要的大部分情況,而且相對來說邏輯簡單,容易理解。

源碼地址

最後更新:2017-09-02 16:02:43

  上一篇:go  快速排序實現-JAVA
  下一篇:go  Spring IoC 學習(4)