前言:
目前同学们对“springboot配置主从数据库”都比较注重,小伙伴们都想要学习一些“springboot配置主从数据库”的相关内容。那么小编在网摘上汇集了一些有关“springboot配置主从数据库””的相关知识,希望兄弟们能喜欢,各位老铁们快快来学习一下吧!一平超凡 | 作者
urlify.cn/jEnuIn | 来源
1、背景
在实际的项目中,一般一个项目都会有主数据库和从数据库,主从数据库之间的数据同步是通过数据库的配置来完成的,一般地这个工作都是由DBA来进行完成。但是,如果我们的项目中的业务量比较大的时候,我们希望读操作从数据库中读取数据,写操作的时候才将数据保存至主数据库,然后主数据库和从数据库之间通过通信将数据完成同步;那么,我们的程序是如何将做到读操作的时候从从库中读取数据,写操作的时候是如何将数据写入到主库的呢?这个问题,就是今天要解决的问题;
目前市面上实现主从数据源切换的方式主要有两种,一种是利用第三方插件的形式实现,另外一种就是通过使用AOP进行实现。我采用的实现方式就是利用SpringAOP的方式实现;
2、实现2.1 导入所需要的依赖包
我的项目使用的SpringBoot实现的,ORM框架使用的是Mybatis,数据源使用的是阿里的Druid。配置如下:
2.2 配置数据源
为了节约成本,我只在本地的计算机上进行了代码实现,所以我只是在本地的同一个mysql服务上配置了多个数据库,数据库之间也没有进行主从的配置,毕竟我的主要目的是想看看代码的实现效果;配置文件如下:
# 主库spring.datasource.master.name=masterspring.datasource.master.driver-class-name=com.mysql.jdbc.Driverspring.datasource.master.url=jdbc:mysql://localhost:3306/master?serverTimezone=UTC&useSSL=falsespring.datasource.master.username=rootspring.datasource.master.password=root# 从库1spring.datasource.slaver1.name=slaver1spring.datasource.slaver1.driver-class-name=com.mysql.jdbc.Driverspring.datasource.slaver1.url=jdbc:mysql://localhost:3306/slaver1?serverTimezone=UTC&useSSL=falsespring.datasource.slaver1.username=rootspring.datasource.slaver1.password=root# 从库2spring.datasource.slaver2.name=slaver1spring.datasource.slaver2.driver-class-name=com.mysql.jdbc.Driverspring.datasource.slaver2.url=jdbc:mysql://localhost:3306/slaver2?serverTimezone=UTC&useSSL=falsespring.datasource.slaver2.username=rootspring.datasource.slaver2.password=root2.3 业务操作层的实现
数据的操作需要借助Service层和Dao层的进行实现,由于这部分不是实现主从数据源的关键部分,所以此处的代码就不进行展示;
2.4 数据源配置
我们都知道,Spring和Mybatis在整合的时候都需要配置 org.mybatis.spring.SqlSessionFactoryBean 的实例,在配置这个实例的时候需要指定数据源。那么如果想要实现主从数据源动态切换的功能,这个数据源的配置就不能使用传统的DataSource了,这里我是用的是 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 数据源。这个数据源是Spring提供的,它可以在获取数据源连接之前通过方法 determineTargetDataSource() 判断获取哪一个数据源的连接;也正是因为这个特性,我们才得以实现数据源动态切换的功能;
数据源配置如下:
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;import com.example.demo.config.db.DataSourceRoutingDataSource;import org.mybatis.spring.SqlSessionFactoryBean;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import org.springframework.core.io.Resource;import org.springframework.core.io.support.PathMatchingResourcePatternResolver;import org.springframework.stereotype.Component;import javax.sql.DataSource;import java.io.IOException;import java.util.HashMap;import java.util.HashSet;import java.util.Map;import java.util.Set;/** * 数据源配置文件 */@Component@Configurationpublic class DatasourceConfig { /** * 创建一个主数据源的实例 */ @Primary @Bean(value = "master") @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource master() { return DruidDataSourceBuilder.create().build(); } /** * 从数据源1 */ @Bean(value = "slaver1") @ConfigurationProperties(prefix = "spring.datasource.slaver1") public DataSource slaver1() { return DruidDataSourceBuilder.create().build(); } /** * 从数据源2 */ @Bean(value = "slaver2") @ConfigurationProperties(prefix = "spring.datasource.slaver2") public DataSource slaver2() { return DruidDataSourceBuilder.create().build(); } /** * DataSourceRoutingDataSource 继承了 AbstractRoutingDataSource; * 主要为了实现determineCurrentLookupKey()方法; */ @Bean(value = "dataSource") public DataSourceRoutingDataSource dataSource() { DataSourceRoutingDataSource dataSource = new DataSourceRoutingDataSource(); // 数据源 Map<Object, Object> dataSources = new HashMap<>(); dataSources.put("master", master()); dataSources.put("slaver1", slaver1()); dataSources.put("slaver2", slaver2()); dataSource.setTargetDataSources(dataSources); dataSource.setDefaultTargetDataSource(master()); // 设置主数据源的键值; Set<Object> masterKeys = new HashSet<>(); masterKeys.add("master"); dataSource.setMasterKeys(masterKeys); // 设置从数据源的键值; Set<Object> slaverKeys = new HashSet<>(); slaverKeys.add("slaver1"); slaverKeys.add("slaver2"); dataSource.setSlaverKeys(slaverKeys); return dataSource; } /** * SqlSessionFactoryBean实例配置 */ @Bean public SqlSessionFactoryBean sqlSessionFactoryBean() throws IOException { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource()); PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); Resource[] resources = resolver.getResources("classpath*:mapper/*.xml"); factoryBean.setMapperLocations(resources); factoryBean.setTypeAliasesPackage("com.example.demo.domain"); return factoryBean; }}
DataSourceRoutingDataSource实现如下:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;import java.util.ArrayList;import java.util.List;import java.util.Set;import java.util.concurrent.atomic.AtomicBoolean;import java.util.concurrent.atomic.AtomicInteger;public class DataSourceRoutingDataSource extends AbstractRoutingDataSource { public static AtomicBoolean MASTER_STATUS = new AtomicBoolean(true); private static List<Object> MASTER_KEYS = new ArrayList<>(); private static AtomicInteger MASTER_INDEX = new AtomicInteger(0); private static List<Object> SLAVER_KEYS = new ArrayList<>(); private static AtomicInteger SLAVER_INDEX = new AtomicInteger(0); /* * 关键点:用于切换数据源 * */ @Override protected Object determineCurrentLookupKey() { if (MASTER_STATUS.get()) { return getNextMaster(); } else { return getNextSlaver(); } } public void setMasterKeys(Set<Object> masterKeys) { MASTER_KEYS.addAll(masterKeys); } public void setSlaverKeys(Set<Object> slaverKeys) { SLAVER_KEYS.addAll(slaverKeys); } /** * 获取下一个主库的key */ private Object getNextMaster() { if (MASTER_KEYS.size() == 1) { return MASTER_KEYS.get(0); } int index = MASTER_INDEX.getAndAdd(1); return MASTER_KEYS.get(index % MASTER_KEYS.size()); } /** * 获取下一个从库的key */ private Object getNextSlaver() { if (SLAVER_KEYS.size() == 1) { return SLAVER_KEYS.get(0); } int index = SLAVER_INDEX.getAndAdd(1); return SLAVER_KEYS.get(index % SLAVER_KEYS.size()); }}2.5 AOP配置
实现上面的步骤其实已经可以进行增删改查的功能了,但是我们目的不在此;我们还要通过AOP进行数据源的切换,所以我们还需要配置AOP;我这里写的比较简单,就是根据service的名称判断是否使用主库;代码如下:
import com.example.demo.config.db.DataSourceRoutingDataSource;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Aspect@Componentpublic class ServiceAspect { @Pointcut(value = "execution(* com.example.demo.service.*.*(..))") public void point() {} @Before(value = "point()") public void before(JoinPoint joinPoint) { String name = joinPoint.getSignature().getName(); if (name.startsWith("get") || name.startsWith("query") || name.startsWith("find")) { DataSourceRoutingDataSource.MASTER_STATUS.set(false); } else { DataSourceRoutingDataSource.MASTER_STATUS.set(true); } }}
import com.example.demo.config.db.DataSourceRoutingDataSource;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Aspect@Componentpublic class ServiceAspect { @Pointcut(value = "execution(* com.example.demo.service.*.*(..))") public void point() {} @Before(value = "point()") public void before(JoinPoint joinPoint) { String name = joinPoint.getSignature().getName(); if (name.startsWith("get") || name.startsWith("query") || name.startsWith("find")) { DataSourceRoutingDataSource.MASTER_STATUS.set(false); } else { DataSourceRoutingDataSource.MASTER_STATUS.set(true); } }}3、总结
动态数据源的实现方式还是比较简单的,核心就在于配置数据源为 org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 类型的数据源;如果感兴趣的话,可以看一看内部的实现源码;
项目源码: