springboot+dynamic-datasource realizes dynamic switching of multiple data sources, non-annotation
I. Introduction
Recently, I have been analyzing the functions of the SaaS platform多租户
, which must involve the functions of the database. Multi-tenant design solutions must consider tenant isolation data and tenant shared data. Shared data is easy to implement, but isolated data is relatively complicated. Generally, isolation and scalability must be considered . , tenant cost and operation and maintenance complexity ;
usually there are three main solutions for SaaS multi-tenant data storage:
- Independent database: one database per tenant.
- Shared database, isolated data schema: multiple or all tenants share
database
, buttenant
differschema
. - Shared database, shared data architecture: tenants share one
database
, one , and distinguish tenants' dataschema
in the table .tenantID
Note: Since multi-data functions are involved in daily work, today we share the first solution, which is not only suitable for multi-tenant application scenarios;
2. Plan ideas
Details:
1) The tenant database connection information is queried from the tenant management database;
2) The corresponding tenant database information is dynamically added and deleted according to the tenant configuration; 3) The database connection used is determined
based on the fields in the tenant request header ;clientId
非@DS注解方式
3. Code implementation
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>
Note: Using dynamic-datasource
the function of implementing multiple data sources, in dynamic-datasource
the usage samples, it is more about using @DS注解
the implementation. Today's sample sharing uses aop to dynamically switch data sources
Controller.java
@RestController
@AllArgsConstructor
public class TestController {
private final UserService userService;
@GetMapping("/test/list")
public List<User> getUserList(){
return userService.queryAll();
}
}
Test functional interface, beans and dao are no longer examples
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;
}
}
}
constant configuration
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;
}
Data source information table
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;
}
};
}
}
Query all data source information, load each database connection
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==");
}
}
Use aop to use different database connections based on the clientId data in the request header
Request 1
Request 2