springboot+dynamic-datasource实现多数据源动态切换,非注解
- 一、前言
- 二、方案思路
- 三、代码实现
一、前言
最近在分析SaaS平台多租户
的功能,必然涉及数据库部分的功能,多租户的设计方案要考虑租户隔离数据和租户共享数据,共享数据好实现,但是隔离数据相对复杂一些,一般要考虑隔离性、扩展性、租户成本和运维复杂性;
通常SaaS多租户在数据存储上存在三种主要的方案:
- 独立数据库:一个租户一个数据库。
- 共享数据库,隔离数据架构:多个或所有租户共享
database
,但不同的tenant
和schema
。 - 共享数据库,共享数据架构:租户共享一个
database
、一个schema
,在表中通过tenantID
区分租户的数据。
注:由于平时工作中会涉及到多数据的功能,所以今天我们分享第一种方案,不只是适用于多租户的应用场景;
二、方案思路
详情:
1)租户数据库连接信息是从租户管理数据库中查询到的;
2)根据租户配置动态的新增、删除对应租户数据库信息;
3)根据租户请求头中的clientId
字段判断使用的数据库连接,非@DS注解方式
;
三、代码实现
pom.xml
<!--mybatis-plus的springboot支持-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
注:使用dynamic-datasource
实现多数据源功能,在dynamic-datasource
的使用样例中更多的是使用@DS注解
实现,今天样例分享使用aop动态切换数据源
Controller.java
@RestController
@AllArgsConstructor
public class TestController {
private final UserService userService;
@GetMapping("/test/list")
public List<User> getUserList(){
return userService.queryAll();
}
}
测试功能接口,bean和dao不再示例
application.yml
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启sql日志
map-underscore-to-camel-case: true
# 该配置就是将带有下划线的表字段映射为驼峰格式的实体类属性
spring:
# 配置时区
jackson:
time-zone: GMT+8
# 数据源相关配置
datasource:
dynamic:
# 主数据源
primary: db1
datasource:
# 数据源1
db1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8
username: root
password: admin_123
# druid 全局配置
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 30000
validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: true
test-on-return: false
server:
port: 8089
SourceConstants.java
package com.sk.common;
public class SourceConstants {
/** 数据源查询sql */
public static final String SELECT_SOURCE = "select s.slave, s.username, s.password, s.url, s.driver_class_name from te_source s where s.status = 0 and s.del_flag = 0";
/** 数据源字段 */
public enum Details {
SLAVE("slave", "数据源编码"),
USERNAME("username", "用户名"),
PASSWORD("password", "密码"),
URL_PREPEND("url_prepend", "连接地址"),
URL("url", "连接地址"),
URL_APPEND("url_append", "连接参数"),
DRIVER_CLASS_NAME("driver_class_name", "驱动");
private final String code;
private final String info;
Details(String code, String info) {
this.code = code;
this.info = info;
}
public String getCode() {
return code;
}
public String getInfo() {
return info;
}
}
}
常量配置
SourceProperties.java
@Data
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.db1")
public class SourceProperties {
/** 数据源驱动 */
private String driverClassName;
/** 数据源路径 */
private String url;
/** 数据源账号 */
private String username;
/** 数据源密码 */
private String password;
}
数据源信息表
DROP TABLE IF EXISTS `te_source`;
CREATE TABLE `te_source` (
`slave` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`driver_class_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`status` int(255) NULL DEFAULT NULL,
`del_flag` int(255) NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of te_source
-- ----------------------------
INSERT INTO `te_source` VALUES ('db2', 'root', 'admin_123', 'jdbc:mysql://localhost:3306/test02?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8', 'com.mysql.cj.jdbc.Driver', 0, 0);
INSERT INTO `te_source` VALUES ('db3', 'root', 'admin_123', 'jdbc:mysql://localhost:3306/test03?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8', 'com.mysql.cj.jdbc.Driver', 0, 0);
DynamicDataSourceLoading.java
/**
* 子数据源加载
*
* @author xueyi
*/
@Configuration
@AllArgsConstructor
public class DynamicDataSourceLoading {
private final SourceProperties sourceProperties;
@Bean
public DynamicDataSourceProvider jdbcDynamicDataSourceProvider() {
return new AbstractJdbcDataSourceProvider(sourceProperties.getDriverClassName(), sourceProperties.getUrl(), sourceProperties.getUsername(), sourceProperties.getPassword()) {
@Override
protected Map<String, DataSourceProperty> executeStmt(Statement statement) throws SQLException {
ResultSet rs = statement.executeQuery(SourceConstants.SELECT_SOURCE);
Map<String, DataSourceProperty> map = new HashMap<>();
while (rs.next()) {
String name = rs.getString(Details.SLAVE.getCode());
String username = rs.getString(Details.USERNAME.getCode());
String password = rs.getString(Details.PASSWORD.getCode());
//String url = rs.getString(Details.URL_PREPEND.getCode()).concat(rs.getString(Details.URL_APPEND.getCode()));
String url = rs.getString(Details.URL.getCode());
String driver = rs.getString(Details.DRIVER_CLASS_NAME.getCode());
DataSourceProperty property = new DataSourceProperty();
property.setUsername(username);
property.setPassword(password);
property.setUrl(url);
property.setDriverClassName(driver);
map.put(name, property);
}
return map;
}
};
}
}
查询所有数据源信息,加载各数据库连接
TransformDataSource.java
@Log4j2
@Aspect // FOR AOP
@Configuration // 配置类
public class TransformDataSource {
@Pointcut("execution( * com.sk.controller..*.*(..))")
/**
* 这个方法的方法名要和下面注解方法名一致
*/
public void doPointcut() {
}
@Before("doPointcut()")
public void doBefore(JoinPoint joinPoint) {
// 请求开始时间
ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String headValue = sra.getRequest().getHeader("clientId");
log.info("--------------clientId:{}", headValue);
DynamicDataSourceContextHolder.poll();
DynamicDataSourceContextHolder.push(headValue);
}
@After("doPointcut()")
public void doAfter() {
System.out.println("==doAfter==");
}
}
使用aop根据请求头中的clientId数据使用不同的数据库连接
请求1
请求2