Spring Cloud Alibaba系列之-Seata分布式事務(wù)(九)

一、什么是分布式事務(wù)問(wèn)題坝疼?

1.1 單體應(yīng)用

單體應(yīng)用中搜贤,一個(gè)業(yè)務(wù)操作需要調(diào)用三個(gè)模塊完成,此時(shí)數(shù)據(jù)的一致性由本地事務(wù)來(lái)保證钝凶。

單體應(yīng)用
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)法保證铆隘。

微服務(wù)應(yīng)用
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 是一致的。

事務(wù)
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ù)的提交和回滾企垦。

分布式事務(wù)處理過(guò)程的三個(gè)組件
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)求完疫。
分布式事務(wù)過(guò)程

三伏伯、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 行為
TCC模式

根據(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ù)模式敞峭。

XA
  • 執(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);
    }
}
user-service
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);
    }
}
product-service
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
image.png

默認(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ù)更新成功


用戶表
產(chǎn)品表
訂單表
5.4.3 制造異常,暴露分布式事務(wù)問(wèn)題

我們?cè)赱product-service]服務(wù)的下單扣減庫(kù)存接口中劈猿,人為制造一個(gè)異常拙吉,讓扣減庫(kù)存失敗

人為制造一個(gè)異常

我們?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

seata-server
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配置
registry.conf配置一
registry.conf配置二

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ò)誤信息

閃退
執(zhí)行bat
具體錯(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目錄下


nacos-config.txt目錄位置
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腳本放置目錄

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
git bash執(zhí)行nacos-config.sh腳本

執(zhí)行完成后nacos會(huì)新增seata配置榕茧。

nacos中seata配置
6.2 spring-cloud垃沦、spring-boot、alibaba-cloud版本升級(jí)

spring-cloud-alibaba 版本說(shuō)明

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)題。

注釋降級(jí)服務(wù)接口
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]

七虐译、附錄

項(xiàng)目源碼地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市吴趴,隨后出現(xiàn)的幾起案子漆诽,更是在濱河造成了極大的恐慌,老刑警劉巖锣枝,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件厢拭,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡撇叁,警方通過(guò)查閱死者的電腦和手機(jī)供鸠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)陨闹,“玉大人楞捂,你說(shuō)我怎么就攤上這事∏骼鳎” “怎么了寨闹?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)君账。 經(jīng)常有香客問(wèn)我繁堡,道長(zhǎng),這世上最難降的妖魔是什么乡数? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任椭蹄,我火速辦了婚禮,結(jié)果婚禮上瞳脓,老公的妹妹穿的比我還像新娘塑娇。我一直安慰自己,他們只是感情好劫侧,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著哨啃,像睡著了一般烧栋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拳球,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天审姓,我揣著相機(jī)與錄音,去河邊找鬼祝峻。 笑死魔吐,一個(gè)胖子當(dāng)著我的面吹牛扎筒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播酬姆,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼嗜桌,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了辞色?” 一聲冷哼從身側(cè)響起骨宠,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎相满,沒(méi)想到半個(gè)月后层亿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡立美,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年匿又,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片建蹄。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡琳省,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出躲撰,到底是詐尸還是另有隱情针贬,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布拢蛋,位于F島的核電站桦他,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏谆棱。R本人自食惡果不足惜快压,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望垃瞧。 院中可真熱鬧蔫劣,春花似錦、人聲如沸个从。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)嗦锐。三九已至嫌松,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間奕污,已是汗流浹背萎羔。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留碳默,地道東北人贾陷。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓缘眶,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親髓废。 傳聞我的和親對(duì)象是個(gè)殘疾皇子巷懈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容