SpringBoot + MybatisPlus 实现多数据源事务

针对使用SpringBoot + MybatisPlus架构,需要用到多数据源,并且在一个方法内可能多个数据源都有事务要求的情况,做实现方法分享,源代码如下:https://github.com/guzhangyu/learn-spring-cloud/tree/master/springboot-multidb

1、 多数据源配置

动态指定数据源的类:

@Slf4j
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        String dataSourceName = DynamicDataSourceContextHolder.getDataSourceRouterKey();
        log.info("当前数据源是:{}", dataSourceName);
        return DynamicDataSourceContextHolder.getDataSourceRouterKey();
    }
}

其中 DynamicDataSourceContextHolder用于保存当前线程上下文的数据源名:

/**
 * 数据源设置工具类
 */
@Slf4j
public class DynamicDataSourceContextHolder {

    /**
     * 存储已经注册的数据源的key
     */
    public static List<String> dataSourceIds = new ArrayList<>();

    /**
     * 线程级别的私有变量
     */
    private static final ThreadLocal<String> HOLDER = new ThreadLocal<>();

    public static String getDataSourceRouterKey () {
        return HOLDER.get();
    }

    public static void setDataSourceRouterKey (String dataSourceRouterKey) {
        log.info("切换至{}数据源", dataSourceRouterKey);
        HOLDER.set(dataSourceRouterKey);
    }

    /**
     * 设置数据源之前一定要先移除
     */
    public static void removeDataSourceRouterKey () {
        HOLDER.remove();
    }

    /**
     * 判断指定DataSrouce当前是否存在
     *
     * @param dataSourceId
     * @return
     */
    public static boolean containsDataSource(String dataSourceId){
        return dataSourceIds.contains(dataSourceId);
    }

}

加载多数据源的配置并注册数据源类,在启动类上需要加上 @Import(DynamicDataSourceRegister.class) 注解:

/**
 * 动态数据源注册
 * 实现 ImportBeanDefinitionRegistrar 实现数据源注册
 * 实现 EnvironmentAware 用于读取application.yml配置
 */
