spring整合mybatis的核心思路 & 数据源动态切换 & 多数据源事务控制

  • Post author:
  • Post category:其他



mybatis-spring-demo 的 gitee代码地址



学习链接


怎么连接多个数据库?SpringBoot多数据源完整 视频教程



配套资料&代码


AbstractRoutingDataSource 图解 – 自己的链接


spring事务源码学习


zzhua-mybatis框架全面分析 – 自己的链接



mybatis-spring源码详尽分析&Spring扫包 – 自己的链接



一、多数据源实现方法



1. spring整合mybatis的核心思路



整合原理

@MapperScan注解 -> 引入了MapperScannerRegistrar注册器 -> 该注册器向容器中MapperScannerConfigurer这个configurer是个BeanDefinitionRegistryPostProcessor,它在重写的postProcessBeanDefinitionRegistry方法中使用了ClassPathMapperScanner来扫描指定的包中的mapper接口 -> ClassPathMapperScanner(如果未指定sqlSessionTemplateBeanName,也没有指定sqlSessionFactoryBeanName,将会设置按类型自动注入SqlSessionFactory和SqlSessionTemplate(实现了SqlSession接口,委托给SqlSessionInterceptor,即每次调用SqlSession中的任何方法都会走SqlSessionInterceptor#invoke方法,这是为了控制事务),仅须其中一个即可) -> 扫包中的每一个beanDefinition都会设置beanClass为MapperFactoryBean,而MapperFactoryBean#getObject方法就会获取sqlSessionTemplate(它实现了SqlSession接口),并调用sqlSessionTemplate.getMapper(接口类)方法,来获取动态代理 -> 而每次使用动态代理的方法都会调用sqlSessionTemplate -> 而sqlSessionTemplate则也会委托给SqlSessionInterceptor-> 此时去从SqlSessionFactory获取sqlSession

源码详细流程,可参考:


zzhua-mybatis框架全面分析



mybatis-spring源码详尽分析&Spring扫包



2. spring整合mybatis最简单的整合步骤



简单介绍

整合的原理,在上面已经详细分析了,这个仅仅是简单的spring 整合 mybatis 的一个示例

在这里插入图片描述



1. 导入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zzhua</groupId>
    <artifactId>mybatis-spring-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.0.2.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.2</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.7</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.17</version>
        </dependency>



        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.6.0</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>

        </plugins>
    </build>


</project>



2. 准备基础类



User

@Data
public class User {

    private String userAccount;

}



UserMapper

public interface UserMapper {

    List<User> findAll();

    @Select("select * from `user` u where u.user_account = #{userAccount}")
    User findOne(String userAccount);

}



userMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zzhua.mapper.UserMapper">

   <select id="findAll" resultType="user">
       select * from `user`
   </select>


</mapper>



MybatisConfig配置类

package com.zzhua.config;

import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import java.io.IOException;

@Configuration
@MapperScan(basePackages = "com.zzhua.mapper")
public class MybatisConfig {

    @Bean // 如果不配置该bean,将会抛出Caused by: java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required异常
    public SqlSessionFactoryBean sqlSessionFactoryBean() throws IOException { // 代码写法可参考SqlSessionFactoryBean#afterPropertiesSet

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

        // 设置数据源
        DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
        driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
        driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        driverManagerDataSource.setUsername("root");
        driverManagerDataSource.setPassword("root");
        sqlSessionFactoryBean.setDataSource(driverManagerDataSource);

        // 自定义Configuration
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.getTypeAliasRegistry().registerAliases("com.zzhua.pojo");
        sqlSessionFactoryBean.setConfiguration(configuration);

        // 指定mapper.xml文件
        PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources("classpath:com/zzhua/mapper/**.xml"));

        return sqlSessionFactoryBean; // # 之后会调用SqlSessionFactoryBean#afterPropertiesSet()方法
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
    }

}



3. 测试



TestMybatisSpring

public class TestMybatisSpring {
    public static void main(String[] args) {

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MybatisConfig.class);

        UserMapper userMapper = context.getBean(UserMapper.class);

        List<User> userList = userMapper.findAll();
        System.out.println(userList);

        User user = userMapper.findOne("zzhua");
        System.out.println(user);


    }
}



