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