spring boot 整合多数据源

2023-09-15 12:33:28

多数据源产生的场景

一般情况下,不会有多数据源这样的场景出现,但老项目或者特殊需求的项目,可能会有这样的场景

  • 同一个应用需要访问两个数据库
  • 不用数据库中间件的读写分离

 注入数据源选择的时机

声明两个数据源实例,在getConnection的时候根据业务的不同,注入不同数据源的连接

环境准备

准备sql脚本,建立两个库,这里mysql为例


create database stu;
create database tech;
use stu;
create table student
(
    id varchar(50) not null comment '主键',
    name varchar(50) null comment '姓名',
    stu_no varchar(50) null comment '学号',
    constraint student_pk primary key (id)
);

insert into student values ('1','张同学','111');
insert into student values ('2','李同学','222');
use tech;
create table teacher
(
    id varchar(50) not null comment '主键',
    name varchar(50) null comment '姓名',
    teach_no varchar(50) null comment '教师号',
    constraint teacher_pk primary key (id)
);

insert into teacher values ('1','王老师','111');
insert into teacher values ('2','高老师','222');

实现DataSource方式实现多数据源

配置多数据源

server:
  port: 9000
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    datasource1:
      url: jdbc:mysql://shilei.tech:3306/stu?useSSL=true&serverTimezone=Asia/Shanghai
      username: root
      password: root123456
      driver-class-name: com.mysql.cj.jdbc.Driver

    datasource2:
      url: jdbc:mysql://shilei.tech:3306/tech?useSSL=true&serverTimezone=Asia/Shanghai
      username: root
      password: root123456
      driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      initial-size: 5
      min-idle: 1
      max-active: 20

mybatis-plus:
  mapper-locations: classpath:/mapper/*.xml
  type-aliases-package: com.datasource.dynamicdatasource.model

添加数据源配置

package com.datasource.dynamicdatasource.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

/**
 * @author sl
 */
@Configuration
public class DataSourceConfig {

    @Bean("dataSource1")
    @ConfigurationProperties(prefix = "spring.datasource.datasource1")
    public DataSource dataSource1(){
        DruidDataSource druidDataSource = new DruidDataSource();
        return druidDataSource;
    }


    @Bean("dataSource2")
    @ConfigurationProperties(prefix = "spring.datasource.datasource2")
    public DataSource dataSource2(){
        DruidDataSource druidDataSource = new DruidDataSource();
        return druidDataSource;
    }
}

 实现DataSource多数据源

package com.datasource.dynamicdatasource.config;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

/**
 * @author sl
 * @Primary主要注入的bean
 */
@Configuration
@Primary
public class DynamicDataSource implements DataSource {


    public static ThreadLocal<String> nameFlag = new ThreadLocal<>();

    @Resource
    private DataSource dataSource1;

    @Resource
    private DataSource dataSource2;

