SpringBoot多数据源(主从数据源)配置

?前言

学习springboot配置多数据源,先回顾一下springboot配置单数据源的方式
SpringBoot配置mybatis-mysql数据源

?主从数据源搭建

项目依赖

本次记录多数据源配置主要是通过druid + mybatis plus + aop的形式实现的,mybatis plus是一个很方便的数据库操作框架,自己也有实现多数据源的jar包,这里没有使用她封装的方法,主要是学习所以是自行实现了一遍简单的多数据源配置和动态切换数据源。

<!-- mybatis-plus多数据源配置jar -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
</dependency>

使用到的依赖

<dependencies>

    <!-- druid数据连接池 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
    </dependency>

    <!-- mybatis plus -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
    </dependency>

    <!-- spring-aop -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

    <!-- springboot配置依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- mysql驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

Yml文件配置数据源

这里可以看到数据源配置属性路径并非spring.datasource, 这里主要是想通过学习spring-boot配置文件自动装配, 来获取配置并初始化数据源。

utmost:
  # 主从数据源配置
  datasource:
    dynamic:
      master:
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.92.10:3306/utmost_01?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        userName: root
        password: root
      slave:
        enabled: true  # 是否启用从数据源
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.92.10:3306/utmost_02?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        userName: root
        password: root
@Getter
@Setter
@ConfigurationProperties(prefix = UtmostDataSourceProperties.PREFIX)
public class UtmostDataSourceProperties {

    /**
     * 配置前缀
     */
    public static final String PREFIX = "utmost.datasource.dynamic";

    /**
     * master数据源配置前缀
     */
    public static final String MASTER_PREFIX = "utmost.datasource.dynamic.master";

    /**
     * slave数据源配置前缀
     */
    public static final String SLAVE_PREFIX = "utmost.datasource.dynamic.slave";

    /**
     * 设置默认数据库, 默认master
     */
    public String primary = "master";

    /**
     * 设置启用数据源, 默认true
     */
    public boolean enabled = true;

    /**
     * 主数据源
     */
    public SingleDataSourceProperty master;

    /**
     * 从数据源
     */
    public SingleDataSourceProperty slave;
}
@Data
@Accessors(chain = true)
public class SingleDataSourceProperty {

    /**
     * JDBC driver
     */
    private String driverClassName;

    /**
     * JDBC 数据库地址
     */
    private String url;

    /**
     * JDBC 用户名
     */
    private String userName;

    /**
     * JDBC 用户密码
     */
    private String password;
}

怎么通过UtmostDataSourceProperties类来获取属性, 主要通过@ConfigurationProperties注解实现, 前面依赖中引入了spring-boot-configuration-processor依赖, 在使用yml配置数据源时就会出现一定的提示作用。这是因为在打包编译的时候会生成一个spring-configuration-metadata.json文件,这里就不赘述了,先了解到这是springboot帮助我们生成的作用于提示的文件就可以了。
image.png

mybatis-plus配置

