龙空技术网

SpringBoot主从动态数据源应该这么搞?

佳运分享 253

前言:

目前同学们对“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=root
2.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 类型的数据源;如果感兴趣的话,可以看一看内部的实现源码;

项目源码:

标签: #springboot配置主从数据库 #springboot配置主从数据源