項目依賴
- SpringBoot 2.5.5
- SpringCloud 2020.0.4
- Alibaba Spring Cloud 2021.1
- Mybatis Plus 3.4.0
- Seata 1.4.1(需要與服務器部署的Seata版本保持一致)
- 。。。。
Seata介紹
什麼是Seata
- 一個開源分佈式事務框架,由阿里中間件團隊發起的開源項目Fescar,後更名爲Seata
- 中文文檔地址:http://seata.io/zh-cn/docs/user/quickstart.html
Seata三大組件
- TC:Transaction Coordinator事務協調器,管理全局的分支事務的狀態,用於全局性事務的提交和回滾
- TM:Transaction Manager 事務管理器,用戶開啓、提交或者回滾【全局事務】
- RM:Resource Manager資源管理器,用於分支事務上的資源管理,向TC註冊分支事務,上報分支事務的狀態,接收TC的命令來提交或者回滾分支事務
- 傳統XA協議實現2PC方案的RM是在數據庫層,RM本質上就是數據庫自身
- Seata的RM是以jar包的形式嵌入在應用程序裏面
架構:TC爲單獨部署的Server服務端,TM和RM爲嵌入到應用中的Client客戶端
XID
- TM請求TC開啓一個全局事務,TC會生成一個XID作爲該全局事務的編號XID,XID會在微服務的調用鏈路中傳播,保證將多個微服務對的子事務關聯在一起
Seata部署安裝
下載Seata地址
http://seata.io/zh-cn/blog/download.html
注:我這邊下載的是1.4.1,seata部署版本需要與SpringBoot依賴的版本相對應!!!!!!
Seata部署
前期準備
準備好Nacos、mysql
注:nacos配置中心數據是持久化到mysql的!!!!
部署&修改配置
修改存儲模式DB
上傳至服務器,目錄爲:/usr/local/software
# 1、創建目錄
mkdir -p /usr/local/software
# 2、解壓
unzip seata-server-1.4.1.zip
# 3、修改存儲模式 DB
cd seata/conf/
vi file.conf
注:修改爲自己的mysql!!!!
## transaction log store, only used in seata-server store { ## store mode: file、db、redis mode = "file" ## database store property db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc. datasource = "druid" ## mysql/oracle/postgresql/h2/oceanbase etc. dbType = "mysql" driverClassName = "com.mysql.cj.jdbc.Driver" url = "jdbc:mysql://47.116.143.16:3306/seata?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai" user = "root" password = "root" minConn = 5 maxConn = 100 globalTable = "global_table" branchTable = "branch_table" lockTable = "lock_table" queryLimit = 100 maxWait = 5000 } }
將seata需要的3張表導入數據庫中,分別是:global_table、branch_table、lock_table
官網地址:http://seata.io/zh-cn/docs/user/quickstart.html
github地址:https://github.com/seata/seata/blob/develop/script/server/db/mysql.sql
-- -------------------------------- The script used when storeMode is 'db' -------------------------------- -- the table to store GlobalSession data CREATE TABLE IF NOT EXISTS `global_table` ( `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `status` TINYINT NOT NULL, `application_id` VARCHAR(32), `transaction_service_group` VARCHAR(32), `transaction_name` VARCHAR(128), `timeout` INT, `begin_time` BIGINT, `application_data` VARCHAR(2000), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`xid`), KEY `idx_status_gmt_modified` (`status` , `gmt_modified`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -- the table to store BranchSession data CREATE TABLE IF NOT EXISTS `branch_table` ( `branch_id` BIGINT NOT NULL, `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `resource_group_id` VARCHAR(32), `resource_id` VARCHAR(256), `branch_type` VARCHAR(8), `status` TINYINT, `client_id` VARCHAR(64), `application_data` VARCHAR(2000), `gmt_create` DATETIME(6), `gmt_modified` DATETIME(6), PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -- the table to store lock data CREATE TABLE IF NOT EXISTS `lock_table` ( `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(128), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking', `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_status` (`status`), KEY `idx_branch_id` (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; CREATE TABLE IF NOT EXISTS `distributed_lock` ( `lock_key` CHAR(20) NOT NULL, `lock_value` VARCHAR(20) NOT NULL, `expire` BIGINT, primary key (`lock_key`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0); INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
修改Seata 配置中心&註冊中心
修改Seata的配置
# 修改Seata配置
cd /usr/local/software/seata/conf
vi registry.conf
注:修改成自己的nacos信息
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
loadBalance = "RandomLoadBalance"
loadBalanceVirtualNodes = 10
nacos {
application = "seata-server"
serverAddr = "47.116.143.16:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "47.116.143.16:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
}
因爲Seata的配置中心是nacos,需要把Seata的配置,通過腳本推送到nacos中
官網地址:https://seata.io/zh-cn/docs/user/configuration/nacos.html
腳本地址:https://github.com/seata/seata/blob/develop/script/config-center/nacos/nacos-config.sh
config.txt地址(可以暫時不修改配置參數,直接到nacos中修改配置):https://github.com/seata/seata/blob/develop/script/config-center/config.txt
將Seata配置參數推送到nacos配置中心
# 1、將github中的nacos-config.sh,傳到服務器上,目錄爲:/usr/local/software/seata/conf
# 我這邊使用的是,將腳本文件拷出,在服務創建文件夾,賦予權限
touch nacos-config.sh
chmod +x nacos-config.sh
# 2、將config.txt,放到服務器上,目錄爲:/usr/local/software/seata
執行腳本
sh nacos-config.sh -h 47.116.143.16 -p 8848 -g SEATA_GROUP -u nacos -w nacos
-h:nacos主機地址
-p:nacos端口號
-g:nacos分組
-t:nacos命名空間
-u:nacos賬號
-w:nacos密碼
推送成功,已將Seata配置參數推送到Nacos配置中心
在nacos配置中心裏,修改Seata參數,具體修改參考官網如下
具體config.txt裏的參數解釋:https://seata.io/zh-cn/docs/user/configurations.html
新建2個配置需要與微服務中的配置對應上
service.vgroupMapping.${spring.alibaba.seata.tx-service-group}=default
如下
service.vgroupMapping.order_service_group=default
service.vgroupMapping.product_service_group=default
注意:分組爲:SEATA_GROUP
啓動Seata服務
- ./seata-server.sh啓動,默認端口8091(守護進程方式啓動 nohup ./seata-server.sh &)
注意:如果seata部署在服務器,微服務在本地啓動的話,2個服務不在一個局域網下,因此沒法通信,啓動Seata時,需要指定ip和端口號
sh seata-server.sh -p 8091 -h 47.116.143.16
Seata AT模式日期序列化問題解決方案
後端服務引入kryo依賴
<dependency> <groupId>com.esotericsoftware</groupId> <artifactId>kryo</artifactId> <version>4.0.2</version> </dependency> <dependency> <groupId>de.javakaffee</groupId> <artifactId>kryo-serializers</artifactId> <version>0.42</version> </dependency>
修改Seata在nacos配置中心配置
將
client.undo.logSerialization=jackson
修改爲
client.undo.logSerialization=kryo
微服務整合Seata
前期準備
在每個微服務所連的庫,新建一張表
-- 注意此處0.3.0+ 增加唯一索引 ux_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;
聚合工程搭建
。。。。
項目結構
- ybchen-common:公共模塊
- ybchen-order-service:訂單微服務
- ybchen-product-service:商品微服務
數據庫分表爲:order(訂單微服務庫)、product(商品微服務庫)、seata(Seata全局事務涉及的表)、nacos(Nacos配置中心,mysql持久化)
Seata依賴
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <exclusions> <exclusion> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.4.1</version> </dependency> <!-- seata 自身序列化bug問題-開始 --> <dependency> <groupId>com.esotericsoftware</groupId> <artifactId>kryo</artifactId> <version>4.0.2</version> </dependency> <dependency> <groupId>de.javakaffee</groupId> <artifactId>kryo-serializers</artifactId> <version>0.42</version> </dependency> <!-- seata 自身序列化bug問題-結束 -->
分佈式事務演示
關鍵代碼片段
order-service
@Autowired OrderMapper orderMapper; @Autowired ProductStockControllerFeign productStockControllerFeign; @Override //開啓分佈式事務 Seta AT模式 @GlobalTransactional public ReturnT<String> add() { OrderDO orderDO = new OrderDO(); int outTradeNo = new Random().nextInt(1000); orderDO.setOutTradeNo("T" + outTradeNo); orderDO.setCreateTime(new Date()); int rows = orderMapper.insert(orderDO); if (rows > 0) { //扣減商品庫存 ReturnT<String> reduceReturn = productStockControllerFeign.reduce(); if (ReturnT.isSuccess(reduceReturn)) { log.info("購買成功"); //TODO 模擬異常方式二 // int num = 1 / 0; return ReturnT.buildSuccess("購買成功"); } // 解決全局攔截器問題,通過接口響應狀態碼,來判斷是否主動拋異常!!!!!!! if (reduceReturn.getCode() != 0) { log.info("扣減商品庫存失敗,接口響應:{}", reduceReturn); throw new BizException(110, "扣減商品庫存失敗"); } log.info("扣減商品庫存失敗"); return ReturnT.buildError("扣減商品庫存失敗"); } log.info("購買失敗"); return ReturnT.buildError("購買失敗"); }
product-service
@Autowired ProductStockMapper productStockMapper; @Override public ReturnT<String> reduceProductStock() { ProductStockDO stockDO = new ProductStockDO(); stockDO.setProductId(10086); stockDO.setBuyNum(1); stockDO.setCreateTime(new Date()); int rows = productStockMapper.insert(stockDO); //TODO 模擬異常方式一 // int num = 1 / 0; if (rows > 0) { log.info("扣減商品庫存成功,rows=" + rows); return ReturnT.buildSuccess("扣減商品庫存成功"); } else { log.info("扣減商品庫存失敗,rows=" + rows); return ReturnT.buildError("扣減失敗"); } }
正常情況
場景描述:product微服務和order微服務均正常,2個微服務的事務全部提交成功,2個庫都插入數據成功
異常情況一(product微服務異常)
場景描述:product微服務發生異常,order微服務正常情況,出現異常情況時,需要2個微服務的事務全部回滾,2個庫插入的數據都回滾
異常情況二(order微服務異常)
場景描述:order微服務發生異常,product微服務正常,出現異常情況時,需要2個微服務的事務全部回滾,2個庫插入的數據都回滾
異常情況三(product微服務未啓動)
場景描述:order微服務正常啓動,product微服務未啓動,需要把order微服務插入的數據回滾
項目源碼
https://github.com/543210188/ybchen-seata
https://gitee.com/yenbin_chen/ybchen-seatay