SpringBoot-主從動態數據源


1、背景

在實際的項目中,一般一個項目都會有主數據庫和從數據庫,主從數據庫之間的數據同步是通過數據庫的配置來完成的,一般地這個工作都是由DBA來進行完成。但是,如果我們的項目中的業務量比較大的時候,我們希望讀操作從數據庫中讀取數據,寫操作的時候才將數據保存至主數據庫,然后主數據庫和從數據庫之間通過通信將數據完成同步;那么,我們的程序是如何將做到讀操作的時候從從庫中讀取數據,寫操作的時候是如何將數據寫入到主庫的呢?這個問題,就是今天要解決的問題;

目前市面上實現主從數據源切換的方式主要有兩種,一種是利用第三方插件的形式實現,另外一種就是通過使用AOP進行實現。我采用的實現方式就是利用SpringAOP的方式實現;

 2、實現

2.1 導入所需要的依賴包

我的項目使用的SpringBoot實現的,ORM框架使用的是Mybatis,數據源使用的是阿里的Druid。配置如下:

 1 <!--springBoot-->
 2 <dependency>
 3     <groupId>org.springframework.boot</groupId>
 4     <artifactId>spring-boot-starter-web</artifactId>
 5 </dependency>
 6 <dependency>
 7     <groupId>org.springframework.boot</groupId>
 8     <artifactId>spring-boot-starter-aop</artifactId>
 9 </dependency>
10 <!--mybatis-->
11 <dependency>
12     <groupId>org.mybatis.spring.boot</groupId>
13     <artifactId>mybatis-spring-boot-starter</artifactId>
14     <version>2.1.3</version>
15 </dependency>
16 <!--mysql-->
17 <dependency>
18     <groupId>mysql</groupId>
19     <artifactId>mysql-connector-java</artifactId>
20     <scope>runtime</scope>
21 </dependency>
22 <!--druid-->
23 <dependency>
24     <groupId>com.alibaba</groupId>
25     <artifactId>druid-spring-boot-starter</artifactId>
26     <version>1.1.10</version>
27 </dependency>
28 <!--lombok-->
29 <dependency>
30     <groupId>org.projectlombok</groupId>
31     <artifactId>lombok</artifactId>
32     <optional>true</optional>
33 </dependency>

2.2 配置數據源

  為了節約成本,我只在本地的計算機上進行了代碼實現,所以我只是在本地的同一個mysql服務上配置了多個數據庫,數據庫之間也沒有進行主從的配置,畢竟我的主要目的是想看看代碼的實現效果;配置文件如下:

 1 # 主庫
 2 spring.datasource.master.name=master
 3 spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver
 4 spring.datasource.master.url=jdbc:mysql://localhost:3306/master?serverTimezone=UTC&useSSL=false
 5 spring.datasource.master.username=root
 6 spring.datasource.master.password=root
 7 # 從庫1
 8 spring.datasource.slaver1.name=slaver1
 9 spring.datasource.slaver1.driver-class-name=com.mysql.jdbc.Driver
10 spring.datasource.slaver1.url=jdbc:mysql://localhost:3306/slaver1?serverTimezone=UTC&useSSL=false
11 spring.datasource.slaver1.username=root
12 spring.datasource.slaver1.password=root
13 # 從庫2
14 spring.datasource.slaver2.name=slaver1
15 spring.datasource.slaver2.driver-class-name=com.mysql.jdbc.Driver
16 spring.datasource.slaver2.url=jdbc:mysql://localhost:3306/slaver2?serverTimezone=UTC&useSSL=false
17 spring.datasource.slaver2.username=root
18 spring.datasource.slaver2.password=root

2.3 業務操作層的實現

數據的操作需要借助Service層和Dao層的進行實現,由於這部分不是實現主從數據源的關鍵部分,所以此處的代碼就不進行展示;

2.4 數據源配置

我們都知道,Spring和Mybatis在整合的時候都需要配置 org.mybatis.spring.SqlSessionFactoryBean 的實例,在配置這個實例的時候需要指定數據源。那么如果想要實現主從數據源動態切換的功能,這個數據源的配置就不能使用傳統的DataSource了,這里我是用的是 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 數據源。這個數據源是Spring提供的,它可以在獲取數據源連接之前通過方法 determineTargetDataSource() 判斷獲取哪一個數據源的連接;也正是因為這個特性,我們才得以實現數據源動態切換的功能;

數據源配置如下:

 1 import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
 2 import com.example.demo.config.db.DataSourceRoutingDataSource;
 3 import org.mybatis.spring.SqlSessionFactoryBean;
 4 import org.springframework.boot.context.properties.ConfigurationProperties;
 5 import org.springframework.context.annotation.Bean;
 6 import org.springframework.context.annotation.Configuration;
 7 import org.springframework.context.annotation.Primary;
 8 import org.springframework.core.io.Resource;
 9 import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