3. mybatis插件实现切换数据源



简单介绍

读写分离的数据源:如果是mybatis可以结合插件实现读写分离动态切换数据源。



mybatis提供的插件机制

中,可以拦截到当前执行sql的类型(是查询 或者 修改),从而

实现读写分离

,查询使用指定的数据库,而涉及到修改使用另外的数据库。

在这里插入图片描述



1. DsPlugin插件

// Interceptor可拦截mybatis的四大对象中的方法
//      四大对象包括: Executor、StatementHandler、ParameterHandler、ResultSetHandler
@Intercepts({
    @Signature(
            type = Executor.class,
            method = "update",
            args = {MappedStatement.class, Object.class}
    ),
    @Signature(
            type = Executor.class,
            method = "query",
            args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
    )
})
public class DsPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        Object[] args = invocation.getArgs();

        MappedStatement ms = ((MappedStatement) args[0]);

		// 是查询操作的话,就走user这个数据库
        if (SqlCommandType.SELECT.equals(ms.getSqlCommandType())) {
            CustomizeRoutingDataSource.setDsKey("user");
        } else {
        	// 是增删改操作的话,就走user2这个数据库
            CustomizeRoutingDataSource.setDsKey("user2");
        }

        Object result = invocation.proceed();
        CustomizeRoutingDataSource.clearDsKey();
        return result;
    }
}



2. CustomizeRoutingDataSource

public class CustomizeRoutingDataSource extends AbstractRoutingDataSource {

    private static final ThreadLocal<String> DS_KEY = new ThreadLocal<>();

    @Override
    protected Object determineCurrentLookupKey() {
        return DS_KEY.get();
    }

    public static void setDsKey(String dsKey) {
        DS_KEY.set(dsKey);
    }

    public static void clearDsKey() {
        DS_KEY.remove();
    }

}



2. 修改MybatisConfig

@Configuration
@MapperScan(basePackages = "com.zzhua.dao.mapper", sqlSessionFactoryRef = "sqlSessionFactoryBean")
public class MybatisConfig {

    @Bean
    public DataSource userDataSource() {
        DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
        driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
        driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        driverManagerDataSource.setUsername("root");
        driverManagerDataSource.setPassword("root");
        return driverManagerDataSource;
    }

    @Bean
    public DataSource user2DataSource() {
        DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
        driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1/user2?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
        driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        driverManagerDataSource.setUsername("root");
        driverManagerDataSource.setPassword("root");
        return driverManagerDataSource;
    }

    @Primary
    @Bean
    public DataSource primaryDataSource() {
        CustomizeRoutingDataSource routingDataSource = new CustomizeRoutingDataSource();

        // 指定默认数据源
        routingDataSource.setDefaultTargetDataSource(userDataSource());

        // 指定 标识->数据源 的map
        HashMap<Object, Object> dsMap = new HashMap<>();
        dsMap.put("user", userDataSource());
        dsMap.put("user2", user2DataSource());
        routingDataSource.setTargetDataSources(dsMap);


        return routingDataSource;
    }

    @Bean // 如果不配置该bean,将会抛出Caused by: java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required异常
    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource primaryDataSource) throws IOException { // 代码写法可参考SqlSessionFactoryBean#afterPropertiesSet

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

        // 设置数据源
        sqlSessionFactoryBean.setDataSource(primaryDataSource);

        // 自定义Configuration
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.getTypeAliasRegistry().registerAliases("com.zzhua.pojo");

        // 添加插件
        configuration.addInterceptor(new DsPlugin());

        sqlSessionFactoryBean.setConfiguration(configuration);

        // 指定mapper.xml文件
        PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources("classpath:com/zzhua/mapper/**.xml"));

        return sqlSessionFactoryBean; // # 之后会调用SqlSessionFactoryBean#afterPropertiesSet()方法
    }

}



3.测试



TestMybatisSpring

public class TestMybatisSpring {
    public static void main(String[] args) {

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MybatisConfig.class);

        PersonMapper personMapper = context.getBean(PersonMapper.class);

		// insert属于修改操作,将会走user2数据库
        personMapper.addPerson("zzhua11");
        personMapper.addPerson("zj11");