@Slf4j
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {


    /**
     * 配置上下文(也可以理解为配置文件的获取工具)
     */
    private Environment evn;

    /**
     * 别名
     */
    private final static ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();

    /**
     * 由于部分数据源配置不同,所以在此处添加别名,避免切换数据源出现某些参数无法注入的情况
     */
    static {
        aliases.addAliases("url", new String[]{"jdbc-url"});
        aliases.addAliases("username", new String[]{"user"});
    }

    /**
     * 存储我们注册的数据源
     */
    private static final Map<String, DataSource> customDataSources = new HashMap<String, DataSource>();

    /**
     * 参数绑定工具 springboot2.0新推出
     */
    private Binder binder;

    /**
     * ImportBeanDefinitionRegistrar接口的实现方法,通过该方法可以按照自己的方式注册bean
     *
     * @param annotationMetadata
     * @param beanDefinitionRegistry
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {

        // 获取数据源配置
        Map config = (Map)binder.bind("spring.datasource.druid.master", Map.class).get().get("0");
        registerDataSource(config);

        config = (Map)binder.bind("spring.datasource.druid.slave", Map.class).get().get("0");
        registerDataSource(config);

        // bean定义类
        GenericBeanDefinition define = new GenericBeanDefinition();
        // 设置bean的类型,此处DynamicRoutingDataSource是继承AbstractRoutingDataSource的实现类
        define.setBeanClass(DynamicRoutingDataSource.class);
        // 需要注入的参数
        MutablePropertyValues mpv = define.getPropertyValues();
        // 添加默认数据源,避免key不存在的情况没有数据源可用
        mpv.add("defaultTargetDataSource", customDataSources.get("master"));
        // 添加其他数据源
        mpv.add("targetDataSources", customDataSources);
        // 将该bean注册为datasource,不使用springboot自动生成的datasource
        beanDefinitionRegistry.registerBeanDefinition("datasource", define);
        log.info("注册数据源成功,一共注册{}个数据源", customDataSources.keySet().size());
    }

    private DataSourceTransactionManager registerDataSource(Map config) {
        // 绑定参数
        DataSource consumerDatasource = bind(getDataSourceType((String) config.get("type")), config);
//      //设置事务
        DataSourceTransactionManager secondDataSourceTransactionManager = new DataSourceTransactionManager();
        secondDataSourceTransactionManager.setDataSource(consumerDatasource);

        // 获取数据源的key,以便通过该key可以定位到数据源
        String key = config.get("key").toString();
        customDataSources.put(key, consumerDatasource);
        // 数据源上下文,用于管理数据源与记录已经注册的数据源key
        DynamicDataSourceContextHolder.dataSourceIds.add(key);
        log.info("注册数据源{}成功", key);

        return secondDataSourceTransactionManager;
    }

    public DataSource getDataSource(String key){
        return customDataSources.get(key);
    }


    /**
     * 通过字符串获取数据源class对象
     *
     * @param typeStr
     * @return
     */
    private Class<? extends DataSource> getDataSourceType(String typeStr) {
        Class<? extends DataSource> type;
        try {
            if (StringUtils.hasLength(typeStr)) {
                // 字符串不为空则通过反射获取class对象
                type = (Class<? extends DataSource>) Class.forName(typeStr);
            } else {
                // 默认为hikariCP数据源,与springboot默认数据源保持一致
                type = DruidDataSource.class;
            }
            return type;
        } catch (Exception e) {
            throw new IllegalArgumentException("can not resolve class with type: " + typeStr); //无法通过反射获取class对象的情况则抛出异常,该情况一般是写错了,所以此次抛出一个runtimeexception
        }
    }

    /**
     * 绑定参数,以下三个方法都是参考DataSourceBuilder的bind方法实现的,目的是尽量保证我们自己添加的数据源构造过程与springboot保持一致
     *
     * @param result
     * @param properties
     */
    private void bind(DataSource result, Map properties) {
        ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
        Binder binder = new Binder(new ConfigurationPropertySource[]{source.withAliases(aliases)});
        // 将参数绑定到对象
        binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(result));
    }

    private <T extends DataSource> T bind(Class<T> clazz, Map properties) {
        ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
        Binder binder = new Binder(new ConfigurationPropertySource[]{source.withAliases(aliases)});
        // 通过类型绑定参数并获得实例对象
        return binder.bind(ConfigurationPropertyName.EMPTY, Bindable.of(clazz)).get();
    }

    /**
     * @param clazz
     * @param sourcePath 参数路径,对应配置文件中的值,如: spring.datasource
     * @param <T>
     * @return
     */
    private <T extends DataSource> T bind(Class<T> clazz, String sourcePath) {
        Map properties = binder.bind(sourcePath, Map.class).get();
        return bind(clazz, properties);
    }

    /**
     * EnvironmentAware接口的实现方法,通过aware的方式注入,此处是environment对象
     *
     * @param environment
     */
    @Override
    public void setEnvironment(Environment environment) {
        log.info("开始注册数据源");
        this.evn = environment;
        // 绑定配置器
        binder = Binder.get(evn);
    }
}
spring:
  datasource:
    druid:
      defaultDs: master
      master:
      - key: master
        name: master
        type: com.alibaba.druid.pool.DruidDataSource
        url: jdbc:mysql://localhost:3306/dorm?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC
        username: root
        password: root

      slave:
      - key: slave
        name: slave
        type: com.alibaba.druid.pool.DruidDataSource
        url: jdbc:mysql://localhost:3306/jkm?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC
        username: root
        password: root
        
mybatis-plus:
  global-config:
    id-dtype: 0
    field-strategy: 0
    db-config:
      logic-delete-value: 0
      logic-not-delete-value: 1
  mapper-locations: classpath:mybatis/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    cache-enabled: true

通过aop实现在mapper层切换数据源:

