SpringBoot integrates Mybatis to implement multi-data source configuration and cross-data source transaction instances

Pom dependency

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.2.RELEASE</version>
    <relativePath/>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.14</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
    </dependency>
    <!-- Paging plugin -->
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper-spring-boot-starter</artifactId>
        <version>1.3.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
</dependencies>

application.yml

# data source
spring:
datasource:
# master main data source configuration
master:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ds1?characterEncoding=UTF-8 & serverTimezone=GMT+8
username: root
password: root
# cluster is configured from the data source
cluster:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ds2?characterEncoding=UTF-8 & serverTimezone=GMT+8
username: root
password: root
#pagehelperPaging plug-in configuration
pagehelper:
master:
helper-dialect: mysql #Set database type
reasonable: true #Turn on rationalization: page number <=0 to query the first page, page number >= total number of pages to query the last page
support-methods-arguments: true #Support passing paging parameters through Mapper interface parameters
params: count=countsql

Multiple data source configuration

Create master-slave data source configuration in Java code. Use @Primary: to specify the injection priority of the bean. Bean objects modified by @Primary are injected first

MasterDataSourceConfig

/**
 * @ClassName: MasterDataSourceConfig
 * @Description: mysql main library configuration class
 */
@Configuration
@MapperScan(basePackages = MasterDataSourceConfig.PACKAGE,sqlSessionTemplateRef = "masterSqlSessionTemplate")
public class MasterDataSourceConfig {
    /**
     * Set the path to scan the package
     */
    public static final String PACKAGE = "cn.zysheep.dao.master";
    /**
     * Mapper configuration file path
     */
    public static final String MAPPER_LOCATION = "classpath*:mapper/master/*.xml";
    /**
     * Entity alias
     */
    public static final String ENTITY_ALIASES = "cn.zysheep.entity";
    @Value("${spring.datasource.master.driverClassName}")
    private String driverClassName;
    @Value("${spring.datasource.master.url}")
    private String url;
    @Value("${spring.datasource.master.username}")
    private String user;
    @Value("${spring.datasource.master.password}")
    private String password;
    @Value("${pagehelper.master.helper-dialect}")
    private String helperDialect;
    @Value("${pagehelper.master.reasonable}")
    private String reasonable;
    /**
     *Create data source
     * @return DataSource
     * @Primary: used to specify the injection priority of the bean. Bean objects modified by @Primary are injected first
     */
    @Bean(name = "masterDataSource")
    @Primary
    public DataSource masterDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setUsername(user);
        dataSource.setPassword(password);
        return dataSource;
    }
    /**
     * Create factory
     *@param dataSource
     *@throwsException
     *@return SqlSessionFactory
     */
    @Bean(name = "masterSqlSessionFactory")
    @Primary
    public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_LOCATION));
        sessionFactory.setTypeAliasesPackage(ENTITY_ALIASES);
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        //The field value can also be inserted if it is empty
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.setCallSettersOnNulls(true);
        // mybatis log
        configuration.setLogImpl(StdOutImpl.class);
        sessionFactory.setConfiguration(configuration);
        PageInterceptor pageInterceptor = new PageInterceptor();
        //Paging configuration
        Properties p = new Properties();
        p.setProperty("helperDialect", helperDialect);
        p.setProperty("reasonable", reasonable);
        pageInterceptor.setProperties(p);
        sessionFactory.setPlugins(pageInterceptor);
        return sessionFactory.getObject();
    }
    /**
     * Create transaction
     *@param dataSource
     *@return DataSourceTransactionManager
     */
    @Bean(name = "masterTransactionManager")
    @Primary
    public DataSourceTransactionManager masterDataSourceTransactionManager(@Qualifier("masterDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
    /**
     * Create template
     *@param sqlSessionFactory
     *@return SqlSessionTemplate
     */
    @Bean(name = "masterSqlSessionTemplate")
    @Primary
    public SqlSessionTemplate masterSqlSessionTemplate(@Qualifier("masterSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

ClusterDataSourceConfig

/**
 * @ClassName: DataSourceConfig
 * @Description: mysql slave library configuration class
 */
@Configuration
@MapperScan(basePackages = ClusterDataSourceConfig.PACKAGE,sqlSessionTemplateRef = "clusterSqlSessionTemplate")
public class ClusterDataSourceConfig {
    /**
     * Set the path to scan the package
     */
    public static final String PACKAGE = "cn.zysheep.dao.cluster";
    /**
     * Mapper configuration file path
     */
    public static final String MAPPER_LOCATION = "classpath*:mapper/cluster/*.xml";
    /**
     * Entity alias
     */
    public static final String ENTITY_ALIASES = "cn.zysheep.entity";
    @Value("${spring.datasource.cluster.driverClassName}")
    private String driverClassName;
    @Value("${spring.datasource.cluster.url}")
    private String url;
    @Value("${spring.datasource.cluster.username}")
    private String user;
    @Value("${spring.datasource.cluster.password}")
    private String password;
    @Value("${pagehelper.master.helper-dialect}")
    private String helperDialect;
    @Value("${pagehelper.master.reasonable}")
    private String reasonable;
    /**
     *Create data source
     *@return DataSource
     */
    @Bean(name = "clusterDataSource")
    public DataSource masterDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driverClassName);
        dataSource.setUrl(url);
        dataSource.setUsername(user);
        dataSource.setPassword(password);
        return dataSource;
    }
    /**
     * Create factory
     *@param dataSource
     *@throwsException
     *@return SqlSessionFactory
     */
    @Bean(name = "clusterSqlSessionFactory")
    public SqlSessionFactory masterSqlSessionFactory(@Qualifier("clusterDataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_LOCATION));
        sessionFactory.setTypeAliasesPackage(ENTITY_ALIASES);
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        //The field value can also be inserted if it is empty
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.setCallSettersOnNulls(true);
        // mybatis log
        configuration.setLogImpl(StdOutImpl.class);
        sessionFactory.setConfiguration(configuration);
        PageInterceptor pageInterceptor = new PageInterceptor();
        //Paging configuration
        Properties p = new Properties();
        p.setProperty("helperDialect", helperDialect);
        p.setProperty("reasonable", reasonable);
        pageInterceptor.setProperties(p);
        sessionFactory.setPlugins(pageInterceptor);
        return sessionFactory.getObject();
    }
    /**
     * Create transaction manager
     *@param dataSource
     *@return DataSourceTransactionManager
     */
    @Bean(name = "clusterTransactionManager")
    public DataSourceTransactionManager masterDataSourceTransactionManager(@Qualifier("clusterDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
    /**
     * Create template
     *@param sqlSessionFactory
     *@return SqlSessionTemplate
     */
    @Bean(name = "clusterSqlSessionTemplate")
    public SqlSessionTemplate masterSqlSessionTemplate(@Qualifier("clusterSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

Startup class

Add the @EnableTransactionManagement annotation to the startup class to enable annotation transactions

@EnableTransactionManagement
@SpringBootApplication(exclude = PageHelperAutoConfiguration.class)
public class MultiDataSourceApplication {
    public static void main(String[] args) {
        SpringApplication.run(MultiDataSourceApplication.class, args);
    }
}

Use

Add the @Transactional annotation to the service layer class or method. And specify the value of the attribute transaction manager transactionManager, and select different data transaction managers according to the different data sources used.

@Transactional(transactionManager = "masterTransactionManager")
public void insertUser() {
    // TODO add user business operation
}
@Transactional(transactionManager = "clusterTransactionManager")
public void listUser() {
    // TODO query user business operations
}

Thinking: @Transactional can only specify one transaction manager, and annotations are not allowed to be repeated, so you can only use the transaction manager of one data source. So what should we do if a method involves multiple data source operations and needs to ensure transaction consistency?

Implement cross-data source transactions

Using programmatic transactions to implement cross-data source transactions based on annotations + AOP

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiDataSourceTransactional {
    /**
     *Transaction manager array
     */
    String[] transactionManagers();
}

First, transactions from multiple data sources are opened separately, and then each transaction executes the corresponding SQL (this is the so-called first-stage commit). Finally, if all transactions are successful, all transactions will be submitted. As long as one fails, all transactions will be rolled back. –This is the so-called second stage submission.

/**
 *Points to note:
 * 1) The order of declaring transactions and committing transactions or rolling back transactions should be reversed, that is, first in, last out, so a stack is used for storage.
 *2) Remember to clear local variables after the thread execution ends.
 * 3) You can refer to the attribute upgrade functions of @Transactional, such as isolation level rollback exceptions, etc.
 */
@Component
@Aspect
public class MultiTransactionAop {
    /**
     * Thread local variables: why use the stack? ※In order to achieve the last-in-first-out effect※
     */
    private static final ThreadLocal<Stack<Map<DataSourceTransactionManager, TransactionStatus>>> THREAD_LOCAL = new ThreadLocal<>();
    private static Logger logger = LoggerFactory.getLogger(MultiTransactionAop.class);
    /**
     * Used to get the transaction manager
     */
    @Autowired
    private ApplicationContext applicationContext;
    /**
     * Statement of affairs
     */
    private DefaultTransactionDefinition def = new DefaultTransactionDefinition();
    {
        // non-read-only mode
        def.setReadOnly(false);
        //Transaction isolation level: using database
        def.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT);
        // Transaction propagation behavior
        def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    }
    /**
     * Cut-off point
     */
    @Pointcut("@annotation(cn.zysheep.datasource.anntation.MultiDataSourceTransactional)")
    public void pointcut() {
    }
    /**
     *Declaration transaction
     *
     * @param transactional annotation
     */
    @Before("pointcut() & amp; & amp; @annotation(transactional)")
    public void before(MultiDataSourceTransactional transactional) {
        //Declare in order according to the set transaction name and put it in ThreadLocal
        String[] transactionManagerNames = transactional.transactionManagers();
        Stack<Map<DataSourceTransactionManager, TransactionStatus>> pairStack = new Stack<>();
        for (String transactionManagerName : transactionManagerNames) {
            DataSourceTransactionManager transactionManager = applicationContext.getBean(transactionManagerName, DataSourceTransactionManager.class);
            TransactionStatus transactionStatus = transactionManager.getTransaction(def);
            Map<DataSourceTransactionManager, TransactionStatus> transactionMap = new HashMap<>();
            transactionMap.put(transactionManager, transactionStatus);
            pairStack.push(transactionMap);
        }
        THREAD_LOCAL.set(pairStack);
    }
    /**
     * Post enhancement, equivalent to AfterReturningAdvice, executed when the method exits
     *
     * Submit transaction
     */
    @AfterReturning("pointcut()")
    public void afterReturning() {
        // ※Pop from the top of the stack (last in, first out)
        Stack<Map<DataSourceTransactionManager, TransactionStatus>> pairStack = THREAD_LOCAL.get();
        while (!pairStack.empty()) {
            Map<DataSourceTransactionManager, TransactionStatus> pair = pairStack.pop();
            pair.forEach((key,value)->key.commit(value));
        }
        THREAD_LOCAL.remove();
    }
    /**
     * Exception throwing enhancement, equivalent to ThrowsAdvice
     *
     * Rollback transaction
     */
    @AfterThrowing(value = "pointcut()")
    public void afterThrowing() {
        // ※Pop from the top of the stack (last in, first out)
        Stack<Map<DataSourceTransactionManager, TransactionStatus>> pairStack = THREAD_LOCAL.get();
        logger.info("=========================");
        logger.info("Pair Stack:{}", pairStack);
        logger.info("=========================");
        while (!pairStack.empty()) {
            Map<DataSourceTransactionManager, TransactionStatus> pair = pairStack.pop();
            pair.forEach((key,value)->key.rollback(value));
        }
        THREAD_LOCAL.remove();
    }
}

Add our previous multi-data source transaction annotation to the transaction method

 /**
     * Test multiple data source transactions
     */
    @MultiDataSourceTransactional(transactionManagers = {"masterTransactionManager", "clusterTransactionManager"})
    @Override
    public void saveUser() {
        String rid = UUID.randomUUID().toString();
        User user = new User();
        user.setId(rid);
        userMapper.saveUser(user);
        Log log = new Log();
        log.setId(rid);
        logMapper.saveLog(log);
        throw new RuntimeException();
    }

This concludes this article about SpringBoot integrating Mybatis to implement multi-data source configuration and cross-data source transaction instances.

The knowledge points of the article match the official knowledge files, and you can further learn relevant knowledge. Cloud native entry-level skills treeHomepageOverview 16743 people are learning the system