springboot 中动态切换数据源(多数据源应用设计)

9 篇文章 7 订阅




最近在做一个租户隔离的项目,要求不同租户数据放入不同的数据库实现物理隔离,涉及到多个数据库的应用,除了使用数据库中间件,还想到了一个不错的解决方案,就是动态切换当前请求线程的数据源。所以写篇文章来记录一下。

本文使用了 springboot + mybatis-plus

项目源码可以参考
https://gitee.com/qiu_yunzhao/daily_function_test/tree/master/dynamic_dataSource

哔站有个不错的视频教程:
https://www.bilibili.com/video/BV11Z4y1f7cT?p=1

原理图

请添加图片描述

数据库

创建两个数据库 ds0与ds1
在这里插入图片描述
在这里插入图片描述

项目结构

在这里插入图片描述

下边这些都是平时最常用的就不在多做介绍

启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DynamicDataSourceApplication {

    public static void main(String[] args) {
        SpringApplication.run(DynamicDataSourceApplication.class, args);
    }

}

entity

package com.haoqian.dynamic_data_dource.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * @author haoqian
 * @since 2021-08-22
 */
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("tbl_employee")
public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    private String lastName;

    private String email;

    private String gender;

    private Integer age;

}

controller

package com.haoqian.dynamic_data_dource.controller;

import com.haoqian.dynamic_data_dource.entity.Employee;
import com.haoqian.dynamic_data_dource.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @author haoqian
 * @since 2021-08-22
 */
@RestController
@RequestMapping("/employee")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    @GetMapping("/{id}")
    public Employee select(@PathVariable("id") Integer id) {
        return employeeService.getEmpById(id);
    }

}

service

import com.haoqian.dynamic_data_dource.entity.Employee;
import com.baomidou.mybatisplus.extension.service.IService;

public interface EmployeeService extends IService<Employee> {
    Employee getEmpById(int id);
}
import com.haoqian.dynamic_data_dource.entity.Employee;
import com.haoqian.dynamic_data_dource.mapper.EmployeeMapper;
import com.haoqian.dynamic_data_dource.service.EmployeeService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {

    @Override
    public Employee getEmpById(int id) {
        return this.baseMapper.getEmpById(id);
    }
}

mapper

import com.haoqian.dynamic_data_dource.entity.Employee;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;

public interface EmployeeMapper extends BaseMapper<Employee> {
    Employee getEmpById(int id);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.haoqian.dynamic_data_dource.mapper.EmployeeMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.haoqian.dynamic_data_dource.entity.Employee">
        <id column="id" property="id"/>
        <result column="last_name" property="lastName"/>
        <result column="email" property="email"/>
        <result column="gender" property="gender"/>
        <result column="age" property="age"/>
    </resultMap>

    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id, last_name, email, gender, age
    </sql>

    <select id="getEmpById" resultType="com.haoqian.dynamic_data_dource.entity.Employee">
      SELECT * FROM tbl_employee WHERE id=#{id}
   </select>

</mapper>




重点来了,下边是实现动态切换数据库的核心代码

配置文件

我们在配置文件中配置文章开头介绍的两个数据库

spring:
  # 设置druid数据源
  datasource:
    #配置ds0据库
    primary:
      jdbc-url: jdbc:mysql://192.168.0.150:3306/ds0?characterEncoding=UTF-8&serverTimezone=GMT%2B8
      username: root
      password: aaaaaa
      driver‐class‐name: com.mysql.cj.jdbc.Driver # 注意MySQL8.x的驱动
    #配置ds1数据库
    secondary:
      jdbc-url: jdbc:mysql://192.168.0.150:3306/ds1?characterEncoding=UTF-8&serverTimezone=GMT%2B8
      username: root
      password: aaaaaa
      driver‐class‐name: com.mysql.cj.jdbc.Driver # 注意MySQL8.x的驱动

# mybatis-plus配置(与mybatis配置项几乎一样,就是这里用mybatis-plus而不是mybatis)
# 可配置项见官网 https://mybatis.plus/config/#globalconfig-2
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启sql日志打印(支持配置文件和注解)
    map-underscore-to-camel-case: true # 驼峰下划线映射规则(默认true)

线程上下文 (DataSourceHolder)

线程上下文用于存储当前线程使用的数据源

package com.haoqian.dynamic_data_dource.datasource;

/**
 * @author qyz
 */
public class DataSourceHolder {

    /**
     * 线程本地环境 (存储数据库名称)
     */
    private static final ThreadLocal<String> DATA_SOURCES = new ThreadLocal<>();

