SpringBoot动态数据源与@Transactional

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

上一篇:@Transactional注解


下一篇:问题汇总