    @Override
    public Connection getConnection() throws SQLException {
        if("student".equals(nameFlag.get())){
            return dataSource1.getConnection();
        }
        return dataSource2.getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return null;
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return null;
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return false;
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return null;
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {

    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {

    }

    @Override
    public int getLoginTimeout() throws SQLException {
        return 0;
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        return null;
    }
}

测试多数据源

package com.datasource.dynamicdatasource.controller;


import com.datasource.dynamicdatasource.config.DynamicDataSource;
import com.datasource.dynamicdatasource.model.Student;
import com.datasource.dynamicdatasource.model.Teacher;
import com.datasource.dynamicdatasource.service.StudentService;
import com.datasource.dynamicdatasource.service.TeacherService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @author sl
 */
@RestController
public class TestDataSourceController {

    @Autowired
    private StudentService studentService;

    @Autowired
    private TeacherService teacherService;

    @GetMapping("/stu")
    public String getStu(){
        DynamicDataSource.nameFlag.set("student");
        List<Student> allStudent = studentService.findAllStudent();
        return allStudent.toString();
    }

    @GetMapping("/tech")
    public String getTech(){
        DynamicDataSource.nameFlag.set("teacher");
        List<Teacher> allTeacher = teacherService.findAllTeacher();
        return allTeacher.toString();
    }
}

效果如下所示

此实现方式的弊端

实现DataSource接口我们本质上只使用了一个方法,就是getConnection()这个无参的方法,其他方法,当内部调用时可能会导致错误,我们不可能实现所有的方法,所以我们继承AbstractRoutingDataSource抽象类

继承AbstractRoutingDataSource实现多数据源

AbstractRoutingDataSource的结构

可以看到AbstractRoutingDataSource继承自DataSource,提供了一些实现方法

AbstractRoutingDataSource的重要属性 

targetDataSources 所有数据源 (需指定)

defaultTargetDataSource 默认数据源(需指定)

resolvedDataSources= targetDataSources 负责最终切换的数据源map 等于 tagetDataSources

继承AbstractRoutingDataSource实现多数据源

package com.datasource.dynamicdatasource.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

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


/**
 * @author sl
 * @Primary主要注入的bean
 */
@Configuration
@Primary
public class DynamicDataSource extends AbstractRoutingDataSource {


    public static ThreadLocal<String> nameFlag = new ThreadLocal<>();

    @Resource
    private DataSource dataSource1;

    @Resource
    private DataSource dataSource2;


    @Override
    protected Object determineCurrentLookupKey() {
        // 返回当前数据源的标识
        return nameFlag.get();
    }

    @Override
    public void afterPropertiesSet() {

        // 为targetDataSources 初始化所有数据源
        Map<Object,Object> targetDataSources=new HashMap<>();

        targetDataSources.put("student",dataSource1);
        targetDataSources.put("teacher",dataSource2);

        super.setTargetDataSources(targetDataSources);
        // 设置默认数据源
        super.setDefaultTargetDataSource(dataSource1);

        // 循环给resolvedDataSources,也就是最终数据源map
        super.afterPropertiesSet();

    }
}

determineCurrentLookupKey的作用 

看一段源码,就是通过determineCurrentLookupKey获取数据源的key,然后去resolvedDataSources中取数据源,resolvedDataSources数据源其实就是targetDataSources

	protected DataSource determineTargetDataSource() {
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
		Object lookupKey = determineCurrentLookupKey();
		DataSource dataSource = this.resolvedDataSources.get(lookupKey);
		if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
			dataSource = this.resolvedDefaultDataSource;
		}
		if (dataSource == null) {
			throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
		}
		return dataSource;
	}

 测试多数据源

package com.datasource.dynamicdatasource.controller;


import com.datasource.dynamicdatasource.config.DynamicDataSource;
import com.datasource.dynamicdatasource.model.Student;
import com.datasource.dynamicdatasource.model.Teacher;
import com.datasource.dynamicdatasource.service.StudentService;
import com.datasource.dynamicdatasource.service.TeacherService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @author sl
 */
@RestController
public class TestDataSourceController {

    @Autowired
    private StudentService studentService;

    @Autowired
    private TeacherService teacherService;

    @GetMapping("/stu")
    public String getStu(){
        // 默认数据源就是student
        List<Student> allStudent = studentService.findAllStudent();
        return allStudent.toString();
    }

    @GetMapping("/tech")
    public String getTech(){
        DynamicDataSource.nameFlag.set("teacher");
        List<Teacher> allTeacher = teacherService.findAllTeacher();
        return allTeacher.toString();
    }
}

AOP自定义注解方式+AbstractRoutingDataSource实现多数据源

数据源的切换还是使用AbstractRoutingDataSource,只不过切换方式采用aop拦截自定义注解切换数据源,这种方式也是mybatis-plus多数据源插件所采用的方式

自定义注解

package com.datasource.dynamicdatasource.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author sl
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyDataSource {

    String value() default "student";
}

配置切面

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
package com.datasource.dynamicdatasource.aspect;

import com.datasource.dynamicdatasource.annotation.MyDataSource;
import com.datasource.dynamicdatasource.config.DynamicDataSource;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @author sl
 *@Aspect 标识是一个切面
 */
@Aspect
@Component
public class DatasourceAspect {