    /**
     * 设置数据源(动态切换数据源),就是调用这个setDataSource方法
     */
    public static void setDataSource(String customerType) {
        DATA_SOURCES.set(customerType);
    }

    /**
     * 获取数据源
     */
    public static String getDataSource() {
        return DATA_SOURCES.get();
    }

    /**
     * 清除数据源
     */
    public static void clearDataSource() {
        DATA_SOURCES.remove();
    }
}

动态数据源 DynamicDataSource

核心是需要继承 AbstractRoutingDataSource 实现determineCurrentLookupKey()方法,通过该方法的返回值实现不同线程中动态切换数据源。

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * @author qyz
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * 每次请求动态请求哪一个数据源
     *
     * @return
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceHolder.getDataSource();
    }

    /**
     * 此处数据库配置,可以来源于redis等,然后再初始化所有数据源
     * 重点说明:一个DruidDataSource数据源,它里面本身就是线程池了,所以我们不需要考虑线程池的问题
     *
     * 这里我们选择将数据源的配置放到了配置文件中
     * 
     * @param database 数据库名称
     * @return 数据源
     */
//    public DataSource druidDataSource(int database) {
//        DruidDataSource datasource = new DruidDataSource();
//        datasource.setUrl("jdbc:mysql://localhost:3306/" + database);
//        datasource.setUsername("root");
//        datasource.setPassword("aaaaaa");
//        datasource.setDriverClassName("com.mysql.jdbc.Driver");
//        datasource.setInitialSize(5);
//        datasource.setMinIdle(5);
//        datasource.setMaxActive(20);
//        //datasource.setDbType("com.alibaba.druid.pool.DruidDataSource");
//        datasource.setMaxWait(60000);
//        datasource.setTimeBetweenEvictionRunsMillis(60000);
//        datasource.setMinEvictableIdleTimeMillis(300000);
//        datasource.setValidationQuery("SELECT 1 FROM DUAL");
//        datasource.setTestWhileIdle(true);
//        datasource.setTestOnBorrow(false);
//        datasource.setTestOnReturn(false);
//        try {
//            datasource.setFilters("stat,wall,log4j");
//        } catch (SQLException e) {
//            e.printStackTrace();
//        }
//        return datasource;
//    }
}

数据源配置

编写数据源配置类,将数据源交由spring管理

package com.haoqian.dynamic_data_dource.datasource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @author qyz
 */
@Configuration
@MapperScan(basePackages = "com.haoqian.dynamic_data_dource.mapper", sqlSessionFactoryRef = "SqlSessionFactory")
public class DynamicDataSourceConfig {

    /**
     * 将第1个数据源对象放入Spring容器中
     *
     * @ConfigurationProperties 读取application.properties中的前缀为spring.datasource.primary的配置参数并映射成为一个对象
     */
    @Bean(name = "dateSource1")
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSource DateSource1() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 将第2个数据源对象放入Spring容器中
     */
    @Bean(name = "dateSource2")
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSource DateSource2() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 将动态代理数据源对象放入Spring容器中
     */
    @Bean(name = "dynamicDataSource")
    public DynamicDataSource DynamicDataSource(@Qualifier("dateSource1") DataSource primaryDataSource,
                                               @Qualifier("dateSource2") DataSource secondaryDataSource) {
        // 这个地方是比较核心的targetDataSource 集合是我们数据库和名字之间的映射
        Map<Object, Object> targetDataSource = new HashMap<>();
        targetDataSource.put("ds0", primaryDataSource);
        targetDataSource.put("ds1", secondaryDataSource);

        DynamicDataSource dataSource = new DynamicDataSource();
        // 设置所有的数据源
        dataSource.setTargetDataSources(targetDataSource);
        // 设置默认使用的数据源对象
        dataSource.setDefaultTargetDataSource(primaryDataSource);

        return dataSource;
    }


    @Bean(name = "SqlSessionFactory")
    public SqlSessionFactory SqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dynamicDataSource);
        bean.setMapperLocations(
                // 设置数据库mapper的xml文件路径
                new PathMatchingResourcePatternResolver()
                        .getResources("classpath*:com/haoqian/dynamic_data_dource/mapper/*/*.xml"));
        return bean.getObject();
    }
}

AOP

在aop中拦截controller请求,从请求头中获取使用的数据源库,然后讲当前请求使用的数据库配置到线程上下文中,实现动态数据源切换

import com.haoqian.dynamic_data_dource.datasource.DataSourceHolder;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Objects;


