事务补偿机制是什么? | Eddie'Blog
事务补偿机制是什么?

事务补偿机制是什么?

eddie 428 2020-11-25

目录

一、前言

通过例子来说明,在庞大的数据量下,我们可能会分N个库来实现某些场景业务需求。
(不推荐使用事务补偿机制,会产生大量的业务代码)

二、需求

工商银行有一个深圳用户(张某,后续简称:A),想转账给广州的用户(李某,后续简称:B),同程序不同的数据库

分析

  1. A 转账给 B, 在不同的数据库怎么解决?
  2. A 转账给 B, 在程序途中抛出异常怎么解决?
  3. A 转账给 B, 在最后出现异常怎么解决?
  4. 通过1-3的问题,使用事务补偿机制。

三、代码例子

DDL

建表与测试数据
CREATE TABLE `xa_246`.`account_a`  (
  `id` int(11) NOT NULL,
  `name` varchar(255) NULL,
  `balance` decimal(10, 2) NULL,
  PRIMARY KEY (`id`)
)

INSERT INTO `xa_246`.`account_a`(`id`, `name`, `balance`) VALUES (1, '用户A', 1000)
SELECT * FROM `xa_246`.`account_a` WHERE `id` = 1


CREATE TABLE `xa_247`.`account_b`  (
  `id` int(11) NOT NULL,
  `name` varchar(255) NULL,
  `balance` decimal(10, 2) NULL,
  PRIMARY KEY (`id`)
)

INSERT INTO `xa_247`.`account_b`(`id`, `name`, `balance`) VALUES (2, '用户B', 1000)
SELECT * FROM `xa_247`.`account_b` WHERE `id` = 2

所需依赖

pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>tcc-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>tcc-demo</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <!-- mybatis生成器 -->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.7</version>
                <dependencies>
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>8.0.17</version>
                    </dependency>
                </dependencies>
            </plugin>

        </plugins>
    </build>

</project>

Mybatis 逆向生成

generatorConfig.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <context id="MysqlTables" targetRuntime="MyBatis3">
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://192.168.8.247:3306/xa_247?serverTimezone=Asia/Shanghai&amp;useSSL=false"
                        userId="eddie"
                        password="Abc@123456">
            <property name="nullCatalogMeansCurrent" value="true" />
        </jdbcConnection>

        <javaTypeResolver >
            <property name="forceBigDecimals" value="false" />
        </javaTypeResolver>

        <javaModelGenerator targetPackage="com.example.tccdemo.db247.model" targetProject="src\main\java">
            <property name="enableSubPackages" value="true" />
            <property name="trimStrings" value="true" />
        </javaModelGenerator>

        <sqlMapGenerator targetPackage="mybatis/db247"  targetProject="src\main\resources">
            <property name="enableSubPackages" value="true" />
        </sqlMapGenerator>

        <javaClientGenerator type="XMLMAPPER" targetPackage="com.example.tccdemo.db247.dao"  targetProject="src\main\java">
            <property name="enableSubPackages" value="true" />
        </javaClientGenerator>

        <table schema="xa_247" tableName="account_b" domainObjectName="AccountB" />

    </context>
</generatorConfiguration>

数据源_246

ConfigDb246
package com.example.tccdemo.config;

import com.mysql.cj.jdbc.MysqlDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.io.IOException;

/**
 * @author eddie.lee
 * @ProjectName tcc-demo
 * @Package com.example.tccdemo.config
 * @ClassName ConfigDb246
 * @description
 * @date created in 2020-11-25 9:59
 * @modified by
 */
@Configuration
@MapperScan(value = "com.example.tccdemo.db246.dao", sqlSessionFactoryRef = "sqlSessionFactoryBean246")
public class ConfigDb246 {

    @Bean("db246")
    public DataSource db246() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser("eddie");
        dataSource.setPassword("Abc@123456");
        dataSource.setUrl("jdbc:mysql://192.168.8.246:3306/xa_246");
        return dataSource;
    }

    @Bean("sqlSessionFactoryBean246")
    public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("db246") DataSource dataSource) throws IOException {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        // 设置Mybatis
        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resourceResolver.getResources("mybatis/db246/*.xml"));
        return sqlSessionFactoryBean;
    }

    /**
     * Spring的事务管理器
     */
    @Bean("tm246")
    public PlatformTransactionManager transactionManager(@Qualifier("db246") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
}

数据源_247

ConfigDb247
package com.example.tccdemo.config;

import com.mysql.cj.jdbc.MysqlDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.io.IOException;

/**
 * @author eddie.lee
 * @ProjectName tcc-demo
 * @Package com.example.tccdemo.config
 * @ClassName ConfigDb247
 * @description
 * @date created in 2020-11-25 9:59
 * @modified by
 */
@Configuration
@MapperScan(value = "com.example.tccdemo.db247.dao", sqlSessionFactoryRef = "sqlSessionFactoryBean247")
public class ConfigDb247 {

