这段时间想起以前的做项目的时候,因为基于分布式架构,不同的租户下的用户,在发送请求的时候,都需要切换到对应租户平台下的数据库,由此产生动态切换数据源的概念,结合threadlocal实现此功能,threadlocal主要防止多线程,保证每个线程做操作时候独立处理事务,防止被其它线程串改我现在把该项目的实现这一功能的流程做个简单demo分离出来,给大家做个借鉴
我分别使用
http://localhost:9000/test/test?dbName=order,
http://localhost:9000/test/test?dbName=order1,
http://localhost:9000/test/test?dbName=order2,
http://localhost:9000/test/test?dbName=order3
利用apipostget提交代码,dbName是用来做切换数据源的关键点,实际项目根据大家自己的获取数据源标识做应用,效果图如下
2020-05-13 22:30:37.854 INFO 17412 --- [nio-9000-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-05-13 22:30:37.854 INFO 17412 --- [nio-9000-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-05-13 22:30:37.863 INFO 17412 --- [nio-9000-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 9 ms
当前线程为:http-nio-9000-exec-1当前线程value/数据源名称为:null
当前线程:Thread[http-nio-9000-exec-1,5,main]使用名称为order数据源
使用数据库名order做操作
当前线程为:http-nio-9000-exec-2当前线程value/数据源名称为:null
当前线程:Thread[http-nio-9000-exec-2,5,main]使用名称为order1数据源
使用数据库名order1做操作
2020-05-13 22:30:41.221 INFO 17412 --- [nio-9000-exec-2] com.alibaba.druid.pool.DruidDataSource : {dataSource-2,order1} inited
当前线程为:http-nio-9000-exec-3当前线程value/数据源名称为:null
当前线程:Thread[http-nio-9000-exec-3,5,main]使用名称为order2数据源
使用数据库名order2做操作
2020-05-13 22:30:44.036 INFO 17412 --- [nio-9000-exec-3] com.alibaba.druid.pool.DruidDataSource : {dataSource-3,order2} inited
当前线程为:http-nio-9000-exec-5当前线程value/数据源名称为:null
当前线程:Thread[http-nio-9000-exec-5,5,main]使用名称为order3数据源
使用数据库名order3做操作
2020-05-13 22:30:47.337 INFO 17412 --- [nio-9000-exec-5] com.alibaba.druid.pool.DruidDataSource : {dataSource-4,order3} inited
1.yml文件配置
#######配置参数###########
server:
port: 9000
defaultdatasource:
# 使用druid数据源
type: com.alibaba.druid.pool.DruidDataSource
druid:
master:
#driver-class-name: com.mysql.cj.jdbc.Driver
name: order
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/order?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8
username: root
password: vincent
first:
#driver-class-name: com.mysql.cj.jdbc.Driver
name : order1
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/order1?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8
username: root
password: vincent
second:
#driver-class-name: com.mysql.cj.jdbc.Driver
name : order2
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/order2?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8
username: root
password: vincent
third:
name : order3
#driver-class-name: com.mysql.cj.jdbc.Driver
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/order3?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8
username: root
password: vincent
spring:
application:
name: testx
datasource:
initialization-mode: always
continue-on-error: true
#mybatis
mybatis-plus:
# configuration:mybatis.mapper-locations=classpath:mapper/*.xml
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:/mybatis/mappers/obj/*Mapper.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.vincent.testx.entity
global-config:
# 数据库相关配置
db-config:
#主键类型 AUTO:"数据库ID自增", INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
id-type: UUID
#字段策略 IGNORED:"忽略判断",NOT_NULL:"非 NULL 判断"),NOT_EMPTY:"非空判断"
field-strategy: not_empty
#驼峰下划线转换
column-underline: true
#数据库大写下划线转换
#capital-mode: true
# 逻辑删除配置
logic-delete-value: 0
logic-not-delete-value: 1
db-type: mysql
#刷新mapper 调试神器
refresh: true
2.配置文件设置数据源对象类
/**
* 数据源属性对象配置类
*/
@Component
@Data
@ConfigurationProperties(prefix = "defaultdatasource.druid")
public class DBProperties {
private DruidDataSource master;
private DruidDataSource first;
private DruidDataSource second;
private DruidDataSource third;
// private DruidDataSource masterSecond;
// private DruidDataSource numOne;
}
3.动态数据源初始化操作类
/**
* 数据源配置初始化操作类
*/
@Configuration
public class DynamicDataSourceConfig {
@Autowired
private DBProperties properties;
@Bean(name = "dataSource")
public DataSource dataSource() {
//按照目标数据源名称和目标数据源对象的映射存放在Map中
Map
4.动态数据源路由类
/**
* 动态路由数据源实现类
*/
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
// private Map
5.ThreadLocal操作类
/**
* 动态数据源持有者,负责利用ThreadLocal存取数据源名称
*/
public class DynamicDataSourceContextHolder {
/**
* 本地线程共享对象
*/
private static final ThreadLocal THREAD_LOCAL = new ThreadLocal<>();
/**
*数据源存放列表
*/
public static List dataSourceDbNos = new ArrayList<>();
/**
* 选择数据源
* @param dnName 数据源名称
*/
public static void setDataSource(String dnName) {
THREAD_LOCAL.set(dnName);
}
/**
*获取当前数据源
* @return
*/
public static String getDataSource() {
return THREAD_LOCAL.get();
}
/**
* 删除数据源
*/
public static void removeDataSource() {
THREAD_LOCAL.remove();
}
}
6.数据源控制类
/**
* 数据源控制类
*/
public class DynamicDataSourceSetUtil {
/**
* 存放数据源
* @param dbName
*/
public static void PutDataSourceIn(String dbName){
//手动生成新的数据源
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
if(dbName!=null&&!dbName.equals("")){
System.out.println("当前线程为:"+Thread.currentThread().getName()+
"当前线程value/数据源名称为:"+DynamicDataSourceContextHolder.getDataSource());
//如果当前线程本地缓存为空,则设置其数据源编号
if(DynamicDataSourceContextHolder.getDataSource()==null){
DynamicDataSourceContextHolder.setDataSource(dbName);
System.out.println("当前线程:"+Thread.currentThread()+"使用名称为"+DynamicDataSourceContextHolder.getDataSource()+"数据源");
}
else{
DynamicDataSourceContextHolder.setDataSource(dbName);
//选择当前数据库
System.out.println("已存在数据源连接,直接切换,切换的数据源为:"+DynamicDataSourceContextHolder.getDataSource());
}
}
}
}
7.AOP手动切入类
/**
* 手动aop方式切入方式
*/
@Aspect
@Order(-1)
@Component
public class DataSourceAspect {
//统一切入所有包下,所有后缀名为XXController的类的所有方法
@Pointcut("execution(* com.vincent.testx..*Controller.*(..))")
public void Pointcut() {
}
//在执行接口前
@Before("Pointcut()")
public void process(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
//方法名
String methodName=signature.getName();
// 类名
String serviceName = signature.getDeclaringTypeName();
// 参数名数组
String[] parameterNames = ((MethodSignature) signature).getParameterNames();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String dbName=request.getParameter("dbName");
//获取参数
DynamicDataSourceSetUtil.PutDataSourceIn(dbName);
}
//执行完切面后,将线程共享中的数据源名称清空
@After("Pointcut()")
public void after(JoinPoint joinPoint){
DynamicDataSourceContextHolder.removeDataSource();
}
}
8.spring上下文工具类
@Component
public class SpringContextUtil implements ApplicationContextAware {
// Spring应用上下文环境
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtil.applicationContext=applicationContext;
}
/**
* @return 获取上下文
*/
public static ApplicationContext getApplicationContext() {
checkApplicationContext();
return applicationContext;
}
/**
* 获取bean对象
*
* @param name
* @return Object
* @throws BeansException
*/
public static Object getBean(String name) throws BeansException {
return getApplicationContext().getBean(name);
}
private static void checkApplicationContext() {
if (applicationContext == null)
throw new IllegalStateException("applicaitonContext未注入,请在applicationContext.xml中定义SpringContextUtil");
}
}
9.user实体类
@Data
public class User {
private int id;
private String name;
private int age;
}
10.userMapper
@Mapper
public interface UserMapper extends BaseMapper {
}
11.userMapper.xml
12.测试用controller
@Controller
@RequestMapping("/test")
public class testController {
@Autowired
private UserMapper userMapper;
@RequestMapping(value = "/test",method = RequestMethod.GET)
public void test(@RequestParam("dbName")String dbName){
System.out.println("使用数据库名"+dbName+"做操作");
User user=new User();
user.setName("vincent_"+ new Random().nextInt(1000));
user.setAge(new Random().nextInt(30));
userMapper.insert(user);
}
}
13.pom文件
4.0.0
com.vincent
testx
0.0.1-SNAPSHOT
testx
Demo project for Spring Boot
1.8
UTF-8
UTF-8
1.1.10
Greenwich.RELEASE
com.vincent
spring_cloud_alibaba_dependence
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-aop
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-configuration-processor
true
mysql
mysql-connector-java
runtime
com.alibaba
druid
1.1.9
com.baomidou
mybatis-plus-boot-starter
3.0.1
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-maven-plugin
14.数据库创建测试脚本,我这里用的HeidiSql工具创建的脚本
-- --------------------------------------------------------
-- 主机: 127.0.0.1
-- 服务器版本: 5.7.30-log - MySQL Community Server (GPL)
-- 服务器OS: Win64
-- HeidiSQL 版本: 10.2.0.5599
-- --------------------------------------------------------
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET NAMES utf8 */;
/*!50503 SET NAMES utf8mb4 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
-- Dumping database structure for order
CREATE DATABASE IF NOT EXISTS `order` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `order`;
-- Dumping structure for table order.user
CREATE TABLE IF NOT EXISTS `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '',
`age` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;
-- Data exporting was unselected.
-- Dumping database structure for order1
CREATE DATABASE IF NOT EXISTS `order1` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `order1`;
-- Dumping structure for table order1.user
CREATE TABLE IF NOT EXISTS `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '',
`age` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
-- Data exporting was unselected.
-- Dumping database structure for order2
CREATE DATABASE IF NOT EXISTS `order2` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `order2`;
-- Dumping structure for table order2.user
CREATE TABLE IF NOT EXISTS `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '',
`age` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
-- Data exporting was unselected.
-- Dumping database structure for order3
CREATE DATABASE IF NOT EXISTS `order3` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `order3`;
-- Dumping structure for table order3.user
CREATE TABLE IF NOT EXISTS `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '',
`age` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- Data exporting was unselected.
/*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */;
/*!40014 SET FOREIGN_KEY_CHECKS=IF(@OLD_FOREIGN_KEY_CHECKS IS NULL, 1, @OLD_FOREIGN_KEY_CHECKS) */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;