		// select属于查询操作,将会走user数据库
        List<Person> persons = personMapper.getPersons();
        System.out.println(persons);
    }
}



4. Aop + 自定义注解实现数据源切换



简单介绍

不同业务的数据源:一般利用AOP,结合自定义注解动态切换数据源

使用AbstractRoutingDataSource作为数据源,暴露给spring使用,而当spring通过数据源 获取数据库连接时,再根据路由标识 确定 此时需要的数据源,从而实现数据源切换。

在user库中新建一张person表



1. 准备基础类



Person

@Data
public class Person {

    private String name;

}



PersonMapper

public interface PersonMapper {

    @Insert("insert into person values(#{name} )")
    void addPerson(String name);

    @Select("select * from person")
    List<Person> getPersons();

}



CustomizeRoutingDataSource

public class CustomizeRoutingDataSource extends AbstractRoutingDataSource {

    private static final ThreadLocal<String> DS_KEY = new ThreadLocal<>();

    @Override
    protected Object determineCurrentLookupKey() {
        return DS_KEY.get();
    }

    public static void setDsKey(String dsKey) {
        DS_KEY.set(dsKey);
    }

    public static void clearDsKey() {
        DS_KEY.remove();
    }

}



2. 修改MyBatisConfig

@Configuration
@MapperScan(basePackages = "com.zzhua.dao.mapper", sqlSessionFactoryRef = "sqlSessionFactoryBean")
public class MybatisConfig {

    @Bean
    public DataSource userDataSource() {
        DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
        driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
        driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        driverManagerDataSource.setUsername("root");
        driverManagerDataSource.setPassword("root");
        return driverManagerDataSource;
    }

    @Bean
    public DataSource user2DataSource() {
        DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
        driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1/user2?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
        driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        driverManagerDataSource.setUsername("root");
        driverManagerDataSource.setPassword("root");
        return driverManagerDataSource;
    }

    @Primary
    @Bean
    public DataSource primaryDataSource() {
    
        CustomizeRoutingDataSource routingDataSource = new CustomizeRoutingDataSource();

        // 指定默认数据源
        routingDataSource.setDefaultTargetDataSource(userDataSource());

        // 指定 标识->数据源 的map
        HashMap<Object, Object> dsMap = new HashMap<>();
        dsMap.put("user", userDataSource());
        dsMap.put("user2", user2DataSource());
        routingDataSource.setTargetDataSources(dsMap);


        return routingDataSource;
    }

    @Bean // 如果不配置该bean,将会抛出Caused by: java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required异常
    public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource primaryDataSource) throws IOException { // 代码写法可参考SqlSessionFactoryBean#afterPropertiesSet

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

        // 设置数据源
        sqlSessionFactoryBean.setDataSource(primaryDataSource);

        // 自定义Configuration
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.getTypeAliasRegistry().registerAliases("com.zzhua.pojo");
        sqlSessionFactoryBean.setConfiguration(configuration);

        // 指定mapper.xml文件
        PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources("classpath:com/zzhua/mapper/**.xml"));

        return sqlSessionFactoryBean; // # 之后会调用SqlSessionFactoryBean#afterPropertiesSet()方法
    }

}



3. 测试



TestMybatisSpring

public class TestMybatisSpring {

    public static void main(String[] args) {

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MybatisConfig.class);

        PersonMapper personMapper = context.getBean(PersonMapper.class);

        // 可使用注解 + AOP的方式简化代码, 在执行数据库操作之前设置所要操作的数据源标识

        CustomizeRoutingDataSource.setDsKey("user");
        personMapper.addPerson("zzhua");
        personMapper.addPerson("zj");
        List<Person> persons = personMapper.getPersons();
        System.out.println(persons);
        CustomizeRoutingDataSource.clearDsKey();


        CustomizeRoutingDataSource.setDsKey("user2");
        personMapper.addPerson("zzhua2");
        personMapper.addPerson("zj2");
        List<Person> persons2 = personMapper.getPersons();
        System.out.println(persons2);
        CustomizeRoutingDataSource.clearDsKey();

    }
}



5. spring整合多份mybatis配置



简单介绍

假设在一个项目中,我们需要操作多个数据源(也就是多个数据库),并且我们都想使用mybatis来操作它们。

