摘要:本文主要介绍在多租户环境下的数据源切换方案,该方案和springboot
提供的方案类似,但是这种方案更适合动态扩展,且在springboot
视角下永远只有一个DataSource
,在实际获取connection
的时候才进行动态更换数据源,更简单。
实现
案例基于
Springboot3
的版本开发,也兼容其他springboot
版本。
pom.xml
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
</dependencies>
application.yml
配置
yaml
multi:
datasource:
tenant1:
tenant-id: 1
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.8.134:30635/test_1?useSSL=false&serverTimezone=UTC
username: super_admin
password: super_admin
tenant2:
tenant-id: 2
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.8.134:30635/test_2?useSSL=false&serverTimezone=UTC
username: super_admin
password: super_admin
MultiDataSourceProperties
简单的属性配置类
arduino
@Data
public class MultiDataSourceProperties {
/** 租户id */
private String tenantId;
/** 数据库驱动名称 */
private String driverClassName;
/** */
private String jdbcUrl;
/** */
private String username;
/** */
private String password;
}
MultiDataSource
实现的数据源接口
其实就是做了一层代理,让获取的数据源根据上下文动态切换。
java
public class MultiDataSource implements DataSource {
private final MultiDataSourceSelector multiDatasourceSelector;
public MultiDataSource(MultiDataSourceSelector multiDatasourceSelector) {
this.multiDatasourceSelector = multiDatasourceSelector;
}
@Override
public Connection getConnection() throws SQLException {
return multiDatasourceSelector.getCurrentDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return multiDatasourceSelector.getCurrentDataSource().getConnection(username, password);
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return multiDatasourceSelector.getCurrentDataSource().getLogWriter();
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
multiDatasourceSelector.getCurrentDataSource().setLogWriter(out);
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
multiDatasourceSelector.getCurrentDataSource().setLoginTimeout(seconds);
}
@Override
public int getLoginTimeout() throws SQLException {
return multiDatasourceSelector.getCurrentDataSource().getLoginTimeout();
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return multiDatasourceSelector.getCurrentDataSource().getParentLogger();
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return multiDatasourceSelector.getCurrentDataSource().unwrap(iface);
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return multiDatasourceSelector.getCurrentDataSource().isWrapperFor(iface);
}
}
MultiDataSourceConfig
配置
这里只需要自己定义一个
DataSource
即可,其他的都交给Spring
管理
typescript
@Configuration
public class MultiDataSourceConfig {
@ConfigurationProperties(prefix = "multi.datasource.tenant1")
@Bean
public MultiDataSourceProperties multiDataSourceProperties1(){
return new MultiDataSourceProperties();
}
@ConfigurationProperties(prefix = "multi.datasource.tenant2")
@Bean
public MultiDataSourceProperties multiDataSourceProperties2(){
return new MultiDataSourceProperties();
}
@Bean
public DataSource dataSource(List<MultiDataSourceProperties> multiDataSourceProperties){
return new MultiDataSource(new MultiDataSourceSelector(multiDataSourceProperties));
}
}
MultiDataSourceSelector
实际的动态数据源构造类,这里可以做成动态的,我只是给了静态的案例。
scss
public class MultiDataSourceSelector {
/**
* 数据源配置列表
*/
private List<MultiDataSourceProperties> multiDataSourcePropertiesList;
private final Map<String, HikariDataSource> DATA_SOURCE_MAP = new ConcurrentHashMap<>();
public MultiDataSourceSelector(List<MultiDataSourceProperties> multiDataSourcePropertiesList) {
this.multiDataSourcePropertiesList = multiDataSourcePropertiesList;
}
public DataSource getCurrentDataSource(){
String tenantId = MultiTenantContextHolder.getTenantId();
if(DATA_SOURCE_MAP.containsKey(tenantId)){
return DATA_SOURCE_MAP.get(tenantId);
}
// TODO 这里如果是动态数据源,就可以写为调用接口的方式,
return DATA_SOURCE_MAP.computeIfAbsent(tenantId, (key) -> {
MultiDataSourceProperties multiDataSourceProperties = multiDataSourcePropertiesList.stream().filter(v -> v.getTenantId().equals(tenantId)).findFirst().orElse(null);
if(null == multiDataSourceProperties){
throw new RuntimeException("not support tenant : " + tenantId);
}
return new HikariDataSource(buildHikariConfig(multiDataSourceProperties));
});
}
/**
* 构建hikari 连接配置信息
* @param properties
* @return
*/
private HikariConfig buildHikariConfig(MultiDataSourceProperties properties){
HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setDriverClassName(properties.getDriverClassName());
hikariConfig.setJdbcUrl(properties.getJdbcUrl());
hikariConfig.setUsername(properties.getUsername());
hikariConfig.setPassword(properties.getPassword());
hikariConfig.setPoolName("tenant-" + properties.getTenantId());
return hikariConfig;
}
}
MultiTenantContextHolder
租户上下文类,用于切换租户用,建议切换为阿里的线程池。
typescript
public class MultiTenantContextHolder {
public static final ThreadLocal<String> TENANT_ID_CONTEXT = new ThreadLocal<>();
public static void setTenantId(String tenantId){
TENANT_ID_CONTEXT.set(tenantId);
}
public static String getTenantId(){
return TENANT_ID_CONTEXT.get();
}
public static void clearTenantId(){
TENANT_ID_CONTEXT.remove();
}
}
测试案例
TestEntity
less
@Data
@TableName("test")
public class TestEntity {
private Long id;
private String name;
private Date createTime;
}
TestMapper
java
@Mapper
public interface TestMapper extends BaseMapper<TestEntity> {
}
TestService
ini
@Service
public class TestService {
@Autowired
private TestMapper testMapper;
@Transactional
public void tranSave(){
for (int i = 0; i < 3; i++) {
TestEntity testEntity = new TestEntity();
testEntity.setId(IdWorker.getId());
testEntity.setName(Thread.currentThread().getName() + "循环次数" + i);
testEntity.setCreateTime(new Date());
testMapper.insert(testEntity);
if(i==2){
int a= 1/0;
}
}
}
public void noSave(){
for (int i = 0; i < 3; i++) {
TestEntity testEntity = new TestEntity();
testEntity.setId(IdWorker.getId());
testEntity.setName(Thread.currentThread().getName() + "循环次数" + i);
testEntity.setCreateTime(new Date());
testMapper.insert(testEntity);
if(i==2){
int a= 1/0;
}
}
}
}
TestController
租户上下文的设置,建议写在
filter
上,下面只是做案例
typescript
@Slf4j
@RestController
@RequestMapping(value = "test")
public class TestController {
@Autowired
private TestMapper testMapper;
@Autowired
private TestService testService;
@GetMapping("kua")
public Object kua(){
MultiTenantContextHolder.setTenantId("1");
TestEntity testEntity = testMapper.selectById(1);
log.info("查询的数据={}",testEntity);
Thread thread = new Thread(() -> {
MultiTenantContextHolder.setTenantId("2");
TestEntity testEntity2 = testMapper.selectById(1);
log.info("查询的数据2={}",testEntity2);
});
thread.start();
testEntity = testMapper.selectById(1);
log.info("查询的数据1={}",testEntity);
return "success";
}
@GetMapping("abc")
public Object abc(String tenantId){
MultiTenantContextHolder.setTenantId(tenantId);
TestEntity testEntity = testMapper.selectById(1);
log.info("查询的数据={}",testEntity);
return "success";
}
@GetMapping("trans")
public Object trans(String tenantId){
MultiTenantContextHolder.setTenantId(tenantId);
testService.tranSave();
return "success";
}
@GetMapping("noSave")
public Object noSave(String tenantId){
MultiTenantContextHolder.setTenantId(tenantId);
testService.noSave();
return "success";
}
}