场景:saas服务,不同的项目,使用同一个服务,不同的租户对应不同的库
数据库操作框架使用 nutz,连接池使用Druid
问题:需要根据请求不同租户的请求,相应不同的数据库,并且支持事务@Transactional
思路:1.使用ThreadLocal,维持多数据源的上下文
2.使用切面的方式切换上下文
3. 自定义AbstractRoutingDataSource的子类,持有数据库上下文的变量,根据当前数据库上下文返回需要的数据库
代码:
1.自定义AbstractRoutingDataSource的子类,并持有数据库上下文的变量
public class MultiDataSource extends AbstractRoutingDataSource {
//持有的数据库上下文,将在切面中设置当前请求的数据库上下文
private static ThreadLocal<String> datasourceHolder = new ThreadLocal();
@Override
protected Object determineCurrentLookupKey() {
return datasourceHolder.get();
}
public static void setDataSource(String dataSource){
datasourceHolder.set(dataSource);
}
public static String getDataSource(){
return datasourceHolder.get();
}
public static void remove(){
datasourceHolder.remove();
}
}
2.自定义切面,切点是所有的service包及子包,根据companyId,来构建不同的DataSource的key
@Slf4j
@Component
@Aspect
@Order(-1) //这个注解很重要,是个坑,跟了很久才决定加上
public class DataSourceSwitchAspect {
@Pointcut("execution(* com.logan.service..*.*(..))")
public void dataSourceSwitchPointCut(){
}
@Around("dataSourceSwitchPointCut()")
public Object before(ProceedingJoinPoint point) throws Throwable {
//设置DataSource
String companyId = HttpRequestUtils.getCurrentCompanyId();
if(StringUtils.isEmpty(companyId)){
MethodSignature ms = (MethodSignature) point.getSignature();
Method method = ms.getMethod();
//这里主要是处理在@Async的时候,MultiDataSource的数据源设置,只能通过参数获取
for (int i = 0; i < method.getParameters().length; i++) {
if (method.getParameters()[i].getName().equals(TO_COMPANY_ID)) {
Object toCompanyId = point.getArgs()[i];
if (toCompanyId != null) {
companyId = toCompanyId.toString();
}
break;
}
}
}
if(StringUtils.isNotEmpty(companyId)){
String dsName = String.format("ds%s",companyId);
MultiDataSource.setDataSource(dsName);
}
try{
return point.proceed();
}finally {
//最后一定要释放,否则MultiDataSource里面的对象会一直增长
MultiDataSource.remove();
}
}
}
3.定义注入多数据源配置信息
@Configuration
public class DruidConfiguration {
@Bean
public ServletRegistrationBean druidStatViewServle2(){
//org.springframework.boot.context.embedded.ServletRegistrationBean提供类的进行注册.
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(),"/druid/*");
//添加初始化参数:initParams
servletRegistrationBean.addInitParameter("loginUsername","account");
servletRegistrationBean.addInitParameter("loginPassword","123456");
//是否能够重置数据.
servletRegistrationBean.addInitParameter("resetEnable","false");
return servletRegistrationBean;
}
/**
* 注册一个:filterRegistrationBean
* @return
*/
@Bean
public FilterRegistrationBean druidStatFilter2(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
//添加过滤规则.
filterRegistrationBean.addUrlPatterns("/*");
//添加不需要忽略的格式信息.
filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid2/*");
return filterRegistrationBean;
}
@Bean
@ConfigurationProperties(prefix = "spring.datasources.ds1")
public DataSource ds1(){
DataSource dataSource = DataSourceBuilder.create().type(DruidDataSource.class).build();
return dataSource;
}
@Bean
@ConfigurationProperties(prefix = "spring.datasources.ds2")
public DataSource ds2(){
DataSource dataSource = DataSourceBuilder.create().type(DruidDataSource.class).build();
return dataSource;
}
@Bean
@ConfigurationProperties(prefix = "spring.datasources.ds3")
public DataSource ds3(){
DataSource dataSource = DataSourceBuilder.create().type(DruidDataSource.class).build();
return dataSource;
}
@Bean
@ConfigurationProperties(prefix = "spring.datasources.ds4")
public DataSource ds4(){
DataSource dataSource = DataSourceBuilder.create().type(DruidDataSource.class).build();
return dataSource;
}
@Bean
public DataSource dataSource(){
MultiDataSource dynamicRoutingDataSource = new MultiDataSource();
//配置多数据源
Map<Object, Object> dataSourceMap = new HashMap<>(2);
dataSourceMap.put("ds1", ds1());
dataSourceMap.put("ds2", ds2());
dataSourceMap.put("ds3", ds3());
dataSourceMap.put("ds4", ds4());
// 将 ds1 数据源作为默认指定的数据源
dynamicRoutingDataSource.setDefaultTargetDataSource(ds1());
dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
return dynamicRoutingDataSource;
}
@Bean
public PlatformTransactionManager txManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
4.使用的是nutz,要想将事务交个Spring托管,必须还要设置DaoRunner
/**
- 为了使得NutDao兼容Spring事务而设置的DaoRunner
*/
@Repository
public class SpringDaoRunnerForNutz implements DaoRunner {
@Override
public void run(DataSource dataSource, ConnCallback callback) {
Connection con = DataSourceUtils.getConnection(dataSource);
try {
callback.invoke(con);
}
catch (Exception e) {
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
} else {
throw new RuntimeException(e);
}
} finally {
DataSourceUtils.releaseConnection(con, dataSource);
}
}
}
使用样例:
@Service
public class TransactionTest {
@Autowired
private UserDODao userDODao;
@Transactional(rollbackFor = Exception.class,isolation = Isolation.READ_COMMITTED,propagation = Propagation.REQUIRED)
public void testTransaction(Integer i,String toCompanyId){
System.out.println(String.format("Async DateContext:%s",MultiDataSource.getDataSource()));
UserDO userDO = userDODao.fetchByUserName("aaa");
String name = userDO.getName();
userDO.setName(name+"1次修改");
System.out.println(String.format("修改1次:%s",userDO.getName()));
userDODao.update(userDO);
if(!i.equals(0)) {
int j = 1 / 0;
}
userDO.setName(name +"2次修改");
System.out.println(String.format("修改2次:%s",userDO.getName()));
userDODao.update(userDO);
userDO = userDODao.fetchByUserName("aaa");
if(userDO != null){
System.out.println(userDO.getName());
}
}
}
踩坑:
虽然我们通过切面去做数据源的上下文切换,但是发现并不起作用,在跟踪TransactionalManager的过程中发现自己定义的切面执行时机晚于@Transactional(我们知道Spring是通过切面去做事务管理的),所以我们需要人为的将切换数据源的切面提前,就有了咱们自定义切面里面增加的注解@Order(-1)