超简单 springboot+ mybatisplus+atomikos实现多数据源分布式事务

jdk环境:1.8

springboot:2.1.3.RELEASE

mybatisplus:3.2.0

本文主要用atomikos的AtomikosNonXADataSourceBean配置连接池,另一个是AtomikosDataSourceBean,是支持第三方如druid配置管理连接池,具体实现请参考我的另一篇文章:超简单 springboot+ mybatisplus+druid 实现多数据源+分布式事务

1、maven

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>
        <!--分布式事务-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jta-atomikos</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

2、新建DataSourceContextHolder.java 提供设置、获取、清除当前数据源的方法;

public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new InheritableThreadLocal<>();

    /**
     * 设置数据源
     *
     * @param db
     */
    public static void setDataSource(String db) {
        contextHolder.set(db);
    }

    /**
     * 取得当前数据源
     *
     * @return
     */
    public static String getDataSource() {
        return contextHolder.get();
    }

    /**
     * 清除上下文数据
     */
    public static void clear() {
        contextHolder.remove();
    }
}

3、DataSource.java 数据源注解 及DataSourceKeyEnum.java 数据源枚举类;并提供获取枚举的方法

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    DataSourceKeyEnum value();
}
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public enum DataSourceKeyEnum {

    MASTER("master"),
    /**
     * 表示 所有的SLAVE, 随机选择一个SLAVE0, 或者SLAVE1
     */
    SLAVE("slave"),

    SLAVE0("slave0"),

    SLAVE1("slave1"),;

    @Getter
    private String value;

    DataSourceKeyEnum(String value) {
        this.value = value;
    }

    public static List<DataSourceKeyEnum> getSlaveList() {
        return Arrays.asList(SLAVE0, SLAVE1);
    }

    /**
     * 根据注解获取数据源
     *
     * @param dataSource
     * @return
     */
    public static DataSourceKeyEnum getDataSourceKey(DataSource dataSource) {
        if (dataSource == null) {
            return MASTER;
        }
        if (dataSource.value() == DataSourceKeyEnum.SLAVE) {
            List<DataSourceKeyEnum> dataSourceKeyList = DataSourceKeyEnum.getSlaveList();
            // FIXME 目前乱序
            Collections.shuffle(dataSourceKeyList);
            return dataSourceKeyList.get(0);
        } else {
            return dataSource.value();
        }
    }
}

4、DataSourceAspect.java 数据源切面类,这里是只做了对mapper层拦截,包括拦截了mybatisplus的公共BaseMapper

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.core.annotation.AnnotationUtils;
import java.lang.reflect.Method;
import java.lang.reflect.Type;

@Component
@Slf4j
@Aspect
@Order(-1)
public class DataSourceAspect {

    @Pointcut("execution(* com.admin.*.dao.*Mapper.*(..))||execution(* com.baomidou.mybatisplus.core.mapper.*Mapper.*(..)))")
    public void pointCut() {

    }

    @Around("pointCut()")
    public Object doBefore(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        DataSource dataSource = AnnotationUtils.findAnnotation(method, DataSource.class);
        DataSourceKeyEnum keyEnum = DataSourceKeyEnum.getDataSourceKey(dataSource);
        log.info("选择的数据源:"+keyEnum.getValue());
        DataSourceContextHolder.setDataSource(keyEnum.getValue());
        Object o=pjp.proceed();
        DataSourceContextHolder.clear();
        return o;
    }

    @Pointcut("execution(* com.baomidou.mybatisplus.extension.service.IService.*Batch*(..)))")
    public void pointCutBatch() {

    }

    //对mybatisplus批量操作切面
    @Around("pointCutBatch()")
    public Object doBeforeBatch(ProceedingJoinPoint pjp) throws Throwable {
        DataSourceContextHolder.setDataSource(DataSourceKeyEnum.MASTER.getValue());
        Object o = pjp.proceed();
        DataSourceContextHolder.clear();
        return o;
    }
}

5、application.yml 相关配置

spring:
  datasource:
    atomikos:
      master:
        url: jdbc:mysql://127.0.0.1:3306/test?charset=utf8mb4&useUnicode=true&characterEncoding=utf8&useSSL=false&autoReconnect=true&serverTimezone=Asia/Shanghai
        user: root
        password: 123456
        uniqueResourceName: master
      slave0:
        url: jdbc:mysql://127.0.0.1:3306/test?charset=utf8mb4&useUnicode=true&characterEncoding=utf8&useSSL=false&autoReconnect=true&serverTimezone=Asia/Shanghai
        user: root
        password: 123456
        uniqueResourceName: slave0
      slave1:
        url: jdbc:mysql://127.0.0.1:3306/test?charset=utf8mb4&useUnicode=true&characterEncoding=utf8&useSSL=false&autoReconnect=true&serverTimezone=Asia/Shanghai
        user: root
        password: 123456
        uniqueResourceName: slave1
  jta:
    atomikos:
      properties:
        log-base-dir: ../logs
    transaction-manager-id: txManager    #默认取计算机的IP地址 需保证生产环境值唯一

