项目背景
项目中SaaS服务需要提供多租户基础功能,通过访问域名区分不同的客户,进而隔离数据源,即一个租户一个数据库。
AbstractRoutingDataSource
spring中对切换数据源提供了动态设置方法,通过determineCurrentLookupKey()设置值切换对应数据源。
org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource 源码的介绍:
基于AbstractRoutingDataSource的多数据源动态切换,可以实现读写分离,但是注意无法动态的增加数据源,只能在项目启动时加载。
实现逻辑
- 定义DynamicDataSource类继承抽象类AbstractRoutingDataSource,并实现了determineCurrentLookupKey()方法。
- 启动时加载多个数据源,并配置到AbstractRoutingDataSource的defaultTargetDataSource和targetDataSources中。
- 对所有controller方法做aop,根据当前域名或者前端设置值修改本地线程动态数据源名称。
新建类 DynamicDataSource
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 动态获取DataSource
*
* @author plsy
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.getDataSource();
}
}
通过基于本地线程的上下文管理来切换数据源
import org.apache.log4j.Logger;
import java.util.LinkedList;
/**
* 动态数据源上下文管理
*
* @author plsy
*/
public class DynamicDataSourceContextHolder {
static Logger logger = Logger.getLogger(DynamicDataSourceContextHolder.class);
/**
* 存放当前线程使用的数据源类型信息
*/
private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
/**
* 数据源使用顺序标识
*/
public static LinkedList<String> dataSourceIds = new LinkedList<>();
/**
* 设置数据源
*
* @param dataSourceName
*/
public static void setDataSource(String dataSourceName) {
contextHolder.set(dataSourceName);
dataSourceIds.add(dataSourceName);
}
/**
* 获取数据源
*/
public static String getDataSource() {
if (contextHolder.get() == null) {
logger.debug("数据源标识为空,使用默认的数据源");
} else {
logger.debug("使用数据源:" + contextHolder.get() + " 如果数据源不存在将使用默认数据源.");
}
return contextHolder.get();
}
/**
* 清除数据源
*/
public static void clearDataSource() {
contextHolder.remove();
dataSourceIds.clear();
}
/**
* 返回上一次使用的数据源
*
* @return
*/
public static void returnDataSource() {
dataSourceIds.removeLast();
setDataSource(dataSourceIds.getLast());
}
}
初始化数据源
package com.gsoft.cos3.datasource;
import com.zaxxer.hikari.util.DriverDataSource;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
@Configuration
public class DataSourceConfig {
private Logger logger = Logger.getLogger(DataSourceConfig.class);
/**
* 自定义数据源
*/
public Map<Object, Object> customDataSources = new HashMap<>();
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Value("${spring.datasource.driver-class-name}")
private String driver;
private static final List<String> custNames = new ArrayList<>();
@Primary
@RefreshScope
@Bean(name = "datasource")
public DynamicDataSource dynamicDataSource() {
logger.info("=====初始化动态数据源=====");
Properties properties = new Properties();
DataSource dataSource = new DriverDataSource(url, driver, properties, username, password);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
// 默认数据源
dynamicDataSource.setDefaultTargetDataSource(dataSource);
// 配置多数据源
Map<Object, Object> dsMap = new HashMap<Object, Object>();
Map<Object, Object> customDataSources = new HashMap<>();
Connection connection = null;
try {
connection = dataSource.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement("select C_CODE,C_DSURL,C_DSDRIVERCLASSNAME,C_DSUSERNAME,C_DSPASSWORD from COS_SAAS_CUSTOMER");
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
String name = resultSet.getString(1);
custNames.add(name);
dsMap.put("url", resultSet.getString(2));
dsMap.put("driver", resultSet.getString(3));
dsMap.put("username", resultSet.getString(4));
dsMap.put("password", resultSet.getString(5));
DataSource ds = buildDataSource(dsMap);
customDataSources.put(name, ds);
}
} catch (SQLException e) {
e.printStackTrace();
}
dynamicDataSource.setTargetDataSources(customDataSources);
this.customDataSources = customDataSources;
logger.info("已加载租户库数据源" + custNames);
return dynamicDataSource;
}
@Primary
@Bean(name = "jdbcTemplate")
public JdbcTemplate jdbcTemplate(@Qualifier("datasource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
public static DataSource buildDataSource(Map<Object, Object> dataSourceMap) {
String driverClassName = dataSourceMap.get("driver").toString();
String url = dataSourceMap.get("url").toString();
String username = dataSourceMap.get("username").toString();
String password = dataSourceMap.get("password").toString();
// hikari配置参数 目前使用默认设置
Properties properties = new Properties();
DriverDataSource driverDataSource = new DriverDataSource(url, driverClassName, properties, username, password);
return driverDataSource;
}
}
AOP
import com.gsoft.cos3.util.Assert;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* DataSourceAspect
*
* @author plsy
*/
@Component
@Aspect
@Order(-1)
public class DataSourceAspect {
Logger logger = LoggerFactory.getLogger(getClass());
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
private void cutController() {
}
@Before("cutController()")
public void before(JoinPoint joinPoint) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 租户标识
String sign = request.getHeader("Site-info");
if (Assert.isNotEmpty(sign)) {
DynamicDataSourceContextHolder.setDataSource(sign);
} else {
DynamicDataSourceContextHolder.clearDataSource();
}
}
}
前端设置切换数据源标识header,切面取值设置到DynamicDataSourceContextHolder中,租户标识与数据源初始化时设置的name相同。