mybatis多数据源切换

1,803 阅读6分钟

最近碰到一个需求,需要根据接口得到订单号,在用订单号去他们的数据库去查询订单的具体信息,然后插入到我们数据库的需求。其中涉及到跨库的问题,涉及到分布式事务和动态数据源配置,这篇文章说一下动态数据源配置,分布式事务的问题一直都是一个老大难的问题,至今没有一个适合所有场景完美解决方案。等以后有空再写一篇分布式事务的。 从其他平台获取订单号

private List<Message> getNewOrder(PlatformApp app) {
        List<OrderMessage> result = messageIntegration.getOrderMessage(new messageQuery(messageTypes), app);
        return result.size()  > 0 ? result : null;
    }

通过订单号在sqlserver在该平台数据库查询

private OrderModel loadOrderFromYzwDb(Integer orderSysNo) {
        OrderSyncModel orderSyncModel = orderSyncMapper.selectBySysNo(orderSysNo);
        if (orderSyncModel == null) {
            LOGGER.error("从sqlserver加载订单数据为null,订单号:[{}]", orderSysNo);
            return null;
        }
        List<OrderDetailSyncModel> orderDetailSyncModels = orderSyncMapper.selectDetail(orderSysNo);
        orderSyncModel.setOrderDetails(orderDetailSyncModels);
        return orderSyncModel.toOrder();
    }

从sqlserver查询出的数据插入mysql

@Transactional(rollbackFor = Exception.class)
public OrderModel create(OrderModel orderModel) {
        orderMapper.insert(orderModel);
        orderModel.getOrderDetails().forEach(orderDetailModel -> {
            //详情
            orderDetailModel.setOrderId(orderModel.getId());
            orderDetailMapper.insert(orderDetailModel);
        });
        return orderModel;
 }

上面两个方法其实是放在一个方法中执行的,相信细心的同学已经发现问题,对就是mysql的数据源不能查询sqlserver的,这个时候就不仅仅需要动态多数据源的配置了,还需要用AOP的方式解决数据源切换的问题,下面来看看怎么配置多数据源

/**
 * 多数据元配置管理
 */
@Configuration
@EnableTransactionManagement
@MapperScan(basePackages = MultiDataSourceConfig.PACKAGE_NAME, sqlSessionTemplateRef = "sqlSessionTemplate")
public class MultiDataSourceConfig {

    protected static final String PACKAGE_NAME = "cn.yzw.superbox.swms.business.dal.mappers";

    /**
     * 创建Mysql DataSrouce数据源
     *
     * @return DataSource
     */
    @Bean("mysqlDataSource")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.druid.mysql")
    public DataSource dataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 创建SQL Server数据源
     *
     * @return DataSource
     */
    @Bean(name = "ecommerceDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.druid.sqlserver.ecommerce")
    public DataSource sqlServerDataSource3() {
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 动态数据源配置
     *
     * @param eCommerce       eCommerce
     * @param mysqlDataSource mysqlDataSource
     * @return DataSource
     */
    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource(@Qualifier("ecommerceDataSource") DataSource eCommerce, @Qualifier("mysqlDataSource") DataSource mysqlDataSource) {
        DynamicDataSource multipleDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        //把sqlserver数据源放到配置中
        targetDataSources.put(YzSqlServerConstant.ECOMMERCE, eCommerce);
         //mysql数据源
        targetDataSources.put(YzSqlServerConstant.SUPERBO, mysqlDataSource);
        //设置默认数据源
        multipleDataSource.setDefaultTargetDataSource(mysqlDataSource);
        //添加数据源
        multipleDataSource.setTargetDataSources(targetDataSources);
        return multipleDataSource;
    }

    /**
     * @param dataSource dataSource
     * @return SqlSessionFactory
     * @throws Exception Exception
     */
    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
        //创建sqlSession工厂
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        //设置数据源
        bean.setDataSource(dataSource);
        //添加mybatis配置文件
        bean.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("mybatis-config.xml"));
        //mapper文件所在
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/mappers/**.xml"));
        return bean.getObject();
    }

    /**
     * 替代线程不安全的defaultSqlSessionFactory
     *
     * @param sqlSessionFactory sqlSessionFactory
     * @return SqlSessionTemplate
     */
    @Bean(name = "sqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    /**
     * 数据源事物
     *
     * @param mysqlDataSource mysql 数据源
     * @return PlatformTransactionManager PlatformTransactionManager
     */
    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource mysqlDataSource) {
        return new DataSourceTransactionManager(mysqlDataSource);
    }
}

下面的mapper文件相信用过mybatis的都不陌生,但是上面的注解是干嘛用的呢,其实就是一个自定义注解,方便在利用springAOP特效时进行切入从而达到更换数据源的目的

public interface OrderSyncMapper {

    /**
     * 获取老商城订单
     * @param sysNo 订单编号
     * @return 订单
     */
    @YzSqlServerAnnotation(name = YzSqlServerConstant.ECOMMERCE)
    OrderSyncModel selectBySysNo(Integer sysNo);

    /**
     * 获取老商城订单明细
     * @param sysNo 订单编号
     * @return 订单明细
     */
    @YzSqlServerAnnotation(name = YzSqlServerConstant.ECOMMERCE)
    List<OrderDetailSyncModel> selectDetail(Integer sysNo);
}

我们来看看mybatis获取数据源的源码是怎么写的,实际上AbstractRoutingDataSource抽象类的一个方法determineTargetDataSource(),可以看到this.resolvedDataSources里面的数据源其实就是我们上边配置数据源的时候搞的multipleDataSource.setTargetDataSources(targetDataSources);afterPropertiesSet()方法里换了个名字罢了

