SpringCloud Alibaba第十四章,升級篇,分佈式事務解決方案Seata
一、分佈式事務概述
1、什麼是分佈式事務
隨着互聯網的快速發展,軟件系統由原來的單體應用轉變爲分佈式應用。
分佈式系統會把一個應用系統拆分爲可獨立部署的多個服務,因此需要服務與服務之間遠程協作才能完成事務操作,這種分佈式系統環境下由不同的服務之間通過網絡遠程協作完成事務稱之爲分佈式事務。
例如用戶註冊送積分事務、創建訂單減庫存事務,銀行轉賬事務等都是分佈式事務。
1.1、 本地事務依賴數據庫本身提供的事務特性來實現 :
begin transaction;
//1.本地數據庫操作:張三減少金額
//2.本地數據庫操作:李四增加金額
commit transation;
1.2、 但是在分佈式環境下,會變成下邊這樣:
begin transaction;
//1.本地數據庫操作:張三減少金額
//2.遠程調用:讓李四增加金額
commit transation;
可以設想,當遠程調用讓李四增加金額成功了,由於網絡問題遠程調用並沒有返回,此時本地事務提交失敗就回滾了張三減少金額的操作,此時張三和李四的數據就不一致了。
因此在分佈式架構的基礎上,傳統數據庫事務就無法使用了,張三和李四的賬戶不在一個數據庫中甚至不在一個應用系統裏,實現轉賬事務需要通過遠程調用,由於網絡問題就會導致分佈式事務問題。
2、分佈式事務產生場景:
2.1、 典型的場景就是微服務架構 微服務之間通過遠程調用完成事務操作。 比如:訂單微服務和庫存微服務,下單的同時訂單微服務請求庫存微服務減庫存。
2.2、 單體系統訪問多個數據庫實例 當單體系統需要訪問多個數據庫(實例)時就會產生分佈式事務。 比如:用戶信息和訂單信息分別在兩個MySQL實例存儲,用戶管理系統刪除用戶信息,需要分別刪除用戶信息及用戶的訂單信息,由於數據分佈在不同的數據實例,需要通過不同的數據庫鏈接去操作數據,此時產生分佈式事務。
2.3、 多服務訪問同一個數據庫實例 比如:訂單微服務和庫存微服務即使訪問同一個數據庫也會產生分佈式事務,原因就是跨JVM進程,兩個微服務持有了不同的數據庫鏈接進行數據庫操作,此時產生分佈式事務。
二、Seata概述
1、介紹
Seata 是一款開源的分佈式事務解決方案,致力於提供高性能和簡單易用的分佈式事務服務。Seata 將爲用戶提供了 AT、TCC、SAGA 和 XA 事務模式,爲用戶打造一站式的分佈式解決方案。
2、術語
XID - Transaction ID ,一個分佈式事務唯一一個ID
TC - 事務協調者
維護全局和分支事務的狀態,驅動全局事務提交或回滾。
TM - 事務管理器
定義全局事務的範圍:開始全局事務、提交或回滾全局事務。
RM - 資源管理器
管理分支事務處理的資源,與TC交談以註冊分支事務和報告分支事務的狀態,並驅動分支事務提交或回滾。
3、Seata處理流程
1、TM向TC申請開啓一個全局事務,全局事務創建成功,並生成一個全局唯一的ID,即XID.
2、XID在微服務調用鏈路的上下文間傳播.
3、RM向TC註冊分支事務,將其納入XID對應全局事務的管轄.
4、TM向TC發起針對XID的全局事務提交或回滾決議.
5、TC調度XID管轄的全部分支事務完成提交或回滾請求.
其他:
官網地址:http://seata.io/
下載地址:http://seata.io/zh-cn/blog/download.html
使用:@GlobalTransactional
三、Seata Server安裝
1、解壓seata-server-0.9.0.zip
2、進入conf目錄修改file.conf文件:自定義事務組名稱、事務存儲模式、數據庫連接信息
service模塊自定義事務組名稱:
vgroup_mapping.my_test_tx_group = "default"
修改爲vgroup_mapping.my_test_tx_group = "seataTxGroup"
store模塊修改事務存儲模式:
mode = "file"
修改爲mode="db"
db模塊修改爲你的數據庫連接:
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "admin123"
3、根據上面的數據庫連接創建對應的庫和表
CREATE SCHEMA `seata` DEFAULT CHARACTER SET utf8 ;
對應表的SQL在conf目錄下的db_store.sql文件裏:
global_table、branch_table、lock_table三張表
4、修改conf下的registry.conf文件,將seata註冊進對應的註冊中心
將registry模塊下的type = "file"修改爲你對應的註冊中心type="nacos"
同時修改對應的註冊中心模塊:
nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "seataTxGroup" ##對應的seata自定義的事務組名稱
}
conf模塊的file就是我們修改的file.conf文件,你也可以將conf註冊信息的內容修改到nacos裏
測試:
1、啓動nacos server,端口號爲8848
2、啓動seata server
看看nacos的服務列表是否有seata server的注入,對應的端口是8901
四、Seata案例
案例爲:訂單–庫存–用戶金額賬戶三個微服務之間的事務
1、創建訂單
2、修改訂單購買的商品對應的庫存,
3、並在用戶金額賬戶中扣除購買所需金額
4、最後修改訂單狀態
1、創建對應的微服務表
訂單表:
CREATE DATABASE seata_order;
use seata_order;
CREATE TABLE t_order(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用戶id',
`product_id` BIGINT(11) DEFAULT NULL COMMENT '產品id',
`count` INT(11) DEFAULT NULL COMMENT '數量',
`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金額',
`status` INT(1) DEFAULT NULL COMMENT '訂單狀態:0:創建中; 1:已完結'
) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
SELECT * FROM t_order;
庫存表:
CREATE DATABASE seata_storage;
use seata_storage;
CREATE TABLE t_storage(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '產品id',
`total` INT(11) DEFAULT NULL COMMENT '總庫存',
`used` INT(11) DEFAULT NULL COMMENT '已用庫存',
`residue` INT(11) DEFAULT NULL COMMENT '剩餘庫存'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO t_storage(`id`,`product_id`,`total`,`used`,`residue`)
VALUES('1','1','100','0','100');
SELECT * FROM t_storage;
賬戶表:
CREATE DATABASE seata_account;
use seata_account;
CREATE TABLE t_account(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用戶id',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT '總額度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用餘額',
`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩餘可用額度'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_account.t_account(`id`,`user_id`,`total`,`used`,`residue`) VALUES('1','1','1000','0','1000');
SELECT * FROM t_account;
2、創建對應的回滾日誌表
##訂單-庫存-賬戶3個庫下都需要建各自的回滾日誌表泳衣記錄每個XID
##對應的SQL在seata server的conf目錄下db_undo_log.sql
-- the table to store seata xid data
-- 0.7.0+ add context
-- you must to init this sql for you business databese. the seata server not need it.
-- 此腳本必須初始化在你當前的業務數據庫中,用於AT 模式XID記錄。與server端無關(注:業務數據庫)
-- 注意此處0.3.0+ 增加唯一索引 ux_undo_log
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
3、創建對應的微服務–賬戶
創建賬戶微服務:seata-order-service-2003
<parent>
<artifactId>cloud_2020</artifactId>
<groupId>com.lee.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-order-service-2003</artifactId>
POM
<?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">
<parent>
<artifactId>cloud_2020</artifactId>
<groupId>com.lee.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-order-service-2003</artifactId>
<dependencies>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</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.1.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 2003
spring:
application:
name: seata-account-service
cloud:
alibaba:
seata:
tx-service-group: my_test_tx_group #和seata-server conf目錄下的file.conf中的自定義事務組名稱一致
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_account
username: root
password: admin123
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
同時將:seata-server conf目錄下的file.conf和register.conf兩個文件拷貝到resource目錄
實體類
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
private Long id;
private Long userId;
private BigDecimal total;
private BigDecimal used;
private BigDecimal residue;
}
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>
{
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message)
{
this(code,message,null);
}
}
數據層
@Mapper
public interface AccountDao {
//扣減賬戶餘額
void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
resource/mapper目錄
<?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.lee.springcloud.dao.AccountDao">
<resultMap id="BaseResultMap" type="com.lee.springcloud.domain.Account">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="total" property="total" jdbcType="DECIMAL"/>
<result column="used" property="used" jdbcType="DECIMAL"/>
<result column="residue" property="residue" jdbcType="DECIMAL"/>
</resultMap>
<update id="decrease">
UPDATE t_account SET
residue = residue - #{money},used = used + #{money}
WHERE user_id = #{userId};
</update>
</mapper>
服務層
public interface AccountService {
//扣減賬戶餘額
void decrease( Long userId,BigDecimal money);
}
@Slf4j
@Service
public class AccountServiceImpl implements AccountService {
@Resource
private AccountDao accountDao;
//扣減賬戶餘額
@Override
public void decrease(Long userId, BigDecimal money) {
log.info("------->account-service中扣減賬戶餘額開始");
//try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
accountDao.decrease(userId,money);
log.info("------->account-service中扣減賬戶餘額結束");
}
}
controller
@RestController
public class AccountController {
@Resource
private AccountService accountService;
//扣減賬戶餘額
@RequestMapping("/account/decrease")
public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){
accountService.decrease(userId,money);
return new CommonResult<>(200,"扣減賬戶餘額成功!");
}
}
配置層:
@Configuration
@MapperScan({"com.lee.springcloud.dao"})
public class MybatisConfig {
}
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
主啓動類
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataAccountMainApp2003 {
public static void main(String[] args){
SpringApplication.run(SeataAccountMainApp2003.class, args);
}
}
4、創建對應的微服務–庫存
創建庫存微服務:seata-order-service-2002
<parent>
<artifactId>cloud_2020</artifactId>
<groupId>com.lee.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-order-service-2002</artifactId>
POM
<?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">
<parent>
<artifactId>cloud_2020</artifactId>
<groupId>com.lee.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-order-service-2002</artifactId>
<dependencies>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 2002
spring:
application:
name: seata-storage-service
cloud:
alibaba:
seata:
tx-service-group: my_test_tx_group #和seata-server conf目錄下的file.conf中的自定義事務組名稱一致
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_storage
username: root
password: admin123
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
同時將:seata-server conf目錄下的file.conf和register.conf兩個文件拷貝到resource目錄
實體類
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>
{
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message)
{
this(code,message,null);
}
}
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Storage {
private Long id;
// 產品id
private Long productId;
//總庫存
private Integer total;
//已用庫存
private Integer used;
//剩餘庫存
private Integer residue;
}
數據層:
@Mapper
public interface StorageDao {
//扣減庫存信息
void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}
<?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.lee.springcloud.dao.StorageDao">
<resultMap id="BaseResultMap" type="com.lee.springcloud.domain.Storage">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="product_id" property="productId" jdbcType="BIGINT"/>
<result column="total" property="total" jdbcType="INTEGER"/>
<result column="used" property="used" jdbcType="INTEGER"/>
<result column="residue" property="residue" jdbcType="INTEGER"/>
</resultMap>
<update id="decrease">
UPDATE
t_storage
SET
used = used + #{count},residue = residue - #{count}
WHERE
product_id = #{productId}
</update>
</mapper>
服務層
public interface StorageService {
// 扣減庫存
void decrease(Long productId, Integer count);
}
@Slf4j
@Service
public class StorageServiceImpl implements StorageService {
@Resource
private StorageDao storageDao;
// 扣減庫存
@Override
public void decrease(Long productId, Integer count) {
log.info("------->storage-service中扣減庫存開始");
storageDao.decrease(productId,count);
log.info("------->storage-service中扣減庫存結束");
}
}
controller
@RestController
public class StorageController {
@Autowired
private StorageService storageService;
//扣減庫存
@RequestMapping("/storage/decrease")
public CommonResult decrease(Long productId, Integer count) {
storageService.decrease(productId, count);
return new CommonResult(200,"扣減庫存成功!");
}
}
config
@Configuration
@MapperScan({"com.lee.springcloud.dao"})
public class MybatisConfig {
}
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
主啓動類:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataStorageServiceApplication2002
{
public static void main(String[] args)
{
SpringApplication.run(SeataStorageServiceApplication2002.class, args);
}
}
5、創建對應的微服務–訂單
創建對應的訂單微服務:seata-order-service-2001
<parent>
<artifactId>cloud_2020</artifactId>
<groupId>com.lee.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-order-service-2001</artifactId>
POM
<?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">
<parent>
<artifactId>cloud_2020</artifactId>
<groupId>com.lee.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-order-service-2001</artifactId>
<dependencies>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<artifactId>seata-all</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--web-actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--mysql-druid-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
tx-service-group: my_test_tx_group #自定義事務組名稱需要與seata-server中的對應
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order
username: root
password: admin123
feign:
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis:
mapperLocations: classpath:mapper/*.xml
同時將:seata-server conf目錄下的file.conf和register.conf兩個文件拷貝到resource目錄
實體類
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>
{
private Integer code;
private String message;
private T data;
public CommonResult(Integer code, String message)
{
this(code,message,null);
}
}
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Order{
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status; //訂單狀態:0:創建中;1:已完結
}
數據層
@Mapper
public interface OrderDao{
//新建訂單
void create(Order order);
//修改訂單狀態,從零改爲1
void update(@Param("userId") Long userId, @Param("status") Integer status);
}
<?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.lee.springcloud.dao.OrderDao">
<resultMap id="BaseResultMap" type="com.lee.springcloud.domain.Order">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="user_id" property="userId" jdbcType="BIGINT"/>
<result column="product_id" property="productId" jdbcType="BIGINT"/>
<result column="count" property="count" jdbcType="INTEGER"/>
<result column="money" property="money" jdbcType="DECIMAL"/>
<result column="status" property="status" jdbcType="INTEGER"/>
</resultMap>
<insert id="create">
insert into t_order (id,user_id,product_id,count,money,status)
values (null,#{userId},#{productId},#{count},#{money},0);
</insert>
<update id="update">
update t_order set status = 1
where user_id=#{userId} and status = #{status};
</update>
</mapper>
服務層
public interface OrderService {
void create(Order order);
}
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
/**
* 創建訂單->調用庫存服務扣減庫存->調用賬戶服務扣減賬戶餘額->修改訂單狀態
*/
@Override
//@GlobalTransactional(name = "test-create-order",rollbackFor = Exception.class)
public void create(Order order){
log.info("----->開始新建訂單");
//新建訂單
orderDao.create(order);
//扣減庫存
log.info("----->訂單微服務開始調用庫存,做扣減Count");
storageService.decrease(order.getProductId(),order.getCount());
log.info("----->訂單微服務開始調用庫存,做扣減end");
//扣減賬戶
log.info("----->訂單微服務開始調用賬戶,做扣減Money");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("----->訂單微服務開始調用賬戶,做扣減end");
//修改訂單狀態,從零到1代表已經完成
log.info("----->修改訂單狀態開始");
orderDao.update(order.getUserId(),0);
log.info("----->修改訂單狀態結束");
log.info("----->下訂單結束了");
}
}
feign
@FeignClient(value = "seata-storage-service")
public interface StorageService{
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
@FeignClient(value = "seata-account-service")
public interface AccountService{
@PostMapping(value = "/account/decrease")
CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
controller
@RestController
public class OrderController{
@Resource
private OrderService orderService;
@GetMapping("/order/create")
public CommonResult create(Order order)
{
orderService.create(order);
return new CommonResult(200,"訂單創建成功");
}
}
配置類
@Configuration
@MapperScan({"com.lee.springcloud.dao"})
public class MybatisConfig {
}
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
主啓動類
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消數據源自動創建的配置
public class SeataOrderMainApp2001{
public static void main(String[] args)
{
SpringApplication.run(SeataOrderMainApp2001.class, args);
}
}
6、測試
1、啓動nacos-server、seata-server、seata-order-service-2001\2002\2003
2、此時order微服務沒有開啓全局事務
//@GlobalTransactional(name = "test-create-order",rollbackFor = Exception.class)
account微服務扣款接口的延遲也沒有開啓
//try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
一切正常
瀏覽器調用:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
結果:
{"code":200,"message":"訂單創建成功","data":null}
數據庫對應的表都做了相應的更改
3、現在我們將account微服務扣款接口延遲,因爲openFeign的默認調用時長爲1s,如果超過1s則調用失敗
將try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }註釋打開
瀏覽器訪問:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
報錯:java.net.SocketTimeoutException: Read timed out
數據庫對應的表:
訂單表數據新增了,但status爲0創建中
庫存表數據被修改了used10 residue90
賬戶表沒動
4、現在我們再將全局事務打開
@GlobalTransactional(name = "test-create-order",rollbackFor = Exception.class)
瀏覽器訪問:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
報錯:java.net.SocketTimeoutException: Read timed out
數據庫對應的表:
沒有改變