Java多数据源动态增加及切换

前言

从事餐饮软件项目,作为阿里本地生活的一份子,从19年下半年开始阿里口碑一批P6 P7开发人员陆续加入开发,此处的话题是一个餐饮pos机项目上的问题。每个餐饮门店都有一台或多台pos机,主要架构上是C++语言作为前端展示,Java开发的jar包程序作为其后端进行业务处理,pos可以有多台而Java后台只有一个,考虑到轻量,选用了sqlite数据库
sqlite每个数据库都是以单个文件的形式存在,每个文件都是一个数据库,文件不宜过大,我们根据业务含义将数据库划分成了4个业务文件,请注意这里不是分库分表,每个文件中表结构都不同,那项目在运行过程中读取不同数据库就需要做到无缝切换,不仅如此,这些数据库文件是有版本的,是可以不段生成的,当新版本的数据库文件生成时我们就需要连接新版本的数据库文件,即动态增加数据源的连接

前提

springBoot框架环境

项目里的多数据源含义

一个项目里,可以同时访问多个不同的数据库

实现

数据源切换的一般实现(推荐)

  1. 可以直接用原生的jdbc DriverManager. getConnection直接连接,但这种方式没有连接池且对于我们来说需要使用mybatis框架
  2. 将存放多个数据源的map(key是自定义的数据源id,value是java DataSource的实体)给到spring提供的AbstractRoutingDataSource并指定默认数据源(可以在determineCurrentLookupKey获取不到数据源时使用默认的数据源)
  3. 继承spring的AbstractRoutingDataSource接口并重写determineCurrentLookupKey方法,通过threadLocal获取当前线程的数据源id
  4. 项目启动时SqlSessionFactory的dataSource指定为重写的AbstractRoutingDataSource业务代码中手动指定当前线程threadLocal的数据源id

两个问题

  1. 不同数据源的事务不能同时回滚
  2. 无法动态的新增数据源

解决方案

如何自动切换

  1. 每个数据源都配置一套SqlSessionFactory,每个SqlSessionFactory都指定自己的mapper
  2. 不用手动指定数据源,但是SqlSessionFactory有多个

每个数据源的配置可以参考下面示例

@Configuration
@PropertySource("classpath:/dataSourceConfig.properties")
@MapperScan(basePackages = "com.alsc.basic.offline.sdk.dao.mapper.record",
    sqlSessionTemplateRef = "recordSqlSessionTemplate")
@Slf4j
public class MyBatisConfigRecord {

    @Autowired
    DataSourcePathService dataSourcePathService;

    /**
     * 加载数据源配置
     */

    @Bean(name = "recordDataSourceProperties")
    @ConfigurationProperties("spring.datasource.record")
    public DataSourceProperties recordDataSourceProperties() {
        return new DataSourceProperties();
    }

    /**
     * 加载数据源
     */

    @Bean(name = "recordDataSource")
    HikariDataSource recordDataSource(
        @Qualifier("recordDataSourceProperties") DataSourceProperties dataSourceProperties) {
        String dataSourceAbsolutePath = ""; //这里你要自己定义路径
        String url = "jdbc:sqlite:" + dataSourceAbsolutePath;
        dataSourceProperties.setUrl(url);
        return dataSourceProperties.initializeDataSourceBuilder()
            .type(HikariDataSource.class)
            .build();
    }

    @Bean(name = "recordSessionFactory")
    public SqlSessionFactory recordSessionFactory(@Qualifier("recordDataSource") DataSource dataSource)
        throws Exception {
        return getSqlSessionFactory(dataSource, new String[] {"classpath*:mapper/record/*.xml"});
    }

    @Bean(name = "recordSqlSessionTemplate")
    public SqlSessionTemplate recordSqlSessionTemplate(
        @Qualifier("recordSessionFactory") SqlSessionFactory recordSessionFactory)
        throws Exception {
        return new SqlSessionTemplate(recordSessionFactory);
    }

    private SqlSessionFactory getSqlSessionFactory(DataSource dataSource, String[] mapperLocations) throws Exception {
        MybatisProperties properties = new MybatisProperties();
        properties.setMapperLocations(mapperLocations);
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        bean.setMapperLocations(properties.resolveMapperLocations());
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        bean.setConfiguration(configuration);
        return bean.getObject();
    }

}