@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
    String value() default "master"; //该值即key值
}
@Aspect
@Component
@Slf4j
public class MapperAspect {

    @Pointcut("execution(* com.learn.springboot.mapper.*Mapper.*(..))")
    public void pointCut(){
    }

    @Before("pointCut()")
    public void before(JoinPoint joinPoint) {
        DataSource annotation = getDataSourceAnnotation(joinPoint);

        String dsId = annotation == null ? "master" : annotation.value();
        log.info("选择数据源:{}", dsId);
        DynamicDataSourceContextHolder.setDataSourceRouterKey(dsId);
    }

    private DataSource getDataSourceAnnotation(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        DataSource annotation = method.getAnnotation(DataSource.class);
        if(annotation!=null) {
            return annotation;
        }

        Class<?>[] interfaces = joinPoint.getTarget().getClass().getInterfaces();
        for(Class<?> anInterface: interfaces) {
            annotation = anInterface.getAnnotation(DataSource.class);
            if(annotation!=null) {
                return annotation;
            }
        }
        return null;
    }

    @After("pointCut()")
    public void after() {
        DynamicDataSourceContextHolder.removeDataSourceRouterKey();
    }
}

2、多数据源事务支持

目标效果是在一个方法中可以指定哪几个数据源要加上事务支持。
Spring的事务管理器 DataSourceTransactionManager:
在这里插入图片描述
绑定到当前线程后,每次拿connection不会调用determineCurrentLookupKey() 方法去获取不同的数据源,从而拿到不同的connection,而是直接去拿这里绑定的connection。
所以使用原生的事务管理器并不能完成我们需要的功能,改成直接在aop中拿connection来开启事务:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface TransactionMulti {
    String[] value() default {};
    int transactionType() default Connection.TRANSACTION_READ_UNCOMMITTED;
}
@Aspect
@Component
@Slf4j
public class MultiTransactionManagerAop {

    private DynamicRoutingDataSource dataSourceRouting = new DynamicRoutingDataSource();


    @Pointcut("@annotation(com.learn.springboot.annotation.TransactionMulti)")
    public void annotationPointcut(){}


    @Around("annotationPointcut()")
    public void roundExecute(ProceedingJoinPoint joinpoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinpoint.getSignature();
        Method method = methodSignature.getMethod();
        TransactionMulti annotation = method.getAnnotation(TransactionMulti.class);
        String[] values = annotation.value();
        int transactionType = annotation.transactionType();
        //把涉及到的连接绑定到线程上,开启事务,关闭自动提交
        begin(values, transactionType);
        //正真执行了 方法
        joinpoint.proceed();
        //commit
        dataSourceRouting.doCommit();
    }

    @AfterThrowing(pointcut = "annotationPointcut()",throwing = "e")
    public void handleThrowing(JoinPoint joinPoint, Exception e) {//controller类抛出的异常在这边捕获
        try {
            dataSourceRouting.rollback();
        } catch (SQLException e1) {
            e1.printStackTrace();
        }
    }



    private void begin(String[] values,int transactionType) throws SQLException {
        for (String value : values) {
            DataSource dataSource = DynamicDataSourceRegister.getDataSource(value);
            if(dataSource == null){
                log.error("没有找到数据源:{}", value);
                continue;
            }
            Connection connection = dataSource.getConnection();
            prepareTransactionalConnection(connection,transactionType);
            connectBegin(connection);
            //绑定到线程上面
            dataSourceRouting.bindConnection(value, connection);
        }
    }