# mybatis-plus 配置
mybatis-plus:
  configuration:
    # 开启驼峰
    map-underscore-to-camel-case: true
    # 关闭一级缓存
    local-cache-scope: statement
    # 关闭二级缓存
    cache-enabled: false
  # sql xml文件映射路径
  mapper-locations: classpath*:/mapper/*.xml
  # MyBaits 别名包扫描路径,通过该属性可以给包中的类注册别名
  type-aliases-package: com.zy.utmost.entity

数据源装配

数据源配置类

@Slf4j
@Configuration
@AllArgsConstructor
@EnableConfigurationProperties(UtmostDataSourceProperties.class)
@ConditionalOnProperty(prefix = UtmostDataSourceProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
public class UtmostDataSourceAutoConfiguration {

    private final UtmostDataSourceProperties utmostDataSourceProperties;

    /**
     * 主数据源(这里配置写的繁琐一点, 可根据个人喜好进行简化.)
     *
     * @return DataSource
     */
    @Bean(name = "masterDataSource")
    @ConfigurationProperties(UtmostDataSourceProperties.MASTER_PREFIX)
    public DataSource masterDataSource() {
        SingleDataSourceProperty master = utmostDataSourceProperties.master;
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setName(DataSourceConstants.MASTER);
        druidDataSource.setDriverClassName(master.getDriverClassName());
        druidDataSource.setUrl(master.getUrl());
        druidDataSource.setUsername(master.getUserName());
        druidDataSource.setPassword(master.getPassword());
        return druidDataSource;
    }

    /**
     * 从 数据源(这里配置写的繁琐一点, 可根据个人喜好进行简化.)
     *
     * @return DataSource
     */
    @Bean(name = "slaveDataSource")
    @ConfigurationProperties(UtmostDataSourceProperties.SLAVE_PREFIX)
    @ConditionalOnProperty(prefix = UtmostDataSourceProperties.SLAVE_PREFIX, name = "enabled", havingValue = "true")
    public DataSource slaveDataSource() {
        SingleDataSourceProperty slave = utmostDataSourceProperties.slave;
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setName(DataSourceConstants.SLAVE);
        druidDataSource.setDriverClassName(slave.getDriverClassName());
        druidDataSource.setUrl(slave.getUrl());
        druidDataSource.setUsername(slave.getUserName());
        druidDataSource.setPassword(slave.getPassword());
        return druidDataSource;
    }

    /**
     * 动态数据源
     *
     * @return DynamicDataSource
     */
    @Bean
    @Primary
    public DynamicDataSource dynamicDataSource() {
        Map<Object, Object> targetMap = new HashMap<>(2);
        targetMap.put(DataSourceConstants.MASTER, masterDataSource());
        targetMap.put(DataSourceConstants.SLAVE, slaveDataSource());
        DynamicDataSource dynamicDataSource = new DynamicDataSource(masterDataSource(), targetMap);
        log.info("动态数据源装配完成...");
        return dynamicDataSource;
    }
}

动态数据源实现类

通过继承重写AbstractRoutingDataSource 数据源路由来实现数据源动态切换的功能.

public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * 使用线程切换数据源
     */
    private static ThreadLocal<String> contextHandler = new ThreadLocal<>();
    /**
     * 数据源key集合
     */
    private static List<Object> dataSourceKeys = new ArrayList<>();

    /**
     * 配置数据源
     *
     * @param defaultDataSource   主数据源
     * @param targetDataSourceMap 其他数据源集合
     */
    public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSourceMap) {
        super.setDefaultTargetDataSource(defaultDataSource);
        super.setTargetDataSources(targetDataSourceMap);
        super.afterPropertiesSet();
        // 初始化所有数据源的key
        addAllDataSourceKeys(targetDataSourceMap.keySet());
    }

    public static void setDataSourceKeys(String key) {
        contextHandler.set(key);
    }

    public static ThreadLocal<String> getDataSourceKeys() {
        return contextHandler;
    }

    public static void removeDataSourceKeys() {
        contextHandler.remove();
    }

    public static boolean containsDataSourceKeys(String key) {
        return dataSourceKeys.contains(key);
    }

    public static boolean addAllDataSourceKeys(Collection<? extends Object> keys) {
        return dataSourceKeys.addAll(keys);
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return contextHandler.get();
    }
}

数据源切换注解

/**
 * 注解命名主要是为了好记所以直接使用了DataSource, 在使用时会发现有很
 * 多类都是以DataSource命名, 使用时需要注意.
 * @author yanzy
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {

    String value() default "";
}

数据源切换实现

/**
 * 实现注解织入切换数据源
 *
 * @author yanzy
 * @date 2021/6/10 23:38
 * @since v1.0
 */
@Aspect
@Order(-10)
@Component
public class DataSourceAspect {

