目录
最近在做一个租户隔离的项目,要求不同租户数据放入不同的数据库实现物理隔离,涉及到多个数据库的应用,除了使用数据库中间件,还想到了一个不错的解决方案,就是动态切换当前请求线程的数据源。所以写篇文章来记录一下。
本文使用了 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();
}
}
测试验证
补充-不同场景中的应用
数据库的读写分离场景:
-
使用mybatis框架时可以使用mybatis的拦截器,在拦截其中获取要执行的sql,解析sql,根据sql语句的读写操作,来改变使用的数据库标识,进而动态切换到对应的数据库。
-
当然,如果项目有预算,且有人维护的话可以使用数据库中间件来实现,如mycat。
不同业务数据存储在不同数据库的场景:
-
使用多数据源应用的方案解决时,一般采用 “AOP+自定义注解” 的方式实现数据源的动态切换。在前置通知中改变使用的数据库标识,进而动态切换到对应的数据库。
-
可以使用微服务架构来进行解决。
-
可以使用mybatis框架提供的功能,直接配置多份mapper与sql映射文件,改设计详见
https://blog.csdn.net/QiuHaoqian/article/details/122724818.
事务问题待解决
多数据源的事务处理是有问题的,一定要注意(可以使用分布式事务,但是很复杂)。
- spring自带的声明式事务无法实现多数据源的事务管理,因为 @Transactional只能指定一个事务管理器。
- spring的编程式事务可以实现,但是比较麻烦
补充:
Dynamic-Datasource (opens new window)- 一个基于 SpringBoot 的多数据源组件,功能强悍,支持 Seata 分布式事务。可以使用它实现多数据源系统。
该组件由 mybatis-plus 的团队开发。其实现原理同上边我们介绍的。并且提供了多数据源事务的支持
@DS
@DSTransactional
这里不在讲述,详见
https://github.com/baomidou/dynamic-datasource-spring-boot-starter