如何动态替换

问题:当前数据库存在版本问题,数据库名称相同但存放在不同的版本目录下,需将当前版本的数据源切换url指向另一个版本
解决:重新生成一个DataSource对象和Environment对象指定给SqlSessionFactory的Configuration

示例代码如下

  private void changeDataSource(String version, SqlSessionFactory sessionFactory, DataSourceIdEnum dataSourceIdEnum,
        DataSourceProperties properties) {
        String dataSourceAbsolutePath = dataSourcePathService.getDataSourceAbsolutePath(dataSourceIdEnum);
        String url = "jdbc:sqlite:" + dataSourceAbsolutePath;
        properties.setUrl(url);
        HikariDataSource hikariDataSource =
            properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        DataSource oldDataSource = sessionFactory.getConfiguration().getEnvironment().getDataSource();
        /**
         * 此处environment属性id,目前了解的主要作用在mybatis的一级缓存
         * 生成CacheKey会用到,即使所有的id都设置相同,生成的CacheKey可能相同
         * 由于不同数据源走不同sqlSession,不会有影响
         */
        Environment environment =
            new Environment(dataSourceIdEnum.getCode() + version, new JdbcTransactionFactory(), hikariDataSource);
        sessionFactory.getConfiguration().setEnvironment(environment);
        closeDataSource(oldDataSource);
    }

SqlSessionFactory的Configuration就是mybatis的配置信息,我们常用的配置如:下划线驼峰命名转换,新增记录返回自动生成的主键等都在这里
此处没有使用事务

事务同步回滚

基本思路

我们可以获取事务方法中所有需要回滚的DataSourceTransactionManager,依次手动回滚

Mybatis手动事务回滚的一般代码实现
            DefaultTransactionDefinition def = new DefaultTransactionDefinition();
            def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
            TransactionStatus transactionStatus = transactionManager.getTransaction(def);
            transactionManager.rollback(transactionStatus);
问题

真正在实现这个demo的时候发现,在第二个数据源的事务回滚时会报错:Transaction synchronization is not active() (事务同步没有被激活)

分析
  1. 事务同步的使用一般是用TransactionSynchronizationManager.registerSynchronization向事务管理器中注册一个实现了TransactionSynchronization接口的类,该接口里提供了围绕事务执行的前置和后置增强方法
  2. Spring中一个线程中的多个事务应该是按照嵌套事务来设计的
  3. 上面的transactionManager.getTransaction执行时,Spring源码中会执行一个startTransaction方法,里面就开始一个事务,会判断ThreadLocal<Set> synchronizations是否为空,不为空就会生成一个挂起资源,第一个事务的挂起资源为空
  4. 在startTransaction中会给synchronizations赋值,即激活事务同步,也就是每个事务开始后都会初始化synchronizations,但下一个事务会读取当前线程的synchronizations并挂起,当做嵌套的事务
    因此当我们执行完第一个事务的回滚后,由于第一个事务没有事务同步的挂起资源,代码会将该事务当做内嵌事务的最外层来处理,会清空synchronizations即关闭事务同步,再执行第二个事务的回滚,由于当前线程的synchronizations已被清空,所以会抛出事务同步没有激活的异常
解决
  1. 获取jdbc的connection回滚
@Aspect
@Component
@Slf4j
public class MultiDataSourceTransactionAspectConnection {

    /**
     * 多数据源组合事务
     * */
    @Pointcut("@annotation(com.alsc.basic.offline.sdk.dao.annotation.TransactionalGroup)")
    public void transactionalGroupAspect() {};