6、复制SqlSessionTemplate.java里所有代码新建到MySqlSessionTemplate.java然后继承SqlSessionTemplate.java,并按照下方替换相应的方法(只贴出了更改了的地方)完整MySqlSessionTemplate.java 点击下载

import lombok.Getter;
import lombok.Setter;

public class MySqlSessionTemplate extends SqlSessionTemplate {

    @Getter
    @Setter
    private Map<String, SqlSessionFactory> targetSqlSessionFactories;

    @Getter
    @Setter
    private SqlSessionFactory defaultTargetSqlSessionFactory;

    public MySqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
                                PersistenceExceptionTranslator exceptionTranslator) {
        super(sqlSessionFactory, executorType, exceptionTranslator);
        notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
        notNull(executorType, "Property 'executorType' is required");

        this.sqlSessionFactory = sqlSessionFactory;
        this.executorType = executorType;
        this.exceptionTranslator = exceptionTranslator;
        this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
                new Class[]{SqlSession.class}, new MySqlSessionTemplate.SqlSessionInterceptor());
        this.defaultTargetSqlSessionFactory = sqlSessionFactory;
    }

    //TODO 主要修改了这一块,并且用到sqlSessionFactory的地方都改调用该方法获取
    public SqlSessionFactory getSqlSessionFactory() {
        SqlSessionFactory targetSqlSessionFactory = targetSqlSessionFactories.get(DataSourceContextHolder.getDataSource());

        if (targetSqlSessionFactory != null) {
            return targetSqlSessionFactory;
        } else if (defaultTargetSqlSessionFactory != null) {
            return defaultTargetSqlSessionFactory;
        } else {
            Assert.notNull(targetSqlSessionFactories, "Property 'targetSqlSessionFactories' or 'defaultTargetSqlSessionFactory' are required");
        }
        return this.sqlSessionFactory;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Configuration getConfiguration() {
        return this.getSqlSessionFactory().getConfiguration();
    }

    private class SqlSessionInterceptor implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            SqlSession sqlSession = getSqlSession(MySqlSessionTemplate.this.getSqlSessionFactory(),
                    MySqlSessionTemplate.this.executorType, MySqlSessionTemplate.this.exceptionTranslator);
            try {
                Object result = method.invoke(sqlSession, args);
                if (!isSqlSessionTransactional(sqlSession, MySqlSessionTemplate.this.getSqlSessionFactory())) {
                    // force commit even on non-dirty sessions because some databases require
                    // a commit/rollback before calling close()
                    sqlSession.commit(true);
                }
                return result;
            } catch (Throwable t) {
                Throwable unwrapped = unwrapThrowable(t);
                if (MySqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
                    // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
                    closeSqlSession(sqlSession, MySqlSessionTemplate.this.sqlSessionFactory);
                    sqlSession = null;
                    Throwable translated = MySqlSessionTemplate.this.exceptionTranslator
                            .translateExceptionIfPossible((PersistenceException) unwrapped);
                    if (translated != null) {
                        unwrapped = translated;
                    }
                }
                throw unwrapped;
            } finally {
                if (sqlSession != null) {
                    closeSqlSession(sqlSession, MySqlSessionTemplate.this.getSqlSessionFactory());
                }
            }
        }
    }
}

7、新建MyGlobalConfig.java并继承GlobalConfig,主要重写mybaitsplus自带的GlobalConfig的getSqlSessionFactory方法;

不重写该类会导致后面mybatisplus自带的saveBatch等批量操作方法无法切换数据源

import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.admin.common.datasource.MySqlSessionTemplate;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Component
public class MyGlobalConfig extends GlobalConfig {

    @Autowired
    private MySqlSessionTemplate sqlSessionTemplate;

    private static MySqlSessionTemplate mySqlSessionTemplate;

    @Override
    public SqlSessionFactory getSqlSessionFactory() {
        return mySqlSessionTemplate.getSqlSessionFactory();
    }

    @PostConstruct
    public void init() {
        MyGlobalConfig.mySqlSessionTemplate = sqlSessionTemplate;
    }
}

8、新建MyBatisPlusConfiguration.java,mybatisplus配置 和多数据源配置

import com.alibaba.druid.pool.xa.DruidXADataSource;
import com.atomikos.jdbc.nonxa.AtomikosNonXADataSourceBean;
import com.baomidou.mybatisplus.autoconfigure.SpringBootVFS;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.parser.ISqlParser;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.tenant.TenantHandler;
import com.baomidou.mybatisplus.extension.plugins.tenant.TenantSqlParser;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import com.admin.util.datasource.DataSourceKeyEnum;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.JdbcType;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Configuration
@MapperScan(basePackages = {"com.admin.*.dao", "com.baomidou.mybatisplus.samples.quickstart.mapper"}, sqlSessionTemplateRef = "sqlSessionTemplate")
public class MyBatisPlusConfiguration {

