spring-cloud-alibaba 分佈式事務seata AT模式
一、seata 是什麼
Seata 是一款開源的分佈式事務解決方案,致力於在微服務架構下提供高性能和簡單易用的分佈式事務服務。
官網介紹: seata 是什麼
本次代碼github:https://github.com/LH-0811/zcloudparent
中的 cloud-alibaba-seata-example 模塊
二、AT模式
基於支持本地 ACID 事務的關係型數據庫。
Java 應用,通過 JDBC 訪問數據庫。
三、獲取seata並配置啓動
本次是基於 seata-1.0.0
3.1 下載seata
下載地址: https://seata.io/zh-cn/blog/download.html
3.2 保證nacos正常運行
參考: https://blog.csdn.net/qq_30110435/article/details/105000289
3.3 配置seata
3.3.1 修改file.conf
主要修改: 自定義事務組名稱 -> 事務日誌存儲模式 -> 數據庫連接信息
3.3.1.1. 修改server模塊,自定義 事務分組名稱
service{
vgroup_mapping.my_test_tx_group = "default" 這個value可以自定義修改
}
3.3.1.2 修改store模塊 修改爲 數據庫存儲 默認是文件存儲
修改
store{
mode = "db"
db {
url = "jdbc:myslq://xxxx:3306/seata"
user = "root"
password = "123456"
}
}
3.3.1.3 自己在本地創建seata數據庫
3.3.1.4 執行數據庫初始化腳本
數據庫腳本在~/seata/conf/db_store.sql。如果1.0.0版本沒有該腳本,則可以下載0.9.0版本獲取該腳本
3.3.1.5 seata註冊 修改 registry.conf
registry{
type = "nacos"
nacos{
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
}
3.3.1.6 nacos 啓動
3.3.1.7 啓動seata
啓動腳本在~/seata/bin/seata-server.sh
四、準備測試用的業務數據庫
4.1 初始化數據庫
因爲業務數據庫要支持seata的業務回滾 所以需要增加undo_log表,腳本在~/seata/conf/undo_log.sql
業務數據 與seata官網的demo相似 增加了 幾個簡單數字段
4.1.1 seata_storage庫存庫創建初始化
在msql中增加庫存庫:seata_storage 並執行數據庫腳本
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for storage_tbl
-- ----------------------------
DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`commodity_code` varchar(255) DEFAULT NULL COMMENT '產品code',
`count` int(11) DEFAULT '0' COMMENT '庫存',
`price` decimal(10,2) DEFAULT NULL COMMENT '價格',
PRIMARY KEY (`id`),
UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of storage_tbl
-- ----------------------------
BEGIN;
INSERT INTO `storage_tbl` VALUES (1, '1', 1000, 2000.00);
COMMIT;
-- ----------------------------
-- Table structure for 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=2 DEFAULT CHARSET=utf8mb4;
SET FOREIGN_KEY_CHECKS = 1;
4.1.2 seata_account賬戶數據庫庫創建初始化
在msql中增加庫存庫:seata_account 並執行數據庫腳本
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for account_tbl
-- ----------------------------
DROP TABLE IF EXISTS `account_tbl`;
CREATE TABLE `account_tbl` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL COMMENT '用戶id',
`money` int(11) DEFAULT '0' COMMENT '用戶餘額',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of account_tbl
-- ----------------------------
BEGIN;
INSERT INTO `account_tbl` VALUES (1, '1', 1000);
INSERT INTO `account_tbl` VALUES (2, '2', 5000);
COMMIT;
-- ----------------------------
-- Table structure for 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 DEFAULT CHARSET=utf8mb4;
SET FOREIGN_KEY_CHECKS = 1;
4.1.3 seata_order訂單數據庫創建初始化
在msql中增加庫存庫:seata_order 並執行數據庫腳本
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for order_tbl
-- ----------------------------
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL COMMENT '用戶id',
`commodity_code` varchar(255) DEFAULT NULL COMMENT '產品code',
`count` int(11) DEFAULT '0' COMMENT '數量',
`money` decimal(11,2) DEFAULT '0.00' COMMENT '金額',
`order_status` tinyint(1) DEFAULT NULL COMMENT '0-創建中 1-創建成功',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1241897922458800129 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Table structure for 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=2 DEFAULT CHARSET=utf8mb4;
SET FOREIGN_KEY_CHECKS = 1;
4.2 創建業務工程
這裏就只寫關鍵代碼了
工程的模塊劃分
源碼可以去 github查看
- spring-cloud-seate-example [父工程 | pom]
- storage-module[庫存服務 pom]
- storage-api [庫存api對外提供的pojo、api、vo | jar ]
- storage-service [庫存服務 | jar ]
- account-module[賬戶服務 pom]
- account-api[賬戶api對外提供的pojo、api、vo | jar ]
- account-service [賬戶服務 | jar ]
- order-module [訂單服務 pom]
- order-api[訂單api對外提供的pojo、api、vo | jar ]
- order-service [訂單服務 | jar ]
- storage-module[庫存服務 pom]
因爲每個服務都有支持seata所以每個服務都需要添加seata需要的配置文件
在微服務中添加seata配置文件
處理 file.conf 文件
將file.conf文件 複製到微服務的resources目錄下
修改 file.conf文件中
service.vgroup_mapping.my_test_tx_group
將後面my_test_tx_group 替換成我們在seata/conf/file.conf中 自定義的事務分組名稱
其value 可以隨意修改
修改後 微服務下的file.conf 文件爲
service {
#transaction service group mapping
vgroup_mapping.seata_tx_group = "default"
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#disable seata
disableGlobalTransaction = false
}
## transaction log store, only used in seata-server
store {
## store mode: file、db
mode = "db"
## file store property
file {
## store location dir
dir = "sessionStore"
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "123456"
}
}
處理 register.conf 文件
同樣將seata/conf/register.conf 文件複製到微服務的resources下
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "file"
nacos {
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = "0"
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "localhost:8848"
namespace = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
app.id = "seata-server"
apollo.meta = "http://192.168.1.204:8801"
}
zk {
serverAddr = "127.0.0.1:2181"
session.timeout = 6000
connect.timeout = 2000
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
這裏要注意,因爲是要存儲到nacos 所以 nacos的鏈接地址要正確
4.2.1 父工程POM
添加版本管理
<dependencyManagement>
<dependencies>
<!-- 版本對照 https://spring.io/projects/spring-cloud-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 版本對照 https://start.spring.io/actuator/info-->
<!-- "Hoxton.SR3": "Spring Boot >=2.2.0.M4 and <2.2.6.BUILD-SNAPSHOT",-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<!--SR3 版本 與openfeign整合sentinel會報錯-->
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--cloud alibaba https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md-->
<!--2.2.x 版本適用於 Spring Boot 2.2.x-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 數據庫連接池 -->
</dependencyManagement>
4.2.2 storage-module 庫存模塊
4.2.2.1 storage-service
增加pom依賴 這裏只貼 需要新增的pom節點
<!--本工程需要使用到的pojo param 等實體類-->
<dependency>
<groupId>com.lhit.spirngcloud3</groupId>
<artifactId>storage-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- 引入nacos服務發現 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- 微服務調用客戶端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
修改yml配置
server:
port: 9202
spring:
application:
name: cloud-seata-storage-server
datasource:
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: 123456
url: jdbc:mysql://localhost:3306/seata_storage?useUnicode=true&useSSL=true&serverTimezone=GMT%2B8
jackson:
time-zone: GMT+8
generator:
WRITE_NUMBERS_AS_STRINGS: true
cloud:
nacos: # 配置nacos
discovery: # 服務發現地址
server-addr: 127.0.0.1:8848 # nacos 啓動地址
alibaba:
seata:
tx-service-group: default # 自定義事務組名稱 與file.conf 中seata-server 定義的一致
mybatis:
configuration:
map-underscore-to-camel-case: true
type-aliases-package: com.lhit.springcloud3.seata
mapper:
not-empty: false
identity: MYSQL
pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
params: count=countSql
management: # 健康檢查暴露端點
endpoints:
web:
exposure:
include: "*"
這個模塊寫一個減庫存的服務 service
import com.lhit.springcloud3.common.entity.CommonMethod;
import com.lhit.springcloud3.common.entity.ResponseEntityBody;
import com.lhit.springcloud3.common.exception.CommonException;
import com.lhit.springcloud3.seata.storage.dao.StorageDao;
import com.lhit.springcloud3.seata.storage.param.SubtractionStorageParam;
import com.lhit.springcloud3.seata.storage.pojo.Storage;
import com.lhit.springcloud3.seata.storage.service.StorageService;
import com.lhit.springcloud3.seata.storage.vo.SubtractionStorageVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tk.mybatis.mapper.entity.Example;
import java.math.BigDecimal;
@Slf4j
@Service("storageService")
public class StorageServiceImpl implements StorageService {
@Autowired
private StorageDao storageDao;
// 減庫存
@Override
public SubtractionStorageVo subtractionStorageVo(SubtractionStorageParam param) throws Exception {
// 檢查參數
CommonMethod.checkParam("StorageServiceImpl", "subtractionStorageVo", param);
// 查詢數據合法性
Storage storage = null;
try {
Storage selectStorage = new Storage();
selectStorage.setCommodityCode(param.getCommodityCode());
storage = storageDao.selectOne(selectStorage);
} catch (Exception e) {
throw CommonException.createWithNotFound_404(e, ResponseEntityBody.create("沒有找打庫存信息"));
}
// 扣減庫存
try {
Storage storageUpdate = new Storage();
storageUpdate.setId(storage.getId());
storageUpdate.setCount(storage.getCount() - param.getCount());
storageDao.updateByPrimaryKeySelective(storageUpdate);
} catch (Exception e) {
throw CommonException.createWithServerError_500(e, ResponseEntityBody.create("扣減庫存失敗"));
}
log.info("扣減庫存完成");
return new SubtractionStorageVo(param.getCommodityCode(), param.getCount(), storage.getPrice().multiply(new BigDecimal(param.getCount())));
}
}
公開一個controller
@Slf4j
@RestController
@RequestMapping("/storage")
@Api(description = "庫存接口")
public class StorageController {
@Autowired
private StorageService storageService;
@PostMapping("/subtraction")
@ApiOperation("扣減庫存")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "body", dataType = "SubtractionStorageParam", dataTypeClass = SubtractionStorageParam.class, name = "param", value = "參數"),
})
public ResponseEntity<ResponseEntityBody<SubtractionStorageVo>> subtractionStorage(@RequestBody SubtractionStorageParam param) throws Exception {
log.info(">>>>>>>>>>>進入創建訂單接口OrderController.createOrder>>>>>>>>>>>>>>>>>>>>>>");
SubtractionStorageVo vo = storageService.subtractionStorageVo(param);
return ResponseEntity.ok(ResponseEntityBody.create("扣減成功",vo));
}
}
4.2.2.2 storage-api
這個模塊主要是公開庫存服務的api和實體類以及請求參數和vo
暴露減庫存api 方便消費者直接使用
import com.lhit.springcloud3.common.entity.ResponseEntityBody;
import com.lhit.springcloud3.seata.storage.param.SubtractionStorageParam;
import com.lhit.springcloud3.seata.storage.vo.SubtractionStorageVo;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
public interface StorageApi {
@PostMapping("/storage/subtraction")
@ApiOperation("扣減庫存")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "body", dataType = "SubtractionStorageParam", dataTypeClass = SubtractionStorageParam.class, name = "param", value = "參數"),
})
ResponseEntity<ResponseEntityBody<SubtractionStorageVo>> subtractionStorage(@RequestBody SubtractionStorageParam param) throws Exception;
}
4.2.3 account-module 賬戶模塊
4.2.3.1 account-service
修改pom
<!--本工程需要使用到的pojo param 等實體類-->
<dependency>
<groupId>com.lhit.spirngcloud3</groupId>
<artifactId>account-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- 引入nacos服務發現 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- 微服務調用客戶端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
修改yml
server:
port: 9203
spring:
application:
name: cloud-seata-account-server
datasource:
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: 123456
url: jdbc:mysql://localhost:3306/seata_account?useUnicode=true&useSSL=true&serverTimezone=GMT%2B8
jackson:
time-zone: GMT+8
generator:
WRITE_NUMBERS_AS_STRINGS: true
cloud:
nacos: # 配置nacos
discovery: # 服務發現地址
server-addr: 127.0.0.1:8848 # nacos 啓動地址
alibaba:
seata:
tx-service-group: default # 自定義事務組名稱 與file.conf 中seata-server 定義的一致
mybatis:
configuration:
map-underscore-to-camel-case: true
type-aliases-package: com.lhit.springcloud3.seata
mapper:
not-empty: false
identity: MYSQL
pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
params: count=countSql
management: # 健康檢查暴露端點
endpoints:
web:
exposure:
include: "*"
編寫service
import com.lhit.springcloud3.common.entity.CommonMethod;
import com.lhit.springcloud3.common.entity.ResponseEntityBody;
import com.lhit.springcloud3.common.exception.CommonException;
import com.lhit.springcloud3.seata.account.dao.AccountDao;
import com.lhit.springcloud3.seata.account.param.AccountSubtractionParam;
import com.lhit.springcloud3.seata.account.pojo.Account;
import com.lhit.springcloud3.seata.account.service.AccountService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Slf4j
@Service("accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
// 扣減餘額
@Override
public void accountSubtraction(AccountSubtractionParam param) throws Exception {
// 檢查參數
CommonMethod.checkParam("AccountServiceImpl", "accountSubtraction", param);
// 檢查參數有效性
Account account = null;
try {
Account selectAccount = new Account();
selectAccount.setUserId(param.getUserId());
account = accountDao.selectOne(selectAccount);
} catch (Exception e) {
throw CommonException.createWithNotFound_404(e, ResponseEntityBody.create("沒有獲取到賬戶信息"));
}
if (param.getAmt().compareTo(account.getMoney()) > 0){
throw CommonException.createWithMethodNotAcceptable_406(ResponseEntityBody.create("餘額不足"));
}
// 扣減餘額
try {
account.setMoney(account.getMoney().subtract(param.getAmt()));
accountDao.updateByPrimaryKeySelective(account);
}catch (Exception e){
throw CommonException.createWithServerError_500(e,ResponseEntityBody.create("扣減餘額失敗"));
}
log.info("扣減餘額完成");
}
}
通過controller公開接口
import com.lhit.springcloud3.common.entity.ResponseEntityBody;
import com.lhit.springcloud3.seata.account.param.AccountSubtractionParam;
import com.lhit.springcloud3.seata.account.service.AccountService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/account")
@Api(description = "賬戶接口")
public class AccountController {
@Autowired
private AccountService accountService;
@PostMapping("/subtraction")
@ApiOperation("扣減餘額")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "body", dataType = "AccountSubtractionParam", dataTypeClass = AccountSubtractionParam.class, name = "param", value = "參數"),
})
public ResponseEntity<ResponseEntityBody<AccountSubtractionParam>> accountSubtraction(@RequestBody AccountSubtractionParam param) throws Exception {
log.info(">>>>>>>>>>>進入創建訂單接口OrderController.createOrder>>>>>>>>>>>>>>>>>>>>>>");
accountService.accountSubtraction(param);
return ResponseEntity.ok(ResponseEntityBody.create("扣減成功"));
}
}
4.2.2.2 account-api
這個模塊主要是公開庫存服務的api和實體類以及請求參數和vo
暴露減庫存api 方便消費者直接使用
i
import com.lhit.springcloud3.common.entity.ResponseEntityBody;
import com.lhit.springcloud3.seata.account.param.AccountSubtractionParam;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
public interface AccountApi {
@PostMapping("/account/subtraction")
@ApiOperation("扣減餘額")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "body", dataType = "AccountSubtractionParam", dataTypeClass = AccountSubtractionParam.class, name = "param", value = "參數"),
})
ResponseEntity<ResponseEntityBody<AccountSubtractionParam>> accountSubtraction(@RequestBody AccountSubtractionParam param) throws Exception;
}
4.2.3 order-module訂單模塊
不在贅述 api和pom以及yml的修改 與前面兩個模塊一樣
這裏只寫關鍵的service和feign客戶端
使用feign來創建調用 庫存好賬號服務的客戶端
@FeignClient(serviceId = "cloud-seata-account-server")
public interface AccountService extends AccountApi {
}
@FeignClient(serviceId = "cloud-seata-storage-server")
public interface StorageService extends StorageApi {
}
自身需要實現的service
public interface OrderService {
// 創建訂單
Order createOrder(OrderCreateParam param) throws Exception;
// 設置訂單到創建完成狀態
void setOrderCreateFinish(OrderSetFinishParam param) throws Exception;
}
service 實現代碼
這裏要 創建訂單 -> 扣庫存 -> 扣賬戶餘額 ->更新訂單狀態
import com.lhit.springcloud3.common.entity.CommonMethod;
import com.lhit.springcloud3.common.entity.ResponseEntityBody;
import com.lhit.springcloud3.common.exception.CommonException;
import com.lhit.springcloud3.common.util.IdWorker;
import com.lhit.springcloud3.seata.account.param.AccountSubtractionParam;
import com.lhit.springcloud3.seata.order.dao.OrderDao;
import com.lhit.springcloud3.seata.order.param.OrderCreateParam;
import com.lhit.springcloud3.seata.order.param.OrderSetFinishParam;
import com.lhit.springcloud3.seata.order.pojo.Order;
import com.lhit.springcloud3.seata.order.service.AccountService;
import com.lhit.springcloud3.seata.order.service.OrderService;
import com.lhit.springcloud3.seata.order.service.StorageService;
import com.lhit.springcloud3.seata.storage.param.SubtractionStorageParam;
import com.lhit.springcloud3.seata.storage.vo.SubtractionStorageVo;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.util.Map;
@Slf4j
@Service("orderService")
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private IdWorker idWorker;
@Autowired
private StorageService storageService;
@Autowired
private AccountService accountService;
// 創建訂單
@Override
// 添加seata 全局事務註解 name 唯一
@GlobalTransactional(name = "create_order",rollbackFor = Exception.class)
public Order createOrder(OrderCreateParam param) throws Exception {
// 檢查參數
CommonMethod.checkParam("進入OrderServiceImpl", "createOrder", param);
// 拼接實體類
Order order = null;
try {
order = new Order();
BeanUtils.copyProperties(param, order);
// 補充屬性
order.setId(idWorker.nextId());
order.setOrderStatus(false);
} catch (Exception e) {
throw CommonException.createWithServerError_500(e, ResponseEntityBody.create("拼裝實體類失敗"));
}
log.info("----> 創建訂單");
orderDao.insertSelective(order);
log.info("----> 調用 庫存服務 扣減庫存");
ResponseEntity<ResponseEntityBody<SubtractionStorageVo>> subtractionStorageResponse = null;
try {
SubtractionStorageParam subtractionStorageParam = new SubtractionStorageParam();
subtractionStorageParam.setCommodityCode(param.getCommodityCode());
subtractionStorageParam.setCount(param.getCount());
subtractionStorageResponse = storageService.subtractionStorage(subtractionStorageParam);
} catch (Exception e) {
throw CommonException.createWithServerError_500(e, ResponseEntityBody.create("扣減庫存失敗"));
}
if (!subtractionStorageResponse.getStatusCode().is2xxSuccessful()) {
throw CommonException.createWithServerError_500(subtractionStorageResponse.getBody());
}
log.info("----> 完成 庫存服務 扣減庫存");
log.info("----> 調用 賬戶服務 扣減餘額");
// 獲取到庫存扣減後的信息
ResponseEntityBody<SubtractionStorageVo> subtractionStorageVo = subtractionStorageResponse.getBody();
ResponseEntity<ResponseEntityBody<AccountSubtractionParam>> accountSubtractionResponse = null;
try {
AccountSubtractionParam accountSubtractionParam = new AccountSubtractionParam();
accountSubtractionParam.setAmt(subtractionStorageVo.getData().getTotalPrice());
accountSubtractionParam.setUserId(param.getUserId());
accountSubtractionResponse = accountService.accountSubtraction(accountSubtractionParam);
} catch (Exception e) {
throw CommonException.createWithServerError_500(e, ResponseEntityBody.create("扣減賬戶餘額失敗"));
}
if (!accountSubtractionResponse.getStatusCode().is2xxSuccessful()) {
throw CommonException.createWithServerError_500(subtractionStorageResponse.getBody());
}
log.info("----> 完成 賬戶服務 扣減餘額");
log.info("----> 修改訂單狀態 開始");
OrderSetFinishParam orderSetFinishParam = new OrderSetFinishParam();
orderSetFinishParam.setUserId(param.getUserId());
orderSetFinishParam.setOrderId(order.getId());
orderSetFinishParam.setMoney(subtractionStorageVo.getData().getTotalPrice());
setOrderCreateFinish(orderSetFinishParam);
log.info("----> 修改訂單狀態 完成");
log.info("完成 訂單創建");
return order;
}
// 設置訂單到創建完成狀態
@Override
public void setOrderCreateFinish(OrderSetFinishParam param) throws Exception {
// 檢查參數
CommonMethod.checkParam("進入OrderServiceImpl", "setOrderCreateFinish", param);
// 檢查數據是否有效
Order order = orderDao.selectByPrimaryKey(param.getOrderId());
if (order == null) {
throw CommonException.createWithBadRequest_400(ResponseEntityBody.create("訂單不存在"));
}
// 檢查數據是否越界
if (!order.getUserId().equals(param.getUserId())) {
throw CommonException.createWithForbidden_403(ResponseEntityBody.create("非法操作,不可修改不屬於用戶的訂單"));
}
// 拼接修改參數
try {
Order orderUpdate = new Order();
orderUpdate.setId(param.getOrderId());
orderUpdate.setOrderStatus(true);
orderUpdate.setMoney(param.getMoney());
orderDao.updateByPrimaryKeySelective(orderUpdate);
} catch (Exception e) {
throw CommonException.createWithServerError_500(e, ResponseEntityBody.create("修改訂單狀態失敗"));
}
log.info("完成訂單狀態修改");
}
}
五 、 總結
通過seata 可以很友好的實現分佈式事務的控制。
只需要在業務代碼添加 @GlobalTransactional(name = “create_order”,rollbackFor = Exception.class) 註解