    @Around(value = "transactionalGroupAspect()")
    public Object transactionalGroupAspectAround(ProceedingJoinPoint pjp) throws Throwable {
        log.info("[多数据源组合事务切面]: 进入多数据源组合事务管理切面");
        Object result = null;
        List<Connection> transactionConnectionList = new ArrayList<>();
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method targetMethod = methodSignature.getMethod();
        TransactionalGroup transactionalGroup = targetMethod.getAnnotation(TransactionalGroup.class);
        AssertUtil.isNotNull(transactionalGroup,"[多数据源组合事务切面]: TransactionalGroup注解不能为空");
        DataSourceIdEnum[] dataSourceIdEnums = transactionalGroup.value();
        for(DataSourceIdEnum dataSourceIdEnum : dataSourceIdEnums){
                DataSourceTransactionManager transactionManager = (DataSourceTransactionManager) SpringContextUtil.getBean(TransactionManagerEnum
                        .getByDataSourceId(dataSourceIdEnum).getTransactionManagerBeanName());
            AssertUtil.isNotNull(transactionManager, CommonErrorCode.NULL_PARAMETER,
                    "[多数据源组合事务切面]: 从spring获取transactionManager为空");
            DefaultTransactionDefinition def = new DefaultTransactionDefinition();
            def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
            Connection connection = transactionManager.getDataSource().getConnection();
            connection.setAutoCommit(false);
            transactionConnectionList.add(connection);
        }
        try {
            result = pjp.proceed();
        }catch (Throwable e){
            if(!CollectionUtils.isEmpty(transactionConnectionList)){
                Iterator<Connection> iterator = transactionConnectionList.iterator();
                while (iterator.hasNext()){
                    Connection connection = iterator.next();
                    connection.rollback();
                }
            }
        }
        log.info("[多数据源组合事务切面]: 多数据源组合事务管理切面结束");
        return result;
    }


}

  1. 由事务开始的顺序倒叙执行事务回滚
    Spring事务源码解读:每个事务开始的时候都会初始化事务同步,下一个新事务会生成一个挂起资源类,前一个事务的同步(TransactionSynchronization)就被存到资源中并清空threadLocal,当然后面又会重新初始化threadLocal,在每个事务回滚或提交结束后会判断当前事务有无挂起资源,若有就重新恢复挂起的事务同步,这样下一个事务执行回滚或提交就会执行这些事务同步,就不会出现事务同步被清空的情况
    这种方式其实就是按照嵌套事务回滚的思路解决
@Aspect
@Component
@Slf4j
public class MultiDataSourceTransactionAspectDemo1 {

    /**
     * 多数据源组合事务
     * */
    @Pointcut("@annotation(com.alsc.basic.offline.sdk.dao.annotation.TransactionalGroup)")
    public void transactionalGroupAspect() {};

    @Around(value = "transactionalGroupAspect()")
    public Object transactionalGroupAspectAround(ProceedingJoinPoint pjp) throws Throwable {
        log.info("[多数据源组合事务切面]: 进入多数据源组合事务管理切面");
        Object result = null;
        List<TransactionManageDTO> transactionManagerDTOList = new ArrayList<>();
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method targetMethod = methodSignature.getMethod();
        TransactionalGroup transactionalGroup = targetMethod.getAnnotation(TransactionalGroup.class);
        DataSourceIdEnum[] dataSourceIdEnums = transactionalGroup.value();
        for(DataSourceIdEnum dataSourceIdEnum : dataSourceIdEnums){
                DataSourceTransactionManager transactionManager =
                        (DataSourceTransactionManager) SpringContextUtil.getBean(TransactionManagerEnum
                        .getByDataSourceId(dataSourceIdEnum).getTransactionManagerBeanName());
            DefaultTransactionDefinition def = new DefaultTransactionDefinition();
            def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
            TransactionStatus transactionStatus = transactionManager.getTransaction(def);
            addtransactionManagerList(transactionManagerDTOList, dataSourceIdEnum, transactionManager, transactionStatus);
        }
        try {
            result = pjp.proceed();
        }catch (Throwable e){
            Collections.reverse(transactionManagerDTOList);
            for(TransactionManageDTO transactionManageDTO : transactionManagerDTOList){
                DataSourceTransactionManager dataSourceTransactionManager = transactionManageDTO.getDataSourceTransactionManager();
                TransactionStatus transactionStatus = transactionManageDTO.getTransactionStatus();
                dataSourceTransactionManager.rollback(transactionStatus);
            }
        }
        log.info("[多数据源组合事务切面]: 多数据源组合事务管理切面结束");
        return result;
    }