    @Bean
    @Primary    //多数据源时需要加上该注解,加一个即可
    @ConfigurationProperties(prefix = "spring.datasource.atomikos.master")
    public AtomikosNonXADataSourceBean userMaster() {
        //也可以直接return new AtomikosNonXADataSourceBean();
        return DataSourceBuilder.create().type(AtomikosNonXADataSourceBean.class).build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.atomikos.slave0")
    public AtomikosNonXADataSourceBean userSlave0() {
        return DataSourceBuilder.create().type(AtomikosNonXADataSourceBean.class).build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.atomikos.slave1")
    public AtomikosNonXADataSourceBean userSlave1() {
        return DataSourceBuilder.create().type(AtomikosNonXADataSourceBean.class).build();
    }

    @Bean(name = "sqlSessionTemplate")
    public MySqlSessionTemplate customSqlSessionTemplate() throws Exception {
        Map<String, SqlSessionFactory> sqlSessionFactoryMap = new HashMap<String, SqlSessionFactory>() {{
            put(DataSourceKeyEnum.MASTER.getValue(), createSqlSessionFactory(userMaster()));
            put(DataSourceKeyEnum.SLAVE0.getValue(), createSqlSessionFactory(userSlave0()));
            put(DataSourceKeyEnum.SLAVE1.getValue(), createSqlSessionFactory(userSlave1()));
        }};
        MySqlSessionTemplate sqlSessionTemplate = new MySqlSessionTemplate(sqlSessionFactoryMap.get(DataSourceKeyEnum.MASTER.getValue()));
        sqlSessionTemplate.setTargetSqlSessionFactories(sqlSessionFactoryMap);
        return sqlSessionTemplate;
    }

    /**
     * 创建数据源
     *
     * @param dataSource
     * @return
     */
    private SqlSessionFactory createSqlSessionFactory(AtomikosNonXADataSourceBean dataSource) throws Exception {
        dataSource.setMaxPoolSize(10);
        dataSource.setMinPoolSize(2);
        dataSource.setPoolSize(2);
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setMaxIdleTime(60);//最大闲置时间,超过最小连接池的连接将关闭
        dataSource.setMaxLifetime(1200);//连接最大闲置时间 单位s 全部的连接超时将关闭
        dataSource.setTestQuery(druidDataSource.getValidationQuery());//前期先每次请求前都执行该操作保证连接有效,后期可用定时任务执行
        dataSource.setMaintenanceInterval(60);//定时维护线程周期 单位秒
        //以上配置可提取到.yml内通过ConfigurationProperties注解注入        

        dataSource.init();//项目启动则初始化连接
        MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
        sqlSessionFactory.setDataSource(dataSource);
        sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:/com/admin/*/dao/xml/*.xml"));
        sqlSessionFactory.setVfs(SpringBootVFS.class);

        MybatisConfiguration configuration = new MybatisConfiguration();
        //configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.setMapUnderscoreToCamelCase(false);
        configuration.setCacheEnabled(false);
        sqlSessionFactory.setConfiguration(configuration);
        sqlSessionFactory.setPlugins(paginationInterceptor());

        //重写了GlobalConfig的MyGlobalConfig注入到sqlSessionFactory使其生效
        MyGlobalConfig globalConfig = new MyGlobalConfig();
        sqlSessionFactory.setGlobalConfig(globalConfig);

        sqlSessionFactory.afterPropertiesSet();
        return sqlSessionFactory.getObject();
    }

    /*
     * 自定义的分页插件,自动识别数据库类型
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }
}

如需druid监控加上以下配置并引入druid相关jar包即可(此方法只能监控除sql以外的。如需监控sql,具体实现请参考我的另一篇文章:超简单 springboot+ mybatisplus+druid 实现多数据源+分布式事务

spring:
  datasource:
    druid:
      web-stat-filter:
        enabled: true
        url-pattern: /*
        exclusions: /druid/*,*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico
        session-stat-enable: true
        session-stat-max-count: 10
      stat-view-servlet:
        enabled: true
        url-pattern: /druid/*
        reset-enable: true
        login-username: admin
        login-password: admin
      aop-patterns: com.admin.*.service.*

事务使用方法如单数据源一样:对应的方法或类上加@Transactional注解即可

遇到的坑:

1、设置maintenanceInterval和maxLifetime后,当maxLifetime大于1800秒左右,程序的连接显示已回收,而mysql连接依然还在,最终程序重复创建新连接(以为旧连接已关闭)导致mysql连接数满了。建议设置在1800秒以内;

原创文章 6 获赞 16 访问量 5068

猜你喜欢

转载自blog.csdn.net/q854214434/article/details/101780242