一、什么是分布式事務(wù)問(wèn)題坝疼?
1.1 單體應(yīng)用
單體應(yīng)用中搜贤,一個(gè)業(yè)務(wù)操作需要調(diào)用三個(gè)模塊完成,此時(shí)數(shù)據(jù)的一致性由本地事務(wù)來(lái)保證钝凶。
1.2 微服務(wù)應(yīng)用
隨著業(yè)務(wù)需求的變化仪芒,單體應(yīng)用被拆分成微服務(wù)應(yīng)用,原來(lái)的三個(gè)模塊被拆分成三個(gè)獨(dú)立的應(yīng)用耕陷,分別使用獨(dú)立的數(shù)據(jù)源桌硫,業(yè)務(wù)操作需要調(diào)用三個(gè)服務(wù)來(lái)完成。此時(shí)每個(gè)服務(wù)內(nèi)部的數(shù)據(jù)一致性由本地事務(wù)來(lái)保證啃炸,但是全局的數(shù)據(jù)一致性問(wèn)題沒(méi)法保證铆隘。
1.3 分布式事務(wù)
在微服務(wù)架構(gòu)中由于全局?jǐn)?shù)據(jù)一致性沒(méi)法保證產(chǎn)生的問(wèn)題就是分布式事務(wù)問(wèn)題。簡(jiǎn)單來(lái)說(shuō)南用,一次業(yè)務(wù)操作需要操作多個(gè)數(shù)據(jù)源或需要進(jìn)行遠(yuǎn)程調(diào)用膀钠,就會(huì)產(chǎn)生分布式事務(wù)問(wèn)題。
二裹虫、Seata
Seata官網(wǎng)文檔
Seata官網(wǎng)下載地址
Seata官網(wǎng)demo
2.1 Seata歷史
2019 年 1 月肿嘲,阿里巴巴中間件團(tuán)隊(duì)發(fā)起了開源項(xiàng)目 Fescar(Fast & EaSy Commit And Rollback),和社區(qū)一起共建開源分布式事務(wù)解決方案筑公。Fescar 的愿景是讓分布式事務(wù)的使用像本地事務(wù)的使用一樣雳窟,簡(jiǎn)單和高效,并逐步解決開發(fā)者們遇到的分布式事務(wù)方面的所有難題匣屡。
Fescar 開源后封救,螞蟻金服加入 Fescar 社區(qū)參與共建,并在 Fescar 0.4.0 版本中貢獻(xiàn)了 TCC(Try Confirm Cancel) 模式捣作。為了打造更中立誉结、更開放、生態(tài)更加豐富的分布式事務(wù)開源社區(qū)券躁,經(jīng)過(guò)社區(qū)核心成員的投票惩坑,大家決定對(duì) Fescar 進(jìn)行品牌升級(jí),并更名為 Seata也拜,意為:Simple Extensible Autonomous Transaction Architecture以舒,是一套一站式分布式事務(wù)解決方案。
2.2 Seata是什么?
Seata 是一款開源的分布式事務(wù)解決方案慢哈,致力于提供高性能和簡(jiǎn)單易用的分布式事務(wù)服務(wù)蔓钟。Seata 將為用戶提供了 AT(默認(rèn))、TCC岸军、SAGA 和 XA 事務(wù)模式奋刽,為用戶打造一站式的分布式解決方案。
2.3 Seata術(shù)語(yǔ)
TC (Transaction Coordinator) - 事務(wù)協(xié)調(diào)者
維護(hù)全局和分支事務(wù)的狀態(tài)艰赞,驅(qū)動(dòng)全局事務(wù)提交或回滾佣谐。
TM (Transaction Manager) - 事務(wù)管理器
定義全局事務(wù)的范圍:開始全局事務(wù)、提交或回滾全局事務(wù)方妖。
RM (Resource Manager) - 資源管理器
管理分支事務(wù)處理的資源狭魂,與TC交談以注冊(cè)分支事務(wù)和報(bào)告分支事務(wù)的狀態(tài),并驅(qū)動(dòng)分支事務(wù)提交或回滾党觅。
2.4 Seata原理和設(shè)計(jì)
2.4.1 定義一個(gè)分布式事務(wù)
我們可以把一個(gè)分布式事務(wù)理解成一個(gè)包含了若干分支事務(wù)的全局事務(wù)雌澄,全局事務(wù)的職責(zé)是協(xié)調(diào)其下管轄的分支事務(wù)達(dá)成一致,要么一起成功提交杯瞻,要么一起失敗回滾镐牺。此外,通常分支事務(wù)本身就是一個(gè)滿足ACID的本地事務(wù)魁莉。這是我們對(duì)分布式事務(wù)結(jié)構(gòu)的基本認(rèn)識(shí)睬涧,與 XA 是一致的。
2.4.2 協(xié)議分布式事務(wù)處理過(guò)程的三個(gè)組件
Transaction Coordinator (TC): 事務(wù)協(xié)調(diào)器旗唁,維護(hù)全局事務(wù)的運(yùn)行狀態(tài)畦浓,負(fù)責(zé)協(xié)調(diào)并驅(qū)動(dòng)全局事務(wù)的提交或回滾;
Transaction Manager (TM): 控制全局事務(wù)的邊界检疫,負(fù)責(zé)開啟一個(gè)全局事務(wù)讶请,并最終發(fā)起全局提交或全局回滾的決議;
Resource Manager (RM): 控制分支事務(wù)屎媳,負(fù)責(zé)分支注冊(cè)夺溢、狀態(tài)匯報(bào),并接收事務(wù)協(xié)調(diào)器的指令烛谊,驅(qū)動(dòng)分支(本地)事務(wù)的提交和回滾企垦。
2.4.3 一個(gè)典型的分布式事務(wù)過(guò)程
- TM 向 TC 申請(qǐng)開啟一個(gè)全局事務(wù),全局事務(wù)創(chuàng)建成功并生成一個(gè)全局唯一的 XID晒来;
- XID 在微服務(wù)調(diào)用鏈路的上下文中傳播钞诡;
- RM 向 TC 注冊(cè)分支事務(wù),將其納入 XID 對(duì)應(yīng)全局事務(wù)的管轄;
- TM 向 TC 發(fā)起針對(duì) XID 的全局提交或回滾決議;
- TC 調(diào)度 XID 下管轄的全部分支事務(wù)完成提交或回滾請(qǐng)求完疫。
三伏伯、Seata各事務(wù)模式
3.1 Seata AT模式
3.1.1 前提
- 基于支持本地 ACID 事務(wù)的關(guān)系型數(shù)據(jù)庫(kù)。
- Java 應(yīng)用哼审,通過(guò) JDBC 訪問(wèn)數(shù)據(jù)庫(kù)。
3.1.2 整體機(jī)制
兩階段提交協(xié)議的演變:
- 一階段:業(yè)務(wù)數(shù)據(jù)和回滾日志記錄在同一個(gè)本地事務(wù)中提交,釋放本地鎖和連接資源剪返。
- 二階段:
提交異步化废累,非常快速地完成脱盲。
回滾通過(guò)一階段的回滾日志進(jìn)行反向補(bǔ)償邑滨。
3.1.3 寫隔離
- 一階段本地事務(wù)提交前,需要確保先拿到 全局鎖 钱反。
- 拿不到全局鎖 掖看,不能提交本地事務(wù)。
- 拿全局鎖的嘗試被限制在一定范圍內(nèi)面哥,超出范圍將放棄哎壳,并回滾本地事務(wù),釋放本地鎖尚卫。
以一個(gè)示例來(lái)說(shuō)明:
兩個(gè)全局事務(wù)tx1和tx2归榕,分別對(duì)a表的m字段進(jìn)行更新操作,m 的初始值1000吱涉。
tx1先開始蹲坷,開啟本地事務(wù),拿到本地鎖邑飒,更新操作 m = 1000 - 100 = 900循签。本地事務(wù)提交前,先拿到該記錄的全局鎖 疙咸,本地提交釋放本地鎖县匠。 tx2后開始,開啟本地事務(wù)撒轮,拿到本地鎖乞旦,更新操作 m = 900 - 100 = 800。本地事務(wù)提交前题山,嘗試拿該記錄的全局鎖 兰粉,tx1 全局提交前,該記錄的全局鎖被tx1持有顶瞳,tx2需要重試等待全局鎖 玖姑。
如果 tx1 的二階段全局回滾,則 tx1 需要重新獲取該數(shù)據(jù)的本地鎖慨菱,進(jìn)行反向補(bǔ)償?shù)母虏僮餮媛纾瑢?shí)現(xiàn)分支的回滾。
此時(shí)符喝,如果 tx2 仍在等待該數(shù)據(jù)的 全局鎖闪彼,同時(shí)持有本地鎖,則 tx1 的分支回滾會(huì)失敗协饲。分支的回滾會(huì)一直重試畏腕,直到 tx2 的 全局鎖 等鎖超時(shí)缴川,放棄 全局鎖 并回滾本地事務(wù)釋放本地鎖,tx1 的分支回滾最終成功描馅。
因?yàn)檎麄€(gè)過(guò)程 全局鎖 在 tx1 結(jié)束前一直是被 tx1 持有的把夸,所以不會(huì)發(fā)生 臟寫 的問(wèn)題。
3.1.4 讀隔離
在數(shù)據(jù)庫(kù)本地事務(wù)隔離級(jí)別 讀已提交(Read Committed) 或以上的基礎(chǔ)上流昏,Seata(AT 模式)的默認(rèn)全局隔離級(jí)別是 讀未提交(Read Uncommitted) 扎即。
如果應(yīng)用在特定場(chǎng)景下吞获,必需要求全局的 讀已提交 况凉,目前 Seata 的方式是通過(guò) SELECT FOR UPDATE 語(yǔ)句的代理。
SELECT FOR UPDATE 語(yǔ)句的執(zhí)行會(huì)申請(qǐng) 全局鎖 各拷,如果 全局鎖 被其他事務(wù)持有刁绒,則釋放本地鎖(回滾 SELECT FOR UPDATE 語(yǔ)句的本地執(zhí)行)并重試。這個(gè)過(guò)程中烤黍,查詢是被 block 住的知市,直到 全局鎖 拿到,即讀取的相關(guān)數(shù)據(jù)是 已提交 的速蕊,才返回嫂丙。
出于總體性能上的考慮,Seata 目前的方案并沒(méi)有對(duì)所有 SELECT 語(yǔ)句都進(jìn)行代理规哲,僅針對(duì) FOR UPDATE 的 SELECT 語(yǔ)句跟啤。
3.2 Seata TCC模式
一個(gè)分布式的全局事務(wù),整體是 兩階段提交的模型唉锌。全局事務(wù)是由若干分支事務(wù)組成的隅肥,分支事務(wù)要滿足兩階段提交的模型要求,即需要每個(gè)分支事務(wù)都具備自己的:
- 一階段 prepare 行為
- 二階段 commit 或 rollback 行為
根據(jù)兩階段行為模式的不同袄简,我們將分支事務(wù)劃分為 Automatic (Branch) Transaction Mode 和 TCC (Branch) Transaction Mode.
AT 模式基于支持本地 ACID 事務(wù)的關(guān)系型數(shù)據(jù)庫(kù):
- 一階段 prepare 行為:在本地事務(wù)中腥放,一并提交業(yè)務(wù)數(shù)據(jù)更新和相應(yīng)回滾日志記錄。
- 二階段 commit 行為:馬上成功結(jié)束绿语,自動(dòng)異步批量清理回滾日志秃症。
- 二階段 rollback 行為:通過(guò)回滾日志,自動(dòng)生成補(bǔ)償操作吕粹,完成數(shù)據(jù)回滾伍纫。
相應(yīng)的,TCC (Try Confirm Cancel)模式昂芜,不依賴于底層數(shù)據(jù)資源的事務(wù)支持:
- 一階段 prepare 行為:調(diào)用自定義的prepare邏輯莹规。
- 二階段 commit 行為:調(diào)用自定義的commit邏輯。
- 二階段 rollback 行為:調(diào)用自定義的rollback邏輯泌神。
所謂 TCC 模式良漱,是指支持把自定義的分支事務(wù)納入到全局事務(wù)的管理中舞虱。
3.3 Seata Saga模式
3.3.1 概述
Saga模式是SEATA提供的長(zhǎng)事務(wù)解決方案,在Saga模式中母市,業(yè)務(wù)流程中每個(gè)參與者都提交本地事務(wù)矾兜,當(dāng)出現(xiàn)某一個(gè)參與者失敗則補(bǔ)償前面已經(jīng)成功的參與者,一階段正向服務(wù)和二階段補(bǔ)償服務(wù)都由業(yè)務(wù)開發(fā)實(shí)現(xiàn)患久。
3.3.2 適用場(chǎng)景
- 業(yè)務(wù)流程長(zhǎng)椅寺、業(yè)務(wù)流程多
- 參與者包含其它公司或遺留系統(tǒng)服務(wù),無(wú)法提供 TCC 模式要求的三個(gè)接口
3.3.3 優(yōu)勢(shì)
- 一階段提交本地事務(wù)蒋失,無(wú)鎖返帕,高性能
- 事件驅(qū)動(dòng)架構(gòu),參與者可異步執(zhí)行篙挽,高吞吐
- 補(bǔ)償服務(wù)易于實(shí)現(xiàn)
3.3.4 缺點(diǎn)
不保證隔離性
3.4 Seata XA模式
3.4.1 前提
- 支持XA 事務(wù)的數(shù)據(jù)庫(kù)荆萤。
- Java 應(yīng)用,通過(guò) JDBC 訪問(wèn)數(shù)據(jù)庫(kù)铣卡。
3.4.2 整體機(jī)制
在Seata 定義的分布式事務(wù)框架內(nèi)链韭,利用事務(wù)資源(數(shù)據(jù)庫(kù)、消息服務(wù)等)對(duì) XA 協(xié)議的支持煮落,以XA 協(xié)議的機(jī)制來(lái)管理分支事務(wù)的一種 事務(wù)模式敞峭。
執(zhí)行階段:
1、可回滾:業(yè)務(wù)SQL 操作放在XA分支中進(jìn)行蝉仇,由資源對(duì)XA協(xié)議的支持來(lái)保證可回滾
2旋讹、持久化:XA分支完成后,執(zhí)行XA prepare量淌,同樣骗村,由資源對(duì)XA協(xié)議的支持來(lái)保證持久化(即,之后任何意外都不會(huì)造成無(wú)法回滾的情況)完成階段:
1呀枢、分支提交:執(zhí)行XA分支的 commit
2胚股、分支回滾:執(zhí)行XA分支的 rollback
四、Seata常見問(wèn)題
4.1 怎么使用Seata框架裙秋,來(lái)保證事務(wù)的隔離性琅拌?
因Seata一階段本地事務(wù)已提交,為防止其他事務(wù)臟讀臟寫需要加強(qiáng)隔離摘刑。
臟讀 select語(yǔ)句加for update进宝,代理方法增加@GlobalLock+@Transactional或@GlobalTransaction
臟寫 必須使用@GlobalTransaction
注:如果你查詢的業(yè)務(wù)的接口沒(méi)有GlobalTransactional 包裹,也就是這個(gè)方法上壓根沒(méi)有分布式事務(wù)的需求枷恕,這時(shí)你可以在方法上標(biāo)注@GlobalLock+@Transactional 注解党晋,并且在查詢語(yǔ)句上加 for update。 如果你查詢的接口在事務(wù)鏈路上外層有GlobalTransactional注解,那么你查詢的語(yǔ)句只要加for update就行未玻。設(shè)計(jì)這個(gè)注解的原因是在沒(méi)有這個(gè)注解之前灾而,需要查詢分布式事務(wù)讀已提交的數(shù)據(jù),但業(yè)務(wù)本身不需要分布式事務(wù)扳剿。 若使用GlobalTransactional注解就會(huì)增加一些沒(méi)用的額外的rpc開銷比如begin 返回xid旁趟,提交事務(wù)等。GlobalLock簡(jiǎn)化了rpc過(guò)程庇绽,使其做到更高的性能锡搜。
4.2 為什么MyBatis沒(méi)有返回自增ID?
方案1:需要修改MyBatis的配置: 在@Options(useGeneratedKeys = true, keyProperty = "id")或者在xml中指定useGeneratedKeys 和 keyProperty屬性
方案2:刪除undo_log表的id字段
4.3 AT 模式和 Spring @Transactional 注解連用時(shí)需要注意什么 ?
@Transactional 可與 DataSourceTransactionManager 和 JTATransactionManager 連用分別表示本地事務(wù)和XA分布式事務(wù)瞧掺,大家常用的是與本地事務(wù)結(jié)合耕餐。當(dāng)與本地事務(wù)結(jié)合時(shí),@Transactional和@GlobalTransaction連用夸盟,@Transactional 只能位于標(biāo)注在@GlobalTransaction的同一方法層次或者位于@GlobalTransaction 標(biāo)注方法的內(nèi)層蛾方。這里分布式事務(wù)的概念要大于本地事務(wù)像捶,若將 @Transactional 標(biāo)注在外層會(huì)導(dǎo)致分布式事務(wù)空提交上陕,當(dāng)@Transactional 對(duì)應(yīng)的 connection 提交時(shí)會(huì)報(bào)全局事務(wù)正在提交或者全局事務(wù)的xid不存在。
五拓春、項(xiàng)目[ac-mall-cloud]實(shí)現(xiàn)下單功能
功能描述:用戶下單释簿,產(chǎn)品庫(kù)存扣減[product-service]、用戶余額扣減[user-servcie]硼莽、生成訂單[order-service]
5.1 數(shù)據(jù)庫(kù)準(zhǔn)備
5.1.1 創(chuàng)建數(shù)據(jù)庫(kù)
為[user-servcie]庶溶、[product-service]、[order-service]三個(gè)微服務(wù)分別建立數(shù)據(jù)庫(kù)懂鸵,腳本如下
1偏螺、[user-servcie]數(shù)據(jù)庫(kù)腳本
-- 創(chuàng)建數(shù)據(jù)庫(kù)
create database ac_mall_cloud_user_service_db character set utf8mb4;
-- 創(chuàng)建用戶
create user 'user_service_u '@'%' identified by 'user_service_PWD_123';
-- 授權(quán)用戶
grant all privileges on ac_mall_cloud_user_service_db.* to 'user_service_u'@'%';
-- 刷新
flush privileges;
2、[product-servcie]數(shù)據(jù)庫(kù)腳本
-- 創(chuàng)建數(shù)據(jù)庫(kù)
create database ac_mall_cloud_product_service_db character set utf8mb4;
-- 創(chuàng)建用戶
create user 'product_service_u '@'%' identified by 'product_service_PWD_123';
-- 授權(quán)用戶
grant all privileges on ac_mall_cloud_product_service_db.* to 'product_service_u'@'%';
-- 刷新
flush privileges;
3匆光、[order-servcie]數(shù)據(jù)庫(kù)腳本
-- 創(chuàng)建數(shù)據(jù)庫(kù)
create database ac_mall_cloud_order_service_db character set utf8mb4;
-- 創(chuàng)建用戶
create user 'order_service_u '@'%' identified by 'order_service_PWD_123';
-- 授權(quán)用戶
grant all privileges on ac_mall_cloud_order_service_db.* to 'order_service_u'@'%';
-- 刷新
flush privileges;
5.1.2 創(chuàng)建數(shù)據(jù)表
1套像、在ac_mall_cloud_user_service_db庫(kù)里創(chuàng)建user表
CREATE TABLE `user` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_name` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用戶名',
`balance` decimal(10,0) DEFAULT NULL COMMENT '余額',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `user` (`id`, `user_name`, `balance`) VALUES ('1', 'AC', '1000');
2、在ac_mall_cloud_product_service_db庫(kù)里創(chuàng)建product表
CREATE TABLE `product` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`product_name` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '產(chǎn)品名',
`stock` int(11) DEFAULT NULL COMMENT '庫(kù)存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `product` (`id`, `product_name`, `stock`) VALUES ('1', 'iPhone X', '50');
3终息、在ac_mall_cloud_order_service_db庫(kù)里創(chuàng)建order表
CREATE TABLE `product_order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`amount` decimal(10,0) DEFAULT NULL COMMENT '訂單金額',
`user_id` bigint(11) COMMENT '用戶ID',
`user_name` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '用戶名',
`product_id` bigint(11) COMMENT '產(chǎn)品ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
5.2 下單功能代碼實(shí)現(xiàn)
5.2.1 maven依賴
分別在[user-servcie]夺巩、[product-service]、[order-service]的pom.xml中添加mysql周崭、mybaits-plus依賴
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
5.2.2 配置文件
分別在[user-servcie]柳譬、[product-service]、[order-service]的application.yml中添加mysql续镇、mybaits-plus配置美澳,以[user-servcie]為例,[product-service]、[order-service]同理
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://47.105.146.74:3306/ac_mall_cloud_user_service_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone =GMT%2B8
username: user_service_u
password: user_service_PWD_123
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
5.3 代碼
5.3.1 [user-service]代碼
@Data
public class User {
private int id;
private String userName;
private double balance;
}
@Repository
@Mapper
public interface UserDao extends BaseMapper<User> {
}
@Service
public class UserServiceImpl implements IUserService {
@Autowired
UserDao userDao;
public User getUser(String id) {
System.out.println("獲取用戶信息");
return userDao.selectById(id);
}
/**
* 下單扣減余額
* @param userId
* @param amount
*/
@Transactional(rollbackFor = Exception.class)
public void deductionBalance(int userId, double amount) {
User user = userDao.selectById(userId);
double newBalance=user.getBalance()-amount;
user.setBalance(newBalance);
userDao.updateById(user);
System.out.println("下單扣減余額成功!");
}
}
@RestController
@RequestMapping(ModulePrePath.API+"/users")
public class UserApi {
@Autowired
IUserService userService;
/**
* 獲取用戶信息
* @param userId
* @return
*/
@GetMapping("/{userId}")
public User getUser(@PathVariable String userId){
return userService.getUser(userId);
}
/**
* 下單扣減余額
* @param userId
* @param amount
*/
@PutMapping("/deduction_balance/{userId}/{amount}")
void deductionBalance(@PathVariable int userId,@PathVariable double amount){
userService.deductionBalance(userId,amount);
}
}
5.3.2 [product-service]代碼
@Data
public class Product {
private int id;
private String productName;
private int stock;
}
@Repository
@Mapper
public interface ProductDao extends BaseMapper<Product> {
}
@Service
public class ProductServiceImpl implements IProductService {
@Autowired
ProductDao productDao;
/**
* 下單扣減庫(kù)存
* @param productId
* @param subCount
*/
@Transactional(rollbackFor = Exception.class)
public void subStock(int productId, int subCount) {
Product product = productDao.selectById(productId);
int newStock = product.getStock()-subCount;
product.setStock(newStock);
productDao.updateById(product);
System.out.println("下單扣減庫(kù)存成功!");
}
}
@RestController
@RequestMapping(ModulePrePath.API+"/products")
public class ProductApi {
@Autowired
IProductService productService;
/**
* 更新銷量
* @param productId
*/
@PutMapping("/{productId}")
public void updateSales(@PathVariable String productId){
System.out.println("商品:"+productId+"更新銷量數(shù)");
}
/**
* 下單扣減庫(kù)存
* @param productId
* @param subCount
*/
@PutMapping("/sub_stock/{productId}/{subCount}")
public void subStock(@PathVariable int productId,@PathVariable int subCount){
productService.subStock(productId,subCount);
}
}
5.3.3 [order-service]代碼
@Data
public class ProductOrder {
private int id;
private double amount;
private int userId;
private String userName;
private int productId;
}
@Repository
@Mapper
public interface OrderDao extends BaseMapper<ProductOrder> {
}
@Service
public class OrderServiceImpl implements IOrderService {
@Autowired
OrderDao orderDao;
@Autowired
RestTemplate restTemplate;
@Autowired
UserServiceClient userServiceClient;
@Autowired
ProductServiceClient productServiceClient;
//final static String USER_SERVICE_URL="http://127.0.0.1:8010/users/{userId}";
final static String USER_SERVICE_URL="http://user-service/users/{userId}"; //用服務(wù)名來(lái)替換IP
@Transactional(rollbackFor = Exception.class)
public ProductOrder makeOrder(int productId, int userId) {
/**
* RestTemplate是java創(chuàng)造出來(lái)的制跟,在java能夠訪問(wèn)到網(wǎng)絡(luò)資源的包是java.net.URLConnenction/Socket
* RestTemplate是對(duì)URLConnenction的封裝
* apache--HttpClient 也是對(duì)URLConnenction/HttpURLConnenction的封裝
* oKHttp 也封裝了URLConnenction
* netty/rpc/grpc/thirt/tomcat
*/
// 1柴墩、根據(jù)用戶ID調(diào)用用戶服務(wù)接口數(shù)據(jù),查詢用戶的名字
//UserDto userDto = restTemplate.getForObject(USER_SERVICE_URL,UserDto.class,userId);
//換成OpenFeign
UserDto userDto = userServiceClient.getUser(userId);
String userName=userDto.getUserName();
double amount = 100;
// 2凫岖、生成訂單
ProductOrder productOrder = new ProductOrder();
productOrder.setAmount(amount);
productOrder.setUserId(userId);
productOrder.setUserName(userName);
productOrder.setProductId(productId);
// 3江咳、保存數(shù)據(jù)庫(kù)
orderDao.insert(productOrder);
// 4、更新產(chǎn)品銷量
productServiceClient.updateSales(productId);
// 5哥放、下單減庫(kù)存
productServiceClient.subStock(productId,1);
// 6歼指、下單扣減用戶余額
userServiceClient.deductionBalance(userId,amount);
return productOrder;
}
}
@FeignClient(name="product-service",fallbackFactory = ProductFeignClientFallbackFactory.class)
public interface ProductServiceClient {
/**
* 更新產(chǎn)品銷量
* @param productId
*/
@PutMapping(ModulePrePath.API+"/products/{productId}")
void updateSales(@PathVariable("productId") int productId);
/**
* 下單扣減庫(kù)存
* @param productId
* @param subCount
*/
@PutMapping(ModulePrePath.API+"/products/sub_stock/{productId}/{subCount}")
void subStock(@PathVariable("productId") int productId,@PathVariable("subCount") int subCount);
}
@FeignClient("user-service")
public interface UserServiceClient {
/**
* 獲取用戶信息
* @param userId
* @return
*/
@GetMapping(ModulePrePath.API+"/users/{userId}")
UserDto getUser(@PathVariable("userId") int userId);
/**
* 下單扣減余額
* @param userId
* @param amount
*/
@PutMapping(ModulePrePath.API+"/users/deduction_balance/{userId}/{amount}")
void deductionBalance(@PathVariable("userId") int userId,@PathVariable("amount") double amount);
}
5.4下單接口測(cè)試
依次啟動(dòng)[auth-service]、[gateway-service]甥雕、[user-service]踩身、[product-service]、[order-service]
5.4.1 處理超時(shí)問(wèn)題
調(diào)用下單接口社露,有很大機(jī)率會(huì)出現(xiàn)超時(shí)異常挟阻,如下
java.net.SocketTimeoutException: connect timed out
默認(rèn)Feign 客戶端只等待1秒鐘,但是服務(wù)端處理需要超過(guò)1秒鐘峭弟,導(dǎo)致Feign 客戶端不想等待了附鸽,直接返回報(bào)錯(cuò)。為了避免這樣的情況瞒瘸,我們需要設(shè)置Feign客戶端的超時(shí)控制坷备。OpenFeign 內(nèi)與 Ribbon 整合了,支持負(fù)載均衡情臭,它的超時(shí)控制也由最底層的Ribbon進(jìn)行控制省撑,在[order-service]的application.yml添加配置:
#設(shè)置feign 客戶端超時(shí)時(shí)間(openFeign默認(rèn)支持ribbon)
ribbon:
#指的是建立連接所用的時(shí)間,適用于網(wǎng)絡(luò)狀況正常的情況下俯在,兩端連接所用的時(shí)間
ReadTimeout: 25000
#指的是建立連接后從服務(wù)器讀取到可用資源所用的時(shí)間
ConnectTimeout: 25000
[order-service]application.yml完整配置
server:
port: 8020
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: 47.105.146.74:8848
sentinel:
transport:
dashboard: 127.0.0.1:8888
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://47.105.146.74:3306/ac_mall_cloud_order_service_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone =GMT%2B8
username: order_service_u
password: order_service_PWD_123
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
feign:
sentinel:
#為feign整合Sentinel
enabled: true
#設(shè)置feign 客戶端超時(shí)時(shí)間(openFeign默認(rèn)支持ribbon)
ribbon:
#指的是建立連接所用的時(shí)間竟秫,適用于網(wǎng)絡(luò)狀況正常的情況下,兩端連接所用的時(shí)間
ReadTimeout: 25000
#指的是建立連接后從服務(wù)器讀取到可用資源所用的時(shí)間
ConnectTimeout: 25000
5.4.2 配置完超時(shí)時(shí)間后重啟項(xiàng)目重新訪問(wèn)下單接口
1跷乐、Postman結(jié)果
2肥败、數(shù)據(jù)庫(kù)更新成功
5.4.3 制造異常,暴露分布式事務(wù)問(wèn)題
我們?cè)赱product-service]服務(wù)的下單扣減庫(kù)存接口中劈猿,人為制造一個(gè)異常拙吉,讓扣減庫(kù)存失敗
我們?cè)賮?lái)調(diào)用下單接口,發(fā)現(xiàn)接口返回成功了揪荣。用戶余額扣減了筷黔,訂單記錄也生成了,但庫(kù)存沒(méi)有扣減(異常導(dǎo)致失斦叹薄)佛舱,該操作跨越三個(gè)數(shù)據(jù)庫(kù)椎例,有兩次遠(yuǎn)程調(diào)用,這就是事務(wù)失效的原因请祖,需要用分布式事務(wù)的方式來(lái)解決該問(wèn)題订歪。
六、Seata完成分布式事務(wù)
6.1 Seata的安裝與配置
6.1.1 下載
我們先從官網(wǎng)下載seata-server肆捕,https://github.com/seata/seata/releases
刷晋,本文采用的是 seata 1.3.0
6.1.2 Seata Server需要依賴的表
由于我們準(zhǔn)備使用了db模式存儲(chǔ)事務(wù)日志,所以我們需要先創(chuàng)建一個(gè)seat-server數(shù)據(jù)庫(kù)
-- 創(chuàng)建數(shù)據(jù)庫(kù)
create database seata_db character set utf8mb4;
-- 創(chuàng)建用戶
create user 'seata_u '@'%' identified by 'seata_PWD_123';
-- 授權(quán)用戶
grant all privileges on seata_db.* to 'seata_u'@'%';
-- 刷新
flush privileges;
在seat-server數(shù)據(jù)庫(kù)創(chuàng)建如下三個(gè)表慎陵,用于seata服務(wù)眼虱, 0.0.9版本才有這個(gè)文件1.0.0版本后需要手動(dòng)添加。
表的地址 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_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- 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 = utf8;
-- 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),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
AT模式下每個(gè)業(yè)務(wù)數(shù)據(jù)庫(kù)需要?jiǎng)?chuàng)建undo_log表席纽,用于seata記錄分支的回滾信息
表的地址 https://github.com/seata/seata/blob/1.3.0/script/client/at/db/mysql.sql
-- 注意此處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;
6.1.3 修改file.conf捏悬、registry.conf
解壓seata-server安裝包到指定目錄,修改conf目錄下的file.conf润梯、registry.conf配置文件过牙,主要修改自定義事務(wù)組名稱,事務(wù)日志存儲(chǔ)模式為db及數(shù)據(jù)庫(kù)連接信息纺铭。
注意:registry.conf里的group="SEATA_GROUP"
最好配置為SEATA_GROUP不要配置其它寇钉,否則服務(wù)會(huì)出現(xiàn)注冊(cè)不到的問(wèn)題,提示No available service
file.conf完整配置
## transaction log store, only used in seata-server
store {
## store mode: file彤蔽、db摧莽、redis
mode = "db"
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "hikari"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://47.105.146.74:3306/seata_db"
user = "seata_u"
password = "seata_PWD_123"
minConn = 5
maxConn = 30
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
maxBranchSessionSize = 16384
# globe session size , if exceeded throws exceptions
maxGlobalSessionSize = 512
# file buffer size , if exceeded allocate new buffer
fileWriteBufferCacheSize = 16384
# when recover batch read size
sessionReloadReadSize = 100
# async, sync
flushDiskMode = async
}
## redis store property
redis {
host = "127.0.0.1"
port = "6379"
password = ""
database = "0"
minConn = 1
maxConn = 10
queryLimit = 100
}
}
registry.conf完整配置
registry {
# file 庙洼、nacos 顿痪、eureka、redis油够、zk蚁袭、consul、etcd3石咬、sofa
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "47.105.146.74:8848"
group = "SEATA_GROUP"
namespace = "public"
cluster = "default"
username = "nacos"
password = "nacos"
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = 0
password = ""
cluster = "default"
timeout = 0
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
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 = "47.105.146.74:8848"
namespace = "public"
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
appId = "seata-server"
apolloMeta = "http://192.168.1.204:8801"
namespace = "application"
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
6.1.4 啟動(dòng)seata-server
點(diǎn)擊seata-server-1.3.0\seata\bin\seata-server.bat
啟動(dòng)seata-server
補(bǔ)充:?jiǎn)?dòng)bat閃退問(wèn)題
首先我們?cè)谀夸浻胏md打開看錯(cuò)誤信息
可以根據(jù)窗口中的具體錯(cuò)誤信息針對(duì)性地進(jìn)行解決焕窝。
注意:mysql8 的驅(qū)動(dòng)要改成com.mysql.cj.jdbc.Driver
而不是com.mysql.jdbc.Driver
6.1.5 配置nacos-config.txt
官網(wǎng)seata-server-0.9.0的conf目錄下有該文件蹬挺,后面的版本無(wú)該文件需要手動(dòng)下載執(zhí)行。
文件地址 https://github.com/seata/seata/blob/1.3.0/script/config-center/config.txt
修改以下參數(shù)配置
store.mode=db
store.db.datasource=hikari
store.db.url=jdbc:mysql://47.105.146.74:3306/seata_db?useUnicode=true
store.db.user=seata_u
store.db.password=seata_PWD_123
service.vgroupMapping.my_test_tx_group=default
nacos-config.txt完整配置如下
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
service.vgroupMapping.my_test_tx_group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
store.mode=db
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=hikari
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://47.105.146.74:3306/seata_db?useUnicode=true
store.db.user=seata_u
store.db.password=seata_PWD_123
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.maxConn=10
store.redis.minConn=1
store.redis.database=0
store.redis.password=null
store.redis.queryLimit=100
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
將nacos-config.txt 放到seata-server目錄下
6.1.6 執(zhí)行nacos-config.sh腳本
官網(wǎng)seata-server-0.9.0的conf目錄下有該文件它掂,后面的版本無(wú)該文件需要手動(dòng)下載執(zhí)行巴帮。
腳本地址 https://github.com/seata/seata/blob/1.3.0/script/config-center/nacos/nacos-config.sh
將nacos-config.sh腳本 放到seata-server目錄下
nacos-config.sh內(nèi)容
#!/usr/bin/env bash
# Copyright 1999-2019 Seata.io Group.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at、
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
while getopts ":h:p:g:t:u:w:" opt
do
case $opt in
h)
host=$OPTARG
;;
p)
port=$OPTARG
;;
g)
group=$OPTARG
;;
t)
tenant=$OPTARG
;;
u)
username=$OPTARG
;;
w)
password=$OPTARG
;;
?)
echo " USAGE OPTION: $0 [-h host] [-p port] [-g group] [-t tenant] [-u username] [-w password] "
exit 1
;;
esac
done
if [[ -z ${host} ]]; then
host=localhost
fi
if [[ -z ${port} ]]; then
port=8848
fi
if [[ -z ${group} ]]; then
group="SEATA_GROUP"
fi
if [[ -z ${tenant} ]]; then
tenant=""
fi
if [[ -z ${username} ]]; then
username=""
fi
if [[ -z ${password} ]]; then
password=""
fi
nacosAddr=$host:$port
contentType="content-type:application/json;charset=UTF-8"
echo "set nacosAddr=$nacosAddr"
echo "set group=$group"
failCount=0
tempLog=$(mktemp -u)
function addConfig() {
curl -X POST -H "${contentType}" "http://$nacosAddr/nacos/v1/cs/configs?dataId=$1&group=$group&content=$2&tenant=$tenant&username=$username&password=$password" >"${tempLog}" 2>/dev/null
if [[ -z $(cat "${tempLog}") ]]; then
echo " Please check the cluster status. "
exit 1
fi
if [[ $(cat "${tempLog}") =~ "true" ]]; then
echo "Set $1=$2 successfully "
else
echo "Set $1=$2 failure "
(( failCount++ ))
fi
}
count=0
for line in $(cat $(dirname "$PWD")/config.txt | sed s/[[:space:]]//g); do
(( count++ ))
key=${line%%=*}
value=${line#*=}
addConfig "${key}" "${value}"
done
echo "========================================================================="
echo " Complete initialization parameters, total-count:$count , failure-count:$failCount "
echo "========================================================================="
if [[ ${failCount} -eq 0 ]]; then
echo " Init nacos config finished, please start seata-server. "
else
echo " init nacos config fail. "
fi
如果本地是windows,使用git工具git bash執(zhí)行nacos-config.sh腳本:
sh nacos-config.sh -h 47.105.146.74 -p 8848
執(zhí)行完成后nacos會(huì)新增seata配置榕茧。
6.2 spring-cloud垃沦、spring-boot、alibaba-cloud版本升級(jí)
spring-cloud-alibaba 版本說(shuō)明
為了使用Seata 1.3.0用押,我們需要先將[ac-mall-cloud]的spring-cloud肢簿、spring-boot、alibaba-cloud版本對(duì)應(yīng)升級(jí)蜻拨,對(duì)應(yīng)版本如下
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
<mysql.version>8.0.17</mysql.version>
<mybatis.plus.version>3.2.0</mybatis.plus.version>
<druid.version>1.1.10</druid.version>
<boot.version>2.3.2.RELEASE</boot.version>
<alibaba.cloud.version>2.2.5.RELEASE</alibaba.cloud.version>
<alibaba.seata.version>2.2.0.RELEASE</alibaba.seata.version>
<lombok.version>1.18.10</lombok.version>
</properties>
6.3 [ac-mall-cloud]代碼
6.3.1 父級(jí)工程配置
在父級(jí)工程 [ac-mall-cloud] pom.xml中引入Seata依賴
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>${alibaba.seata.version}</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
<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.3.0</version>
</dependency>
[ac-mall-cloud] pom.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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>ac-mall-cloud</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>user-service</module>
<module>product-service</module>
<module>order-service</module>
<module>gateway-service</module>
<module>auth-service</module>
</modules>
<packaging>pom</packaging>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
<mysql.version>8.0.17</mysql.version>
<mybatis.plus.version>3.2.0</mybatis.plus.version>
<druid.version>1.1.10</druid.version>
<boot.version>2.3.2.RELEASE</boot.version>
<alibaba.cloud.version>2.2.5.RELEASE</alibaba.cloud.version>
<alibaba.seata.version>2.2.0.RELEASE</alibaba.seata.version>
<lombok.version>1.18.10</lombok.version>
</properties>
<!-- 管理子類所有的jar包的版本译仗,這樣的目的是方便去統(tǒng)一升級(jí)和維護(hù) -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${alibaba.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-seata</artifactId>
<version>${alibaba.seata.version}</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
<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.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>${alibaba.cloud.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>${alibaba.cloud.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>${alibaba.cloud.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis.plus.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 所有的子工程都會(huì)自動(dòng)加入下面的依賴 -->
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
</dependencies>
<!-- SpringBoot 工程編譯打包的插件,放在父pom中就直接給所有子工程繼承 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
6.3.2 [user-service]官觅、[product-service]纵菌、[order-service]pom.xml中引入Seata依賴
以 [user-service]pom.xml 為例,[product-service]休涤、[order-service]pom.xml同理
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-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>
</dependency>
[user-service]pom.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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ac-mall-cloud</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.ac</groupId>
<artifactId>user-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</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>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-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>
</dependency>
</dependencies>
</project>
6.3.3 [user-service]漏峰、[product-service]、[order-service]application.yml 添加 seata配置
以 [user-service]application.yml為例噪馏,[product-service]刊懈、[order-service]application.yml同理
seata:
enabled: true
application-id: user-service # 同spring.application.name 配置的項(xiàng)目名稱
tx-service-group: my_test_tx_group
enable-auto-data-source-proxy: true #開啟數(shù)據(jù)庫(kù)代理
config:
type: nacos
nacos:
namespace:
server-addr: 47.105.146.74:8848
group: SEATA_GROUP
username: nacos
password: nacos
registry:
type: nacos
nacos:
application: seata-server
server-addr: 47.105.146.74:8848
namespace: public
username: nacos
password: nacos
[user-service]application.yml完整配置如下
server:
port: 8010
spring:
application:
name: user-service
cloud:
nacos:
discovery:
server-addr: 47.105.146.74:8848
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://47.105.146.74:3306/ac_mall_cloud_user_service_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone =GMT%2B8
username: user_service_u
password: user_service_PWD_123
#hikari數(shù)據(jù)庫(kù)連接池
hikari:
pool-name: YH_HikariCP
minimum-idle: 10 #最小空閑連接數(shù)量
idle-timeout: 600000 #空閑連接存活最大時(shí)間,默認(rèn)600000(10分鐘)
maximum-pool-size: 100 #連接池最大連接數(shù)捷凄,默認(rèn)是10
auto-commit: true #此屬性控制從池返回的連接的默認(rèn)自動(dòng)提交行為,默認(rèn)值:true
max-lifetime: 1800000 #此屬性控制池中連接的最長(zhǎng)生命周期忱详,值0表示無(wú)限生命周期,默認(rèn)1800000即30分鐘
connection-timeout: 30000 #數(shù)據(jù)庫(kù)連接超時(shí)時(shí)間,默認(rèn)30秒跺涤,即30000
connection-test-query: SELECT 1
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
seata:
enabled: true
application-id: user-service # 同spring.application.name 配置的項(xiàng)目名稱
tx-service-group: my_test_tx_group
enable-auto-data-source-proxy: true #開啟數(shù)據(jù)庫(kù)代理
config:
type: nacos
nacos:
namespace:
server-addr: 47.105.146.74:8848
group: SEATA_GROUP
username: nacos
password: nacos
registry:
type: nacos
nacos:
application: seata-server
server-addr: 47.105.146.74:8848
namespace: public
username: nacos
password: nacos
6.3.4 先取消降級(jí)接口(巨坑)
在[order-service]中匈睁,先將產(chǎn)品的降級(jí)接口注釋以免影響事務(wù)回滾。在做本案例時(shí)由于沒(méi)有先注釋該降級(jí)接口代碼桶错,導(dǎo)致分布式事務(wù)一直沒(méi)有得到回滾航唆,經(jīng)過(guò)定位和分析才定位到這個(gè)問(wèn)題。
6.3.4 加@GlobalTransactional
注解
在[order-service]下單接口上加分布式事務(wù)@GlobalTransactional
注解
下單接口完整代碼如下
package com.ac.order.service.impl;
import com.ac.order.dao.OrderDao;
import com.ac.order.dto.UserDto;
import com.ac.order.entity.ProductOrder;
import com.ac.order.feign.ProductServiceClient;
import com.ac.order.feign.UserServiceClient;
import com.ac.order.service.IOrderService;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
/**
* @author Alan Chen
* @description
* @date 2020/10/15
*/
@Service
public class OrderServiceImpl implements IOrderService {
@Autowired
OrderDao orderDao;
@Autowired
RestTemplate restTemplate;
@Autowired
UserServiceClient userServiceClient;
@Autowired
ProductServiceClient productServiceClient;
//final static String USER_SERVICE_URL="http://127.0.0.1:8010/users/{userId}";
final static String USER_SERVICE_URL="http://user-service/users/{userId}"; //用服務(wù)名來(lái)替換IP
@GlobalTransactional(rollbackFor = Exception.class)
@Transactional(rollbackFor = Exception.class)
public ProductOrder makeOrder(int productId, int userId) {
System.out.println("開始分支事務(wù)院刁,XID = " + RootContext.getXID());
/**
* RestTemplate是java創(chuàng)造出來(lái)的糯钙,在java能夠訪問(wèn)到網(wǎng)絡(luò)資源的包是java.net.URLConnenction/Socket
* RestTemplate是對(duì)URLConnenction的封裝
* apache--HttpClient 也是對(duì)URLConnenction/HttpURLConnenction的封裝
* oKHttp 也封裝了URLConnenction
* netty/rpc/grpc/thirt/tomcat
*/
// 1、根據(jù)用戶ID調(diào)用用戶服務(wù)接口數(shù)據(jù)退腥,查詢用戶的名字
//UserDto userDto = restTemplate.getForObject(USER_SERVICE_URL,UserDto.class,userId);
//換成OpenFeign
UserDto userDto = userServiceClient.getUser(userId);
String userName=userDto.getUserName();
double amount = 100;
// 2任岸、生成訂單
ProductOrder productOrder = new ProductOrder();
productOrder.setAmount(amount);
productOrder.setUserId(userId);
productOrder.setUserName(userName);
productOrder.setProductId(productId);
// 3、保存數(shù)據(jù)庫(kù)
orderDao.insert(productOrder);
// 4狡刘、更新產(chǎn)品銷量
productServiceClient.updateSales(productId);
// 5享潜、下單減庫(kù)存
productServiceClient.subStock(productId,1);
// 6、下單扣減用戶余額
userServiceClient.deductionBalance(userId,amount);
return productOrder;
}
}
即使加了 @GlobalTransactional
分布式注解颓帝,本地?cái)?shù)據(jù)庫(kù)事務(wù)注解 @Transactional(rollbackFor = Exception.class)
仍然不能省略米碰, 否則依然會(huì)生成新的訂單數(shù)據(jù)窝革。
6.3.5 測(cè)試分布式事務(wù)效果
重啟各服務(wù),重新調(diào)用下單接口
[user-service]控制臺(tái)
2021-04-19 15:37:46.950 INFO 10316 --- [nio-8010-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-04-19 15:37:46.950 INFO 10316 --- [nio-8010-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2021-04-19 15:37:46.955 INFO 10316 --- [nio-8010-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 5 ms
獲取用戶信息
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7024bfae] was not registered for synchronization because synchronization is not active
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@62a2a35d] will not be managed by Spring
==> Preparing: SELECT id,balance,user_name FROM user WHERE id=?
==> Parameters: 1(String)
<== Columns: id, balance, user_name
<== Row: 1, 1000, AC
<== Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7024bfae]
2021-04-19 15:37:47.448 WARN 10316 --- [nio-8010-exec-1] c.a.c.seata.web.SeataHandlerInterceptor : xid in change during RPC from 192.168.124.33:8091:127434137383604224 to null
[product-service]控制臺(tái)
開始分支事務(wù)吕座,XID = 192.168.124.33:8091:127434137383604224
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@16b2b486]
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@16f2857d] will be managed by Spring
==> Preparing: SELECT id,stock,product_name FROM product WHERE id=?
==> Parameters: 1(Integer)
<== Columns: id, stock, product_name
<== Row: 1, 50, iPhone X
<== Total: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@16b2b486]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@16b2b486] from current transaction
==> Preparing: UPDATE product SET stock=?, product_name=? WHERE id=?
==> Parameters: 49(Integer), iPhone X(String), 1(Integer)
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@16b2b486]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@16b2b486]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@16b2b486]
2021-04-19 15:37:49.679 WARN 21328 --- [nio-8030-exec-1] c.a.c.seata.web.SeataHandlerInterceptor : xid in change during RPC from 192.168.124.33:8091:127434137383604224 to null
2021-04-19 15:37:49.703 ERROR 21328 --- [nio-8030-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause
java.lang.ArithmeticException: / by zero
at com.ac.product.service.impl.ProductServiceImpl.subStock(ProductServiceImpl.java:38) ~[classes/:na]
at com.ac.product.service.impl.ProductServiceImpl$$FastClassBySpringCGLIB$$e5fb083d.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:367) ~[spring-tx-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118) ~[spring-tx-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at com.ac.product.service.impl.ProductServiceImpl$$EnhancerBySpringCGLIB$$6977031b.subStock(<generated>) ~[classes/:na]
at com.ac.product.api.ProductApi.subStock(ProductApi.java:36) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_131]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_131]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_131]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_131]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doPut(FrameworkServlet.java:920) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:655) ~[tomcat-embed-core-9.0.37.jar:4.0.FR]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) ~[tomcat-embed-core-9.0.37.jar:4.0.FR]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.37.jar:9.0.37]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1589) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.37.jar:9.0.37]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_131]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_131]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.37.jar:9.0.37]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_131]
2021-04-19 15:37:49.755 WARN 21328 --- [nio-8030-exec-1] c.a.c.seata.web.SeataHandlerInterceptor : xid in change during RPC from 192.168.124.33:8091:127434137383604224 to null
[order-service]控制臺(tái)
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@13d892bf]
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@5ca7c9fe] will be managed by Spring
==> Preparing: INSERT INTO product_order ( id, amount, product_id, user_name, user_id ) VALUES ( ?, ?, ?, ?, ? )
2021-04-19 15:37:47.793 INFO 3656 --- [erListUpdater-0] c.netflix.config.ChainedDynamicProperty : Flipping property: user-service.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
==> Parameters: 0(Integer), 100.0(Double), 1(Integer), AC(String), 1(Integer)
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@13d892bf]
2021-04-19 15:37:48.259 INFO 3656 --- [nio-8020-exec-1] c.netflix.config.ChainedDynamicProperty : Flipping property: product-service.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2021-04-19 15:37:48.261 INFO 3656 --- [nio-8020-exec-1] c.netflix.loadbalancer.BaseLoadBalancer : Client: product-service instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=product-service,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2021-04-19 15:37:48.263 INFO 3656 --- [nio-8020-exec-1] c.n.l.DynamicServerListLoadBalancer : Using serverListUpdater PollingServerListUpdater
2021-04-19 15:37:48.315 INFO 3656 --- [nio-8020-exec-1] c.netflix.config.ChainedDynamicProperty : Flipping property: product-service.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2021-04-19 15:37:48.317 INFO 3656 --- [nio-8020-exec-1] c.n.l.DynamicServerListLoadBalancer : DynamicServerListLoadBalancer for client product-service initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=product-service,current list of Servers=[192.168.124.33:8030],Load balancer stats=Zone stats: {unknown=[Zone:unknown; Instance count:1; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;]
},Server stats: [[Server:192.168.124.33:8030; Zone:UNKNOWN; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 08:00:00 CST 1970; First connection made: Thu Jan 01 08:00:00 CST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
]}ServerList:com.alibaba.cloud.nacos.ribbon.NacosServerList@458c3ac8
2021-04-19 15:37:49.265 INFO 3656 --- [erListUpdater-0] c.netflix.config.ChainedDynamicProperty : Flipping property: product-service.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@13d892bf]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@13d892bf]
2021-04-19 15:37:49.931 INFO 3656 --- [nio-8020-exec-1] i.seata.tm.api.DefaultGlobalTransaction : [192.168.124.33:8091:127434137383604224] rollback status: Rollbacked
2021-04-19 15:37:49.962 ERROR 3656 --- [nio-8020-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is feign.FeignException$InternalServerError: [500] during [PUT] to [http://product-service/api/products/sub_stock/1/1] [ProductServiceClient#subStock(int,int)]: [{"timestamp":"2021-04-19T07:37:49.731+00:00","status":500,"error":"Internal Server Error","message":"","path":"/api/products/sub_stock/1/1"}]] with root cause
feign.FeignException$InternalServerError: [500] during [PUT] to [http://product-service/api/products/sub_stock/1/1] [ProductServiceClient#subStock(int,int)]: [{"timestamp":"2021-04-19T07:37:49.731+00:00","status":500,"error":"Internal Server Error","message":"","path":"/api/products/sub_stock/1/1"}]
at feign.FeignException.serverErrorStatus(FeignException.java:231) ~[feign-core-10.10.1.jar:na]
at feign.FeignException.errorStatus(FeignException.java:180) ~[feign-core-10.10.1.jar:na]
at feign.FeignException.errorStatus(FeignException.java:169) ~[feign-core-10.10.1.jar:na]
at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:92) ~[feign-core-10.10.1.jar:na]
at feign.AsyncResponseHandler.handleResponse(AsyncResponseHandler.java:96) ~[feign-core-10.10.1.jar:na]
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:138) ~[feign-core-10.10.1.jar:na]
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:89) ~[feign-core-10.10.1.jar:na]
at com.alibaba.cloud.sentinel.feign.SentinelInvocationHandler.invoke(SentinelInvocationHandler.java:107) ~[spring-cloud-starter-alibaba-sentinel-2.2.5.RELEASE.jar:2.2.5.RELEASE]
at com.sun.proxy.$Proxy95.subStock(Unknown Source) ~[na:na]
at com.ac.order.service.impl.OrderServiceImpl.makeOrder(OrderServiceImpl.java:79) ~[classes/:na]
at com.ac.order.service.impl.OrderServiceImpl$$FastClassBySpringCGLIB$$1db3649d.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:367) ~[spring-tx-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118) ~[spring-tx-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at io.seata.spring.annotation.GlobalTransactionalInterceptor$1.execute(GlobalTransactionalInterceptor.java:150) ~[seata-all-1.3.0.jar:1.3.0]
at io.seata.tm.api.TransactionalTemplate.execute(TransactionalTemplate.java:104) ~[seata-all-1.3.0.jar:1.3.0]
at io.seata.spring.annotation.GlobalTransactionalInterceptor.handleGlobalTransaction(GlobalTransactionalInterceptor.java:147) ~[seata-all-1.3.0.jar:1.3.0]
at io.seata.spring.annotation.GlobalTransactionalInterceptor.invoke(GlobalTransactionalInterceptor.java:122) ~[seata-all-1.3.0.jar:1.3.0]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at com.ac.order.service.impl.OrderServiceImpl$$EnhancerBySpringCGLIB$$53288503.makeOrder(<generated>) ~[classes/:na]
at com.ac.order.controller.OrderController.saveOrder(OrderController.java:25) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_131]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_131]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_131]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_131]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:878) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:792) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:626) ~[tomcat-embed-core-9.0.37.jar:4.0.FR]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:733) ~[tomcat-embed-core-9.0.37.jar:4.0.FR]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.37.jar:9.0.37]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1589) [tomcat-embed-core-9.0.37.jar:9.0.37]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.37.jar:9.0.37]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_131]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_131]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.37.jar:9.0.37]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_131]