    private void addtransactionManagerList(List<TransactionManageDTO> transactionManagerDTOList, DataSourceIdEnum dataSourceIdEnum, DataSourceTransactionManager transactionManager, TransactionStatus transactionStatus) {
        TransactionManageDTO transactionManageDTO = new TransactionManageDTO();
        transactionManageDTO.setDataSourceId(dataSourceIdEnum);
        transactionManageDTO.setDataSourceTransactionManager(transactionManager);
        transactionManageDTO.setTransactionStatus(transactionStatus);
        transactionManagerDTOList.add(transactionManageDTO);
    }



}
  1. 多个事务执行 transactionManager.getTransaction后(即开始事务后)都执行一遍TransactionSynchronizationManager.clear() 这样下一个事务再开始时就不会有挂起的资源,都相当于最外层事务,我们理解为完全独立的事务,回滚可以按原顺序执行但每个事务回滚后都要再执行一遍TransactionSynchronizationManager.initSynchronization(); 重新初始化一遍,但是这样就不能使用自定义的事务同步
    这种方式虽然不能使用事务同步,是一种退而求其次的方式,但若没有对事务同步有强要求的也可以采取
@Aspect
@Component
@Slf4j
public class MultiDataSourceTransactionAspectDemo2 {

    /**
     * 多数据源组合事务
     * */
    @Pointcut("@annotation(com.alsc.basic.offline.sdk.dao.annotation.TransactionalGroup)")
    public void transactionalGroupAspect() {};

    @Around(value = "transactionalGroupAspect()")
    public Object transactionalGroupAspectAround(ProceedingJoinPoint pjp) throws Throwable {
        log.info("[多数据源组合事务切面]: 进入多数据源组合事务管理切面");
        Object result = null;
        List<TransactionManageDTO> transactionManagerDTOList = new ArrayList<>();
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method targetMethod = methodSignature.getMethod();
        TransactionalGroup transactionalGroup = targetMethod.getAnnotation(TransactionalGroup.class);
        DataSourceIdEnum[] dataSourceIdEnums = transactionalGroup.value();
        for(DataSourceIdEnum dataSourceIdEnum : dataSourceIdEnums){
                DataSourceTransactionManager transactionManager =
                        (DataSourceTransactionManager) SpringContextUtil.getBean(TransactionManagerEnum
                        .getByDataSourceId(dataSourceIdEnum).getTransactionManagerBeanName());
            DefaultTransactionDefinition def = new DefaultTransactionDefinition();
            def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
            TransactionStatus transactionStatus = transactionManager.getTransaction(def);
            addtransactionManagerList(transactionManagerDTOList, dataSourceIdEnum, transactionManager, transactionStatus);
            TransactionSynchronizationManager.clear();
        }
        try {
            result = pjp.proceed();
        }catch (Throwable e){
            for(TransactionManageDTO transactionManageDTO : transactionManagerDTOList){
                TransactionSynchronizationManager.initSynchronization();
                DataSourceTransactionManager dataSourceTransactionManager = transactionManageDTO.getDataSourceTransactionManager();
                TransactionStatus transactionStatus = transactionManageDTO.getTransactionStatus();
                dataSourceTransactionManager.rollback(transactionStatus);
            }
        }
        log.info("[多数据源组合事务切面]: 多数据源组合事务管理切面结束");
        return result;
    }


    private void addtransactionManagerList(List<TransactionManageDTO> transactionManagerDTOList, DataSourceIdEnum dataSourceIdEnum, DataSourceTransactionManager transactionManager, TransactionStatus transactionStatus) {
        TransactionManageDTO transactionManageDTO = new TransactionManageDTO();
        transactionManageDTO.setDataSourceId(dataSourceIdEnum);
        transactionManageDTO.setDataSourceTransactionManager(transactionManager);
        transactionManageDTO.setTransactionStatus(transactionStatus);
        transactionManagerDTOList.add(transactionManageDTO);
    }



}

demo用到的辅助类和注解

@Data
@ToString
public class TransactionManageDTO {

    private DataSourceIdEnum dataSourceId;

    private DataSourceTransactionManager dataSourceTransactionManager;

    private TransactionStatus transactionStatus;
}


@Target({ ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TransactionalGroup {
    /**
     *
     * 配置的数据源id
     * */
    DataSourceIdEnum[] value();

    /*******  以下参数,参见 @Transactional  ********/

    //事务传播行为
    Propagation propagation() default Propagation.REQUIRED;

    //事务隔离级别
    Isolation isolation() default Isolation.DEFAULT;

    //哪些异常回滚
    Class<? extends Throwable>[] rollbackFor() default {};

    // 哪些异常不回滚事务
    Class<? extends Throwable>[] noRollbackFor() default {};


}


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值