现在演示示例:我们有2个数据库user库 和 user2库 这2个数据库,然后,我们分别使用mybatis来做一个简单的查询示例。

创建2个数据库,一个user库(里面有一张user表)和user2库(里面有一张user2表)。现在需要使用mybatis来操作这2个库中的表。

在这里插入图片描述



1. MybatisConfig



MybatisConfig1

这个负责扫描com.zzhua.dao.mapper这个包,数据源指定的使用的user库

@Configuration
@MapperScan(basePackages = "com.zzhua.dao.mapper", sqlSessionFactoryRef = "sqlSessionFactoryBean")
public class MybatisConfig1 {

    @Bean // 如果不配置该bean,将会抛出Caused by: java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required异常
    public SqlSessionFactoryBean sqlSessionFactoryBean() throws IOException { // 代码写法可参考SqlSessionFactoryBean#afterPropertiesSet

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

        // 设置数据源
        DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
        driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
        driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        driverManagerDataSource.setUsername("root");
        driverManagerDataSource.setPassword("root");
        sqlSessionFactoryBean.setDataSource(driverManagerDataSource);

        // 自定义Configuration
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.getTypeAliasRegistry().registerAliases("com.zzhua.pojo");
        sqlSessionFactoryBean.setConfiguration(configuration);

        // 指定mapper.xml文件
        PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources("classpath:com/zzhua/mapper/**.xml"));

        return sqlSessionFactoryBean; // # 之后会调用SqlSessionFactoryBean#afterPropertiesSet()方法
    }

   

}



MybatisConfig2

这个负责扫描com.zzhua.dao.mapper2这个包,数据源指定的使用的user2库

@Configuration
@MapperScan(basePackages = "com.zzhua.dao.mapper2", sqlSessionFactoryRef = "sqlSessionFactoryBean2")
public static class MybatisConfig2 {

    @Bean
    // 如果不配置该bean,将会抛出Caused by: java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required异常
    public SqlSessionFactoryBean sqlSessionFactoryBean2() throws IOException { // 代码写法可参考SqlSessionFactoryBean#afterPropertiesSet

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

        // 设置数据源
        DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
        driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1/user2?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
        driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        driverManagerDataSource.setUsername("root");
        driverManagerDataSource.setPassword("root");
        sqlSessionFactoryBean.setDataSource(driverManagerDataSource);

        // 自定义Configuration
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.getTypeAliasRegistry().registerAliases("com.zzhua.pojo");
        sqlSessionFactoryBean.setConfiguration(configuration);

        // 指定mapper.xml文件
        PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources("classpath:com/zzhua/mapper2/**.xml"));

        return sqlSessionFactoryBean; // # 之后会调用SqlSessionFactoryBean#afterPropertiesSet()方法
    }
}



2. 测试



TestMybatisSpring

下面的测试,可以成功的分别查询到 user库 和 user2库 不同数据库中的数据,也即实现了使用mybatis操作多个数据源。

public class TestMybatisSpring {
    public static void main(String[] args) {

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MybatisConfig.class);

        UserMapper userMapper = context.getBean(UserMapper.class);

        List<User> userList = userMapper.findAll();
        System.out.println(userList);

        User user = userMapper.findOne("zzhua");
        System.out.println(user);

        System.out.println("--------------");

        UserMapper2 userMapper2 = context.getBean(UserMapper2.class);
        List<User2> userList2 = userMapper2.findAll2();
        System.out.println(userList2);

        User2 user2 = userMapper2.findOne2("zzhua2");
        System.out.println(user2);

    }
}



二、多数据源事务控制

在多数据源下,由于涉及到数据库的多个读写。一旦发生异常就可能会导致数据不一致的情况, 在这种情况希望使用事务进行回退。


Spring的声明式事务在一次请求线程中只能使用一个数据源进行控制

,可参考:

spring事务源码学习

但是是对于多源数据库:

  • 单一事务管理器(TransactionManager)无法切换数据源,需要配置多个TransactionManager。
  • @Transactionnal是无法管理多个数据源的。

    如果想真正实现多源数据库事务控制,肯定是需要分布式事务

    。这里讲解多源数据库事务控制的一种变通方式。



