SpringBoot整合atomikos實現跨庫事務

code2roc發表於2022-11-24

背景

框架之前完成了多資料來源的動態切換及事務的處理,想更近一步提供一個簡單的跨庫事務處理功能,經過網上的搜尋調研,大致有XA事務/SEGA事務/TCC事務等方案,因為業務主要涉及政府及企業且併發量不大,所以採用XA事務,雖然效能有所損失,但是可以保證資料的強一致性

方案設計

針對註冊的資料來源複製一份用於XA事務,使得本地事務和XA全域性事務相互獨立可選擇的使用

Maven配置

引入atomikos第三方元件

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jta-atomikos</artifactId>
        </dependency>

註冊XA資料來源

使用Druid連線池,需要使用DruidXADataSource資料來源物件,再使用AtomikosDataSourceBean進行包裝

註冊資料來源時針對同一個連線註冊兩份,一份正常資料來源,一份用於XA事務的資料來源,資料來源標識區分並關聯

因為spring預設註冊了XA事務管理器後,所有事務操作不再走本地事務,我們透過切換不同的資料來源決定走本地事務還是XA事務

 //主資料來源xa模式
    @Bean
    @Qualifier("masterXADataSource")
    public DataSource masterXADataSource() {
        DruidXADataSource datasource = new DruidXADataSource();
        if(driverClassName.equals("com.mysql.cj.jdbc.Driver")){
            if(!dbUrl.contains("useOldAliasMetadataBehavior")){
                dbUrl += "&useOldAliasMetadataBehavior=true";
            }
            if(!dbUrl.contains("useAffectedRows")){
                dbUrl += "&useAffectedRows=true";
            }
        }
        datasource.setUrl(this.dbUrl);
        datasource.setUsername(username);
        datasource.setPassword(password);
        datasource.setDriverClassName(driverClassName);
        //configuration
        datasource.setInitialSize(1);
        datasource.setMinIdle(3);
        datasource.setMaxActive(20);
        datasource.setMaxWait(60000);
        datasource.setTimeBetweenEvictionRunsMillis(60000);
        datasource.setMinEvictableIdleTimeMillis(60000);
        datasource.setValidationQuery("select 'x'");
        datasource.setTestWhileIdle(true);
        datasource.setTestOnBorrow(false);
        datasource.setTestOnReturn(false);
        datasource.setPoolPreparedStatements(true);
        datasource.setMaxPoolPreparedStatementPerConnectionSize(20);
        datasource.setLogAbandoned(false); //移除洩露連線發生是是否記錄日誌
        try {
            datasource.setFilters("stat,slf4j");
        } catch (SQLException e) {
            logger.error("druid configuration initialization filter", e);
        }
        datasource.setConnectionProperties("druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000");//connectionProperties);

        AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
        atomikosDataSourceBean.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
        atomikosDataSourceBean.setUniqueResourceName("master-xa");
        atomikosDataSourceBean.setXaDataSource(datasource);
        atomikosDataSourceBean.setPoolSize(5);
        atomikosDataSourceBean.setMaxPoolSize(20);
        atomikosDataSourceBean.setTestQuery("select 1");
        return atomikosDataSourceBean;
    }

註冊XA事務管理器

使用spring內建的JtaTransactionManager事務管理器物件,設定AllowCustomIsolationLevels為true,否則指定自定義事務隔離級別會報錯

    //xa模式全域性事務管理器
    @Bean(name = "jtaTransactionManager")
    public PlatformTransactionManager transactionManager() throws Throwable {
        UserTransactionManager userTransactionManager = new UserTransactionManager();
        UserTransaction userTransaction = new UserTransactionImp();
        JtaTransactionManager jtaTransactionManager =  new JtaTransactionManager(userTransaction, userTransactionManager);
        jtaTransactionManager.setAllowCustomIsolationLevels(true);
        return jtaTransactionManager;
    }

定義XA事務切面

自定義註解@GlobalTransactional並定義對應切面,使用指定註解時在ThreadLocal變數值進行標識,組合

@Transactional註解指定XA事務管理器,在切換資料原時判斷當前是否在XA事物中,從而切換不同的資料來源

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Transactional(rollbackFor = Exception.class,isolation = Isolation.READ_UNCOMMITTED,transactionManager = "jtaTransactionManager")
public @interface GlobalTransactional {
}
@Aspect
@Component
@Order(value = 99)
public class GlobalTransitionAspect {
    private static Logger logger = LoggerFactory.getLogger(GlobalTransitionAspect.class);
    @Autowired
    private DynamicDataSource dynamicDataSource;

    /**
     * 切面點 指定註解
     */
    @Pointcut("@annotation(com.code2roc.fastkernel.datasource.GlobalTransactional) " +
            "|| @within(com.code2roc.fastkernel.datasource.GlobalTransactional)")
    public void dataSourcePointCut() {

    }

    /**
     * 攔截方法指定為 dataSourcePointCut
     */
    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        GlobalTransactional methodAnnotation = method.getAnnotation(GlobalTransactional.class);
        if (methodAnnotation != null) {
            DataSourceContextHolder.tagGlobal();
            logger.info("標記全域性事務");
        }
        try {
            return point.proceed();
        } finally {
            logger.info("清除全域性事務");
            DataSourceContextHolder.clearGlobal();
        }
    }
}
public class DataSourceContextHolder {
    private static Logger log = LoggerFactory.getLogger(DataSourceContextHolder.class);
    // 對當前執行緒的操作-執行緒安全的
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    private static final ThreadLocal<String> contextGlobalHolder = new ThreadLocal<String>();

    // 呼叫此方法,切換資料來源
    public static void setDataSource(String dataSource) {
        contextHolder.set(dataSource);
        log.debug("已切換到資料來源:{}", dataSource);
    }

    // 獲取資料來源
    public static String getDataSource() {
        String value = contextHolder.get();
        if (StringUtil.isEmpty(value)) {
            value = "master";
        }
        if (!StringUtil.isEmpty(getGlobal())) {
            value = value + "-xa";
        }
        return value;
    }

    // 刪除資料來源
    public static void clearDataSource() {
        contextHolder.remove();
        log.debug("已切換到主資料來源");
    }

    //====================全域性事務標記================
    public static void tagGlobal() {
        contextGlobalHolder.set("1");
    }

    public static String getGlobal() {
        String value = contextGlobalHolder.get();
        return value;
    }

    public static void clearGlobal() {
        contextGlobalHolder.remove();
    }
    //===================================================
}

配置XA事務日誌

透過在resource資料夾下建立transactions.properties檔案可以指定XA事務日誌的儲存路徑

com.atomikos.icatch.log_base_dir= tempfiles/transition/

相關文章