    private Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);

    /**
     * - @within扫描类注解, @annotation扫描方法上注解
     * 定义切面
     */
    @Pointcut("@annotation(com.zy.utmost.annotation.DataSource) || @within(com.zy.utmost.annotation.DataSource)")
    public void annotationPointCut() {
    }

    /**
     * 前置事件 (方法前切换数据源)
     * @param point 织入点
     * @param dataSource 注解实例
     */
    @Before("annotationPointCut() && @annotation(dataSource))")
    public void beforeMethod(JoinPoint point, DataSource dataSource) {
        switchDataSource(point, dataSource);
    }

    /**
     * 前置事件 (类前切换数据源)
     * @param point 织入点
     * @param dataSource 注解实例
     */
    @Before("annotationPointCut() && @within(dataSource))")
    public void beforeClass(JoinPoint point, DataSource dataSource) {
        switchDataSource(point, dataSource);
    }

    private void switchDataSource(JoinPoint point,DataSource dataSource) {
        String key = dataSource.value();
        if (!DynamicDataSource.containsDataSourceKeys(key)) {
            logger.debug("数据源切换失败: [{}] - 数据源不存在, 自动使用默认数据源.", key);
        } else {
            DynamicDataSource.setDataSourceKeys(key);
            logger.debug("数据源切换成功: [{}] - 已切换至 - [{}] - 数据源.", point.getSignature().getName(), key);
        }
    }

    /**
     * 后置增强 (方法/类执行完毕后,将数据源切回默认)
     *
     * @param point 织入点
     */
    @After("annotationPointCut()")
    public void after(JoinPoint point) {
        if (null != DynamicDataSource.getDataSourceKeys()) {
            DynamicDataSource.removeDataSourceKeys();
            logger.debug("数据源切换成功: 切换为主数据源.");
        }
    }
}

搭建完测试

测试使用的crud类就不附上代码了, 直接使用spring-boot-test来给数据库插入数据, 验证一下数据源是否可以正常切换.

数据库

首先这里使用了两个数据库, 都创建了utmost这个数据库实例并创建有sys_user表.
image.png

service实现类(方法增加注解切换数据源)

@Service
public class SysUserServiceImpl implements SysUserService {

    @Resource
    private SysUserMapper sysUserMapper;
    
    // 主库插入
    @Override
    public Integer insertUserMaster(SysUser sysUser) {
        return sysUserMapper.insert(sysUser);
    }
    
    // 从库插入
    @Override
    @DataSource(value = DataSourceConstants.SLAVE)
    public Integer insertUserSlave(SysUser sysUser) {
        return sysUserMapper.insert(sysUser);
    }
}

?测试类

@SpringBootTest
public class SysUserTableTest {

    @Autowired
    private SysUserService sysUserService;

    @Test
    public void insertByMaster() {

        SysUser sysUser = new SysUser();
        sysUser.setUserName("admin");
        sysUser.setLoginName("admin");
        sysUser.setPassword("admin");
        sysUserService.insertUserMaster(sysUser);
    }

    @Test
    public void insertBySlave() {
        SysUser sysUser = new SysUser();
        sysUser.setUserName("admin");
        sysUser.setLoginName("admin");
        sysUser.setPassword("admin");
        sysUserService.insertUserSlave(sysUser);
    }
}

测试主库插入, 不标注注解时, 默认使用主库.
image.png
image.png

测试从库插入, 调用使用了切换数据源注解得到方法
image.png
image.png

类标注注解测试

@Service
@DataSource(value = DataSourceConstants.SLAVE)
public class SysUserServiceImpl implements SysUserService {

    @Resource
    private SysUserMapper sysUserMapper;

    @Override
    public Integer insertUserMaster(SysUser sysUser) {
        return sysUserMapper.insert(sysUser);
    }

    @Override
    public Integer insertUserSlave(SysUser sysUser) {
        return sysUserMapper.insert(sysUser);
    }
}

调用insertUserMaster, 验证数据源是否切换成功.
image.png
image.png

?总结

好啦~ 以上主要记录一下自己在学习过程中是如何配置多数据源的(严格来说代码中的写法应该是主从数据源 嘿嘿)。
一般主从数据源主要是为了做读写分离的,后面学习总结完读写分离操作后在进行分享记录啦~~


版权声明:本文为Confused_原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。