spring事务在多数据源情况下的问题



示例

以上面spring整合多分mybatis配置为基础



MybatisConfig1 mybatis+spring整合user库

@Configuration
@MapperScan(basePackages = "com.zzhua.dao.mapper", sqlSessionFactoryRef = "sqlSessionFactoryBean")
public class MybatisConfig1 {


    @Bean
    public DataSource dataSource1() {
        // 设置数据源
        DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
        driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
        driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        driverManagerDataSource.setUsername("root");
        driverManagerDataSource.setPassword("root");
        return driverManagerDataSource;
    }

    @Bean
    // 如果不配置该bean,将会抛出Caused by: java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required异常
    public SqlSessionFactoryBean sqlSessionFactoryBean() throws IOException { // 代码写法可参考SqlSessionFactoryBean#afterPropertiesSet

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();

        sqlSessionFactoryBean.setDataSource(dataSource1());

        // 自定义Configuration
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.getTypeAliasRegistry().registerAliases("com.zzhua.pojo");
        sqlSessionFactoryBean.setConfiguration(configuration);

        // 指定mapper.xml文件
        PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources("classpath:com/zzhua/mapper/**.xml"));

        return sqlSessionFactoryBean; // # 之后会调用SqlSessionFactoryBean#afterPropertiesSet()方法
    }



}



MybatisConfig2类 mybatis+spring整合user2库

@Configuration
@MapperScan(basePackages = "com.zzhua.dao.mapper2", sqlSessionFactoryRef = "sqlSessionFactoryBean2")
public class MybatisConfig2 {

    @Bean
    public DataSource dataSource2() {
        // 设置数据源
        DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
        driverManagerDataSource.setUrl("jdbc:mysql://127.0.0.1/user2?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true");
        driverManagerDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        driverManagerDataSource.setUsername("root");
        driverManagerDataSource.setPassword("root");
        return driverManagerDataSource;
    }

    @Bean
    // 如果不配置该bean,将会抛出Caused by: java.lang.IllegalArgumentException: Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required异常
    public SqlSessionFactoryBean sqlSessionFactoryBean2(DataSource dataSource2) throws IOException { // 代码写法可参考SqlSessionFactoryBean#afterPropertiesSet

        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();



        sqlSessionFactoryBean.setDataSource(dataSource2());

        // 自定义Configuration
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.getTypeAliasRegistry().registerAliases("com.zzhua.pojo");
        sqlSessionFactoryBean.setConfiguration(configuration);

        // 指定mapper.xml文件
        PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resourcePatternResolver.getResources("classpath:com/zzhua/mapper2/**.xml"));

        return sqlSessionFactoryBean; // # 之后会调用SqlSessionFactoryBean#afterPropertiesSet()方法
    }
}



Config类 配置事务管理器

@Import({MybatisConfig1.class,MybatisConfig2.class, UserService.class})
public class Config {

    @Autowired
    private DataSource dataSource1;

    @Autowired
    private DataSource dataSource2;

    @Bean
    public PlatformTransactionManager transactionManager1(DataSource dataSource1) {
        return new DataSourceTransactionManager(dataSource1);
    }

    @Bean
    public PlatformTransactionManager transactionManager2(DataSource dataSource2) {
        return new DataSourceTransactionManager(dataSource2);
    }

}



UserService

@Service
@EnableTransactionManagement
public class UserService {

    @Autowired
    private UserMapper userMapper; // 数据库user

    @Autowired
    private UserMapper2 userMapper2; // 数据库user2

	// 指定此事务注解所要使用的事务管理器(必须要配置了事务管理器,才可以使用这个注解)
	// springboot下在检测到项目中只有一个数据源的情况下,会通过自动配置原理,
	//           将此数据源注入,配置到一个DataSourceTransactionManager事务管理器中
    @Transactional(transactionManager = "transactionManager1")
    public void addUserTx() {

        User user1 = new User();
        user1.setUserAccount("user1");
        userMapper.addUser(user1);

        User user2 = new User();
        user2.setUserAccount("user2");
        userMapper2.addUser(user2);

        int i = 1 / 0;

    }

}



测试

发现只有user库回滚了,user2库仍然会添加1条数据,这样就说明事务控制是不彻底的,需要另外的方式去实现。

