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