    /**
     * 切点规则
     */
    @Pointcut("@annotation(com.datasource.dynamicdatasource.annotation.MyDataSource)")
    public void pointcut() {
    }

    @Before("pointcut()")
    public void dataSourceAspect(JoinPoint joinPoint){
        // 获取方法
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();

        // 判断方法中是否添加了注解
        if(method.isAnnotationPresent(MyDataSource.class)){
            // 获取方法上的注解
            MyDataSource annotation = method.getAnnotation(MyDataSource.class);

            String value = annotation.value();
            //  设置数据源
            DynamicDataSource.nameFlag.set(value);
        }
    }
}

测试自定义注解切换数据源

    @GetMapping("/tech")
    @MyDataSource("teacher")
    public String getTech(){
        List<Teacher> allTeacher = teacherService.findAllTeacher();
        return allTeacher.toString();
    }

dynamic-datasource多数据源组件实现多数据源

官方文档及搭建指南地址:多数据源 | MyBatis-Plus

引入依赖

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
  <version>3.6.1</version>
</dependency>

配置数据源

server:
  port: 9000
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    dynamic:
      #设置默认的数据源或者数据源组,默认值即为master
      primary: master
      #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
      strict: false
      datasource:
        master:
          url: jdbc:mysql://shilei.tech:3306/stu?useSSL=true&serverTimezone=Asia/Shanghai
          username: root
          password: root123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        teacher:
          url: jdbc:mysql://shilei.tech:3306/tech?useSSL=true&serverTimezone=Asia/Shanghai
          username: root
          password: root123456
          driver-class-name: com.mysql.cj.jdbc.Driver
      druid:
        initial-size: 5
        min-idle: 1
        max-active: 20

mybatis-plus:
  mapper-locations: classpath:/mapper/*.xml
  type-aliases-package: com.datasource.dynamicdatasource.model

测试数据源切换

数据源切换使用@DS注解,不使用此注解,使用默认数据源,方法上使用>类上使用 

package com.datasource.dynamicdatasource.controller;


import com.baomidou.dynamic.datasource.annotation.DS;
import com.datasource.dynamicdatasource.model.Student;
import com.datasource.dynamicdatasource.model.Teacher;
import com.datasource.dynamicdatasource.service.StudentService;
import com.datasource.dynamicdatasource.service.TeacherService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @author sl
 */
@RestController
public class TestDataSourceController {

    @Autowired
    private StudentService studentService;

    @Autowired
    private TeacherService teacherService;

    @GetMapping("/stu")
    public String getStu(){
        List<Student> allStudent = studentService.findAllStudent();
        return allStudent.toString();
    }

    @GetMapping("/tech")
    @DS("teacher")
    public String getTech(){
        List<Teacher> allTeacher = teacherService.findAllTeacher();
        return allTeacher.toString();
    }
}

项目启动日志中可以看到两个数据源的加载信息