/**
 * 拦截controller方法,从请求头中获取使用的数据库编号
 *
 * @author qyz
 */
@Aspect
@Order(1)
@Configuration
public class DataSourceAspect {

    /**
     * 数据库名称在请求头中的key(从header中取)
     */
    private static final String DSNO = "dsNo";

    /**
     * 切入点,放在controller的每个方法上进行切入,更新数据源
     */
    @Pointcut("execution(* com.haoqian.dynamic_data_dource.controller..*.*(..))")
    private void anyMethod() {
    }

    @Before("anyMethod()")
    public void dataSourceChange() {
        // 请求头head中获取对应数据库编号 name=dsNo
        String dsNo = ((ServletRequestAttributes) Objects
                .requireNonNull(RequestContextHolder.getRequestAttributes()))
                .getRequest()
                .getHeader(DSNO);
        System.out.println("当前数据源: " + dsNo);
        if (StringUtils.isBlank(dsNo)) {
            // TODO 根据业务抛异常
            throw new NullPointerException("请求头中没有" + DSNO);
        }
        // 根据请求头中数据库名称来更改对应的数据源(核心)
        DataSourceHolder.setDataSource(dsNo);
    }

    @After("anyMethod()")
    public void after() {
        // 数据源重置(必须在请求完成后清空ThreadLocal线程上下文,否则会内存溢出)
        DataSourceHolder.clearDataSource();
    }
}


测试验证

在这里插入图片描述
在这里插入图片描述


补充-不同场景中的应用

数据库的读写分离场景:

  1. 使用mybatis框架时可以使用mybatis的拦截器,在拦截其中获取要执行的sql,解析sql,根据sql语句的读写操作,来改变使用的数据库标识,进而动态切换到对应的数据库。

  2. 当然,如果项目有预算,且有人维护的话可以使用数据库中间件来实现,如mycat。

不同业务数据存储在不同数据库的场景:

  1. 使用多数据源应用的方案解决时,一般采用 “AOP+自定义注解” 的方式实现数据源的动态切换。在前置通知中改变使用的数据库标识,进而动态切换到对应的数据库。

  2. 可以使用微服务架构来进行解决。

  3. 可以使用mybatis框架提供的功能,直接配置多份mapper与sql映射文件,改设计详见
    https://blog.csdn.net/QiuHaoqian/article/details/122724818.

事务问题待解决

多数据源的事务处理是有问题的,一定要注意(可以使用分布式事务,但是很复杂)。

  1. spring自带的声明式事务无法实现多数据源的事务管理,因为 @Transactional只能指定一个事务管理器。
  2. spring的编程式事务可以实现,但是比较麻烦

补充:

Dynamic-Datasource (opens new window)- 一个基于 SpringBoot 的多数据源组件,功能强悍,支持 Seata 分布式事务。可以使用它实现多数据源系统。

该组件由 mybatis-plus 的团队开发。其实现原理同上边我们介绍的。并且提供了多数据源事务的支持

@DS

@DSTransactional

这里不在讲述,详见

https://github.com/baomidou/dynamic-datasource-spring-boot-starter

  • 3
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SpringBoot是一个非常流行并且易于使用的后端框架。在传统的数据库应用程序,我们通常都需要使用JdbcTemplate来操作数据库。但是在某些情况下,我们需要动态切换数据源。比如,我们可能要从单一的应用程序连接到不同的数据源(例如生产环境与测试环境)。 为了解决这一问题,我们可以在SpringBoot使用DynamicDataSource这个开源项目来实现动态切换数据源。 在使用DynamicDataSource之前,我们需要先在application.properties文件配置我们的数据源。比如,我们可以配置两个数据源: ``` spring.datasource.primary.url=jdbc:mysql://localhost:3306/db1 spring.datasource.primary.username=root spring.datasource.primary.password=root spring.datasource.secondary.url=jdbc:mysql://localhost:3306/db2 spring.datasource.secondary.username=root spring.datasource.secondary.password=root ``` 然后我们在代码实现DynamicDataSource的相关接口,实现动态切换数据源的功能。具体来说,我们需要实现AbstractRoutingDataSource这个类的determineCurrentLookupKey()方法,这个方法的返回值决定了当前应该使用哪个数据源。 接下来,我们可以在代码使用@Autowired注解来注入DynamicDataSource,然后根据需要调用DynamicDataSource的setDataSource()方法来切换数据源,从而实现动态切换数据源。 总之,使用SpringBoot和JdbcTemplate结合DynamicDataSource能够非常方便地实现动态切换数据源的功能,从而提高代码的灵活性和可维护性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值