10 import org.springframework.stereotype.Component;
11 
12 import javax.sql.DataSource;
13 import java.io.IOException;
14 import java.util.HashMap;
15 import java.util.HashSet;
16 import java.util.Map;
17 import java.util.Set;
18 
19 /**
20  * 數據源配置文件
21  */
22 @Component
23 @Configuration
24 public class DatasourceConfig {
25 
26     /**
27      * 創建一個主數據源的實例
28      */
29     @Primary
30     @Bean(value = "master")
31     @ConfigurationProperties(prefix = "spring.datasource.master")
32     public DataSource master() {
33         return DruidDataSourceBuilder.create().build();
34     }
35 
36     /**
37      * 從數據源1
38      */
39     @Bean(value = "slaver1")
40     @ConfigurationProperties(prefix = "spring.datasource.slaver1")
41     public DataSource slaver1() {
42         return DruidDataSourceBuilder.create().build();
43     }
44 
45     /**
46      * 從數據源2
47      */
48     @Bean(value = "slaver2")
49     @ConfigurationProperties(prefix = "spring.datasource.slaver2")
50     public DataSource slaver2() {
51         return DruidDataSourceBuilder.create().build();
52     }
53 
54     /**
55      * DataSourceRoutingDataSource 繼承了 AbstractRoutingDataSource;
56      *  主要為了實現determineCurrentLookupKey()方法;
57      */
58     @Bean(value = "dataSource")
59     public DataSourceRoutingDataSource dataSource() {
60         DataSourceRoutingDataSource dataSource = new DataSourceRoutingDataSource();
61         // 數據源
62         Map<Object, Object> dataSources = new HashMap<>();
63         dataSources.put("master", master());
64         dataSources.put("slaver1", slaver1());
65         dataSources.put("slaver2", slaver2());
66         dataSource.setTargetDataSources(dataSources);
67         dataSource.setDefaultTargetDataSource(master());
68         // 設置主數據源的鍵值;
69         Set<Object> masterKeys = new HashSet<>();
70         masterKeys.add("master");
71         dataSource.setMasterKeys(masterKeys);
72         // 設置從數據源的鍵值;
73         Set<Object> slaverKeys = new HashSet<>();
74         slaverKeys.add("slaver1");
75         slaverKeys.add("slaver2");
76         dataSource.setSlaverKeys(slaverKeys);
77 
78         return dataSource;
79     }
80 
81     /**
82      * SqlSessionFactoryBean實例配置
83      */
84     @Bean
85     public SqlSessionFactoryBean sqlSessionFactoryBean() throws IOException {
86         SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
87         factoryBean.setDataSource(dataSource());
88 
89         PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
90         Resource[] resources = resolver.getResources("classpath*:mapper/*.xml");
91         factoryBean.setMapperLocations(resources);
92         factoryBean.setTypeAliasesPackage("com.example.demo.domain");
93         return factoryBean;
94     }
95 }

DataSourceRoutingDataSource實現如下:

 1 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
 2 
 3 import java.util.ArrayList;
 4 import java.util.List;
 5 import java.util.Set;
 6 import java.util.concurrent.atomic.AtomicBoolean;
 7 import java.util.concurrent.atomic.AtomicInteger;
 8 
 9 public class DataSourceRoutingDataSource extends AbstractRoutingDataSource {
10 
11     public static AtomicBoolean MASTER_STATUS = new AtomicBoolean(true);
12 
13     private static List<Object> MASTER_KEYS = new ArrayList<>();
14     private static AtomicInteger MASTER_INDEX = new AtomicInteger(0);
15     private static List<Object> SLAVER_KEYS = new ArrayList<>();
16     private static AtomicInteger SLAVER_INDEX = new AtomicInteger(0);
17 
18     /*
19     * 關鍵點:用於切換數據源
20     * */
21     @Override
22     protected Object determineCurrentLookupKey() {
23         if (MASTER_STATUS.get()) {
24             return getNextMaster();
25         } else {
26             return getNextSlaver();
27         }
28     }
29 
30     public void setMasterKeys(Set<Object> masterKeys) {
31         MASTER_KEYS.addAll(masterKeys);
32     }
33 
34     public void setSlaverKeys(Set<Object> slaverKeys) {
35         SLAVER_KEYS.addAll(slaverKeys);
36     }
37 
38     /**
39      * 獲取下一個主庫的key
40      */
41     private Object getNextMaster() {
42         if (MASTER_KEYS.size() == 1) {
43             return MASTER_KEYS.get(0);
44         }
45         int index = MASTER_INDEX.getAndAdd(1);
46         return MASTER_KEYS.get(index % MASTER_KEYS.size());
47     }
48 
49     /**
50      * 獲取下一個從庫的key
51      */
52     private Object getNextSlaver() {
53         if (SLAVER_KEYS.size() == 1) {
54             return SLAVER_KEYS.get(0);
55         }
56         int index = SLAVER_INDEX.getAndAdd(1);
57         return SLAVER_KEYS.get(index % SLAVER_KEYS.size());
58     }
59 }

 

2.5 AOP配置

實現上面的步驟其實已經可以進行增刪改查的功能了,但是我們目的不在此;我們還要通過AOP進行數據源的切換,所以我們還需要配置AOP;我這里寫的比較簡單,就是根據service的名稱判斷是否使用主庫;代碼如下:

 1 import com.example.demo.config.db.DataSourceRoutingDataSource;
 2 import org.aspectj.lang.JoinPoint;
 3 import org.aspectj.lang.annotation.Aspect;
 4 import org.aspectj.lang.annotation.Before;
 5 import org.aspectj.lang.annotation.Pointcut;
 6 import org.springframework.stereotype.Component;
 7 
 8 @Aspect
 9 @Component
10 public class ServiceAspect {
11 
12     @Pointcut(value = "execution(* com.example.demo.service.*.*(..))")
13     public void point() {}
14 
15     @Before(value = "point()")
16     public void before(JoinPoint joinPoint) {
17         String name = joinPoint.getSignature().getName();
18         if (name.startsWith("get") || name.startsWith("query") || name.startsWith("find")) {
19             DataSourceRoutingDataSource.MASTER_STATUS.set(false);
20         } else {
21             DataSourceRoutingDataSource.MASTER_STATUS.set(true);
22         }
23     }
24 }

3、總結

動態數據源的實現方式還是比較簡單的,核心就在於配置數據源為 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 類型的數據源;如果感興趣的話,可以看一看內部的實現源碼;

項目源碼:https://gitee.com/chao_actor/cnblogs.git

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM