目录

  • 原理图
  • 数据库
  • 项目结构
    • 启动类
    • entity
    • controller
    • service
    • mapper
    • 配置文件
    • 线程上下文 (DataSourceHolder)
    • 动态数据源 DynamicDataSource
    • 数据源配置
    • AOP
  • 测试验证
  • 补充-不同场景中的应用
  • 事务问题待解决
  • 补充:

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

本文使用了 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 {@Autowiredprivate 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 {@Overridepublic 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%2B8username: rootpassword: aaaaaadriver‐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%2B8username: rootpassword: aaaaaadriver‐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*/@Overrideprotected 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=dsNoString 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

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

  1. java实现在Linux系统中动态切换IP

    java实现在Linux系统中动态切换IP 本文提供了一个可以在linux系统中,使用Java代码切换本地IP地址的方案: Tips: 1.本程序用于在Linux服务器上切换IP,在windows上无 ...

  2. 2 数据源配置_如何在程序运行中动态切换数据源?架构师必读秘笈

    说起动态数据源,大家应该也不陌生.例如在读写分离系统中,则要对请求中的读写操作进行分离,让读和写落在不同的数据库上:例如在多租户系统中,则要根据请求来源租户的不同,让其落在不同租户的数据库上:例如在分 ...

  3. Springboot/MybatisPlus动态切换数据源

    1.1 简述 最近项目中有动态切换数据源需求,主要是要动态切换数据源进行数据的获取,现将项目中实现思路及代码提供出来,供大家参考.当然切换数据源还有其他的方式比如使用切面的方式,其实大体思路是一样的. ...

  4. springboot中动态代理的那些事

    动态代理代理模式 & 静态代理动态代理动态生成的代理类切面编程AOP 在前一篇文章中,把springboot的基本流程梳理了一遍.但里面有一个问题没有往深入了说:springboot作为一个j ...

  5. springboot中动态获取bean工具类

    在springmvc或者springboot的项目中,经常遇到需要获取其他的bean的类,从而使用该bean内部的一些方法,以供业务调用,我们知道,在spring项目中,某个被spring管理的类要调 ...

  6. SpringBoot中banner个性启动(内附自定义设计网站)

    SpringBoot项目启动时会在控制台打印一个默认的启动图案,这个图案就是banner 1.首先需要在resources下创建一个banner.txt文件 2.打开新建的banner.txt文件 里 ...

  7. springboot多数据源动态切换,事务下切换数据源(非分布式事务)

    目录 1.业务场景 2.主要思路 3.加载默认数据源 4.多数据源规则配置 5.程序启动加载子公司数据源 6.自定义事务注解 7.程序中调用 1.业务场景 因为业务业务需求,需要把基础数据与子公司业务 ...

  8. Proxool配置多数据源动态切换

    2019独角兽企业重金招聘Python工程师标准>>> 前段时间遇到多数据源动态切换问题,总结一下,做个记录,以备后续之需! 首先附上proxool连接池的配置方法:http://3 ...

  9. Spring+Mybatis多数据源配置(四)——AbstractRoutingDataSource实现数据源动态切换

    欢迎支持笔者新作:<深入理解Kafka:核心设计与实践原理>和<RabbitMQ实战指南>,同时欢迎关注笔者的微信公众号:朱小厮的博客. 欢迎跳转到本文的原文链接:https: ...

最新文章

  1. 自学python的书籍-不可错过的十本Python好书
  2. 深究AngularJS——ui-router详解
  3. python的django框架与springboot_Python系统教学|为什么Django框架在Python开发很重要?...
  4. 如何在Vizio电视上禁用运动平滑
  5. go语言 os.Rename() cannot move the file to a different disk drive 怎么办
  6. 高等数学中常见的导数公式
  7. 元宇宙的第一步,应该在汽车里迈出去?
  8. 使用proteus仿真STM32超声波SRF04测距!Code+Proteus
  9. 凭什么看不起外包员工?程序员外包到底怎么了?
  10. 我也来一个“羊了个羊”
  11. python爬考研_Python爬取考研必备单词
  12. 2021-11-06Python是一种代表简单主义思想的语言
  13. 51单片机的电子密码锁的设计与仿真
  14. 数据结构查找-7-3 词频统计 (30 分)
  15. 使用Python提取txt文件中的数据到excel中
  16. 多重引导的另一种方法
  17. Android Drawable 那些不为人知的高效用法
  18. 读《富兰克林自传》的一些体会
  19. 渗透测试用工具(一)端口扫描Nmap
  20. freehand8_在Illustrator和Freehand中创建仿制3D图形

热门文章

  1. CSS设置元素叠加显示
  2. thinkpad x12018换固态_【求助】ThinkPad X1Carbon 更换ssd - 笔记本电脑(Notebook)版 - 北大未名BBS...
  3. 微信公众号开发官方定制ui
  4. 七夕到了,你还单身吗?
  5. mysql/mariadb 进阶知识之表设计和查询
  6. 【小记】steam 神奇软件
  7. One、that、it 做代词的区别
  8. 在Virtual Box中安装Windows7 64位虚拟机系统
  9. win10 系统网络图标只剩下飞行模式,没有wifi和本地连接 解决方法
  10. 侧边栏固定定位到版心两侧