场景: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)