protected DataSource determineTargetDataSource() {
                //数据源为空时报出业务异常
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
                //从抽象方法中获取key,抽象方法需自己实现
		Object lookupKey = determineCurrentLookupKey();
                //通过key取出数据源
		DataSource dataSource = this.resolvedDataSources.get(lookupKey);
                //取不到的时候就是用默认数据源配置,上面已经配置过默认数据源了
		if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
			dataSource = this.resolvedDefaultDataSource;
		}
                //还没有数据源,那就抛异常吧
		if (dataSource == null) {
			throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
		}
		return dataSource;
	}

//数据源转换
public void afterPropertiesSet() {
		if (this.targetDataSources == null) {
			throw new IllegalArgumentException("Property 'targetDataSources' is required");
		}
		this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
		this.targetDataSources.forEach((key, value) -> {
			Object lookupKey = resolveSpecifiedLookupKey(key);
			DataSource dataSource = resolveSpecifiedDataSource(value);
			this.resolvedDataSources.put(lookupKey, dataSource);
		});
		if (this.defaultTargetDataSource != null) {
			this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
		}
	}

这里还有个关键步骤没有提到,在看看看配置数据源那个方法,DynamicDataSource这个类其实就上面说的抽象类AbstractRoutingDataSource的子类,子类当然可以用父类的公共方法

@Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource(@Qualifier("ecommerceDataSource") DataSource eCommerce, @Qualifier("mysqlDataSource") DataSource mysqlDataSource) {
        DynamicDataSource multipleDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(YzSqlServerConstant.ECOMMERCE, eCommerce);
        targetDataSources.put(YzSqlServerConstant.SUPERBO, mysqlDataSource);
        //设置默认数据源
        multipleDataSource.setDefaultTargetDataSource(mysqlDataSource);
        //添加数据源
        multipleDataSource.setTargetDataSources(targetDataSources);
        return multipleDataSource;
    }

现在继续回到determineTargetDataSource()方法上来Object lookupKey = determineCurrentLookupKey();这是一个抽象方法具体实现还需自己手动写,因为这个方法主要是取出数据源的key,获得了key才能获取相应的数据源,获取key的逻辑被你控制了也就控制了获取数据源的方法,下面是重写的determineCurrentLookupKey()方法。

public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * 动态路由数据库
     *
     * @return Object
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceHolder.getDataSource();
    }
}

这里是写了一个DataSourceHolder类来获取的key,内部还是用的ThreadLocal来保证每个线程属于自己的值不至在多线程并发环境下线程的数据源切来切去的问题。

public class DataSourceHolder {

    private static final ThreadLocal<String> HOLDER = new ThreadLocal<String>() {
        @Override
        protected String initialValue() {
            HOLDER.set(YzSqlServerConstant.PRODUCT);
            return HOLDER.get();
        }
    };

    public static void setDataSource(String name) {
        HOLDER.set(name);
    }
    
    public static String getDataSource() {
        return HOLDER.get();
    }
    
    public static void destroy() {
        HOLDER.remove();
    }
    
    public static boolean contain(String name) {
        return name != null && name.equals(HOLDER.get());
    }
}

现在配置都搞好了,现在到了springAOP出场的时候了,不得不说AOP真是强大。对@YzSqlServerAnnotation注解的方法进行拦截切入,方式执行之前替换成sqlserver的数据源,结束后释放。sqlSessionFactory找不到数据源的时候会使用默认的数据源,前面已经设置了mysql成默认数据源

@Aspect
@Order(-1) // 该AOP在@Transactional之前执行
@Component
public class DataSourceAspect {
    private static final Logger LOGGER = LogManager.getLogger(DataSourceAspect.class);
    //设置切点,这里的切入点就是打了@YzSqlServerAnnotation的方法就进行切入
    @Pointcut(value = "@annotation(cn.yzw.superbox.swms.business.constant.YzSqlServerAnnotation)")
    public void pointcut() {
    }

    //AOP前置方法
    @Before(value = "pointcut()")
    public void before(JoinPoint joinPoint) {
        //获得注解
        YzSqlServerAnnotation yzSqlServerAnnotation = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(YzSqlServerAnnotation.class);
        //获得注解上的值,也就是数据源的key值
        String dbName = yzSqlServerAnnotation.name();
        if (!StringUtils.isEmpty(dbName)) {
        //不为空就把数据源的key放到holder里面,这样就可以通过key查找数据源了
            DataSourceHolder.setDataSource(dbName);
        } else {
            LOGGER.error("dbName is null");
        }
    }

    //这个执行完sqlserver的方法就可以移除掉了,防止遇到mysql方法还用sqlserver的数据源,
    //因为mysql已经设置了默认数据源了不需要AOP方式来切换
    @After(value = ("pointcut()"), argNames = "joinPoint")
    public void doAfterAdvice(JoinPoint joinPoint) {
        DataSourceHolder.destroy();
    }

}

这里面还有个问题,就是这个AOP的执行顺序必须在@Transactional之前 ,如果在之后举个栗子spring事务管理器'PlatformTransactionManager'现在拿到的sqlserver的数据源,现在遇到mysql一个操作需要开启事务,因为我们写的切面逻辑执行顺序事务切面之后,所以就没法吧PlatformTransactionManager的数据源进行替换。啪的一下就会异常。 ##总结 AbstractRoutingDataSource负责获取数据源,逻辑是执行determineCurrentLookupKey()方法找到key取出响应的数据源,如果没找到就使用默认的数据源。而自己可以重写该方法达到控制取出数据源的逻辑,也就实现了数据源间的切换。又因为因为holder是用TheadLocal来实现了TheadLocal内部结构又是一个map,map的key用的当前线程id做的key所以确保了每一个线程可以拥有自己需要的数据源,在多线程环境也不会出现数据源混用的问题。在实际项目中其实用了七八个不同的数据源,但是实现方式都是不变的。