目錄
1 頁面環境搭建
1.1 靜態資源上傳到nginx
1.2 SwitchHosts增加配置
1.3 網關配置
1.4 訂單模塊基礎配置
1.4.1 引入 thymeleaf 依賴
1.4.2 application.yml配置
1.4.3 bootstrap.properties配置
1.4.4 開啟nacos注冊發現和遠程調用
1.5 修改各個頁面的靜態資源路徑
1.6 測試
1.6.1 訂單確認頁
1.6.2 訂單詳情頁
1.6.3?訂單列表頁
1.6.4 訂單支付頁
2 整合SpringSession
2.1 引入依賴
2.2 開啟SpringSession
2.3 session數據存儲方式配置
2.4?修改訂單相關頁面同步用戶登錄信息
2.5 整合效果
3 自定義線程池
3.1 線程池屬性配置類
3.2 yml中線程池相關配置
3.3 自定義線程池
4 訂單基本概念
4.1 訂單中心
4.1.1 訂單構成
4.1.1.1?用戶信息
4.1.1.2 訂單基礎信息
4.1.1.3 商品信息
4.1.1.4 優惠信息
4.1.1.5 支付信息
4.1.1.6 物流信息
4.1.2 訂單狀態
4.2 訂單流程
4.2.1 訂單創建與支付
4.2.2 逆向流程
5 訂單實現
5.1 訂單登錄攔截
5.2 訂單確認頁
5.2.1 模型抽取
5.2.1.1 訂單確認頁Vo
5.2.1.2 訂單頁用戶收貨地址Vo
?5.2.1.3 訂單頁購物項Vo
5.2.2 訂單確認頁流程
5.2.3?功能實現
5.2.3.1 controller層
5.2.3.2 service層
5.2.3.3 遠程調用接口
5.2.2.3.1 遠程查詢會員所有的收貨地址
5.2.2.3.2 遠程查詢購物車中所有選中的購物項?
5.2.2.3.3 遠程查詢商品庫存信息
5.2.4?Feign遠程調用丟失請求頭問題
5.2.5?Feign異步情況丟失上下文問題
5.2.6?創建防重令牌
5.2.7?模擬運費效果
5.2.7.1 后端實現
5.2.7.2 前端實現
5.2.8 提交訂單
5.2.8.1 封裝下單接口入參出參vo
5.2.8.1.1?訂單提交接口入參vo
5.2.8.1.2?訂單提交接口出參vo
5.2.8.2 原子驗證防重令牌
5.2.8.3 創建訂單、訂單項等信息
5.2.8.4 驗價
5.2.8.5 保存訂單
5.2.8.6 鎖定庫存
5.2.8.7 提交訂單完整代碼
5.2.8.8 提交訂單的問題
5.2.8.9 Seata分布式事務
5.2.8.9.1 Windows下安裝Seata
5.2.8.9.2 服務端整合Seata(AT模式)
5.2.8.9.3 啟用分布式事務
5.2.8.9.4 測試
5.2.8.10 最終一致性庫存解鎖邏輯:【可靠消息+最終一致性】【庫存解鎖】
5.2.8.10.1 庫存服務整合RabbitMQ,創建交換機、隊列、綁定
5.2.8.10.2 監聽庫存解鎖
5.2.8.11?最終一致性庫存解鎖邏輯:【可靠消息+最終一致性】【定時關單】
5.2.8.11.1?訂單服務整合RabbitMQ,創建交換機、隊列、綁定
5.2.8.11.2 監聽訂單定時關單
5.2.8.11.3 訂單創建成功,機器卡頓,消息延遲導致庫存無法解鎖(關單邏輯升級)?
5.2.8.12 消息丟失、積壓、重復等解決方案(如何保證消息可靠性?)
5.2.8.12.1 消息丟失
5.2.8.12.2 消息重復
5.2.8.12.3 消息積壓
5.3? 訂單支付頁
5.3.1 加密分類
5.3.1.1 對稱加密
5.3.1.2 非對稱加密
5.3.2?支付寶支付
5.3.2.1 支付寶加密原理
5.3.2.1.1?什么是公鑰、 私鑰、 加密、 簽名和驗簽
5.3.2.2 支付寶-電腦網站支付Demo測試
5.3.2.2.1 使用支付寶沙箱環境進行測試
5.3.2.2.2 系統默認密鑰
5.3.2.2.3 修改Demo中配置AlipayConfig?
5.3.2.2.4 啟動Demo測試
5.3.2.3 支付寶支付流程
5.3.3 內網穿透
5.3.3.1 簡介
5.3.3.2 使用場景
5.3.3.3 內網穿透的幾個常用軟件
5.3.3.4 natapp內網穿透
5.3.4 整合支付
5.3.4.1?導入依賴
5.3.4.2 yml配置?
5.3.4.3?支付Vo
5.3.4.4?阿里云支付模板
5.3.4.5 訂單支付寶支付接口
5.3.4.6 前端頁面修改pay.html
5.3.4.7 支付測試
5.3.5 訂單列表頁渲染(member服務)
5.3.5.1 靜態資源上傳到nginx
5.3.5.2 會員服務整合thymeleaf
5.3.5.3?網關配置
5.3.5.4 SwitchHosts添加配置
5.3.5.5 整合SpringSession(登錄后才可以查看訂單信息)
5.3.5.6 配置攔截器
5.3.5.7 訂單支付成功回調頁面接口
5.3.5.8 設置支付寶支付成功回調url
5.3.5.9?訂單列表頁渲染?
5.3.6 接收支付寶異步通知
5.3.6.1 支付寶異步通知信息vo
5.3.6.2 接收支付寶異步通知接口
5.3.6.3 登錄攔截器放行異步通知接口
5.3.6.4 設置支付寶異步通知路徑
5.3.6.5 設置接收異步通知信息相關日期格式
5.3.6.6 解決訂單號長度報錯
5.3.7?異步通知內網穿透環境搭建?
5.3.7.1 修改內網穿透隧道配置??
5.3.7.2?修改nginx配置
5.3.8 支付測試
5.3.9 收單
6 接口冪等性?
6.1 什么是冪等性
6.2 哪些情況需要防止
6.3 什么情況下需要冪等
6.4 冪等解決方案
6.4.1 token機制
6.4.2 各種鎖機制
6.4.2.1 數據庫悲觀鎖
6.4.2.2 數據庫樂觀鎖
6.4.2.3 業務層分布式鎖
6.4.3 各種唯一約束
6.4.3.1 數據庫唯一約束
6.4.3.2 redis set 防重
6.4.4 防重表
6.4.5 全局請求唯一id
7 本地事務與分布式事務
7.1 本地事務
7.1.1 事務的基本性質
7.1.2 事務的隔離級別
7.1.3 事務的七種傳播行為
7.1.4 SpringBoot 事務關鍵點
7.1.4.1 事務的自動配置
7.1.4.2 事務的坑?
7.2 分布式事務
7.2.1 為什么有分布式事務
7.2.2 CAP定理與BASE理論
7.2.2.1 CAP定理
7.2.2.2 面臨問題
7.2.2.3 BASE理論
7.2.2.4 強一致性、弱一致性、最終一致性
7.2.3 分布式事務的幾種方案
7.2.3.1 2PC模式
7.2.3.2 柔性事務-TCC事務補償型方案(手動補償)
7.2.3.3 柔性事務-最大努力通知型方案
7.2.3.4 柔性事務-可靠消息+最終一致性方案(異步確保型)
1 頁面環境搭建
1.1 靜態資源上傳到nginx
等待付款 -》detail
訂單頁? ? -》list
結算頁? ? -》confirm
收銀頁? ? -》pay
1.2 SwitchHosts增加配置
添加訂單服務的域名與ip映射:xxx.xxx.11.10 order.gulimall.com?
1.3 網關配置
gulimall-gateway/src/main/resources/application.yml
- id: gulimall_order_routeuri: lb://gulimall-orderpredicates:# 由以下的主機域名訪問轉發到訂單服務- Host=order.gulimall.com
1.4 訂單模塊基礎配置
1.4.1 引入 thymeleaf 依賴
gulimall-order/pom.xml
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
1.4.2 application.yml配置
開啟nacos注冊發現、關閉thymeleaf緩存。
gulimall-order/src/main/resources/application.yml
spring:cloud:nacos:discovery:server-addr: xxx.xxx.xxx.10:8848thymeleaf:cache: false
1.4.3 bootstrap.properties配置
服務名、開啟nacos配置中心(可以不開啟)。
gulimall-order/src/main/resources/bootstrap.properties
spring.application.name=gulimall-order
spring.cloud.nacos.config.server-addr=xxx.xxx.xxx.10:8848
spring.cloud.nacos.config.namespace=de91d4bf-xxxx-xxxx-b095-d8ac87337d8a
1.4.4 開啟nacos注冊發現和遠程調用
主類開啟注冊發現、遠程調用。
@EnableFeignClients // 遠程調用
@EnableDiscoveryClient // 注冊發現
1.5 修改各個頁面的靜態資源路徑
1. confirm.html
src="? =>? src="/static/order/confirm/
href="? =>??href="/static/order/confirm/
2. detail.html
src="? =>? src="/static/order/detail/
href="? =>??href="/static/order/detail/
3. list.html
src="? =>? src="/static/order/list/
href="? =>??href="/static/order/list/
4. pay.html
src="? =>? src="/static/order/pay/
href="? =>??href="/static/order/pay/
1.6 測試
測試代碼
gulimall-order/src/main/java/com/wen/gulimall/order/web/HelloController.java
@Controller
public class HelloController {/*** 測試訂單相關頁面訪問* @param page* @return*/@GetMapping("/{page}.html")public String listPage(@PathVariable String page){return page;}
}
1.6.1 訂單確認頁
注意:如果頁面展示不全,刪除確認頁面注釋代碼中的 /*。
http://order.gulimall.com/confirm.html
1.6.2 訂單詳情頁
?http://order.gulimall.com/detail.html
1.6.3?訂單列表頁
http://order.gulimall.com/list.html
1.6.4 訂單支付頁
http://order.gulimall.com/pay.html
2 整合SpringSession
注意: SpringSession的配置類GulimallSessionConfig.java在公共模塊(gulimall-common)已經配置,這里購物車模塊直接引入公共模塊即可。
公共模塊(gulimall-common)可以參考我之前的博客:谷粒商城篇章7 ---- P211-P235 ---- 認證服務【分布式高級篇四】_谷粒商城p235-CSDN博客
2.1 引入依賴
<!-- 整合SpringSession完成session共享問題 -->
<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- redis -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclusion><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId></exclusion></exclusions>
</dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId>
</dependency>
2.2 開啟SpringSession
gulimall-order/src/main/java/com/wen/gulimall/order/GulimallOrderApplication.java
主類上添加以下注解:
@EnableRedisHttpSession
2.3 session數據存儲方式配置
gulimall-order/src/main/resources/application.yml
spring:redis:host: 172.1.11.10session:store-type: redis
2.4?修改訂單相關頁面同步用戶登錄信息
1.訂單詳情頁detail.html的134-140行,修改如下:<li style="width: 100px"><a href="" th:if="${session.loginUser}!=null">歡迎:[[${session.loginUser==null?"":session.loginUser.nickname}]]</a><a href="http://auth.gulimall.com/login.html" th:if="${session.loginUser}==null">歡迎,請登錄</a>
</li>
<li th:if="${session.loginUser}==null"><a href="http://auth.gulimall.com/reg.html" class="li_2">免費注冊</a>
</li>2.訂單確認頁confirm.html的78行,修改如下:
<li>[[${session.loginUser==null?"":session.loginUser.nickname}]]<img src="/static/order/confirm/img/03.png" style="margin-bottom: 0px;margin-left3: 3px;" /><img src="/static/order/confirm/img/06.png" /></li>3.訂單支付頁pay.html的14行,修改如下:
<li><span>[[${session.loginUser==null?"":session.loginUser.nickname}]]</span><span>退出</span></li>
2.5 整合效果
登錄成功后用戶信息共享,如下:
3 自定義線程池
3.1 線程池屬性配置類
gulimall-order/src/main/java/com/wen/gulimall/order/config/ThreadPoolConfigProperties.java
@Data
@Component
@ConfigurationProperties(prefix = "gulimall.thread")
public class ThreadPoolConfigProperties {private Integer coreSize;private Integer maxSize;private Integer keepAliveTime;
}
3.2 yml中線程池相關配置
gulimall:thread:core-size: 20max-size: 200keep-alive-time: 10
3.3 自定義線程池
gulimall-order/src/main/java/com/wen/gulimall/order/config/MyThreadConfig.java
@Configuration
public class MyThreadConfig {@Beanpublic ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties poolProperties){return new ThreadPoolExecutor(poolProperties.getCoreSize(),poolProperties.getMaxSize(),poolProperties.getKeepAliveTime(),TimeUnit.SECONDS,new LinkedBlockingQueue<>(100000),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());}
}
4 訂單基本概念
4.1 訂單中心
電商系統涉及到 3 流, 分別時信息流, 資金流, 物流, 而訂單系統作為中樞將三者有機的集
合起來。
訂單模塊是電商系統的樞紐, 在訂單這個環節上需求獲取多個模塊的數據和信息, 同時對這
些信息進行加工處理后流向下個環節, 這一系列就構成了訂單的信息流通。
4.1.1 訂單構成
4.1.1.1?用戶信息
????????用戶信息包括用戶賬號、 用戶等級、 用戶的收貨地址、 收貨人、 收貨人電話等組成, 用戶賬戶需要綁定手機號碼, 但是用戶綁定的手機號碼不一定是收貨信息上的電話。 用戶可以添加多個收貨信息, 用戶等級信息可以用來和促銷系統進行匹配, 獲取商品折扣, 同時用戶等級還可以獲取積分的獎勵等。
4.1.1.2 訂單基礎信息
????????訂單基礎信息是訂單流轉的核心, 其包括訂單類型、 父/子訂單、 訂單編號、 訂單狀態、 訂單流轉的時間等。
(1) 訂單類型包括實體商品訂單和虛擬訂單商品等, 這個根據商城商品和服務類型進行區分。
(2) 同時訂單都需要做父子訂單處理, 之前在初創公司一直只有一個訂單, 沒有做父子訂單處理后期需要進行拆單的時候就比較麻煩, 尤其是多商戶商場, 和不同倉庫商品的時候,父子訂單就是為后期做拆單準備的。
(3) 訂單編號不多說了, 需要強調的一點是父子訂單都需要有訂單編號, 需要完善的時候可以對訂單編號的每個字段進行統一定義和詮釋。
(4) 訂單狀態記錄訂單每次流轉過程, 后面會對訂單狀態進行單獨的說明。
(5) 訂單流轉時間需要記錄下單時間, 支付時間, 發貨時間, 結束時間/關閉時間等等
4.1.1.3 商品信息
????????商品信息從商品庫中獲取商品的 SKU 信息、 圖片、 名稱、 屬性規格、 商品單價、 商戶信息等, 從用戶下單行為記錄的用戶下單數量, 商品合計價格等。
4.1.1.4 優惠信息
????????優惠信息記錄用戶參與的優惠活動, 包括優惠促銷活動, 比如滿減、 滿贈、 秒殺等, 用戶使用的優惠券信息, 優惠券滿足條件的優惠券需要默認展示出來, 具體方式已在之前的優惠券篇章做過詳細介紹, 另外還虛擬幣抵扣信息等進行記錄。
為什么把優惠信息單獨拿出來而不放在支付信息里面呢?
因為優惠信息只是記錄用戶使用的條目, 而支付信息需要加入數據進行計算, 所以做為區分。
4.1.1.5 支付信息
(1) 支付流水單號, 這個流水單號是在喚起網關支付后支付通道返回給電商業務平臺的支付流水號, 財務通過訂單號和流水單號與支付通道進行對賬使用。
(2) 支付方式用戶使用的支付方式, 比如微信支付、 支付寶支付、 錢包支付、 快捷支付等。支付方式有時候可能有兩個——余額支付+第三方支付。
(3) 商品總金額, 每個商品加總后的金額; 運費, 物流產生的費用; 優惠總金額, 包括促銷活動的優惠金額, 優惠券優惠金額, 虛擬積分或者虛擬幣抵扣的金額, 會員折扣的金額等之和; 實付金額, 用戶實際需要付款的金額。
????????用戶實付金額=商品總金額+運費-優惠總金額
4.1.1.6 物流信息
????????物流信息包括配送方式, 物流公司, 物流單號, 物流狀態, 物流狀態可以通過第三方接口來獲取和向用戶展示物流每個狀態節點。
4.1.2 訂單狀態
1. 待付款
????????用戶提交訂單后, 訂單進行預下單, 目前主流電商網站都會喚起支付, 便于用戶快速完成支
付, 需要注意的是待付款狀態下可以對庫存進行鎖定, 鎖定庫存需要配置支付超時時間, 超時后將自動取消訂單, 訂單變更關閉狀態。
2. 已付款/待發貨
????????用戶完成訂單支付, 訂單系統需要記錄支付時間, 支付流水單號便于對賬, 訂單下放到 WMS系統, 倉庫進行調撥, 配貨, 分揀, 出庫等操作。
3. 待收貨/已發貨
????????倉儲將商品出庫后, 訂單進入物流環節, 訂單系統需要同步物流信息, 便于用戶實時知悉物
品物流狀態
4. 已完成
????????用戶確認收貨后, 訂單交易完成。 后續支付側進行結算, 如果訂單存在問題進入售后狀態
5. 已取消
????????付款之前取消訂單。 包括超時未付款或用戶商戶取消訂單都會產生這種訂單狀態。
6. 售后中
????????用戶在付款后申請退款, 或商家發貨后用戶申請退換貨。
????????售后也同樣存在各種狀態, 當發起售后申請后生成售后訂單, 售后訂單狀態為待審核, 等待
商家審核, 商家審核通過后訂單狀態變更為待退貨, 等待用戶將商品寄回, 商家收貨后訂單狀態更新為待退款狀態, 退款到用戶原賬戶后訂單狀態更新為售后成功。
4.2 訂單流程
????????訂單流程是指從訂單產生到完成整個流轉的過程, 從而行程了一套標準流程規則。 而不同的產品類型或業務類型在系統中的流程會千差萬別, 比如上面提到的線上實物訂單和虛擬訂單的流程, 線上實物訂單與 O2O 訂單等, 所以需要根據不同的類型進行構建訂單流程。
????????不管類型如何訂單都包括正向流程和逆向流程, 對應的場景就是購買商品和退換貨流程, 正向流程就是一個正常的網購步驟: 訂單生成–>支付訂單–>賣家發貨–>確認收貨–>交易成功。而每個步驟的背后, 訂單是如何在多系統之間交互流轉的, 可概括如下圖
4.2.1 訂單創建與支付
- 訂單創建前需要預覽訂單, 選擇收貨信息等? ??
- 訂單創建需要鎖定庫存, 庫存有才可創建, 否則不能創建
- 訂單創建后超時未支付需要解鎖庫存
- 支付成功后, 需要進行拆單, 根據商品打包方式, 所在倉庫, 物流等進行拆單
- 支付的每筆流水都需要記錄, 以待查賬
- 訂單創建, 支付成功等狀態都需要給 MQ 發送消息, 方便其他系統感知訂閱
4.2.2 逆向流程
- 修改訂單, 用戶沒有提交訂單, 可以對訂單一些信息進行修改, 比如配送信息,優惠信息,及其他一些訂單可修改范圍的內容, 此時只需對數據進行變更即可。
- 訂單取消, 用戶主動取消訂單和用戶超時未支付, 兩種情況下訂單都會取消訂單, 而超時情況是系統自動關閉訂單, 所以在訂單支付的響應機制上面要做支付的限時處理, 尤其是在前面說的下單減庫存的情形下面, 可以保證快速的釋放庫存。另外需要需要處理的是促銷優惠中使用的優惠券, 權益等視平臺規則, 進行相應補回給用戶。
- 退款, 在待發貨訂單狀態下取消訂單時, 分為缺貨退款和用戶申請退款。 如果是全部退款則訂單更新為關閉狀態, 若只是做部分退款則訂單仍需進行進行, 同時生成一條退款的售后訂單, 走退款流程。 退款金額需原路返回用戶的賬戶。
- 發貨后的退款, 發生在倉儲貨物配送, 在配送過程中商品遺失, 用戶拒收, 用戶收貨后對商品不滿意, 這樣情況下用戶發起退款的售后訴求后, 需要商戶進行退款的審核, 雙方達成一致后, 系統更新退款狀態, 對訂單進行退款操作, 金額原路返回用戶的賬戶, 同時關閉原訂單數據。 僅退款情況下暫不考慮倉庫系統變化。 如果發生雙方協調不一致情況下, 可以申請平臺客服介入。 在退款訂單商戶不處理的情況下, 系統需要做限期判斷, 比如 5 天商戶不處理, 退款單自動變更同意退款 。
5 訂單實現
5.1 訂單登錄攔截
????????訂單系統必然會涉及到用戶相關信息,所以進入訂單系統的所有請求必須是已登錄的狀態下。這里編寫用戶登錄攔截器,對未登錄情況下的訂單請求進行攔截。
自定義攔截器
gulimall-order/src/main/java/com/wen/gulimall/order/interceptor/LoginUserInterceptor.java
/*** @author W* @createDate 2024/02/27* @description: 登錄攔截器* 從session中(redis中)獲取了登錄信息,封裝到ThreadLocal* 自定義攔截器需要添加到webmvc中,否則不起作用*/
@Component
public class LoginUserInterceptor implements HandlerInterceptor {// 同一個線程共享數據private static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);if(attribute!=null){// 登錄成功loginUser.set(attribute);return true;}else {// 沒登錄,去登錄request.getSession().setAttribute("msg","請先進行登錄");response.sendRedirect("http://auth.gulimall.com/login.html");return false;}}
}
將攔截器添加到webMvc中
gulimall-order/src/main/java/com/wen/gulimall/order/config/OrderWebConfiguration.java
@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {@Resourceprivate LoginUserInterceptor interceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 訂單服務的所有請求都要走登錄攔截registry.addInterceptor(interceptor).addPathPatterns("/**");}
}
5.2 訂單確認頁
5.2.1 模型抽取
5.2.1.1 訂單確認頁Vo
gulimall-order/src/main/java/com/wen/gulimall/order/vo/OrderConfirmVo.java
public class OrderConfirmVo {// 收貨地址@Setter @Getterprivate List<MemberAddressVo> address;// 所有選中的購物項@Setter @Getterprivate List<OrderItemVo> items;// 發票信息// 優惠券信息...,這里使用積分@Setter @Getterprivate Integer integration;// 訂單的防重令牌@Setter @Getterprivate String orderToken;@Setter @Getterprivate Map<Long, Boolean> stocks;// 商品總數量public Integer getCount(){Integer i = 0;if(items!=null){for (OrderItemVo item : items) {i+=item.getCount();}}return i;}// 訂單總額//private BigDecimal total;public BigDecimal getTotal() {BigDecimal sum = new BigDecimal("0");if(items!=null){for (OrderItemVo item : items) {BigDecimal multiply = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));sum = sum.add(multiply);}}return sum;}// 應付價格//private BigDecimal payPrice;public BigDecimal getPayPrice() {return getTotal();}
}
5.2.1.2 訂單頁用戶收貨地址Vo
gulimall-order/src/main/java/com/wen/gulimall/order/vo/MemberAddressVo.java
@Data
public class MemberAddressVo {private Long id;/*** member_id*/private Long memberId;/*** 收貨人姓名*/private String name;/*** 電話*/private String phone;/*** 郵政編碼*/private String postCode;/*** 省份/直轄市*/private String province;/*** 城市*/private String city;/*** 區*/private String region;/*** 詳細地址(街道)*/private String detailAddress;/*** 省市區代碼*/private String areacode;/*** 是否默認*/private Integer defaultStatus;
}
?5.2.1.3 訂單頁購物項Vo
gulimall-order/src/main/java/com/wen/gulimall/order/vo/OrderItemVo.java
/*** @author W* @createDate 2024/02/28* @description: 訂單確認頁的購物項* 不需要選中標志*/
@Data
public class OrderItemVo {private Long skuId; // 商品編號private String title; // 標題private String image;// 圖片private List<String> skuAttr;// 商品銷售屬性集合private BigDecimal price; // 單價private Integer count; // 數量private BigDecimal totalPrice;// 總價private BigDecimal weight = new BigDecimal("0.085");// 商品重量
}
5.2.2 訂單確認頁流程
1. 判斷是否登錄,使用登錄攔截器LoginUserInterceptor;
2. 遠程獲取用戶所有的地址列表【gulimall_ums庫=》ums_member_receive_address表】,字段default_status值1-表示默認地址,0-非默認地址,默認地址高亮;
3. 遠程查詢購物車中所有的購物項,【gulimall-cart調用商品服務,獲取購物項的最新價格】;
4. 訂單總額,根據購物項的價格和數量計算,然后求和;
5. 應付價格【暫時跟訂單總額一樣】,實際總額要減去優惠價格等;
6. 遠程查詢庫存服務;
7. 根據選中的地址,通過ajax請求調用/ware/wareinfo/fare?addrId=addrIdValue接口獲取運費信息和地址信息;
8. 防重令牌(冪等性:提交一次和多次的效果是一樣的)防止訂單重復提交,數據庫只保存一條訂單信息。
5.2.3?功能實現
5.2.3.1 controller層
gulimall-order/src/main/java/com/wen/gulimall/order/web/OrderWebController.java
@Controller
public class OrderWebController {@Resourceprivate OrderService orderService;@GetMapping("/toTrade")public String toTrade(Model model, HttpServletRequest request) throws ExecutionException, InterruptedException {OrderConfirmVo orderConfirmVo = orderService.confirmOrder();model.addAttribute("orderConfirmData",orderConfirmVo);return "confirm";}}
5.2.3.2 service層
?gulimall-order/src/main/java/com/wen/gulimall/order/service/OrderService.java
public interface OrderService extends IService<OrderEntity> {.../*** 訂單確認頁返回需要的數據* @return*/OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException;
}
?gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java
業務流程,如下:
(1)獲取登錄用戶信息;
(2)遠程查詢登錄用戶所有的收貨地址列表;
(3)遠程查詢購物車中所有的購物項列表;
(4)遠程查詢商品庫存信息;
(5)查詢用戶積分;
(6)價格等信息自動計算;
(7)防重令牌(冪等性),防止訂單重復提交。
@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {@Resourceprivate MemberFeignService memberFeignService;@Resourceprivate CartFeignService cartFeignService;@Resourceprivate WmsFeignService wmsFeignService;@Resourceprivate ThreadPoolExecutor executor;...@Overridepublic OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {OrderConfirmVo orderConfirmVo = new OrderConfirmVo();MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();// 獲取之前的請求// RequestContextHolder是同一個線程共享請求數據RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {// 每一個線程都來共享之前的請求數據RequestContextHolder.setRequestAttributes(requestAttributes);// 1. 遠程查詢會員所有的收貨地址List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());orderConfirmVo.setAddress(address);}, executor);CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {RequestContextHolder.setRequestAttributes(requestAttributes);// 2. 遠程查詢購物車中所有選中的購物項List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();orderConfirmVo.setItems(currentUserCartItems);// feign在遠程調用之前需要構造新的請求,調用很多攔截器// RequestInterceptor interceptor : RequestInterceptors}, executor).thenRunAsync(()->{// 遠程查詢庫存List<OrderItemVo> items = orderConfirmVo.getItems();List<Long> collect = items.stream().map(OrderItemVo::getSkuId).collect(Collectors.toList());// 遠程調用庫存服務R skusHasStock = wmsFeignService.getSkusHasStock(collect);List<SkuStockVo> data = skusHasStock.getData(new TypeReference<List<SkuStockVo>>() {});if(data!=null) {// 以map的形式顯示每個商品是否有庫存Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));orderConfirmVo.setStocks(map);}},executor);// 3. 查詢用戶積分Integer integration = memberRespVo.getIntegration();orderConfirmVo.setIntegration(integration);// 4. 其他數據自動計算// TODO 5. 放重令牌(冪等性)防止訂單重復提交String token = UUID.randomUUID().toString().replaceAll("-", "");// 防重令牌設置30分鐘的過期時間,存放在redisstringRedisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberRespVo.getId(),token,30, TimeUnit.MINUTES);orderConfirmVo.setOrderToken(token);CompletableFuture.anyOf(getAddressFuture, cartFuture).get();return orderConfirmVo;}
}
5.2.3.3 遠程調用接口
5.2.2.3.1 遠程查詢會員所有的收貨地址
1.?模擬數據
2.?controller層
gulimall-member/src/main/java/com/wen/gulimall/member/controller/MemberReceiveAddressController.java
@RestController
@RequestMapping("member/memberreceiveaddress")
public class MemberReceiveAddressController {@Autowiredprivate MemberReceiveAddressService memberReceiveAddressService;@GetMapping("/{memberId}/addresses")public List<MemberReceiveAddressEntity> getAddress(@PathVariable Long memberId){return memberReceiveAddressService.getAddress(memberId);}...
}
3. service層
gulimall-member/src/main/java/com/wen/gulimall/member/service/MemberReceiveAddressService.java
public interface MemberReceiveAddressService extends IService<MemberReceiveAddressEntity> {...List<MemberReceiveAddressEntity> getAddress(Long memberId);
}
gulimall-member/src/main/java/com/wen/gulimall/member/service/impl/MemberReceiveAddressServiceImpl.java?
@Service("memberReceiveAddressService")
public class MemberReceiveAddressServiceImpl extends ServiceImpl<MemberReceiveAddressDao, MemberReceiveAddressEntity> implements MemberReceiveAddressService {...@Overridepublic List<MemberReceiveAddressEntity> getAddress(Long memberId) {return this.list(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id",memberId));}}
4. Feign接口
gulimall-order/src/main/java/com/wen/gulimall/order/feign/MemberFeignService.java
@FeignClient("gulimall-member")
public interface MemberFeignService {@GetMapping("/member/memberreceiveaddress/{memberId}/addresses")List<MemberAddressVo> getAddress(@PathVariable Long memberId);
}
5.2.2.3.2 遠程查詢購物車中所有選中的購物項?
1. controller層
gulimall-cart/src/main/java/com/wen/gulimall/cart/controller/CartController.java
@Controller
public class CartController {@Resourceprivate CartService cartService;@ResponseBody@GetMapping("/currentUserCartItems")public List<CartItemVo> getCurrentUserCartItems(){return cartService.getUserCartItems();}...
}
2. service層
gulimall-cart/src/main/java/com/wen/gulimall/cart/service/CartService.java
public interface CartService {...List<CartItemVo> getUserCartItems();
}
gulimall-cart/src/main/java/com/wen/gulimall/cart/service/impl/CartServiceImpl.java
@Service
public class CartServiceImpl implements CartService {@Resourceprivate ProductFeignService productFeignService;@Resourceprivate ThreadPoolExecutor executor;...@Overridepublic List<CartItemVo> getUserCartItems() {UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();if(userInfoTo.getUserId()==null){return null;}else {String cartKey = CartConstant.CART_PREFIX + userInfoTo.getUserId();List<CartItemVo> cartItems = getCartItems(cartKey);// 獲取所有被選中的購物項List<CartItemVo> collect = cartItems.stream().filter(item -> item.getCheck()).map(item ->{BigDecimal price = productFeignService.getPrice(item.getSkuId());// TODO 更新為最新價格item.setPrice(price);return item;}).collect(Collectors.toList());return collect;}}
}
3. Feign接口
?gulimall-order/src/main/java/com/wen/gulimall/order/feign/CartFeignService.java
@FeignClient("gulimall-cart")
public interface CartFeignService {@GetMapping("/currentUserCartItems")List<OrderItemVo> getCurrentUserCartItems();
}
4. 遠程調用商品服務查詢商品的最新價格
gulimall-product/src/main/java/com/wen/gulimall/product/app/SkuInfoController.java
@RestController
@RequestMapping("product/skuinfo")
public class SkuInfoController {@Autowiredprivate SkuInfoService skuInfoService;/*** 訂單確認頁查詢商品此時的價格* @param skuId* @return*/@GetMapping("/{skuId}/price")public BigDecimal getPrice(@PathVariable Long skuId){SkuInfoEntity byId = skuInfoService.getById(skuId);return byId.getPrice();}...
}
gulimall-cart/src/main/java/com/wen/gulimall/cart/feign/ProductFeignService.java?
@FeignClient("gulimall-product")
public interface ProductFeignService {...@GetMapping("/product/skuinfo/{skuId}/price")BigDecimal getPrice(@PathVariable Long skuId);
}
5.2.2.3.3 遠程查詢商品庫存信息
1. 庫存vo
gulimall-order/src/main/java/com/wen/gulimall/order/vo/SkuStockVo.java
@Data
public class SkuStockVo {private Long skuId;private Boolean hasStock;
}
2. controller層
gulimall-ware/src/main/java/com/wen/gulimall/ware/controller/WareSkuController.java
@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {@Autowiredprivate WareSkuService wareSkuService;@PostMapping("/hasstock")public R getSkusHasStock(@RequestBody List<Long> skuIds){// sku_id, stockList<SkuHasStockVo> vos = wareSkuService.getSkusHasStock(skuIds);R ok = R.ok();ok.setData(vos);return ok;}...
}
3. service層
gulimall-ware/src/main/java/com/wen/gulimall/ware/service/WareSkuService.java
public interface WareSkuService extends IService<WareSkuEntity> {...List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds);
}
gulimall-ware/src/main/java/com/wen/gulimall/ware/service/impl/WareSkuServiceImpl.java?
@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {...@Overridepublic List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds) {List<SkuHasStockVo> collect = skuIds.stream().map(skuId -> {SkuHasStockVo skuHasStockVo = new SkuHasStockVo();// 查詢是否有庫存 = (庫存數-鎖定庫存)> 0Long count = this.baseMapper.getSkuStock(skuId);skuHasStockVo.setSkuId(skuId);skuHasStockVo.setHasStock(count==null?false:count > 0);return skuHasStockVo;}).collect(Collectors.toList());return collect;}}
4. Feign接口
gulimall-order/src/main/java/com/wen/gulimall/order/feign/WmsFeignService.java
@FeignClient("gulimall-ware")
public interface WmsFeignService {@PostMapping("/ware/waresku/hasstock")R getSkusHasStock(@RequestBody List<Long> skuIds);
}
5.2.4?Feign遠程調用丟失請求頭問題
問題產生原因:通過feign進行遠程調用時,會創建一個新的RequestTemplate,沒有請求頭,Cookie信息沒有了,導致在遠程調用cart服務時,購物車攔截器無法從session中獲取登錄信息,無法獲取userId。
解決方案:將老請求頭的信息同步給新請求頭。
原理:feign進行遠程調用時會創建新的請求,然后調用很多攔截器(debug了解原理),我們可以自定義攔截器設置請求頭。
gulimall-order/src/main/java/com/wen/gulimall/order/config/GuliFeignConfig.java
/*** @author W* @createDate 2024/02/29* @description: 解決 Feign遠程調用請求頭丟失問題* 遠程調用會創建一個新的請求,新的請求沒有請求頭* 使用RequestInterceptor攔截器為新構建的請求添加請求頭*/
@Configuration
public class GuliFeignConfig {@Beanpublic RequestInterceptor requestInterceptor(){return new RequestInterceptor() {@Overridepublic void apply(RequestTemplate template) {// 1.RequestContextHolder拿到剛在進來的請求ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();assert requestAttributes != null;HttpServletRequest request = requestAttributes.getRequest(); // 老請求if(request!=null) {// 同步請求頭數據,CookieString cookie = request.getHeader("Cookie");// 給構建的新請求同步老請求的Cookietemplate.header("Cookie", cookie);}}};}
}
5.2.5?Feign異步情況丟失上下文問題
產生問題:RequestInterceptor攔截器報空指針異常,獲取當前請求上下文的RequestContextHolder類本質上是一個ThreadLocal,同一個線程共享請求數據,異步線程無法共享之前的請求數據。
解決方案:獲取主線程的請求數據設置到子線程的請求上下文RequestContextHolder中,如下。
5.2.6?創建防重令牌
????????使用令牌機制實現下訂單的冪等性,在訂單確認頁到來之前為訂單確認頁生成一個令牌,提交訂單時帶上這個令牌。令牌存在兩個地方,服務器一個,頁面一個。
????????訂單冪等性做了兩種:防重令牌和數據庫訂單編號唯一性。
????????訂單防重復提交=》訂單的冪等性,提交一次和多次的效果是一樣的。(接口冪等性詳解見)
令牌前綴常量
gulimall-order/src/main/java/com/wen/gulimall/order/constant/OrderConstant.java
public class OrderConstant {/** 訂單確認也防重復提交令牌前綴 */public static final String USER_ORDER_TOKEN_PREFIX = "order:token:";
}
?gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java
// TODO 5. 放重令牌(冪等性)防止訂單重復提交
String token = UUID.randomUUID().toString().replaceAll("-", "");
// 防重令牌設置30分鐘的過期時間,存放在redis
stringRedisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberRespVo.getId(),token,30, TimeUnit.MINUTES);
orderConfirmVo.setOrderToken(token);
?訂單表(oms_order),訂單編號添加唯一索引。
5.2.7?模擬運費效果
5.2.7.1 后端實現
運費vo
gulimall-ware/src/main/java/com/wen/gulimall/ware/vo/FareVo.java
@Data
public class FareVo {private MemberAddressVo address;private BigDecimal fare;
}
gulimall-ware/src/main/java/com/wen/gulimall/ware/controller/WareInfoController.java
@RestController
@RequestMapping("ware/wareinfo")
public class WareInfoController {@Autowiredprivate WareInfoService wareInfoService;@GetMapping("/fare")public R getFare(@RequestParam("addrId") Long addrId){FareVo fare = wareInfoService.getFare(addrId);return R.ok().setData(fare);}...
}
?gulimall-ware/src/main/java/com/wen/gulimall/ware/service/WareInfoService.java
public interface WareInfoService extends IService<WareInfoEntity> {.../*** 根據用戶的收貨地址計算運費* @param addrId* @return*/FareVo getFare(Long addrId);
}
?gulimall-ware/src/main/java/com/wen/gulimall/ware/service/impl/WareInfoServiceImpl.java
@Service("wareInfoService")
public class WareInfoServiceImpl extends ServiceImpl<WareInfoDao, WareInfoEntity> implements WareInfoService {@Resourceprivate MemberFeignService memberFeignService;...@Overridepublic FareVo getFare(Long addrId) {FareVo fareVo = new FareVo();// 獲取地址的詳細信息R r = memberFeignService.attrInfo(addrId);MemberAddressVo data = r.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {});if(data!=null){// 調用第三方運費計算系統// 這里截取用戶手機號碼最后一位作為我們的運費String phone = data.getPhone();String fare = phone.substring(phone.length() - 1);BigDecimal bigDecimal = new BigDecimal(fare);fareVo.setFare(bigDecimal);fareVo.setAddress(data);return fareVo;}return null;}}
?遠程獲取地址詳細信息
地址vo
gulimall-ware/src/main/java/com/wen/gulimall/ware/vo/MemberAddressVo.java?
@Data
public class MemberAddressVo {private Long id;/*** member_id*/private Long memberId;/*** 收貨人姓名*/private String name;/*** 電話*/private String phone;/*** 郵政編碼*/private String postCode;/*** 省份/直轄市*/private String province;/*** 城市*/private String city;/*** 區*/private String region;/*** 詳細地址(街道)*/private String detailAddress;/*** 省市區代碼*/private String areacode;/*** 是否默認*/private Integer defaultStatus;
}
遠程調用接口
gulimall-ware/src/main/java/com/wen/gulimall/ware/feign/MemberFeignService.java
@FeignClient("gulimall-member")
public interface MemberFeignService {@RequestMapping("/member/memberreceiveaddress/info/{id}")R attrInfo(@PathVariable("id") Long id);
}
5.2.7.2 前端實現
5.2.8 提交訂單
下單流程:
- 下單:去創建訂單、驗令牌、驗價格、鎖庫存...。
- 提交訂單成功,攜帶返回數據轉發到支付選項頁。
- 提交訂單失敗,攜帶錯誤信息重定向到訂單確認頁,重新確認訂單信息。
5.2.8.1 封裝下單接口入參出參vo
5.2.8.1.1?訂單提交接口入參vo
入參vo
gulimall-order/src/main/java/com/wen/gulimall/order/vo/OrderSubmitVo.java
/*** @author W* @createDate 2024/03/08* @description: 封裝訂單提交的數據*/
@Data
public class OrderSubmitVo {private Long addrId;// 收貨地址的idprivate Integer payType;// 支付方式// 無需提交需要購買的商品,去購物車在獲取一遍// 優惠、發票private String orderToken;// 防重令牌private BigDecimal payPrice;// 應付價格 驗價 可能加入購物車時的價格和提交訂單時商品的價格不一樣private String note;// 訂單備注// 用戶相關信息,直接去session取出登錄的用戶
}
訂單確認頁,構造訂單提交數據的表單
gulimall-order/src/main/resources/templates/confirm.html
給表單的addrId和應付價payPrice賦值,如下
5.2.8.1.2?訂單提交接口出參vo
gulimall-order/src/main/java/com/wen/gulimall/order/vo/SubmitOrderResponseVo.java
@Data
public class SubmitOrderResponseVo {private OrderEntity order;private Integer code; // 0-成功 下單錯誤狀態碼
}
5.2.8.2 原子驗證防重令牌
通過redis的Lua腳本保證驗證令牌和刪除令牌的原子性,防止訂單重復提交。
gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java
// 1. 驗證令牌【令牌的對比和刪除必須保證原子性】傳統寫法可能會存在重復提交
// 0-令牌失敗;1-刪除成功
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String orderToken = submitVo.getOrderToken();
// 通過Lua腳本原子驗證令牌和刪除令牌
Long result = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
if(result==0L){// 令牌驗證失敗responseVo.setCode(1);return responseVo;
}else {// 令牌驗證成功// 下單:去創建訂單,驗令牌,驗價格,鎖庫存...// 1.創建訂單、訂單項等信息// 2.訂單驗價// 3.保存訂單// 4.鎖定庫存
}
5.2.8.3 創建訂單、訂單項等信息
1. 創建訂單
gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java
/*** 創建訂單* @return*/
private OrderCreateTo createOrder(){OrderCreateTo orderCreateTo = new OrderCreateTo();// 1. 生成訂單號,36位【注意數據庫訂單表訂單號的長度】String orderSn = IdWorker.getTimeId();// 創建訂單OrderEntity orderEntity = buildOrder(orderSn);// 2. 獲取所有的訂單項List<OrderItemEntity> orderItemEntities = buildOrderItems(orderSn);// 3. 驗價,計算相關價格、積分等信息computePrice(orderEntity,orderItemEntities);orderCreateTo.setOrder(orderEntity);orderCreateTo.setOrderItems(orderItemEntities);return orderCreateTo;
}
2. 根據訂單號構建訂單
將訂單提交vo放到本地線程中便于使用
gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java
遠程調用庫存服務,獲取收貨地址和運費信息
gulimall-order/src/main/java/com/wen/gulimall/order/feign/WmsFeignService.java
@FeignClient("gulimall-ware")
public interface WmsFeignService {...@GetMapping("/ware/wareinfo/fare")R getFare(@RequestParam("addrId") Long addrId);
}
訂單狀態的枚舉
gulimall-order/src/main/java/com/wen/gulimall/order/enume/OrderStatusEnum.java?
/*** @author W* @createDate 2024/03/12* @description: 訂單狀態的枚舉*/
public enum OrderStatusEnum {CREATE_NEW(0,"待付款"),PAYED(1,"已付款"),SENDED(2,"已發貨"),RECIEVED(3,"已完成"),CANCLED(4,"已取消"),SERVICING(5,"售后中"),SERVICED(6,"售后完成");private Integer code;private String msg;OrderStatusEnum(Integer code,String msg){this.code = code;this.msg = msg;}public Integer getCode() {return code;}public String getMsg() {return msg;}
}
構建訂單
gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java
/*** 構建訂單* @param orderSn* @return*/
private OrderEntity buildOrder(String orderSn) {// 獲取當前用戶登錄信息MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();// 創建訂單OrderEntity orderEntity = new OrderEntity();orderEntity.setMemberId(memberRespVo.getId());orderEntity.setOrderSn(orderSn);OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();// 遠程調用庫存服務,獲取收貨地址和運費信息R fare = wmsFeignService.getFare(orderSubmitVo.getAddrId());FareVo fareResp = fare.getData(new TypeReference<FareVo>() {});// 設置運費信息orderEntity.setFreightAmount(fareResp.getFare());// 設置收貨人信息orderEntity.setReceiverName(fareResp.getAddress().getName());orderEntity.setReceiverPhone(fareResp.getAddress().getPhone());orderEntity.setReceiverPostCode(fareResp.getAddress().getPostCode());orderEntity.setReceiverProvince(fareResp.getAddress().getProvince());orderEntity.setReceiverCity(fareResp.getAddress().getCity());orderEntity.setReceiverRegion(fareResp.getAddress().getRegion());orderEntity.setReceiverDetailAddress(fareResp.getAddress().getDetailAddress());// 設置訂單的狀態orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());orderEntity.setAutoConfirmDay(7); // 自動確認收貨時間 (天)return orderEntity;
}
3. 構建所有訂單項數據
gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java
/*** 構建所有訂單項數據* @return*/
public List<OrderItemEntity> buildOrderItems(String orderSn){// 最后確定每個購物項的價格List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();if(currentUserCartItems!=null && currentUserCartItems.size()>0){return currentUserCartItems.stream().map(cartItem -> {OrderItemEntity itemEntity = buildOrderItem(cartItem);itemEntity.setOrderSn(orderSn);return itemEntity;}).collect(Collectors.toList());}return null;
}
4.?構建每一個訂單項數據??
遠程調用商品服務,獲取商品spu信息
gulimall-order/src/main/java/com/wen/gulimall/order/feign/ProductFeignService.java
/*** @description: 根據skuId獲取spu信息*/
@FeignClient("gulimall-product")
public interface ProductFeignService {@GetMapping("/product/spuinfo/skuId/{skuId}")R getSpuInfoBySkuId(@PathVariable Long skuId);
}
?gulimall-product/src/main/java/com/wen/gulimall/product/app/SpuInfoController.java
@RestController
@RequestMapping("product/spuinfo")
public class SpuInfoController {@Autowiredprivate SpuInfoService spuInfoService;...@GetMapping("/skuId/{skuId}")public R getSpuInfoBySkuId(@PathVariable Long skuId){SpuInfoEntity spuInfo = spuInfoService.getSpuInfoBySkuId(skuId);return R.ok().setData(spuInfo);}...
}
?gulimall-product/src/main/java/com/wen/gulimall/product/service/SpuInfoService.java
public interface SpuInfoService extends IService<SpuInfoEntity> {.../*** 根據skuId獲取Spu信息* @param skuId* @return*/SpuInfoEntity getSpuInfoBySkuId(Long skuId);
}
gulimall-product/src/main/java/com/wen/gulimall/product/service/impl/SpuInfoServiceImpl.java?
@Service("spuInfoService")
public class SpuInfoServiceImpl extends ServiceImpl<SpuInfoDao, SpuInfoEntity> implements SpuInfoService {...@Resourceprivate SkuInfoService skuInfoService;@Resourceprivate BrandService brandService;...@Overridepublic SpuInfoEntity getSpuInfoBySkuId(Long skuId) {SkuInfoEntity byId = skuInfoService.getById(skuId);SpuInfoEntity spuInfo = getById(byId.getSpuId());// 查詢品牌名稱BrandEntity brand = brandService.getById(spuInfo.getBrandId());spuInfo.setBrandName(brand.getName());return spuInfo;}}
給SpuInfoEntity添加一個屬性brandName,如下
@TableField(exist = false)
private String brandName;
創建SpuInfoVo接收遠程獲取的商品SPU信息
gulimall-order/src/main/java/com/wen/gulimall/order/vo/SpuInfoVo.java
@Data
public class SpuInfoVo {private Long id;/*** 商品名稱*/private String spuName;/*** 商品描述*/private String spuDescription;/*** 所屬分類id*/private Long catalogId;/*** 品牌id*/private Long brandId;/*** 品牌名稱*/private String brandName;/****/private BigDecimal weight;/*** 上架狀態[0 - 下架,1 - 上架]*/private Integer publishStatus;/****/private Date createTime;/****/private Date updateTime;
}
構建每一個訂單項
gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java
/*** 構建每一個訂單項數據* @param cartItem* @return*/
private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {OrderItemEntity orderItemEntity = new OrderItemEntity();// 1. 訂單信息:訂單號// 2. 商品的spu信息Long skuId = cartItem.getSkuId();R r = productFeignService.getSpuInfoBySkuId(skuId);SpuInfoVo spuInfo = r.getData(new TypeReference<SpuInfoVo>() {});orderItemEntity.setSpuId(spuInfo.getId());orderItemEntity.setSpuName(spuInfo.getSpuName());orderItemEntity.setSpuBrand(spuInfo.getBrandName());// 3. 商品的sku信息orderItemEntity.setSkuId(skuId);orderItemEntity.setSkuName(cartItem.getTitle());orderItemEntity.setSkuPic(cartItem.getImage());orderItemEntity.setSkuPrice(cartItem.getPrice());orderItemEntity.setSkuQuantity(cartItem.getCount());String skuAttr = StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(), ";");orderItemEntity.setSkuAttrsVals(skuAttr);// 4. 優惠信息【不做】// 5. 積分信息orderItemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());orderItemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());// 6. 訂單項的價格信息orderItemEntity.setPromotionAmount(new BigDecimal("0"));orderItemEntity.setCouponAmount(new BigDecimal("0"));orderItemEntity.setIntegrationAmount(new BigDecimal("0"));// 當前訂單項的實際金額BigDecimal orign = orderItemEntity.getSkuPrice().multiply(new BigDecimal(orderItemEntity.getSkuQuantity().toString()));BigDecimal subtract = orign.subtract(orderItemEntity.getPromotionAmount()).subtract(orderItemEntity.getCouponAmount()).subtract(orderItemEntity.getIntegrationAmount());orderItemEntity.setRealAmount(subtract);return orderItemEntity;
}
5.2.8.4 驗價
將訂單確認頁提交的價格和后臺計算的價格,進行對比,如果不一致,則提醒用戶商品的價格發生變化。
gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java
/*** 計算價格、積分等信息* @param orderEntity* @param orderItemEntities*/
private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> orderItemEntities) {BigDecimal total = new BigDecimal("0");BigDecimal promotion = new BigDecimal("0");BigDecimal integration = new BigDecimal("0");BigDecimal coupon = new BigDecimal("0");BigDecimal growth = new BigDecimal("0");BigDecimal gift = new BigDecimal("0");// 訂單總額,疊加每個訂單項的總金額for (OrderItemEntity orderItemEntity : orderItemEntities) {total = total.add(orderItemEntity.getRealAmount());promotion = promotion.add(orderItemEntity.getPromotionAmount());integration = integration.add(orderItemEntity.getIntegrationAmount());coupon = coupon.add(orderItemEntity.getCouponAmount());growth = growth.add(new BigDecimal(orderItemEntity.getGiftGrowth().toString()));gift = gift.add(new BigDecimal(orderItemEntity.getGiftIntegration().toString()));}// 1. 訂單相關價格orderEntity.setTotalAmount(total);// 應付總額 = 訂單總額 + 運費orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));orderEntity.setPromotionAmount(promotion);orderEntity.setIntegrationAmount(integration);orderEntity.setCouponAmount(coupon);// 設置積分等信息orderEntity.setIntegration(gift.intValue());orderEntity.setGrowth(growth.intValue());orderEntity.setDeleteStatus(0);// 訂單未刪除
}
5.2.8.5 保存訂單
gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java
/*** 保存訂單數據* @param orderCreateTo*/
private void saveOrder(OrderCreateTo orderCreateTo) {// 獲取訂單OrderEntity order = orderCreateTo.getOrder();// 保存訂單this.save(order);// 獲取訂單項信息List<OrderItemEntity> orderItems = orderCreateTo.getOrderItems();// 批量保存訂單項orderItemService.saveBatch(orderItems);
}
5.2.8.6 鎖定庫存
1. 找到每個商品在那個倉庫都有庫存
2. 鎖定庫存
? ? ? ? 1)有商品在任何倉庫都沒有庫存拋異常
????????2)修改數據庫wms_ware_sku表中鎖定庫存字段stock_locked默認值為0,將原來為null的都改為0.
3.修改數據庫訂單表中訂單號的長度,由之前的32變成36。
4.如果扣庫存失敗,訂單應該也不能下成功,查看數據庫訂單相關表中有訂單信息,說明事務沒有生效
1. 遠程調用庫存系統,為每個訂單所庫存?
gulimall-order/src/main/java/com/wen/gulimall/order/feign/WmsFeignService.java
@FeignClient("gulimall-ware")
public interface WmsFeignService {...@PostMapping("/ware/waresku/lock/order")R orderLockStock(@RequestBody WareSkuLockVo vo);
}
gulimall-ware/src/main/java/com/wen/gulimall/ware/controller/WareSkuController.java?
@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {@Autowiredprivate WareSkuService wareSkuService;@PostMapping("/lock/order")public R orderLockStock(@RequestBody WareSkuLockVo vo){try {Boolean stock = wareSkuService.orderLockStock(vo);return R.ok();} catch (NoStockException e) {return R.error(BizCodeEnum.NO_STOCK_EXCEPTION.getCode(),BizCodeEnum.NO_STOCK_EXCEPTION.getMsg());}}...
}
?gulimall-ware/src/main/java/com/wen/gulimall/ware/service/WareSkuService.java
public interface WareSkuService extends IService<WareSkuEntity> {...Boolean orderLockStock(WareSkuLockVo vo);
}
?gulimall-ware/src/main/java/com/wen/gulimall/ware/service/impl/WareSkuServiceImpl.java
- 找出所有庫存大于要鎖定商品數的倉庫;
- 遍歷所有滿足條件的倉庫,逐個嘗試進行鎖庫存,如果鎖庫存成功退出遍歷;如果鎖庫存失敗拋異常,一個商品鎖失敗就等于所有商品鎖庫存都不成功。
@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {@Resourceprivate ProductFeignService productFeignService;@Resourceprivate WareSkuDao wareSkuDao;.../*** 為某個訂單鎖庫存** (rollbackFor = NoStockException.class) 默認運行時異常都會回滾* @param vo* @return*/@Transactional@Overridepublic Boolean orderLockStock(WareSkuLockVo vo) {// 1. 按照下單的收貨地址,找到一個就近的倉庫,鎖定庫存// 1. 找到每個商品在哪個倉庫都有庫存List<OrderItemVo> locks = vo.getLocks();List<SkuWareHasStock> collect = locks.stream().map(item -> {SkuWareHasStock skuWareHasStock = new SkuWareHasStock();Long skuId = item.getSkuId();skuWareHasStock.setSkuId(skuId);// 買幾件skuWareHasStock.setNum(item.getCount());// 查詢這個商品在哪里有庫存 stock-鎖定num>0List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);skuWareHasStock.setWareId(wareIds);return skuWareHasStock;}).collect(Collectors.toList());// 2. 鎖定庫存for (SkuWareHasStock stock : collect) {Boolean skuStocked = false;Long skuId = stock.getSkuId();List<Long> wareIds = stock.getWareId();if(wareIds == null || wareIds.size()==0){// 有一個商品在任何倉庫都沒有庫存throw new NoStockException(skuId);}for (Long wareId : wareIds) {// 成功返回1,否則就是0Long count = wareSkuDao.lockSkuStock(skuId,wareId,stock.getNum());if(count == 1){// 當前商品鎖定庫存成功skuStocked = true;break;}else {// 當前倉庫鎖失敗,重試下一個倉庫}}if(!skuStocked){// 當前商品所有的倉庫都沒有鎖住throw new NoStockException(skuId);}}// 3. 肯定所有的商品庫存都鎖定成功return true;}@Dataclass SkuWareHasStock {private Long skuId;private Integer num;private List<Long> wareId;}
}
5.2.8.7 提交訂單完整代碼
提交訂單問題提示:
(1)修改數據庫wms_ware_sku表中鎖定庫存字段stock_locked默認值為0,將原來為null的都改為0;
(2)修改數據庫訂單號(order_sn)所在表oms_order、oms_order_item、oms_order_return_apply、oms_payment_info中order_sn的長度,由32改為36;
(3)把沒有庫存異常NoStockException移動至common模塊,便于訂單服務和庫存服務使用;
(4)如果庫存沒有所成功,訂單也不能下成功,使用事務(分布式事務)進行處理。
1. controller層
gulimall-order/src/main/java/com/wen/gulimall/order/web/OrderWebController.java
@Controller
public class OrderWebController {@Resourceprivate OrderService orderService;.../*** 下單功能* @param submitVo* @return*/@PostMapping("/submitOrder")public String submitOrder(OrderSubmitVo submitVo, Model model, RedirectAttributes redirectAttributes){try {// 下單:去創建訂單,驗令牌,驗價格,鎖庫存...SubmitOrderResponseVo responseVo = orderService.submitOrder(submitVo);System.out.println("訂單提交的價格:"+submitVo);if(responseVo.getCode()==0){// 下單成功來到支付選項頁model.addAttribute("submitOrderResp",responseVo);return "pay";}else {String msg = "下單失敗;";switch (responseVo.getCode()){case 1: msg += "訂單信息過期,請刷新后再次提交" ;break;case 2: msg += "訂單商品價格發生變化,請確認后再次提交" ;break;case 3: msg += "庫存鎖定失敗,商品庫存不足" ;break;}redirectAttributes.addFlashAttribute("msg",msg);// 下單失敗回到訂單確認頁重新確認訂單信息return "redirect:http://order.gulimall.com/toTrade";}} catch (Exception e) {if(e instanceof NoStockException){String msg = e.getMessage();redirectAttributes.addFlashAttribute("msg",msg);}return "redirect:http://order.gulimall.com/toTrade";}}
}
2. service層
gulimall-order/src/main/java/com/wen/gulimall/order/service/OrderService.java
public interface OrderService extends IService<OrderEntity> {.../*** 下單* @param submitVo* @return*/SubmitOrderResponseVo submitOrder(OrderSubmitVo submitVo);
}
?gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java
@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {private ThreadLocal<OrderSubmitVo> confirmVoThreadLocal = new ThreadLocal<>();@Resourceprivate MemberFeignService memberFeignService;@Resourceprivate CartFeignService cartFeignService;@Resourceprivate WmsFeignService wmsFeignService;@Resourceprivate ProductFeignService productFeignService;@Resourceprivate ThreadPoolExecutor executor;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate OrderItemService orderItemService;...@Transactional@Overridepublic SubmitOrderResponseVo submitOrder(OrderSubmitVo submitVo) {confirmVoThreadLocal.set(submitVo);SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();responseVo.setCode(0);// 下單流程:去創建訂單,驗令牌,驗價格,鎖庫存...// 1. 驗證令牌【令牌的對比和刪除必須保證原子性】傳統寫法可能會存在重復提交// 0-令牌失敗;1-刪除成功String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";String orderToken = submitVo.getOrderToken();// 通過Lua腳本原子驗證令牌和刪除令牌Long result = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);if(result==0L){// 令牌驗證失敗responseVo.setCode(1);return responseVo;}else {// 令牌驗證成功// 下單:去創建訂單,驗令牌,驗價格,鎖庫存...// 1.創建訂單、訂單項等信息OrderCreateTo orderCreateTo = createOrder();// 2.訂單驗價BigDecimal payAmount = orderCreateTo.getOrder().getPayAmount();BigDecimal payPrice = submitVo.getPayPrice();if(Math.abs(payAmount.subtract(payPrice).doubleValue())<0.01){// 金額對比// 3.保存訂單saveOrder(orderCreateTo);// 4.鎖定庫存。只要有異常回滾訂單數據// 訂單號,所有訂單項(skuId,skuName,num)WareSkuLockVo lockVo = new WareSkuLockVo();lockVo.setOrderSn(orderCreateTo.getOrder().getOrderSn());List<OrderItemVo> locks = orderCreateTo.getOrderItems().stream().map(item -> {OrderItemVo orderItemVo = new OrderItemVo();orderItemVo.setSkuId(item.getSkuId());orderItemVo.setTitle(item.getSkuName());orderItemVo.setCount(item.getSkuQuantity());return orderItemVo;}).collect(Collectors.toList());lockVo.setLocks(locks);R r = wmsFeignService.orderLockStock(lockVo);if (r.getCode() == 0) {// 鎖成功responseVo.setOrder(orderCreateTo.getOrder());return responseVo;}else {String msg = (String) r.get("msg");throw new NoStockException(msg);// 鎖定失敗//responseVo.setCode(3);//return responseVo;}}else {responseVo.setCode(2);return responseVo;}}//String redisToken = stringRedisTemplate.opsForValue().get(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId());//if(orderToken!=null && orderToken.equals(redisToken)){// // 令牌驗證通過//}else {// // 不通過//}}...
}
5.2.8.8 提交訂單的問題
1. 本地事務在分布式情況下出現的問題
分布式情況下,其他服務事務不一致問題:
- 遠程服務假失敗
? ? ? ? ? ? ? ? 遠程服務其實成功了,由于網絡故障等沒有返回,導致:訂單回滾,庫存扣減
- 遠程服務執行完成,下面的其他方法出現問題
? ? ? ? ? ? ? ? 導致:已經執行的遠程請求,肯定不能回滾(本地事務只針對同一個數據庫連接)
5.2.8.9 Seata分布式事務
1. Seata術語
(1)TC (Transaction Coordinator) - 事務協調者:維護全局和分支事務的狀態,驅動全局事務提交或回滾。
(2)TM (Transaction Manager) - 事務管理器:?定義全局事務的范圍:開始全局事務、提交或回滾全局事務。
(3)RM (Resource Manager) - 資源管理器:管理分支事務處理的資源,與TC交談以注冊分支事務和報告分支事務的狀態,并驅動分支事務提交或回滾。
2. Seata事務模式
Seata 將為用戶提供了 AT、TCC、SAGA 和 XA 事務模式。
- AT模式:自動事務模式,根據回滾日志表undo_log自動回滾。
- TCC模式:自己手寫事務補償回滾。
3. Seata分布式交易解決方案?
Seata官方手冊:快速啟動 | Apache Seata?
5.2.8.9.1 Windows下安裝Seata
?說明:我使用的的hispring-cloud-alibaba:2021.0.4.0 對應?seata-all:1.5.2,所以選擇 Seata1.5.2 版本,這里和老師的部署方式不一樣。我這里使用store.mode.type=db,老師的store.mode.type=file。
部署步驟參考以下博客:
Windows下部署Seata1.5.2,解決Seata無法啟動問題-CSDN博客
5.2.8.9.2 服務端整合Seata(AT模式)
說明:
1. Seata的AT模式不適用于高并發場景,因為AT模式實現的過程用了大量的鎖機制。所以訂單服務的下單功能不適用AT模式,適用于商品服務的保存商品接口(并發不高),流程如下:
/product/spuinfo/save:
(1)?保存spu的基本信息、保存spu的描述圖片、保存spu的規格參數、保存當前spu對應的所有sku信息;
(2) 遠程保存spu積分信息
(3) 遠程保存sku優惠券信息
2. 下面使用AT模式實現【創建訂單+鎖定庫存】,不推薦使用(原因:下單屬于高并發場景)
1. 引入依賴
在公共模塊引入spring-cloud-starter-alibaba-seata方便其他模塊使用。
gulimall-common/pom.xml
<!-- 分布式事務 -->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
2. application.yml中添加配置
gulimall-order/src/main/resources/application.yml
gulimall-ware/src/main/resources/application.yml
seata:tx-service-group: default_tx_group # 事務分組,必須和服務器配置一樣service:# can not get cluster name in registry config 'service.vgroupMapping.default_tx_group', please make sure registry config correctvgroup-mapping:default_tx_group: defaultgrouplist:default: localhost:8091
注意:事務分組必須和Seata服務端配置一樣;grouplist.default必須配置,端口默認為?7091+1000,端口可在Seata的application.yml中自定義。
3.?使用seata的DataSourceProxy代理自己的數據源
gulimall-order/src/main/java/com/wen/gulimall/order/config/MySeataConfig.java
@Configuration
public class MySeataConfig {@Beanpublic DataSource dataSource(DataSourceProperties dataSourceProperties){HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();if (StringUtils.hasText(dataSourceProperties.getName())) {dataSource.setPoolName(dataSourceProperties.getName());}return new DataSourceProxy(dataSource);}
}
gulimall-ware/src/main/java/com/wen/gulimall/ware/config/WareMybatisPlusConfig.java
@EnableTransactionManagement // 開啟事務
@MapperScan("com.wen.gulimall.ware.dao")
@Configuration
public class WareMybatisPlusConfig {/*** 新的分頁插件,一緩和二緩遵循mybatis的規則,需要設置 MybatisConfiguration#useDeprecatedExecutor = false 避免緩存出現問題(該屬性會在舊插件移除后一同移除)*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));return interceptor;}// Seata數據源代理@Beanpublic DataSource dataSource(DataSourceProperties dataSourceProperties){HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();if (StringUtils.hasText(dataSourceProperties.getName())) {dataSource.setPoolName(dataSourceProperties.getName());}return new DataSourceProxy(dataSource);}
}
5.2.8.9.3 啟用分布式事務
在分布式事務入口使用@GlobalTransactional,每個遠程的小事務用@Transactional。
5.2.8.9.4 測試
重啟服務進行測試。
注意:測試完成,刪除訂單服務下單接口Seata分布式入口注解@GlobalTransactional。
5.2.8.10 最終一致性庫存解鎖邏輯:【可靠消息+最終一致性】【庫存解鎖】
注意:對于高并發場景的分布式事務,不考慮2PC模式和TCC模式。
下單異常發消息解鎖庫存。
庫存工作單詳情表wms_ware_order_task_detail添加ware_id和lock_status字段
5.2.8.10.1 庫存服務整合RabbitMQ,創建交換機、隊列、綁定
1. 引入依賴
gulimall-ware/pom.xml
<!-- RabbitMQ -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2. yml配置(設置發送端確認、抵達確認、手動ack)?
gulimall-ware/src/main/resources/application.yml
spring:rabbitmq:host: 172.1.11.10port: 5672virtual-host: /# 開啟發送端確認publisher-confirm-type: correlated# 開啟發送端消息抵達隊列的確認,默認是falsepublisher-returns: true# 只要消息抵達隊列,以異步發送優先回調我們的returnConfirmtemplate:mandatory: true# 開啟消費端手動ack確認listener:simple:acknowledge-mode: manual
3. 設置消息抵達確認回調
問題:MyRabbitConfig中嘗試注入RabbitTemplate,同時也需要由MyRabbitConfig創建的MessageConverter注入到RabbitTemplate會導致循環依賴。
解決方案:將自定義消息轉換器對象的創建messageConverter()移到另外一個配置類中。
gulimall-ware/src/main/java/com/wen/gulimall/ware/config/MyRabbitConfig.java
@Configuration
public class MyRabbitConfig {@Resourceprivate RabbitTemplate rabbitTemplate;// @Bean//public MessageConverter messageConverter(){// return new Jackson2JsonMessageConverter();//}@PostConstruct //MyRabbitConfig對象創建完成之后,執行這個方法public void initRabbitTemplate(){// 設置確認回調rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {/*** 只要消息抵達Broker就ack=true* @param correlationData 當前消息的唯一關聯數據(這個是消息的唯一id)* @param b (ack) 消息是否成功收到* @param s (cause) 失敗的原因*/@Overridepublic void confirm(CorrelationData correlationData, boolean b, String s) {System.out.println("confirm...correlationData["+correlationData+"]==>ack["+b+"]==>cause["+s+"]");}});// 設置消息抵達隊列的確認回調rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {/*** 只要消息沒有投遞給指定的隊列,就觸發這個失敗回調* @param message the returned message. 投遞失敗的消息詳細信息* @param replyCode the reply code. 回復的狀態碼* @param replyText the reply text. 回復的文本內容* @param exchange the exchange. 當時這個消息接收的交換機* @param routingKey the routing key. 當時這個消息用的哪個路由鍵*/@Overridepublic void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {System.out.println("Fail Message["+message+"]==>replyCode["+replyCode+"]==>replyText["+replyText+"]==>exchange["+exchange+"]==>routingKey["+routingKey+"]");}});}
}
4. 自定義消息轉換器(使用JSON序列化機制)
gulimall-ware/src/main/java/com/wen/gulimall/ware/config/MyMessageConverter.java
@Configuration
public class MyMessageConverter {/*** 使用JSON序列化機制,進行消息轉換* @return*/@Beanpublic MessageConverter messageConverter(){return new Jackson2JsonMessageConverter();}
}
5. 創建庫存解鎖所需延時隊列、交換機、綁定
gulimall-ware/src/main/java/com/wen/gulimall/ware/config/MyRabbitMQConfig.java
@Configuration
public class MyRabbitMQConfig {@RabbitListener(queues = "stock.release.stock.queue")public void listen(Message message, Channel channel) throws IOException {System.out.println("收到庫存解鎖消息,準備解鎖");// 確認收到消息channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);}/*** 延時隊列* @return*/@Beanpublic Queue stockDelayQueue(){Map<String, Object> arguments = new HashMap<>();arguments.put("x-dead-letter-exchange","stock-event-exchange");arguments.put("x-dead-letter-routing-key","stock.release");arguments.put("x-message-ttl",60000);// String name, 【隊列名稱】// boolean durable, 【是否持久化】// boolean exclusive, 【是否排它】// boolean autoDelete,【是否自動刪除】// @Nullable Map<String, Object> arguments 【自定義參數,死信路由、死信路由鍵、消息存活時間】return new Queue("stock.delay.queue",true,false,false,arguments);}/*** 普通隊列/死信隊列(接收過期消息(死信)的隊列)* @return*/@Beanpublic Queue stockReleaseStockQueue(){return new Queue("stock.release.stock.queue",true,false,false);}/*** 交換機* @return*/@Beanpublic Exchange stockEventExchange(){return new TopicExchange("stock-event-exchange",true,false);}/*** 交換機與延時隊列綁定* @return*/@Beanpublic Binding stockLockedDelay(){return new Binding("stock.delay.queue",Binding.DestinationType.QUEUE,"stock-event-exchange","stock.locked",null);}/*** 死信路由綁定死信隊列* @return*/@Beanpublic Binding stockLockedRelease(){return new Binding("stock.release.stock.queue",Binding.DestinationType.QUEUE,"stock-event-exchange","stock.release.#",null);}}
啟動庫存服務進行測試,使用路由鍵stock.locked發送消息至延時隊列,下單兩個商品,看到延時隊列中有兩條消息,一分鐘后可以監聽到消息說明成功。
5.2.8.10.2 監聽庫存解鎖
庫存鎖定成功,向MQ延時隊列中發送一條庫存工作單記錄。
庫存工作單記錄,包含:哪件商品、哪個倉庫、鎖定了多少庫存。
庫存工作單記錄實體類
gulimall-common/src/main/java/com/wen/common/to/mq/StockLockedTo.java?
@Data
public class StockLockedTo {private Long id; // 庫存工作單idprivate StockDetailTo detail;// 庫存工作單詳情
}
庫存工作單詳情
gulimall-common/src/main/java/com/wen/common/to/mq/StockDetailTo.java?
@Data
public class StockDetailTo {private Long id;/*** sku_id*/private Long skuId;/*** sku_name*/private String skuName;/*** 購買個數*/private Integer skuNum;/*** 工作單id*/private Long taskId;/*** 倉庫id*/private Long wareId;/*** 1-已鎖定 2-已解鎖 3-扣減*/private Integer lockStatus;
}
監聽庫存死信隊列,解鎖庫存
gulimall-ware/src/main/java/com/wen/gulimall/ware/listener/StockReleaseListener.java
@RabbitListener(queues = {"stock.release.stock.queue"})
@Service
public class StockReleaseListener {@Resourceprivate WareSkuService wareSkuService;@RabbitHandlerpublic void handleStockLockedRelease(StockLockedTo lockedTo, Message message, Channel channel) throws IOException {System.out.println("收到解鎖庫存的消息....");try {wareSkuService.unlockStock(lockedTo);channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);} catch (IOException e) {channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}
}
解鎖庫存
gulimall-ware/src/main/java/com/wen/gulimall/ware/service/impl/WareSkuService.java
public interface WareSkuService extends IService<WareSkuEntity> {...void unlockStock(StockLockedTo lockedTo);
}
gulimall-ware/src/main/java/com/wen/gulimall/ware/service/impl/WareSkuServiceImpl.java??
@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {@Resourceprivate ProductFeignService productFeignService;@Resourceprivate OrderFeignService orderFeignService;@Resourceprivate WareSkuDao wareSkuDao;@Resourceprivate WareOrderTaskService wareOrderTaskService;@Resourceprivate WareOrderTaskDetailService wareOrderTaskDetailService;@Resourceprivate RabbitTemplate rabbitTemplate;/*** 1. 庫存自動解鎖。* 下單成功,庫存鎖定成功,接下來的業務調用失敗,導致訂單回滾。之前鎖定的庫存就要自動解鎖。* 2. 訂單失敗* 鎖庫存失敗** 只要解鎖庫存失敗,一定要告訴mq服務端解鎖失敗。*/...@Overridepublic void unlockStock(StockLockedTo lockedTo) {StockDetailTo detail = lockedTo.getDetail();Long detailId = detail.getId();WareOrderTaskDetailEntity byId = wareOrderTaskDetailService.getById(detailId);// 解鎖// 1. 查詢數據庫關于這個訂單的鎖定庫存信息。// 有:證明庫存鎖定成功了// 解鎖:訂單情況。// 1. 沒有這個訂單。必須解鎖// 2. 有這個訂單。不是解鎖庫存。// 訂單狀態:已取消,解鎖庫存// 沒取消,不能解鎖// 沒有:庫存鎖定失敗,庫存回滾了。這種情況無需解鎖if(byId!=null){// 庫存鎖定成功,解鎖Long id = lockedTo.getId();// 庫存工作單idWareOrderTaskEntity taskEntity = wareOrderTaskService.getById(id);R r = orderFeignService.getOrderStatus(taskEntity.getOrderSn());if(r.getCode()==0){OrderVo orderVo = r.getData(new TypeReference<OrderVo>() {});if(orderVo == null || orderVo.getStatus() == 4){// 訂單不存在或已取消,解鎖庫存if(byId.getLockStatus() == 1) {// 當前庫存工作單詳情,狀態1-已鎖定 ,未解鎖才能解鎖unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);// 手動確認 DeliveryTag-投遞標簽}}}else {// 消息拒絕后重新放到隊列里面,讓別人繼續消費解鎖throw new RuntimeException("遠程解鎖失敗");}}else {// 庫存鎖定失敗,回滾,無需解鎖}}public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId){// 庫存解鎖wareSkuDao.unLockStock(skuId,wareId,num);// 更新庫存工作單的狀態WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();taskDetailEntity.setId(taskDetailId);taskDetailEntity.setLockStatus(2);// 變為已解鎖wareOrderTaskDetailService.updateById(taskDetailEntity);}...}
遠程調用訂單服務,查詢訂單狀態
gulimall-ware/src/main/java/com/wen/gulimall/ware/feign/OrderFeignService.java
@FeignClient("gulimall-order")
public interface OrderFeignService {@GetMapping("/order/order/status/{orderSn}")R getOrderStatus(@PathVariable("orderSn") String orderSn);
}
gulimall-order/src/main/java/com/wen/gulimall/order/controller/OrderController.java
@RestController
@RequestMapping("order/order")
public class OrderController {@Autowiredprivate OrderService orderService;@GetMapping("/status/{orderSn}")public R getOrderStatus(@PathVariable("orderSn") String orderSn){OrderEntity order = orderService.getOrderByOrderSn(orderSn);return R.ok().setData(order);}...
}
?gulimall-order/src/main/java/com/wen/gulimall/order/service/OrderService.java
public interface OrderService extends IService<OrderEntity> {..../*** 庫存服務根據訂單號查詢訂單信息* @param orderSn* @return*/OrderEntity getOrderByOrderSn(String orderSn);
}
gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java?
@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {private ThreadLocal<OrderSubmitVo> confirmVoThreadLocal = new ThreadLocal<>();@Resourceprivate MemberFeignService memberFeignService;@Resourceprivate CartFeignService cartFeignService;@Resourceprivate WmsFeignService wmsFeignService;@Resourceprivate ProductFeignService productFeignService;@Resourceprivate ThreadPoolExecutor executor;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate OrderItemService orderItemService;...@Overridepublic OrderEntity getOrderByOrderSn(String orderSn) {OrderEntity entity = this.getOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));return entity;}...
}
庫存服務遠程調用訂單服務被登錄攔截器攔截跳轉到login.html,修改訂單服務登錄攔截器,放行。
gulimall-order/src/main/java/com/wen/gulimall/order/interceptor/LoginUserInterceptor.java
5.2.8.11?最終一致性庫存解鎖邏輯:【可靠消息+最終一致性】【定時關單】
關單成功給庫存服務發送一個消息通知解鎖庫存
5.2.8.11.1?訂單服務整合RabbitMQ,創建交換機、隊列、綁定
說明:在谷粒商城篇章9----消息隊列【分不式高級篇六】4.6 延時隊列定時關單模擬中已經創建交換機、隊列、綁定關系。地址如下:
谷粒商城篇章9 ---- P248-P261/P292-P294 ---- 消息隊列【分布式高級篇六】_谷粒商城 292-CSDN博客
5.2.8.11.2 監聽訂單定時關單
1. 訂單創建成功發消息給MQ
gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java
2. 監聽訂單關單
下單成功,超過規定時間未支付,關單
gulimall-order/src/main/java/com/wen/gulimall/order/listener/OrderCloseListener.java
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {@Resourceprivate OrderService orderService;/*** 監聽接收死信隊列消息* @param message* @param channel* @param orderEntity* @throws IOException*/@RabbitHandlerpublic void listen(OrderEntity orderEntity, Message message, Channel channel) throws IOException {System.out.println("當前時間"+new Date() +"收到過期訂單消息,準備關閉訂單:------->"+orderEntity.getOrderSn());try {// 確認收到消息orderService.closeOrder(orderEntity);channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);} catch (IOException e) {// 重回隊列channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}
}
關閉訂單
gulimall-order/src/main/java/com/wen/gulimall/order/service/OrderService.java?
public interface OrderService extends IService<OrderEntity> {.../*** 關閉訂單* @param orderEntity*/void closeOrder(OrderEntity orderEntity);
}
@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {private ThreadLocal<OrderSubmitVo> confirmVoThreadLocal = new ThreadLocal<>();@Resourceprivate RabbitTemplate rabbitTemplate;@Resourceprivate MemberFeignService memberFeignService;@Resourceprivate CartFeignService cartFeignService;@Resourceprivate WmsFeignService wmsFeignService;@Resourceprivate ProductFeignService productFeignService;@Resourceprivate ThreadPoolExecutor executor;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate OrderItemService orderItemService;...@Overridepublic void closeOrder(OrderEntity orderEntity) {// 查詢當前這個訂單的最新狀態OrderEntity order = this.getById(orderEntity.getId());if(order.getStatus().equals(OrderStatusEnum.CREATE_NEW.getCode())){// 關單,重新創建訂單,30分鐘前的訂單信息可能與當前數據庫不一致OrderEntity entity = new OrderEntity();entity.setId(orderEntity.getId());entity.setStatus(OrderStatusEnum.CANCLED.getCode());this.updateById(entity);}}...
}
3. 測試自動關單、解鎖庫存
測試之前清空訂單、庫存鎖定、庫存工作單、庫存工作單詳情,便于測試查看,也可不清。
清空mq中消息,重啟訂單、庫存服務,下單后,等待?
?
提交訂單后,
訂單的延時隊列收到一條消息,未支付,一分鐘后自動關單;
庫存的延時隊列收到兩條消息(下單了兩種商品),未支付,兩分鐘后解鎖庫存。
下單成功,未支付,一分鐘內,數據庫數據,如下:
下單成功,未支付,一分鐘后(訂單關閉),兩分鐘內,數據庫數據,如下:
?下單成功,未支付,兩分鐘后(庫存解鎖),數據庫數據,如下:
5.2.8.11.3 訂單創建成功,機器卡頓,消息延遲導致庫存無法解鎖(關單邏輯升級)?
防止訂單服務卡頓,導致訂單狀態消息一直改變不了,庫存消息優先到期,查詢訂單狀態是新建狀態,消費消息不解鎖庫存。 導致卡頓的訂單,永遠不能解鎖。?
解決方案:
訂單關閉成功后發送消息至庫存隊列stock.release.stock.queue,庫存服務監聽關單成功消息,解鎖庫存。
1. 關單成功發送消息解鎖庫存
消息to = OrderEntity
gulimall-common/src/main/java/com/wen/common/to/mq/OrderTo.java
@Data
public class OrderTo {private Long id;/*** member_id*/private Long memberId;/*** 訂單號*/private String orderSn;/*** 使用的優惠券*/private Long couponId;/*** create_time*/private Date createTime;/*** 用戶名*/private String memberUsername;/*** 訂單總額*/private BigDecimal totalAmount;/*** 應付總額*/private BigDecimal payAmount;/*** 運費金額*/private BigDecimal freightAmount;/*** 促銷優化金額(促銷價、滿減、階梯價)*/private BigDecimal promotionAmount;/*** 積分抵扣金額*/private BigDecimal integrationAmount;/*** 優惠券抵扣金額*/private BigDecimal couponAmount;/*** 后臺調整訂單使用的折扣金額*/private BigDecimal discountAmount;/*** 支付方式【1->支付寶;2->微信;3->銀聯; 4->貨到付款;】*/private Integer payType;/*** 訂單來源[0->PC訂單;1->app訂單]*/private Integer sourceType;/*** 訂單狀態【0->待付款;1->待發貨;2->已發貨;3->已完成;4->已關閉;5->無效訂單】*/private Integer status;/*** 物流公司(配送方式)*/private String deliveryCompany;/*** 物流單號*/private String deliverySn;/*** 自動確認時間(天)*/private Integer autoConfirmDay;/*** 可以獲得的積分*/private Integer integration;/*** 可以獲得的成長值*/private Integer growth;/*** 發票類型[0->不開發票;1->電子發票;2->紙質發票]*/private Integer billType;/*** 發票抬頭*/private String billHeader;/*** 發票內容*/private String billContent;/*** 收票人電話*/private String billReceiverPhone;/*** 收票人郵箱*/private String billReceiverEmail;/*** 收貨人姓名*/private String receiverName;/*** 收貨人電話*/private String receiverPhone;/*** 收貨人郵編*/private String receiverPostCode;/*** 省份/直轄市*/private String receiverProvince;/*** 城市*/private String receiverCity;/*** 區*/private String receiverRegion;/*** 詳細地址*/private String receiverDetailAddress;/*** 訂單備注*/private String note;/*** 確認收貨狀態[0->未確認;1->已確認]*/private Integer confirmStatus;/*** 刪除狀態【0->未刪除;1->已刪除】*/private Integer deleteStatus;/*** 下單時使用的積分*/private Integer useIntegration;/*** 支付時間*/private Date paymentTime;/*** 發貨時間*/private Date deliveryTime;/*** 確認收貨時間*/private Date receiveTime;/*** 評價時間*/private Date commentTime;/*** 修改時間*/private Date modifyTime;
}
gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java
2. 監聽關單成功,解鎖庫存
監聽關單
gulimall-ware/src/main/java/com/wen/gulimall/ware/listener/StockReleaseListener.java
解鎖庫存,方法重載
gulimall-ware/src/main/java/com/wen/gulimall/ware/service/WareSkuService.java?
public interface WareSkuService extends IService<WareSkuEntity> {...void unlockStock(OrderTo orderTo);
}
庫存解鎖前,查詢庫存工單詳情,防止重復解鎖庫存
gulimall-ware/src/main/java/com/wen/gulimall/ware/service/impl/WareSkuServiceImpl.java
@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {@Resourceprivate ProductFeignService productFeignService;@Resourceprivate OrderFeignService orderFeignService;@Resourceprivate WareSkuDao wareSkuDao;@Resourceprivate WareOrderTaskService wareOrderTaskService;@Resourceprivate WareOrderTaskDetailService wareOrderTaskDetailService;@Resourceprivate RabbitTemplate rabbitTemplate;.../*** 防止訂單服務卡頓,導致訂單狀態消息一直改變不了,庫存消息優先到期,查詢訂單狀態是新建狀態,消費消息不解鎖庫存。* 導致卡頓的訂單,永遠不能解鎖* @param orderTo*/@Overridepublic void unlockStock(OrderTo orderTo) {String orderSn = orderTo.getOrderSn();// 查一下最新庫存的狀態,防止重復解鎖庫存WareOrderTaskEntity task = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);Long id = task.getId();// 按照工作單找到所有沒有解鎖的庫存,進行解鎖List<WareOrderTaskDetailEntity> list = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>().eq("task_id", id).eq("lock_status", 1));for (WareOrderTaskDetailEntity entity : list) {unLockStock(entity.getSkuId(), entity.getWareId(), entity.getSkuNum(),entity.getId());}}
}
查一下最新庫存的狀態,防止重復解鎖庫存?
gulimall-ware/src/main/java/com/wen/gulimall/ware/service/WareOrderTaskService.java
public interface WareOrderTaskService extends IService<WareOrderTaskEntity> {...WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn);
}
gulimall-ware/src/main/java/com/wen/gulimall/ware/service/impl/WareOrderTaskServiceImpl.java?
@Service("wareOrderTaskService")
public class WareOrderTaskServiceImpl extends ServiceImpl<WareOrderTaskDao, WareOrderTaskEntity> implements WareOrderTaskService {...@Overridepublic WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn) {WareOrderTaskEntity task = this.getOne(new QueryWrapper<WareOrderTaskEntity>().eq("order_sn", orderSn));return task;}}
5.2.8.12 消息丟失、積壓、重復等解決方案(如何保證消息可靠性?)
gulimall_oms庫中,添加mq消息表,記錄消息的狀態,便于重新發送、防重等
CREATE TABLE `mq_message`(`message_id` CHAR(32) NOT NULL ,`content` TEXT, #json`to_exchange` CHAR(255) DEFAULT NULL ,`routing_key` CHAR(255) DEFAULT NULL ,`class_type` CHAR(255) DEFAULT NULL ,`message_status` INT(1) DEFAULT '0' COMMENT '0-新建 1-已發送 2-錯誤抵達 3-已抵達',`create_time` DATETIME DEFAULT NULL ,`update_time` DATETIME DEFAULT NULL
)
5.2.8.12.1 消息丟失
(1)消息發送出去,由于網絡問題沒有抵達服務器
????????? 做好容錯方法(try-catch),發送消息可能會網絡失敗,失敗后要有重試機 制,可記錄到數據庫,采用定期掃描重發的方式
????????? 做好日志記錄,每個消息狀態是否都被服務器收到都應該記錄
????????? 做好定期重發,如果消息沒有發送成功,定期去數據庫掃描未成功的消息進 行重發
(2)消息抵達Broker,Broker要將消息寫入磁盤(持久化)才算成功。此時Broker尚 未持久化完成,宕機。
????????? publisher也必須加入確認回調機制,確認成功的消息,修改數據庫消息狀態。
(3)自動ACK的狀態下。消費者收到消息,但沒來得及消息然后宕機
????????? 一定開啟手動ACK,消費成功才移除,失敗或者沒來得及處理就noAck并重 新入隊。
5.2.8.12.2 消息重復
(1)消息消費成功,事務已經提交,ack時,機器宕機。導致沒有ack成功,Broker的消息 重新由unack變為ready,并發送給其他消費者
(2)消息消費失敗,由于重試機制,自動又將消息發送出去
(3)成功消費,ack時宕機,消息由unack變為ready,Broker又重新發送
????????? 消費者的業務消費接口應該設計為冪等性的。比如扣庫存有 工作單的狀態標志。
????????? 使用防重表(redis/mysql),發送消息每一個都有業務的唯 一標識,處理過就不用處理。
????????? rabbitMQ的每一個消息都有redelivered字段,可以獲取是否 是被重新投遞過來的,而不是第一次投遞過來的。
5.2.8.12.3 消息積壓
(1)消費者宕機積壓
(2)消費者消費能力不足積壓
(3)發送者發送流量太大
????????? 上線更多的消費者,進行正常消費
????????? 上線專門的隊列消費服務,將消息先批量取出來,記錄數據庫,離線慢慢處理。
總結:保證消息的可靠性,最重要的是防止消息丟失。
注意:可將處理消息單獨提出一個服務,發送消息,失敗重試等。
5.3? 訂單支付頁
5.3.1 加密分類
5.3.1.1 對稱加密
????????加密和解密使用同一把鑰匙。
5.3.1.2 非對稱加密
? ? ? ? 加密和解密使用不同鑰匙。
5.3.2?支付寶支付
1. 完成支付功能的流程,如下:
????????支付寶開放平臺-》API-》支付-》電腦網站支付-》創建應用-》綁定應用-》配置密鑰-》上線應用-》開通產品。
說明:審核比較麻煩,需要營業執照等,所以下面使用支付寶的沙箱環境完成支付功能測試。
2. 沙箱環境:
??????支付寶開放平臺-》API-》AIT-API集成工具-》沙箱-》沙箱環境-》快速接入-》沙箱控制臺。
3. 電腦網站支付開發工具包SDK&Demo獲取,地址如下:
小程序文檔 - 支付寶文檔中心??
下載Java版本的Demo并運行,看一下支付的整體流程。
5.3.2.1 支付寶加密原理
- 支付寶采用RSA非對稱加密對信息進行簽名。
- 支付寶的加密原理主要依賴于RSA非對稱加密技術,這種技術使用一對密鑰:公鑰和私鑰。公鑰用于加密數據,而私鑰用于解密數據。
- 在發送訂單數據時,商戶端直接使用明文,但會使用商戶私鑰對數據進行簽名,支付寶使用商戶公鑰對簽名進行驗簽,明文和簽名對應則驗簽成功。
- 支付成功后,支付寶發送支付成功數據時也會使用支付寶私鑰加一個對應的簽名,商戶端收到支付成功數據后使用支付寶公鑰進行驗簽,成功后才能確認支付成功。
5.3.2.1.1?什么是公鑰、 私鑰、 加密、 簽名和驗簽
1. 公鑰、私鑰
????????公鑰和私鑰是一個相對概念 它們的公私性是相對于生成者來說的。 一對密鑰生成后, 保存在生成者手里的就是私鑰, 生成者發布出去大家用的就是公鑰。
2. 加密和數字簽名
(1)加密是指:
- 我們使用一對公私鑰中的一個密鑰來對數據進行加密, 而使用另一個密鑰來進行解密的技術。
- 公鑰和私鑰都可以用來加密, 也都可以用來解密。
- 但這個加解密必須是一對密鑰之間的互相加解密, 否則不能成功。
- 加密的目的是:為了確保數據傳輸過程中的不可讀性, 就是不想讓別人看到。
(2)簽名:
- 給我們將要發送的數據, 做上一個唯一簽名(類似于指紋)。
- 用來互相驗證接收方和發送方的身份;
- 在驗證身份的基礎上再驗證一下傳遞的數據是否被篡改過。 因此使用數字簽名可以用來達到數據的明文傳輸。
(3)驗簽:
- 支付寶為了驗證請求的數據是否商戶本人發的;
- 商戶為了驗證響應的數據是否支付寶發的。
5.3.2.2 支付寶-電腦網站支付Demo測試
支付寶開放平臺Demo下載地址:支付寶-電腦網站支付-SDK&Demo獲取
5.3.2.2.1 使用支付寶沙箱環境進行測試
沙箱地址:支付寶-沙箱
5.3.2.2.2 系統默認密鑰
也可以自定義密鑰。
5.3.2.2.3 修改Demo中配置AlipayConfig?
5.3.2.2.4 啟動Demo測試
說明:我這里新建了一個JavaWeb項目,然后將Demo中的相關代碼等復制到新建的項目zhifu-demo.
看到以下界面說明啟動成功
測試賬號密碼獲取:支付寶-沙箱賬號
支付成功,可以看到以下界面:
trade_no:交易編號
out_trade_no:商戶訂單號
total_amount:付款金額
5.3.2.3 支付寶支付流程
5.3.3 內網穿透
????????如果別人想要調用我們的系統進行支付,那么AlipayConfig中的服務器異步通知頁面路徑和支付成功后要跳轉的頁面地址,必須外網可以正常訪問。localhost:8080是不可以的。
可以使用內網穿透解決。
沒有內網穿透:?
內網穿透的實現:
5.3.3.1 簡介
內網穿透功能可以允許我們使用外網的網址來訪問主機;
正常的外網需要訪問我們項目的流程是:
????????1、 買服務器并且有公網固定 IP
????????2、 買域名映射到服務器的 IP
????????3、 域名需要進行備案和審核?
5.3.3.2 使用場景
1、 開發測試(微信、 支付寶)
2、 智慧互聯
3、 遠程控制
4、 私有云
5.3.3.3 內網穿透的幾個常用軟件
1、 natapp: https://natapp.cn/ 優惠碼: 022B93FD(9 折) [僅限第一次使用]
2、 續斷: www.zhexi.tech 優惠碼: SBQMEA(95 折) [僅限第一次使用]
3、 花生殼: https://www.oray.com/
5.3.3.4 natapp內網穿透
natapp有免費的隧道可以使用。
natapp官方操作文檔:NATAPP1分鐘快速新手圖文教程 - NATAPP-內網穿透 基于ngrok的國內高速內網映射工具?
1. 注冊、實名認證;
2. 購買免費隧道后,在我的隧道配置內網地址和端口;
3. windows啟動命令
????????windows ,點擊開始->運行->命令行提示符,后進入 natapp.exe的目錄,運行
natapp -authtoken=自己的authtoken
?啟動成功后,可以看到natapp分配的網址:
將支付Demo中AlipayConfig里的服務器異步通知路徑和頁面跳轉同步通知頁面路徑的http://localhost:8080改為以上域名地址。
5.3.4 整合支付
5.3.4.1?導入依賴
gulimall-order/pom.xml
<!-- 支付寶的SDK -->
<dependency><groupId>com.alipay.sdk</groupId><artifactId>alipay-sdk-java</artifactId><version>4.39.86.ALL</version>
</dependency>
5.3.4.2 yml配置?
gulimall-order/src/main/resources/application.yml
alipay:app_id: 9021000137684866merchant_private_key: xxxxalipay_public_key: xxxx# 這里先使用Demo的支付回調頁notify_url: http://skb4tx.natappfree.cc/zhifu_demo_war_exploded/notify_url.jspreturn_url: http://skb4tx.natappfree.cc/zhifu_demo_war_exploded/return_url.jspsign_type: RSA2charset: utf-8gatewayUrl: https://openapi-sandbox.dl.alipaydev.com/gateway.do
5.3.4.3?支付Vo
gulimall-order/src/main/java/com/wen/gulimall/order/vo/PayVo.java
@Data
public class PayVo {private String out_trade_no; // 商戶訂單號 必填private String subject; // 訂單名稱 必填private String total_amount; // 付款金額 必填private String body; // 商品描述 可空
}
5.3.4.4?阿里云支付模板
gulimall-order/src/main/java/com/wen/gulimall/order/config/AlipayTemplate.java
@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {//在支付寶創建的應用的idprivate String app_id;// 商戶私鑰,您的PKCS8格式RSA2私鑰private String merchant_private_key;// 支付寶公鑰,查看地址:https://openhome.alipay.com/platform/keyManage.htm 對應APPID下的支付寶公鑰。private String alipay_public_key;// 服務器[異步通知]頁面路徑 需http://格式的完整路徑,不能加?id=123這類自定義參數,必須外網可以正常訪問// 支付寶會悄悄的給我們發送一個請求,告訴我們支付成功的信息private String notify_url;// 頁面跳轉同步通知頁面路徑 需http://格式的完整路徑,不能加?id=123這類自定義參數,必須外網可以正常訪問//同步通知,支付成功,一般跳轉到成功頁private String return_url;// 簽名方式private String sign_type;// 字符編碼格式private String charset;// 支付寶網關; https://openapi.alipaydev.com/gateway.doprivate String gatewayUrl;public String pay(PayVo vo) throws AlipayApiException {//AlipayClient alipayClient = new DefaultAlipayClient(AlipayTemplate.gatewayUrl, AlipayTemplate.app_id, AlipayTemplate.merchant_private_key, "json", AlipayTemplate.charset, AlipayTemplate.alipay_public_key, AlipayTemplate.sign_type);//1、根據支付寶的配置生成一個支付客戶端AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,app_id, merchant_private_key, "json",charset, alipay_public_key, sign_type);//2、創建一個支付請求 //設置請求參數AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();alipayRequest.setReturnUrl(return_url);alipayRequest.setNotifyUrl(notify_url);//商戶訂單號,商戶網站訂單系統中唯一訂單號,必填String out_trade_no = vo.getOut_trade_no();//付款金額,必填String total_amount = vo.getTotal_amount();//訂單名稱,必填String subject = vo.getSubject();//商品描述,可空String body = vo.getBody();alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","+ "\"total_amount\":\""+ total_amount +"\","+ "\"subject\":\""+ subject +"\","+ "\"body\":\""+ body +"\","+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");String result = alipayClient.pageExecute(alipayRequest).getBody();//會收到支付寶的響應,響應的是一個頁面,只要瀏覽器顯示這個頁面,就會自動來到支付寶的收銀臺頁面System.out.println("支付寶的響應:"+result);return result;}
}
5.3.4.5 訂單支付寶支付接口
@Getmapping中屬性produces
:指定控制器支持的響應媒體類型。text/html以頁面的形式響應。gulimall-order/src/main/java/com/wen/gulimall/order/web/PayWebController.java
@Controller
public class PayWebController {@Resourceprivate AlipayTemplate alipayTemplate;@Resourceprivate OrderService orderService;/*** 1.將支付頁讓瀏覽器展示。* 2.支付成功后,我們要跳到用戶的訂單列表頁* @param orderSn* @return* @throws AlipayApiException*/@ResponseBody@GetMapping(value = "/payOrder",produces = {"text/html"})public String payOrder(String orderSn) throws AlipayApiException {PayVo payVo = orderService.getOrderPay(orderSn);String pay = alipayTemplate.pay(payVo);return pay;}
}
獲取當前訂單的支付信息
gulimall-order/src/main/java/com/wen/gulimall/order/service/OrderService.java
public interface OrderService extends IService<OrderEntity> {.../*** 獲取當前訂單的支付信息* @param orderSn* @return*/PayVo getOrderPay(String orderSn);
}
gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java?
@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {private ThreadLocal<OrderSubmitVo> confirmVoThreadLocal = new ThreadLocal<>();@Resourceprivate RabbitTemplate rabbitTemplate;@Resourceprivate MemberFeignService memberFeignService;@Resourceprivate CartFeignService cartFeignService;@Resourceprivate WmsFeignService wmsFeignService;@Resourceprivate ProductFeignService productFeignService;@Resourceprivate ThreadPoolExecutor executor;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate OrderItemService orderItemService;...@Overridepublic PayVo getOrderPay(String orderSn) {PayVo payVo = new PayVo();OrderEntity order = this.getOrderByOrderSn(orderSn);// 保留兩位小數,向上取值BigDecimal bigDecimal = order.getPayAmount().setScale(2, BigDecimal.ROUND_UP);payVo.setTotal_amount(bigDecimal.toString());payVo.setOut_trade_no(orderSn);// 獲取訂單項數據List<OrderItemEntity> orderItems = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", orderSn));OrderItemEntity orderItemEntity = orderItems.get(0);payVo.setSubject(orderItemEntity.getSkuName());payVo.setSubject(orderItemEntity.getSkuAttrsVals());return payVo;}
}
5.3.4.6 前端頁面修改pay.html
gulimall-order/src/main/resources/templates/pay.html
5.3.4.7 支付測試
支付成功,跳轉以下頁面:
5.3.5 訂單列表頁渲染(member服務)
5.3.5.1 靜態資源上傳到nginx
1. 將 資料-》靜態資源-》html-》訂單頁中的index.html復制到會員服務gulimall-member/src/main/resources/templates下,改名為orderList.html
2. 靜態資源上傳至nginx
3. 將orderList.html中的?靜態資源路徑,如下
? ? ? ? src="? ? ->??src="/static/member/
????????href="? ?->? ?href="/static/member/
5.3.5.2 會員服務整合thymeleaf
1. 引入依賴
gulimall-member/pom.xml
<!-- thymeleaf -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2. 關閉thymeleaf緩存
gulimall-member/src/main/resources/application.yml
spring:thymeleaf:cache: false
3.?orderList.html頁面引入thymeleaf
5.3.5.3?網關配置
gulimall-gateway/src/main/resources/application.yml
- id: gulimall_member_routeuri: lb://gulimall-memberpredicates:# 由以下的主機域名訪問轉發到會員服務- Host=member.gulimall.com
5.3.5.4 SwitchHosts添加配置
添加會員服務的域名與ip映射:xxx.xxx.11.10 order.gulimall.com
商城首頁我的訂單跳轉修改,跳轉到會員服務。
gulimall-product的index.html頁面搜索我的訂單
5.3.5.5 整合SpringSession(登錄后才可以查看訂單信息)
?注意: SpringSession的配置類GulimallSessionConfig.java在公共模塊(gulimall-common)已經配置,這里購物車模塊直接引入公共模塊即可。
公共模塊(gulimall-common)可以參考我之前的博客:谷粒商城篇章7 ---- P211-P235 ---- 認證服務【分布式高級篇四】_谷粒商城p235-CSDN博客
1. 引入依賴
<!-- 整合SpringSession完成session共享問題 -->
<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- redis -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclusion><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId></exclusion></exclusions>
</dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId>
</dependency>
2. 開啟SpringSession
在會員服務的啟動類上,添加如下注解:
gulimall-member/src/main/java/com/wen/gulimall/member/GulimallMemberApplication.java
@EnableRedisHttpSession
3.?session數據存儲方式配置
gulimall-member/src/main/resources/application.yml
spring:redis:host: 172.1.11.10session:store-type: redis
5.3.5.6 配置攔截器
登錄攔截器
gulimall-member/src/main/java/com/wen/gulimall/member/interceptor/LoginUserInterceptor.java
@Component
public class LoginUserInterceptor implements HandlerInterceptor {// 同一個線程共享數據public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String requestURI = request.getRequestURI();boolean match = new AntPathMatcher().match("/member/**", requestURI);if(match){return true;}MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);if(attribute!=null){// 登錄成功loginUser.set(attribute);return true;}else {// 沒登錄,去登錄request.getSession().setAttribute("msg","請先進行登錄");response.sendRedirect("http://auth.gulimall.com/login.html");return false;}}
}
添加登錄攔截器
gulimall-order/src/main/java/com/wen/gulimall/order/config/OrderWebConfiguration.java?
@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {@Resourceprivate LoginUserInterceptor interceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 訂單服務的所有請求都要走登錄攔截registry.addInterceptor(interceptor).addPathPatterns("/**");}
}
5.3.5.7 訂單支付成功回調頁面接口
gulimall-member/src/main/java/com/wen/gulimall/member/web/MemberWebController.java
@Controller
public class MemberWebController {@Resourceprivate OrderFeignService orderFeignService;@GetMapping("memberOrder.html")public String memberOrderPage(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum, Model model){// 查詢當前登錄用戶的所有訂單頁表數據Map<String,Object> params = new HashMap<>();params.put("page",pageNum.toString());R r = orderFeignService.listWithItem(params);System.out.println(JSON.toJSONString(r));model.addAttribute("orders",r);return "orderList";}
}
遠程調用訂單服務接口
gulimall-member/src/main/java/com/wen/gulimall/member/feign/OrderFeignService.java?
@FeignClient("gulimall-order")
public interface OrderFeignService {@PostMapping("/order/order/listWithItem")R listWithItem(@RequestBody Map<String, Object> params);
}
gulimall-order/src/main/java/com/wen/gulimall/order/controller/OrderController.java?
@RestController
@RequestMapping("order/order")
public class OrderController {@Autowiredprivate OrderService orderService;.../*** 分頁查詢當前登錄用戶的所有訂單* @param params* @return*/@PostMapping("/listWithItem")//@RequiresPermissions("order:order:list")public R listWithItem(@RequestBody Map<String, Object> params){PageUtils page = orderService.queryPageWithItem(params);return R.ok().put("page", page);}...
}
gulimall-order/src/main/java/com/wen/gulimall/order/service/OrderService.java?
public interface OrderService extends IService<OrderEntity> {...PageUtils queryPageWithItem(Map<String, Object> params);
}
?gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java
@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {private ThreadLocal<OrderSubmitVo> confirmVoThreadLocal = new ThreadLocal<>();@Resourceprivate RabbitTemplate rabbitTemplate;@Resourceprivate MemberFeignService memberFeignService;@Resourceprivate CartFeignService cartFeignService;@Resourceprivate WmsFeignService wmsFeignService;@Resourceprivate ProductFeignService productFeignService;@Resourceprivate ThreadPoolExecutor executor;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate OrderItemService orderItemService;...@Overridepublic PageUtils queryPageWithItem(Map<String, Object> params) {MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();IPage<OrderEntity> page = this.page(new Query<OrderEntity>().getPage(params),new QueryWrapper<OrderEntity>().eq("member_id",memberRespVo.getId()));List<OrderEntity> orderSn = page.getRecords().stream().map(order -> {List<OrderItemEntity> itemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", order.getOrderSn()));// 設置訂單項order.setItemEntities(itemEntities);return order;}).collect(Collectors.toList());page.setRecords(orderSn);return new PageUtils(page);}...
}
?給OrderEntity添加訂單項屬性,如下:
/** 訂單項 */
@TableField(exist = false)
private List<OrderItemEntity> itemEntities;
解決Feign遠程調用請求頭丟失問題
gulimall-member/src/main/java/com/wen/gulimall/member/config/GuliFeignConfig.java
/*** @author W* @createDate 2024/02/29* @description: 解決 Feign遠程調用請求頭丟失問題* 遠程調用會創建一個新的請求,新的請求沒有請求頭* 使用RequestInterceptor攔截器為新構建的請求添加請求頭*/
@Configuration
public class GuliFeignConfig {@Beanpublic RequestInterceptor requestInterceptor(){return new RequestInterceptor() {@Overridepublic void apply(RequestTemplate template) {// 1.RequestContextHolder拿到剛在進來的請求ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();assert requestAttributes != null;HttpServletRequest request = requestAttributes.getRequest(); // 老請求if(request!=null) {// 同步請求頭數據,CookieString cookie = request.getHeader("Cookie");// 給構建的新請求同步老請求的Cookietemplate.header("Cookie", cookie);}}};}
}
5.3.5.8 設置支付寶支付成功回調url
修改return_url為member服務的訂單列表頁請求地址
gulimall-order/src/main/resources/application.yml
return_url: http://member.gulimall.com/memberOrder.html
5.3.5.9?訂單列表頁渲染?
gulimall-member/src/main/resources/templates/orderList.html
<table class="table" th:each="order:${orders.page.list}"><tr><td colspan="7" style="background:#F7F7F7" ><span style="color:#AAAAAA">2017-12-09 20:50:10</span><span><ruby style="color:#AAAAAA">訂單號:</ruby> [[${order.orderSn}]]</span><span>谷粒商城<i class="table_i"></i></span><i class="table_i5 isShow"></i></td></tr><tr class="tr" th:each="item,itemStatus:${order.itemEntities}"><td colspan="3" style="border-right: 1px solid #ccc"><img style="width: 60px;height: 60px" th:src="${item.skuPic}" alt="" class="img"><div><p style="width: 242px;height: auto;overflow: auto">[[${item.skuName}]]</p><div><i class="table_i4"></i>找搭配</div></div><div style="margin-left:15px;">x[[${item.skuQuantity}]]</div><div style="clear:both"></div></td ><td th:if="${itemStatus.index}==0" th:rowspan="${itemStatus.size}">[[${order.receiverName}]]<i><i class="table_i1"></i></i></td><td th:if="${itemStatus.index}==0" th:rowspan="${itemStatus.size}" style="padding-left:10px;color:#AAAAB1;"><p style="margin-bottom:5px;">總額 ¥[[${order.payAmount}]]</p><hr style="width:90%;"><p>在線支付</p></td><td th:if="${itemStatus.index}==0" th:rowspan="${itemStatus.size}"><ul><li style="color:#71B247;" th:if="${order.status}==0">待付款</li><li style="color:#71B247;" th:if="${order.status}==1">已付款</li><li style="color:#71B247;" th:if="${order.status}==2">已發貨</li><li style="color:#71B247;" th:if="${order.status}==3">已完成</li><li style="color:#71B247;" th:if="${order.status}==4">已取消</li><li style="color:#71B247;" th:if="${order.status}==5">售后中</li><li style="color:#71B247;" th:if="${order.status}==6">售后完成</li><li style="margin:4px 0;" class="hide"><i class="table_i2"></i>跟蹤<i class="table_i3"></i><div class="hi"><div class="p-tit">普通快遞 運單號:390085324974</div><div class="hideList"><ul><li>[北京市] 在北京昌平區南口公司進行簽收掃描,快件已被拍照(您的快件已簽收,感謝您使用韻達快遞)簽收</li><li>[北京市] 在北京昌平區南口公司進行簽收掃描,快件已被拍照(您的快件已簽收,感謝您使用韻達快遞)簽收</li><li>[北京昌平區南口公司] 在北京昌平區南口公司進行派件掃描</li><li>[北京市] 在北京昌平區南口公司進行派件掃描;派送業務員:業務員;聯系電話:17319268636</li></ul></div></div></li><li class="tdLi">訂單詳情</li></ul></td><td th:if="${itemStatus.index}==0" th:rowspan="${itemStatus.size}"><button>確認收貨</button><p style="margin:4px 0; ">取消訂單</p><p>催單</p></td></tr>
</table>
5.3.6 接收支付寶異步通知
- 訂單支付成功后,支付寶會回調商戶接口,這時候需要修改訂單狀態;
- 為了確保訂單真的支付成功,需要對支付寶支付成功回調帶來的簽名進行驗簽,驗簽通過修改訂單狀態;
- 由于同步跳轉可能由于網絡問題導致失敗,所以使用異步通知;
- 支付寶使用的最大努力通知型方案,確保商戶與其之間的數據一致性,隔一段時間會通知商戶支付成功,直到返回 success.
支付寶異步通知說明文檔:電腦網站支付-異步通知說明
5.3.6.1 支付寶異步通知信息vo
gulimall-order/src/main/java/com/wen/gulimall/order/vo/PayAsyncVo.java
@ToString
@Data
public class PayAsyncVo {private String gmt_create;private String charset;private String gmt_payment;private Date notify_time;private String subject;private String sign;private String buyer_id;//支付者的idprivate String body;//訂單的信息private String invoice_amount;//支付金額private String version;private String notify_id;//通知idprivate String fund_bill_list;private String notify_type;//通知類型; trade_status_syncprivate String out_trade_no;//訂單號private String total_amount;//支付的總額private String trade_status;//交易狀態 TRADE_SUCCESSprivate String trade_no;//流水號private String auth_app_id;//private String receipt_amount;//商家收到的款private String point_amount;//private String app_id;//應用idprivate String buyer_pay_amount;//最終支付的金額private String sign_type;//簽名類型private String seller_id;//商家的id}
5.3.6.2 接收支付寶異步通知接口
gulimall-order/src/main/java/com/wen/gulimall/order/listener/OrderPayedListener.java
@RestController
public class OrderPayedListener {@Resourceprivate OrderService orderService;@Resourceprivate AlipayTemplate alipayTemplate;/*** 支付寶支付成功異步通知* @param request* @return*/@PostMapping("/payed/notify")public String handleAliPayed(PayAsyncVo vo, HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {// 只要我們收到支付寶給我們的異步通知,告訴我們訂單支付成功。返回success,支付寶就再也不通知//Map<String, String[]> map = request.getParameterMap();//for (String key : map.keySet()) {// String value = request.getParameter(key);// System.out.println("參數名:"+key+"=》參數值"+value);//}// 驗簽,驗證是否是支付寶返回的信息Map<String,String> params = new HashMap<String,String>();Map<String,String[]> requestParams = request.getParameterMap();for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {String name = iter.next();String[] values = requestParams.get(name);String valueStr = "";for (int i = 0; i < values.length; i++) {valueStr = (i == values.length - 1) ? valueStr + values[i]: valueStr + values[i] + ",";}//亂碼解決,這段代碼在出現亂碼時使用//valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");params.put(name, valueStr);}boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(), alipayTemplate.getCharset(), alipayTemplate.getSign_type()); //調用SDK驗證簽名if(signVerified) {// 驗簽成功System.out.println("簽名驗證成功。。。。。");String result = orderService.handlePayResult(vo);return result;}else {System.out.println("簽名驗證失敗。。。。。");return "error";}}
}
處理支付結果
gulimall-order/src/main/java/com/wen/gulimall/order/service/OrderService.java
public interface OrderService extends IService<OrderEntity> {...String handlePayResult(PayAsyncVo vo);
}
?gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java
@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {private ThreadLocal<OrderSubmitVo> confirmVoThreadLocal = new ThreadLocal<>();@Resourceprivate RabbitTemplate rabbitTemplate;@Resourceprivate MemberFeignService memberFeignService;@Resourceprivate CartFeignService cartFeignService;@Resourceprivate WmsFeignService wmsFeignService;@Resourceprivate ProductFeignService productFeignService;@Resourceprivate ThreadPoolExecutor executor;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate OrderItemService orderItemService;@Resourceprivate PaymentInfoService paymentInfoService;.../*** 處理支付寶的支付結果* @param vo* @return*/@Overridepublic String handlePayResult(PayAsyncVo vo) {// 1. 保存交易流水PaymentInfoEntity infoEntity = new PaymentInfoEntity();infoEntity.setAlipayTradeNo(vo.getTrade_no());infoEntity.setOrderSn(vo.getOut_trade_no());infoEntity.setPaymentStatus(vo.getTrade_status());infoEntity.setCallbackTime(vo.getNotify_time());paymentInfoService.save(infoEntity);// 2. 修改訂單狀態if(vo.getTrade_status().equals("TRADE_SUCCESS")||vo.getTrade_status().equals("TRADE_FINISHED")){String outTradeNo = vo.getOut_trade_no();this.baseMapper.updateOrderStatus(outTradeNo,OrderStatusEnum.PAYED.getCode());}return "success";}
}
修改訂單狀態
gulimall-order/src/main/java/com/wen/gulimall/order/dao/OrderDao.java?
@Mapper
public interface OrderDao extends BaseMapper<OrderEntity> {void updateOrderStatus(@Param("outTradeNo") String outTradeNo, @Param("code") Integer code);
}
gulimall-order/src/main/resources/mapper/order/OrderDao.xml
<update id="updateOrderStatus">update oms_order set `status`=#{code} where order_sn = #{outTradeNo}
</update>
5.3.6.3 登錄攔截器放行異步通知接口
gulimall-order/src/main/java/com/wen/gulimall/order/interceptor/LoginUserInterceptor.java
/*** @author W* @createDate 2024/02/27* @description: 登錄攔截器* 從session中(redis中)獲取了登錄信息,封裝到ThreadLocal* 自定義攔截器需要添加到webmvc中,否則不起作用*/
@Component
public class LoginUserInterceptor implements HandlerInterceptor {// 同一個線程共享數據public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String requestURI = request.getRequestURI();AntPathMatcher antPathMatcher = new AntPathMatcher();boolean match = antPathMatcher.match("/order/order/status/**", requestURI);boolean match1 = antPathMatcher.match("/payed/notify", requestURI);if(match || match1){return true;}MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);if(attribute!=null){// 登錄成功loginUser.set(attribute);return true;}else {// 沒登錄,去登錄request.getSession().setAttribute("msg","請先進行登錄");response.sendRedirect("http://auth.gulimall.com/login.html");return false;}}
}
5.3.6.4 設置支付寶異步通知路徑
gulimall-order/src/main/resources/application.yml
notify_url: http://2vgqx5.natappfree.cc/payed/notify
5.3.6.5 設置接收異步通知信息相關日期格式
注意:如果日期格式不正確,調用異步通知接口失敗。(這里和老師的報錯不同,與Spring版本相關)
gulimall-order/src/main/resources/application.yml
spring:mvc:format:date: yyyy-MM-dd HH:mm:ss
5.3.6.6 解決訂單號長度報錯
修改表oms_payment_info中訂單號order_sn的長度
5.3.7?異步通知內網穿透環境搭建?
- 外網允許訪問的內網地址:order.gulimall.com:80;
- 由于支付寶支付成功,異步回調的請求頭不是order.gulimall.com,因此nginx轉發到網關后找不到對應的服務,所以需要修改nginx配置手動設置header為訂單服務域名。
5.3.7.1 修改內網穿透隧道配置??
5.3.7.2?修改nginx配置
listen 80;listen [::]:80;server_name gulimall.com *.gulimall.com *.natappfree.cc;#charset koi8-r;#access_log /var/log/nginx/host.access.log main;location /static/ {root /usr/share/nginx/html;}location / {proxy_pass http://gulimall;proxy_set_header Host $host; # 解決nginx反向代理host信息丟失問題}location /payed/ {proxy_pass http://gulimall;proxy_set_header Host order.gulimall.com; # 解決nginx反向代理host信息丟失問題}
5.3.8 支付測試
支付寶支付成功,同步跳轉到訂單列表頁,異步修改訂單狀態。
5.3.9 收單
1、訂單在支付頁,不支付,一直刷新,訂單過期了才支付,訂單狀態改為已支付了,但是庫存解鎖了。
- 使用支付寶自動收單功能解決。只要一段時間不支付,就不能支付了。
設置訂單超時時間
gulimall-order/src/main/java/com/wen/gulimall/order/config/AlipayTemplate.java
訂單超過1m(分鐘)不支付,效果:
2、 由于時延等問題。訂單解鎖完成,正在解鎖庫存的時候,異步通知才到
- 訂單解鎖,手動調用收單。
3、 網絡阻塞問題,訂單支付成功的異步通知一直不到達
- 查詢訂單列表時,ajax獲取當前未支付的訂單狀態,查詢訂單狀態時,再獲取一下支付寶
此訂單的狀態
4、其他各種問題
- 每天晚上閑時下載支付寶對賬單,一一進行對賬
6 接口冪等性?
6.1 什么是冪等性
????????接口冪等性就是用戶對于同一操作發起的一次請求或者多次請求的結果是一致的, 不會因
為多次點擊而產生了副作用; 比如說支付場景, 用戶購買了商品支付扣款成功, 但是返回結
果的時候網絡異常, 此時錢已經扣了, 用戶再次點擊按鈕, 此時會進行第二次扣款, 返回結
果成功, 用戶查詢余額返發現多扣錢了, 流水記錄也變成了兩條. . . ,這就沒有保證接口
的冪等性。
6.2 哪些情況需要防止
- 用戶多次點擊按鈕
- 用戶界面回退再次提交
- 微服務互相調用,由于網絡問題,導致請求失敗。feign觸發重試機制
- 其他業務情況
6.3 什么情況下需要冪等
以 SQL 為例, 有些操作是天然冪等的。
- SELECT * FROM table WHER id=?, 無論執行多少次都不會改變狀態, 是天然的冪等。
- UPDATE tab1 SET col1=1 WHERE col2=2, 無論執行成功多少次狀態都是一致的, 也是冪等操作。
- delete from user where userid=1, 多次操作, 結果一樣, 具備冪等性
- insert into user(userid,name) values(1,'a') 如 userid 為唯一主鍵, 即重復操作上面的業務, 只會插入一條用戶數據, 具備冪等性。
- UPDATE tab1 SET col1=col1+1 WHERE col2=2, 每次執行的結果都會發生變化, 不是冪等的。
- insert into user(userid,name) values(1,'a') 如 userid 不是主鍵, 可以重復, 那上面業務多次操作, 數據都會新增多條, 不具備冪等性。
6.4 冪等解決方案
6.4.1 token機制
- 服務端提供了發送 token 的接口。 我們在分析業務的時候, 哪些業務是存在冪等問題的,就必須在執行業務前, 先去獲取 token, 服務器會把 token 保存到 redis 中。
- ?然后調用業務接口請求時, 把 token 攜帶過去, 一般放在請求頭部。
- 服務器判斷 token 是否存在 redis 中, 存在表示第一次請求, 然后刪除 token,繼續執行業務。
- 如果判斷 token 不存在 redis 中, 就表示是重復操作, 直接返回重復標記給 client, 這樣就保證了業務代碼, 不被重復執行。
危險性:
1、 先刪除 token 還是后刪除 token;
????????(1) 先刪除可能導致, 業務確實沒有執行, 重試還帶上之前 token, 由于防重設計導致,
請求還是不能執行。
????????(2) 后刪除可能導致, 業務處理成功, 但是服務閃斷, 出現超時, 沒有刪除 token, 別
人繼續重試, 導致業務被執行兩遍
????????(3) 我們最好設計為先刪除 token, 如果業務調用失敗, 就重新獲取 token 再次請求。
2、 Token 獲取、 比較和刪除必須是原子性
????????(1) redis.get(token) 、 token.equals、 redis.del(token)如果這兩個操作不是原子, 可能導
致, 高并發下, 都 get 到同樣的數據, 判斷都成功, 繼續業務并發執行
????????(2) 可以在 redis 使用 lua 腳本完成這個操作
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
6.4.2 各種鎖機制
6.4.2.1 數據庫悲觀鎖
select * from xxxx where id = 1 for update;
悲觀鎖使用時一般伴隨事務一起使用, 數據鎖定時間可能會很長, 需要根據實際情況選用。另外要注意的是, id 字段一定是主鍵或者唯一索引, 不然可能造成鎖表的結果, 處理起來會非常麻煩。
6.4.2.2 數據庫樂觀鎖
這種方法適合在更新的場景中,
update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1
根據 version 版本, 也就是在操作庫存前先獲取當前商品的 version 版本號, 然后操作的時候帶上此 version 號。 我們梳理下, 我們第一次操作庫存時, 得到 version 為 1, 調用庫存服務version 變成了 2; 但返回給訂單服務出現了問題, 訂單服務又一次發起調用庫存服務, 當訂單服務傳如的 version 還是 1, 再執行上面的 sql 語句時, 就不會執行; 因為 version 已經變為 2 了, where 條件就不成立。 這樣就保證了不管調用幾次, 只會真正的處理一次。
樂觀鎖主要使用于處理讀多寫少的問題。
6.4.2.3 業務層分布式鎖
如果多個機器可能在同一時間同時處理相同的數據, 比如多臺機器定時任務都拿到了相同數據處理, 我們就可以加分布式鎖, 鎖定此數據, 處理完成后釋放鎖。 獲取到鎖的必須先判斷這個數據是否被處理過。
6.4.3 各種唯一約束
6.4.3.1 數據庫唯一約束
插入數據, 應該按照唯一索引進行插入, 比如訂單號, 相同的訂單就不可能有兩條記錄插入。我們在數據庫層面防止重復。
這個機制是利用了數據庫的主鍵唯一約束的特性, 解決了在 insert 場景時冪等問題。 但主鍵的要求不是自增的主鍵, 這樣就需要業務生成全局唯一的主鍵。
如果是分庫分表場景下, 路由規則要保證相同請求下, 落地在同一個數據庫和同一表中, 要不然數據庫主鍵約束就不起效果了, 因為是不同的數據庫和表主鍵不相關。
6.4.3.2 redis set 防重
很多數據需要處理, 只能被處理一次, 比如我們可以計算數據的 MD5 將其放入 redis 的 set,每次處理數據, 先看這個 MD5 是否已經存在, 存在就不處理。
6.4.4 防重表
使用訂單號 orderNo 做為去重表的唯一索引, 把唯一索引插入去重表, 再進行業務操作, 且他們在同一個事務中。 這個保證了重復請求時, 因為去重表有唯一約束, 導致請求失敗, 避免了冪等問題。 這里要注意的是, 去重表和業務表應該在同一庫中, 這樣就保證了在同一個事務, 即使業務操作失敗了, 也會把去重表的數據回滾。 這個很好的保證了數據一致性。
之前說的 redis 防重也算
6.4.5 全局請求唯一id
調用接口時, 生成一個唯一 id, redis 將數據保存到集合中(去重) , 存在即處理過。可以使用 nginx 設置每一個請求的唯一 id;
proxy_set_header X-Request-Id $request_id;
7 本地事務與分布式事務
7.1 本地事務
7.1.1 事務的基本性質
????????數據庫事務的幾個特性:原子性(Atomicity)、一致性(Consistency)、隔離性或獨立性(lsolation)和持久性(Durabilily),簡稱就是ACID;
- 原子性:一系列的操作整體不可拆分, 要么同時成功, 要么同時失敗。
- 一致性:數據在事務的前后, 業務整體一致。
- ?A:1000; B:1000; 轉 200 事務成功; A: 800 B: 1200
- 隔離性: 事務之間互相隔離。
- 持久性: 一旦事務成功, 數據一定會落盤在數據庫。
在以往的單體應用中, 我們多個業務操作使用同一條連接操作不同的數據表, 一旦有異常, 我們可以很容易的整體回滾;
? ? ? ? Business: 我們具體的業務代碼
????????Storage: 庫存業務代碼; 扣庫存
????????Order: 訂單業務代碼; 保存訂單
????????Account: 賬號業務代碼; 減賬戶余額
比如買東西業務, 扣庫存, 下訂單, 賬戶扣款, 是一個整體; 必須同時成功或者失敗。
一個事務開始, 代表以下的所有操作都在同一個連接里面。
7.1.2 事務的隔離級別
- READ UNCOMMITTED(讀未提交)
????????該隔離級別的事務會讀到其它未提交事務的數據, 此現象也稱之為臟讀。
- READ COMMITTED( 讀已提交)
????????一個事務可以讀取另一個已提交的事務, 多次讀取會造成不一樣的結果, 此現象稱為不可重復讀問題, Oracle 和 SQL Server 的默認隔離級別。
- REPEATABLE READ( 可重復讀)
????????該隔離級別是 MySQL 默認的隔離級別, 在同一個事務里, select 的結果是事務開始時時間
點的狀態, 因此, 同樣的 select 操作讀到的結果會是一致的, 但是, 會有幻讀現象。 MySQL
的 InnoDB 引擎可以通過 next-key locks 機制( 參考下文"行鎖的算法"一節) 來避免幻讀。
- SERIALIZABLE( 序列化)
????????在該隔離級別下事務都是串行順序執行的, MySQL 數據庫的 InnoDB 引擎會給讀操作隱式
加一把讀共享鎖, 從而避免了臟讀、 不可重讀復讀和幻讀問題。
7.1.3 事務的七種傳播行為
- PROPAGATION_REQUIRED: 如果當前沒有事務, 就創建一個新事務, 如果當前存在事務, 就加入該事務, 該設置是最常用的設置。
- PROPAGATION_SUPPORTS: 支持當前事務, 如果當前存在事務, 就加入該事務, 如果當 前不存在事務, 就以非事務執行。
- PROPAGATION_MANDATORY: 支持當前事務, 如果當前存在事務, 就加入該事務, 如果 當前不存在事務, 就拋出異常。
- PROPAGATION_REQUIRES_NEW: 創建新事務, 無論當前存不存在事務, 都創建新事務。? PROPAGATION_NOT_SUPPORTED: 以非事務方式執行操作, 如果當前存在事務, 就把當 前事務掛起。
- PROPAGATION_NEVER: 以非事務方式執行, 如果當前存在事務, 則拋出異常。
- PROPAGATION_NESTED: 如果當前存在事務, 則在嵌套事務內執行。 如果當前沒有事務, 則執行與 PROPAGATION_REQUIRED 類似的操作。
7.1.4 SpringBoot 事務關鍵點
7.1.4.1 事務的自動配置
? ? ? ? TransactionAutoConfiguration
7.1.4.2 事務的坑?
????????在同一個類里面, 編寫兩個方法, 內部調用的時候, 會導致事務設置失效。 原因是沒有用到代理對象的緣故。事務是使用代理對象控制的。
解決方案:
(1)導入 spring-boot-starter-aop
(2)@EnableTransactionManagement(proxyTargetClass = true)
(3)@EnableAspectJAutoProxy(exposeProxy=true)
(4) AopContext.currentProxy() 調用方法??
示例:
/*** 方法a、b、c在同一個Service類中,同一個對象內事務方法互調默認失效* 原因:繞過了代理對象,事務是使用代理對象控制的* 注意:不能使用this.b()、this.c();也不能注入自己,會導致循環依賴*
*/
@Transactional(timeout = 30) // a事務所有的設置就傳播到了和他公用的所有事務
public void a(){// b和c做任何設置都是沒用的。都是和a公用一個事務//this.b(); 沒用,原因沒有使用代理對象進行調用//this.c(); 沒用// bService.b(); // a事務// cService.c(); // 新事務(不回滾)int i = 10/0;
}@Transactional(propagation = Propagation.REQUIRED,timeout = 30)
public void b(){}
@Transactional(propagation = Propagation.REQUIRES_NEW,timeout = 20)
public void c(){}
解決方案:
1. 引入依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>2. 開啟aspectj動態代理
啟動類上添加@EnableAspectJAutoProxy(exposeProxy = true)注解,暴露代理對象
(1)使用cglib繼承的方式完成動態代理
(2)exposeProxy = true 暴露代理對象3. 在同一類中獲取動態代理對象OrderServiceImpl orderService = (OrderServiceImpl) AopContext.currentProxy();orderService.b();orderService.c();
7.2 分布式事務
7.2.1 為什么有分布式事務
7.2.2 CAP定理與BASE理論
7.2.2.1 CAP定理
CAP 原則又稱 CAP 定理, 指的是在一個分布式系統中
- ?一致性(Consistency):
???????? ? 在分布式系統中的所有數據備份, 在同一時刻是否同樣的值。 (等同于所有節點訪 問同一份最新的數據副本)
- 可用性(Availability)
????????? 在集群中一部分節點故障后, 集群整體是否還能響應客戶端的讀寫請求。 (對數據 更新具備高可用性)
- 分區容錯性(Partition tolerance)
????????? 大多數分布式系統都分布在多個子網絡。 每個子網絡就叫做一個區(partition) 。 分區容錯的意思是, 區間通信可能失敗。 比如, 一臺服務器放在中國, 另一臺服務 器放在美國, 這就是兩個區, 它們之間可能無法通信。
CAP 原則指的是, 這三個要素最多只能同時實現兩點, 不可能三者兼顧。
????????一般來說, 分區容錯無法避免, 因此可以認為 CAP 的 P 總是成立。 CAP 定理告訴我們, 剩下的 C 和 A 無法同時做到。
兩種方案:
(1)CP:
? ? ? ?網絡通信故障(或宕機)的節點不在繼續提供服務(數據不一致),不保證可用性。
(2)AP:
???????? 所有的機器都可以用,包括故障的那臺機器(因為網絡等問題數據未同步),就不能保證一致性了。
????????分布式系統中實現一致性的 raft 算法、 paxos算法:http://thesecretlivesofdata.com/raft/【raft算法演示】。
Raft算法簡介:
(1)基本原理:共識算法就是保證一個集群的多臺機器協同工作,在遇到請求時,數據能夠保持一致。即使遇到機器宕機,整個系統仍然能夠對外保持服務的可用性。
(2)Raft將共識問題分解三個子問題:
Leader election 領導選舉:有且僅有一個leader節點,如果leader宕機,通過選舉機制選出新的leader;
Log replication 日志復制:leader從客戶端接收數據更新/刪除請求,然后日志復制到follower節點,從而保證集群數據的一致性;
Safety 安全性:通過安全性原則來處理一些特殊case,保證Raft算法的完備性。
(3)核心流程:
首先選出leader,leader節點負責接收外部的數據更新/刪除請求;
然后日志復制到其他follower節點,同時通過安全性的準則來保證整個日志復制的一致性;
如果遇到leader故障,followers會重新發起選舉出新的leader。
參考文獻:分布式一致性算法Raft
在線演示:Raft Consensus Algorithm
7.2.2.2 面臨問題
????????對于多數大型互聯網應用的場景, 主機眾多、 部署分散, 而且現在的集群規模越來越大, 所 以節點故障、 網絡故障是常態, 而且要保證服務可用性達到 99.99999%(N 個 9) , 即保證 P 和 A, 舍棄 C。
7.2.2.3 BASE理論
????????是對 CAP 理論的延伸, 思想是即使無法做到強一致性(CAP 的一致性就是強一致性) , 但可以采用適當的采取弱一致性, 即最終一致性。
BASE 是指
- 基本可用(Basically Available)
- 基本可用是指分布式系統在出現故障的時候, 允許損失部分可用性(例如響應時間、功能上的可用性) , 允許損失部分可用性。 需要注意的是, 基本可用絕不等價于系統不可用。
- ?響應時間上的損失: 正常情況下搜索引擎需要在 0.5 秒之內返回給用戶相應的查詢結果, 但由于出現故障(比如系統部分機房發生斷電或斷網故障),?查詢結果的響應時間增加到了 1~2 秒。
- 功能上的損失: 購物網站在購物高峰(如雙十一) 時, 為了保護系統的穩定性,部分消費者可能會被引導到一個降級頁面。
- 基本可用是指分布式系統在出現故障的時候, 允許損失部分可用性(例如響應時間、功能上的可用性) , 允許損失部分可用性。 需要注意的是, 基本可用絕不等價于系統不可用。
- 軟狀態( Soft State)
- 軟狀態是指允許系統存在中間狀態, 而該中間狀態不會影響系統整體可用性。 分布式存儲中一般一份數據會有多個副本, 允許不同副本同步的延時就是軟狀態的體現。 mysql replication 的異步復制也是一種體現。
- 最終一致性( Eventual Consistency)
- 最終一致性是指系統中的所有數據副本經過一定時間后, 最終能夠達到一致的狀態。 弱一致性和強一致性相反, 最終一致性是弱一致性的一種特殊情況。
7.2.2.4 強一致性、弱一致性、最終一致性
????????從客戶端角度, 多進程并發訪問時, 更新過的數據在不同進程如何獲取的不同策略, 決定了
不同的一致性。 對于關系型數據庫, 要求更新過的數據能被后續的訪問都能看到, 這是強一
致性。 如果能容忍后續的部分或者全部訪問不到, 則是弱一致性。 如果經過一段時間后要求
能訪問到更新后的數據, 則是最終一致性。
7.2.3 分布式事務的幾種方案
7.2.3.1 2PC模式
????????數據庫支持的 2PC【 2 phase commit 二階提交】 , 又叫做 XA Transactions。 MySQL 從 5.5 版本開始支持, SQL Server 2005 開始支持, Oracle 7 開始支持。 其中, XA 是一個兩階段提交協議, 該協議分為以下兩個階段:
第一階段: 事務協調器要求每個涉及到事務的數據庫預提交(precommit)此操作, 并反映是 否可以提交。
第二階段: 事務協調器要求每個數據庫提交數據。 其中, 如果有任何一個數據庫否決此次提交, 那么所有數據庫都會被要求回滾它們在此事務 中的那部分信息。
- XA 協議比較簡單, 而且一旦商業數據庫實現了 XA 協議, 使用分布式事務的成本也比較低。
- XA 性能不理想, 特別是在交易下單鏈路, 往往并發量很高, XA 無法滿足高并發場景。
- XA 目前在商業數據庫支持的比較理想, 在 mysql 數據庫中支持的不太理想, mysql 的XA 實現, 沒有記錄 prepare 階段日志, 主備切換回導致主庫與備庫數據不一致。
- 許多 nosql 也沒有支持 XA, 這讓 XA 的應用場景變得非常狹隘。
- 也有 3PC, 引入了超時機制( 無論協調者還是參與者, 在向對方發送請求后, 若長時間未收到回應則做出相應處理)。
7.2.3.2 柔性事務-TCC事務補償型方案(手動補償)
剛性事務: 遵循 ACID 原則, 強一致性。
柔性事務: 遵循 BASE 理論, 最終一致性;
與剛性事務不同, 柔性事務允許一定時間內, 不同節點的數據不一致, 但要求最終一致。
7.2.3.3 柔性事務-最大努力通知型方案
????????按規律進行通知, 不保證數據一定能通知成功, 但會提供可查詢操作接口進行核對。 這種 方案主要用在與第三方系統通訊時, 比如: 調用微信或支付寶支付后的支付結果通知。 這種 方案也是結合 MQ 進行實現, 例如: 通過 MQ 發送 http 請求, 設置最大通知次數。 達到通 知次數后即不再通知。
案例: 銀行通知、 商戶通知等( 各大交易業務平臺間的商戶通知: 多次通知、 查詢校對、 對 賬文件) , 支付寶的支付成功異步回調。
7.2.3.4 柔性事務-可靠消息+最終一致性方案(異步確保型)
????????實現: 業務處理服務在業務事務提交之前, 向實時消息服務請求發送消息, 實時消息服務只記錄消息數據, 而不是真正的發送。 業務處理服務在業務事務提交之后, 向實時消息服務確認發送。 只有在得到確認發送指令后, 實時消息服務才會真正發送。
防止消息丟失: