分佈式事務(1)-理論基礎
分佈式事務(2)---強一致性分佈式事務解決方案
分佈式事務(4)---最終一致性方案之TCC
前面介紹強一致性分佈式解決方案,這裏用Atomikos框架寫一個實戰的demo。模擬下單扣減庫存的操作。
使用Atomikos,mybatis-plus框架搭建項目,springboot版本 2.3.2.RELEASE。
1.項目搭建
依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jta-atomikos</artifactId> </dependency>
庫存:
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import lombok.Data; @Data public class Storage { @TableId(type= IdType.AUTO) private Integer id; private Integer commodityId; private Integer quantity; }
訂單:
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.math.BigDecimal; @Data @TableName("t_order") public class Order { @TableId(type= IdType.AUTO) private Integer id; private String userId; private Integer commodityId; private Integer quantity; private BigDecimal price; private Integer status; }
初始化sql:需要建兩個數據庫,我這裏建了一個njytest1和njytest2,讓訂單表和存庫表在不同數據庫生成初始化表數據。
CREATE TABLE `storage` ( `id` int(11) NOT NULL AUTO_INCREMENT, `commodity_id` int(11) NOT NULL, `quantity` int(11) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `idx_commodity_id` (`commodity_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; INSERT INTO `storage` (`id`, `commodity_id`, `quantity`) VALUES (1, 1, 10); CREATE TABLE `t_order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` varchar(255) DEFAULT NULL, `commodity_id` int(11) NOT NULL, `quantity` int(11) DEFAULT 0, `price` decimal (10,2) DEFAULT NULL , `status` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
2.配置
數據庫1配置類,用於接收數據源1的配置:
import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * Description: * Created by nijunyang on 2021/12/2 23:57 */ @Data @ConfigurationProperties(prefix = "mysql.datasource1") @Component public class DBConfig1 { private String url; private String username; private String password; private int minPoolSize; private int maxPoolSize; private int maxLifetime; private int borrowConnectionTimeout; private int loginTimeout; private int maintenanceInterval; private int maxIdleTime; private String testQuery; }
數據庫2的配置類,用於接收數據源2的配置:
import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * Description: * Created by nijunyang on 2021/12/3 0:00 */ @Data @ConfigurationProperties(prefix = "mysql.datasource2") @Component public class DBConfig2 { private String url; private String username; private String password; private int minPoolSize; private int maxPoolSize; private int maxLifetime; private int borrowConnectionTimeout; private int loginTimeout; private int maintenanceInterval; private int maxIdleTime; private String testQuery; }
application.yml配置文件中對應的兩個數據源配置:
mysql: datasource1: url: jdbc:mysql://localhost:3306/njytest1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: root minPoolsize: 3 maxPoolSize: 25 maxLifetime: 30000 borrowConnectionTimeout: 30 loginTimeout: 30 maintenanceInterval: 60 maxIdleTime: 60 testQuery: SELECT 1 datasource2: url: jdbc:mysql://localhost:3306/njytest2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: root password: root minPoolsize: 3 maxPoolSize: 25 maxLifetime: 30000 borrowConnectionTimeout: 30 loginTimeout: 30 maintenanceInterval: 60 maxIdleTime: 60 testQuery: SELECT 1 logging: level: com.nijunyang.tx.xa.mapper1: debug com.nijunyang.tx.xa.mapper2: debug
我們需要將我們的mapper放到兩個不同的包下面,才能給兩個mapper配置不同的數據源。
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.nijunyang.tx.common.entity.Order; import org.springframework.stereotype.Repository; /** * Description: * Created by nijunyang on 2021/12/3 0:09 */ @Repository public interface OrderMapper extends BaseMapper<Order> { }
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.nijunyang.tx.common.entity.Storage; import org.apache.ibatis.annotations.Update; import org.springframework.stereotype.Repository; /** * Description: * Created by nijunyang on 2021/12/3 0:10 */ @Repository public interface StorageMapper extends BaseMapper<Storage> { @Update("UPDATE storage SET quantity = quantity - #{quantity} WHERE commodity_id = #{commodityId} and quantity >= #{quantity}") void reduce(Integer commodityId, Integer quantity); }
分別配置兩個數據源的mybatis配置:
MyBatisConfig1 制定使用com.nijunyang.tx.xa.mapper1包, 並且指定其sqlSessionTemplate 爲名爲orderSqlSessionTemplate的bean;
MyBatisConfig2 制定使用com.nijunyang.tx.xa.mapper2包, 並且指定其sqlSessionTemplate 爲名爲storageSqlSessionTemplate的bean;
也就是這兩個配置:
@MapperScan(basePackages = "com.nijunyang.tx.xa.mapper1", sqlSessionTemplateRef = "orderSqlSessionTemplate")
@MapperScan(basePackages = "com.nijunyang.tx.xa.mapper2", sqlSessionTemplateRef = "storageSqlSessionTemplate")
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import com.mysql.cj.jdbc.MysqlXADataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import javax.sql.DataSource; import java.sql.SQLException; /** * Description: * Created by nijunyang on 2021/12/3 0:11 */ @Configuration /** * 制定此mapper使用哪個sqlSessionTemplate */ @MapperScan(basePackages = "com.nijunyang.tx.xa.mapper1", sqlSessionTemplateRef = "orderSqlSessionTemplate") public class MyBatisConfig1 { //配置XA數據源 @Primary @Bean(name = "orderDataSource") public DataSource orderDataSource(DBConfig1 dbConfig1) throws SQLException { MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource(); mysqlXaDataSource.setUrl(dbConfig1.getUrl()); mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true); mysqlXaDataSource.setPassword(dbConfig1.getPassword()); mysqlXaDataSource.setUser(dbConfig1.getUsername()); mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true); AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean(); xaDataSource.setXaDataSource(mysqlXaDataSource); xaDataSource.setUniqueResourceName("orderDataSource"); xaDataSource.setMinPoolSize(dbConfig1.getMinPoolSize()); xaDataSource.setMaxPoolSize(dbConfig1.getMaxPoolSize()); xaDataSource.setMaxLifetime(dbConfig1.getMaxLifetime()); xaDataSource.setBorrowConnectionTimeout(dbConfig1.getBorrowConnectionTimeout()); xaDataSource.setLoginTimeout(dbConfig1.getLoginTimeout()); xaDataSource.setMaintenanceInterval(dbConfig1.getMaintenanceInterval()); xaDataSource.setMaxIdleTime(dbConfig1.getMaxIdleTime()); xaDataSource.setTestQuery(dbConfig1.getTestQuery()); return xaDataSource; } @Primary @Bean(name = "orderSqlSessionFactory") public SqlSessionFactory orderSqlSessionFactory(@Qualifier("orderDataSource") DataSource dataSource) throws Exception { // SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); // bean.setDataSource(dataSource); // 這裏用 MybatisSqlSessionFactoryBean 代替了 SqlSessionFactoryBean,否則 MyBatisPlus 不會生效 MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean(); bean.setDataSource(dataSource); return bean.getObject(); } @Primary @Bean(name = "orderSqlSessionTemplate") public SqlSessionTemplate orderSqlSessionTemplate( @Qualifier("orderSqlSessionFactory") SqlSessionFactory sqlSessionFactory) { SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory); return sqlSessionTemplate; }
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import com.mysql.cj.jdbc.MysqlXADataSource; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionTemplate; import org.mybatis.spring.annotation.MapperScan; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.jta.atomikos.AtomikosDataSourceBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; import java.sql.SQLException; /** * Description: * Created by nijunyang on 2021/12/3 0:16 */ @Configuration /** * 制定此mapper使用哪個sqlSessionTemplate */ @MapperScan(basePackages = "com.nijunyang.tx.xa.mapper2", sqlSessionTemplateRef = "storageSqlSessionTemplate") public class MyBatisConfig2 { //配置XA數據源 @Bean(name = "storageDataSource") public DataSource storageDataSource(DBConfig2 dbConfig2) throws SQLException { MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource(); mysqlXaDataSource.setUrl(dbConfig2.getUrl()); mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true); mysqlXaDataSource.setPassword(dbConfig2.getPassword()); mysqlXaDataSource.setUser(dbConfig2.getUsername()); mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true); AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean(); xaDataSource.setXaDataSource(mysqlXaDataSource); xaDataSource.setUniqueResourceName("storageDataSource"); xaDataSource.setMinPoolSize(dbConfig2.getMinPoolSize()); xaDataSource.setMaxPoolSize(dbConfig2.getMaxPoolSize()); xaDataSource.setMaxLifetime(dbConfig2.getMaxLifetime()); xaDataSource.setBorrowConnectionTimeout(dbConfig2.getBorrowConnectionTimeout()); xaDataSource.setLoginTimeout(dbConfig2.getLoginTimeout()); xaDataSource.setMaintenanceInterval(dbConfig2.getMaintenanceInterval()); xaDataSource.setMaxIdleTime(dbConfig2.getMaxIdleTime()); xaDataSource.setTestQuery(dbConfig2.getTestQuery()); return xaDataSource; } @Bean(name = "storageSqlSessionFactory") public SqlSessionFactory storageSqlSessionFactory(@Qualifier("storageDataSource") DataSource dataSource) throws Exception { // SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); // bean.setDataSource(dataSource); // 這裏用 MybatisSqlSessionFactoryBean 代替了 SqlSessionFactoryBean,否則 MyBatisPlus 不會生效 MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean(); bean.setDataSource(dataSource); return bean.getObject(); } @Bean(name = "storageSqlSessionTemplate") public SqlSessionTemplate storageSqlSessionTemplate( @Qualifier("storageSqlSessionFactory") SqlSessionFactory sqlSessionFactory) { SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(sqlSessionFactory); return sqlSessionTemplate; } }
因爲我們要使用自己的數據源,所以啓動類需要剔除數據源的自動配置
3.業務代碼
import com.nijunyang.tx.common.entity.Order; import com.nijunyang.tx.xa.service.OrderService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; /** * Description: * Created by nijunyang on 2021/12/3 0:20 */ @RestController @RequestMapping("order") public class OrderController { @Resource private OrderService orderService; //127.0.0.1:8080/order?userId=1&commodityId=1&quantity=2&price=10 @GetMapping public Object create(Order order) { orderService.create(order); return 1; } }
import com.nijunyang.tx.common.entity.Order; import com.nijunyang.tx.xa.mapper1.OrderMapper; import com.nijunyang.tx.xa.mapper2.StorageMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; /** * Description: * Created by nijunyang on 2021/12/3 0:21 */ @Service public class OrderService { @Resource private OrderMapper orderMapper; @Resource private StorageMapper storageMapper; @Transactional(rollbackFor = Exception.class) public void create(Order order) { orderMapper.insert(order); storageMapper.reduce(order.getCommodityId(), order.getQuantity()); // int a = 1/0; } }
至此項目搭建完畢,訪問 127.0.0.1:8080/order?userId=1&commodityId=1&quantity=2&price=10,可以發現兩個數據庫的t_order表和 storage數據正常寫入。
當我在業務層構造一個異常 int a = 1/0時,會發現兩個庫均不會寫入數據。
實際上通過@Transactional註解拿到的是這個事務管理器org.springframework.transaction.jta.JtaTransactionManager#doBegin,最終開啓事務是由com.atomikos.icatch.jta.UserTransactionManager#begin來開啓事務,這個就是Atomikos提供的事務管理器;
發生異常回滾也是com.atomikos.icatch.jta.UserTransactionManager#rollback,最終com.atomikos.icatch.imp.TransactionStateHandler#rollback會將所有的事務都回滾。