 访问tech以及stu都能正常访问,代表动态数据源添加成功

需要注意的问题

使用多数据源要注意事务的控制,提交和回滚策略,可以观看spring多数据源事务解决方案

更多推荐

(vue2)面经基础版-案例效果分析

配路由先配一级,一级里面配二级。一级路由:首页(二级:嵌套4个小页面)、详情页高亮a->router-link,高亮效果对自带高亮类名router-link(-exact)-active设置注:通过children配置项,可以配置嵌套子路由。并在该组件中准备路由出口<router-view></router-view>

「聊设计模式」之 设计模式的前世今生

🏆本文收录于《聊设计模式》专栏,专门攻坚指数级提升,助你一臂之力,带你早日登顶🚀,欢迎持续关注&&收藏&&订阅!目录:一、什么是设计模式设计模式的定义设计模式的作用二、设计模式的发展历程设计模式的起源设计模式的发展阶段三、设计模式的分类创建型模式结构型模式行为型模式四、常用的设计模式工厂模式单例模式装饰器模式代理模

【SpringBoot项目】SpringBoot+MyBatis+MySQL电脑商城

在b站听了袁老师的开发课,做了一点笔记。01-项目环境搭建_哔哩哔哩_bilibili基于springboot框架的电脑商城项目(一)_springboot商城项目_失重外太空.的博客-CSDN博客项目环境搭建1.项目分析1.项目功能:登录、注册、热销商品、用户管理(密码、个人信息、头像、收货地址)、购物车(展示、增加

Promise的链式调用

catch方法.catch(onRejected)=.then(null,onRejected)链式调用then方法必定会返回一个新的Promise可理解为后续处理也是一个任务新任务的状态取决于后续处理:若没有相关的后续处理,新任务的状态和前任务一致,数据为前任务的数据若有后续处理但还未执行,新任务挂起。若后续处理执行

C++笔记之文档术语——将可调用对象作为函数参数

C++笔记之文档术语——将可调用对象作为函数参数相关博文:C++笔记之函数对象functors与可调用对象文章目录C++笔记之文档术语——将可调用对象作为函数参数1.在函数参数中传递可调用对象2.‘在参数中传入可调用对象’和‘将可调用对象作为函数参数’哪个描述更加专业官方?3."将可调用对象作为函数参数"是不是和‘回调

【Java 基础篇】Java网络编程基础知识详解

网络编程是现代软件开发中不可或缺的一部分,它使我们能够在不同的计算机之间实现数据传输和通信。Java作为一种强大的编程语言,提供了丰富的网络编程库,使开发者能够轻松地创建网络应用程序。本文将介绍Java网络编程的基础知识,面向初学者,详细讨论网络通信的概念、Socket编程、服务器和客户端编程等内容。1.网络通信的基本

二刷力扣--栈和队列

栈和队列栈和队列基础(Python)栈一种先进后出,队列先进后出。Python中可以用list实现栈,用append()模拟入栈,用pop()模拟出栈。也可以用list实现队列,但是效率较低,一般用collections.deque模拟(双端)队列。5.数据结构—Python3.11.5文档使用list进行栈的操作st

git 命令总结

git初始化gitinit添加文件gitadd<file>添加注释gitcommit-m"注释"重新提交覆盖上一次提交内容gitcommit--amend查看当前所处状态gitstatus克隆仓库gitclone<remoteURL>关联远程仓库gitremoteadd<remote><remoteURL>查看git对

【Java 基础篇】Java TCP通信详解

TCP(TransmissionControlProtocol)是一种面向连接的、可靠的网络传输协议,它提供了端到端的数据传输和可靠性保证。TCP通信适用于对数据传输的可靠性和完整性要求较高的场景,如文件传输、网页浏览等。本文将详细介绍Java中如何使用TCP协议进行网络通信,包括TCP套接字、服务器和客户端的创建、数

麒麟信安的2023世界计算大会时刻

9月15至16日,由工业和信息化部、湖南省人民政府主办的2023世界计算大会在长沙隆重举行。麒麟信安连续五年亮相世界计算大会,本届大会麒麟信安作为计算产业的重要建设者、国家新一代自主安全计算系统产业集群内核心企业,在展览展示、主题演讲、工控操作系统创新研究院揭牌仪式等多环节中深度参与。大会以“计算万物湘约未来——计算产

Leetcode算法入门与数组丨3. 数组基础

文章目录前言1数组简介2数组的基本操作2.1访问元素2.2查找元素2.3插入元素2.4改变元素2.5删除元素3总结task03task04前言Datawhale组队学习丨9月Leetcode算法入门与数组丨打卡笔记这篇博客是一个入门型的文章,主要是自己学习的一个记录。内容会参考这篇笔记(很详细):LeetCode算法笔

热文推荐