    @Bean("db247")
    public DataSource db247() {
        MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUser("eddie");
        dataSource.setPassword("Abc@123456");
        dataSource.setUrl("jdbc:mysql://192.168.8.247:3306/xa_247");
        return dataSource;
    }

    @Bean("sqlSessionFactoryBean247")
    public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("db247") DataSource dataSource) throws IOException {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        // 设置Mybatis
        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
        sqlSessionFactoryBean.setMapperLocations(resourceResolver.getResources("mybatis/db247/*.xml"));
        return sqlSessionFactoryBean;
    }

    /**
     * Spring的事务管理器
     */
    @Bean("tm247")
    public PlatformTransactionManager transactionManager(@Qualifier("db247") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

}

业务层

package com.example.tccdemo.service;

import com.example.tccdemo.db246.dao.AccountAMapper;
import com.example.tccdemo.db246.model.AccountA;
import com.example.tccdemo.db247.dao.AccountBMapper;
import com.example.tccdemo.db247.model.AccountB;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.math.BigDecimal;

/**
 * @author eddie.lee
 * @ProjectName tcc-demo
 * @Package com.example.tccdemo.service
 * @ClassName AccountService
 * @description
 * @date created in 2020-11-25 11:22
 * @modified by
 */
@Service
public class AccountService {

    @Resource
    private AccountAMapper accountAMapper;

    @Resource
    private AccountBMapper accountBMapper;

    /**
     * 测试1:
     *      A账户已经扣除200,途中发生异常,导致B账户没有追加200.
     *      没有进行事务的回滚,
     */
    @Transactional
    public void transferAcount() {
        AccountA accountA = accountAMapper.selectByPrimaryKey(1);
        // A账户减去200元
        accountA.setBalance(accountA.getBalance().subtract(new BigDecimal(200)));
        accountAMapper.updateByPrimaryKey(accountA);

        // 抛出异常"java.lang.ArithmeticException: / by zero", 查看B是否继续加200
        int i = 1/0;

        AccountB accountB = accountBMapper.selectByPrimaryKey(2);
        // B账户加上200元
        accountB.setBalance(accountB.getBalance().add(new BigDecimal(200)));
        accountBMapper.updateByPrimaryKey(accountB);
    }

    /**
     * 测试2:
     *      A账户没有扣除金额,B账户加了200
     *      主要原因是:“事务管理器tm246”, 只能保证一个操作数据库的事务, 另外一个不会保证的。
     */
    @Transactional(transactionManager = "tm246")
    public void transferAcount2() {
        AccountA accountA = accountAMapper.selectByPrimaryKey(1);
        // A账户减去200元
        accountA.setBalance(accountA.getBalance().subtract(new BigDecimal(200)));
        accountAMapper.updateByPrimaryKey(accountA);

        AccountB accountB = accountBMapper.selectByPrimaryKey(2);
        // B账户加上200元
        accountB.setBalance(accountB.getBalance().add(new BigDecimal(200)));
        accountBMapper.updateByPrimaryKey(accountB);

        // 抛出异常"java.lang.ArithmeticException: / by zero", 查看B是否继续加200
        int i = 1/0;
    }

    /**
     * 测试3:
     *      1、@Transactional加入事务管理器 tm246
     *      2、A账户减少200元
     *      3、B账户加上200号
     *      4、异常放在try/catch里面
     *      5、通过catch进行事务补偿机制, 因为在(1)事务管理器只针对A账户回滚,
     *         而没有回滚B账户,所以B账户需要减去try之前加的200元
     */
    @Transactional(transactionManager = "tm246", rollbackFor = Exception.class)
    public void transferAcount3() {
        AccountA accountA = accountAMapper.selectByPrimaryKey(1);
        // A账户减去200元
        accountA.setBalance(accountA.getBalance().subtract(new BigDecimal(200)));
        accountAMapper.updateByPrimaryKey(accountA);

        AccountB accountB = accountBMapper.selectByPrimaryKey(2);
        // B账户加上200元
        accountB.setBalance(accountB.getBalance().add(new BigDecimal(200)));
        accountBMapper.updateByPrimaryKey(accountB);

        try {
            // 抛出异常"java.lang.ArithmeticException: / by zero", 查看B是否继续加200
            int i = 1/0;
        } catch (Exception e) {
            // *******************
            // *** 事务补偿机制 ***
            // *******************
            AccountB accountB1 = accountBMapper.selectByPrimaryKey(2);
            // B账户减去200元
            accountB1.setBalance(accountB1.getBalance().subtract(new BigDecimal(200)));
            accountBMapper.updateByPrimaryKey(accountB1);

            throw e;
        }
    }
}

单元测试

@RunWith(SpringRunner.class)
@SpringBootTest
class TccDemoApplicationTests {

    @Autowired
    private AccountService accountService;

    @Test
    void contextLoads() {
    }

    @Test
    void testAcount() {
        accountService.transferAcount();
    }

    @Test
    void testAcount2() {
        accountService.transferAcount2();
    }

    @Test
    void testAcount3() {
        accountService.transferAcount3();
    }

}


# Java # MySQL # 分布式事务