[TOC]
# 目录
## 一、前言
通过例子来说明,在庞大的数据量下,我们可能会分N个库来实现某些场景业务需求。<br> <font color="red">(不推荐使用事务补偿机制,会产生大量的业务代码) </font>
## 二、需求
工商银行有一个深圳用户(张某,后续简称:A),想转账给广州的用户(李某,后续简称:B),同程序不同的数据库
### 分析
1. A 转账给 B, 在不同的数据库怎么解决?
2. A 转账给 B, 在程序途中抛出异常怎么解决?
3. A 转账给 B, 在最后出现异常怎么解决?
4. 通过1-3的问题,使用事务补偿机制。
## 三、代码例子
### DDL
<details>
<summary>建表与测试数据</summary>
```sql
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
```
</details>
### 所需依赖
<details>
<summary>pom.xml</summary>
```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>
```
</details>
### Mybatis 逆向生成
<details>
<summary>generatorConfig.xml</summary>
```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&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>
```
</details>
### 数据源_246
<details>
<summary>ConfigDb246</summary>
```java
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);
}
}
```
</details>
### 数据源_247
<details>
<summary>ConfigDb247</summary>
```java
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);
}
}
```
</details>
### 业务层
```java
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;
}
}
}
```
### 单元测试
```java
@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();
}
}
```
事务补偿机制是什么?