    /**
     * 开启事物的一些准本工作
     */
    private void connectBegin(Connection connection) throws SQLException {
        if(connection!=null){
            try {
                if(connection.getAutoCommit()){
                    connection.setAutoCommit(false);
                }
            } catch (SQLException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

    }

    /**
     * 设置隔离级别
     * @param con
     * @throws SQLException
     */
    protected void prepareTransactionalConnection(Connection con,int transactionType)
            throws SQLException {
        if (TransactionTypeEnum.isNotDefined(transactionType)){
            throw new SqlSessionException("当前事物隔离级别未被定义");
        }
        con.setTransactionIsolation(transactionType);
    }

}
public enum TransactionTypeEnum {

    TRANSACTION_NONE(0,"无事务"),
    TRANSACTION_READ_UNCOMMITTED(1,"允许读脏,不可重读,幻读"),
    TRANSACTION_READ_COMMITTED(2,"仅允许读取已提交的数据,即不能读脏,但是可能发生不可重读和幻读"),
    TRANSACTION_REPEATABLE_READ(4,"不可读脏,保证同一事务重复读取相同数据,但是可能发生幻读"),
    TRANSACTION_SERIALIZABLE(8,"串行事务,保证不读脏,可重复读,不可幻读");

    private int value;

    private String details;

    TransactionTypeEnum() {
    }

    TransactionTypeEnum(int value, String details) {
        this.value = value;
        this.details = details;
    }

    public static boolean isNotDefined(Integer value) {
        TransactionTypeEnum[] transactionTypeEnums = values();
        for (TransactionTypeEnum transactionTypeEnum : transactionTypeEnums) {
            if (transactionTypeEnum.getValue()==value) {
                return false;
            }
        }
        return true;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public String getDetails() {
        return details;
    }

    public void setDetails(String details) {
        this.details = details;
    }
}

这里有一个问题需要解决:因为现在事务自己管理,mybatis每次拿完connection就自动调用close和commit方法,这样导致我自己操作的事务失效,所以要先让mybatis不能自作主张关闭connection。
解决方法: spring data获取connection的时候,给它包装类,覆盖原来的close和commit方法。
类DynamicRoutingDataSource 中覆盖getConnection()方法:

   //把当前事务下的连接塞入,用于事务处理
    final static ThreadLocal<Map<String, ConnectWrap>> connectionThreadLocal = new ThreadLocal<>();

    /**
     * 如果 在connectionThreadLocal 中有 说明开启了事务,就从这里面拿
     *
     * @return
     * @throws SQLException
     */
    @Override
    public Connection getConnection() throws SQLException {
        Map<String, ConnectWrap> stringConnectionMap = connectionThreadLocal.get();
        if (stringConnectionMap == null) {
            //没开事务 直接走
            return determineTargetDataSource().getConnection();
        } else {
            //开了事务,从当前线程中拿,而且拿到的是 包装过的connect 只有我能关闭O__O "…
            String currentName = (String) determineCurrentLookupKey();
            return stringConnectionMap.get(currentName);
        }

    }

在这个包装类中
在这里插入图片描述
只有手动调用 commit(true) 和 close(true) 才会真正提交和关闭连接。
doCommit() 和 rollback() 方法如下:

 /**
     * 提交事物
     *
     * @throws SQLException
     */
    public void doCommit() throws SQLException {
        //System.out.println("commit:" + connectionThreadLocal.get().toString());
        Map<String, ConnectWrap> stringConnectionMap = connectionThreadLocal.get();
        if (stringConnectionMap == null) {
            return;
        }
        for (String dataSourceName : stringConnectionMap.keySet()) {
            ConnectWrap connection = stringConnectionMap.get(dataSourceName);
            connection.commit(true);
            connection.close(true);
        }
        removeConnectionThreadLocal();
    }

    /**
     * 撤销事物
     *
     * @throws SQLException
     */
    public void rollback() throws SQLException {
//        System.out.println("rollback:" + connectionThreadLocal.get().toString());
        Map<String, ConnectWrap> stringConnectionMap = connectionThreadLocal.get();
        if (stringConnectionMap == null) {
            return;
        }
        for (String dataSourceName : stringConnectionMap.keySet()) {
            ConnectWrap connection = stringConnectionMap.get(dataSourceName);
            connection.rollback();
            connection.close(true);
        }
        removeConnectionThreadLocal();
    }

    public void removeConnectionThreadLocal() {
//        System.out.println("remove:" + connectionThreadLocal.get().toString());
        connectionThreadLocal.remove();
    }

3、测试

@DataSource("slave")
public interface IdentifyScoreMapper extends BaseMapper<IdentifyScore> {

    @DataSource("slave")
    @Update("update identify_score set score = #{score} where id = 1")
    void updateScore(@Param("score") Integer socre);
}
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.learn.springboot.annotation.DataSource;
import com.learn.springboot.entity.Student;

/**
 * @Author zhangyugu
 * @Date 2020/9/12 7:19 下午
 * @Version 1.0
 */
@DataSource("master")
public interface StudentMapper extends BaseMapper<Student> {
}
@Service
public class TestTransactionService {

    @Resource
    StudentMapper studentMapper;

    @Resource
    IdentifyScoreMapper identifyScoreMapper;

//    @Transactional(rollbackFor = RuntimeException.class)
    @TransactionMulti(value = {"master", "slave"})
    public void test() {
        identifyScoreMapper.updateScore(77);
        Student student = new Student();
        student.setId(1L);
        student.setName("test");
        studentMapper.updateById(student);

        if(1==1) {
            throw new RuntimeException("ds");
        }

        identifyScoreMapper.updateScore(88);
        student = new Student();
        student.setId(2L);
        student.setName("test");
        studentMapper.updateById(student);
    }
}
@Import(DynamicDataSourceRegister.class)
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class MultidbApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext applicationContext = SpringApplication.run(MultidbApplication.class, args);
        TestTransactionService testTransactionService = applicationContext.getBean(TestTransactionService.class);

        testTransactionService.test();
    }
}

通过抛出异常与不抛出异常两种情况,看数据库数据的变化情况可以进行测试。

你可以通过在Spring Boot中配置多数据源实现事务的管理。首先,确保在pom.xml文件中添加了Spring Boot和MyBatis Plus的依赖。 接下来,你需要创建两个数据源的配置类。例如,假设你有两个数据源,分别是dataSource1和dataSource2,你可以创建如下的配置类: ```java @Configuration public class DataSourceConfig { @Primary @Bean(name = "dataSource1") @ConfigurationProperties(prefix = "spring.datasource.ds1") public DataSource dataSource1() { return DataSourceBuilder.create().build(); } @Bean(name = "dataSource2") @ConfigurationProperties(prefix = "spring.datasource.ds2") public DataSource dataSource2() { return DataSourceBuilder.create().build(); } } ``` 在这个示例中,我们使用了@ConfigurationProperties注解来绑定配置文件中以`spring.datasource.ds1`和`spring.datasource.ds2`为前缀的属性。 接下来,你需要配置事务管理器。在Spring Boot中,你可以使用`DataSourceTransactionManager`来管理事务。在你的配置类中,添加如下代码: ```java @Configuration @EnableTransactionManagement public class TransactionConfig { @Autowired @Qualifier("dataSource1") private DataSource dataSource1; @Autowired @Qualifier("dataSource2") private DataSource dataSource2; @Bean(name = "transactionManager1") public DataSourceTransactionManager transactionManager1() { return new DataSourceTransactionManager(dataSource1); } @Bean(name = "transactionManager2") public DataSourceTransactionManager transactionManager2() { return new DataSourceTransactionManager(dataSource2); } } ``` 在这个示例中,我们创建了两个`DataSourceTransactionManager`,分别与dataSource1和dataSource2关联。 最后,你需要在你的业务代码中使用`@Transactional`注解来开启事务。根据需要,你可以指定使用哪个数据源的事务管理器。 ```java @Service public class YourService { @Autowired @Qualifier("transactionManager1") private PlatformTransactionManager transactionManager1; @Transactional("transactionManager1") public void doSomething() { // 在这里执行你的业务逻辑 } } ``` 在这个示例中,我们使用了`@Transactional`注解,并通过指定`transactionManager1`来使用dataSource1的事务管理器。 这样,你就可以在Spring Boot中配置多数据源并使用事务管理器了。希望对你有所帮助!
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值