public class TestMybatisSpring {
    public static void main(String[] args) {

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);


        UserService userService = context.getBean(UserService.class);

        userService.addUserTx();

        System.out.println();


    }
}



使用编程式事务解决

我们只需要在上面的基础上修改UserService即可



修改UserService

TransactionTemplate是spring提供的,它简化了对事务管理器的使用,可以将它们定义成bean,这里仅作演示,所以写在方法里了

@Service
@EnableTransactionManagement
public class UserService {

    @Autowired
    private UserMapper userMapper; // 数据库user

    @Autowired
    private UserMapper2 userMapper2; // 数据库user2

    @Autowired
    private PlatformTransactionManager transactionManager1;

    @Autowired
    private PlatformTransactionManager transactionManager2;

    public void addUserTx() {

        TransactionTemplate transactionTemplate1 = new TransactionTemplate(transactionManager1);
        TransactionTemplate transactionTemplate2 = new TransactionTemplate(transactionManager2);

        transactionTemplate1.execute(status1 -> {
            transactionTemplate2.execute(status2 -> {

                try {
                    User user1 = new User();
                    user1.setUserAccount("user1");
                    userMapper.addUser(user1);

                    User user2 = new User();
                    user2.setUserAccount("user2");
                    userMapper2.addUser(user2);

                    int i = 1 / 0;
                    return null;
                } catch (Exception e) {

                    status1.setRollbackOnly();
                    status2.setRollbackOnly();

                    return null;
                }
            });

            return null;
        });



    }

}



测试

没有任何改动,user库和user2库在发生异常时,都正常回滚了

public class TestMybatisSpring {
    public static void main(String[] args) {

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);


        UserService userService = context.getBean(UserService.class);

        userService.addUserTx();

        System.out.println();


    }
}



使用声明式事务解决



修改UserService

  • 将原来的addUserTx包到addUser方法中调用,在addUser上添加transactionManager1作为事务管理器
  • 将当前的代理对象暴露到当前本地线程,需要设置exposeProxy为true,并且这里要使用代理对象这样去调用的原因是:aop责任链调用的末端是真实的对象,如果直接去调用(t例如:在addUser方法中,直接this.addUserTx())的话,就只会回滚transactionManager1的数据
  • 当同时使用@EnableAspectJAutoProxy(添加的是:

    AnnotationAwareAspectJAutoProxyCreator

    )和@EnableTransactionManagement(添加的是:

    InfrastructureAdvisorAutoProxyCreator

    )这2个注解时,spring会比较这2个代理创建器的优先级,索引越大的,越优先,默认的排序是:【InfrastructureAdvisorAutoProxyCreator、AspectJAwareAdvisorAutoProxyCreator、AnnotationAwareAspectJAutoProxyCreator】,所以最终只会添加1个自动代理创建器,就是

    AnnotationAwareAspectJAutoProxyCreator

    ,因此,exposeProxy的属性需要通过这个自动代理创建器的注解中设置。
@Service
@EnableAspectJAutoProxy(exposeProxy = true)
@EnableTransactionManagement
public class UserService {

    @Autowired
    private UserMapper userMapper; // 数据库user

    @Autowired
    private UserMapper2 userMapper2; // 数据库user2

    @Autowired
    private PlatformTransactionManager transactionManager1;

    @Autowired
    private PlatformTransactionManager transactionManager2;

    @Transactional(transactionManager = "transactionManager1")
    public void addUser() {

        UserService userService = (UserService) AopContext.currentProxy();
        userService.addUserTx();

    }

    @Transactional(transactionManager = "transactionManager2")
    public void addUserTx() {

        User user1 = new User();
        user1.setUserAccount("user1");
        userMapper.addUser(user1);

        User user2 = new User();
        user2.setUserAccount("user2");
        userMapper2.addUser(user2);

        int i = 1 / 0;


    }

}



测试

改为调用addUser方法,当addUserTx发生异常时,user库和user2库都回滚了

public class TestMybatisSpring {
    public static void main(String[] args) {

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);


        UserService userService = context.getBean(UserService.class);

        userService.addUser();

        System.out.println();


    }
}



版权声明:本文为qq_16992475原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。