在实际的项目中,一般一个项目都会有主数据库和从数据库,主从数据库之间的数据同步是通过数据库的配置来完成的,一般地这个工作都是由DBA来进行完成。但是,如果我们的项目中的业务量比较大的时候,我们希望读操作从数据库中读取数据,写操作的时候才将数据保存至主数据库,然后主数据库和从数据库之间通过通信将数据完成同步;那么,我们的程序是如何将做到读操作的时候从从库中读取数据,写操作的时候是如何将数据写入到主库的呢?这个问题,就是今天要解决的问题;
目前市面上实现主从数据源切换的方式主要有两种,一种是利用第三方插件的形式实现,另外一种就是通过使用AOP进行实现。我采用的实现方式就是利用SpringAOP的方式实现;
我的项目使用的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>
为了节约成本,我只在本地的计算机上进行了代码实现,所以我只是在本地的同一个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
数据的操作需要借助Service层和Dao层的进行实现,由于这部分不是实现主从数据源的关键部分,所以此处的代码就不进行展示;
我们都知道,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 }
实现上面的步骤其实已经可以进行增删改查的功能了,但是我们目的不在此;我们还要通过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 }
动态数据源的实现方式还是比较简单的,核心就在于配置数据源为 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 类型的数据源;如果感兴趣的话,可以看一看内部的实现源码;
项目源码:https://gitee.com/chao_actor/cnblogs.git
原文:https://www.cnblogs.com/chao-actor/p/13619797.html