05穀粒商城-高階篇五

peng_boke發表於2024-10-14

前言

知不足而奮進,望遠山而前行

13.商城業務-分散式事務

13.1本地事務在分散式下的問題

主要步驟:

  • 遠端服務其實成功了,由於網路故障沒有返回,導致訂單回滾,存庫成功扣減
  • 遠端服務執行完成,下面的其他方法出現問題,導致已執行的遠端請求不能回滾

image-20240814233816585

image-20240814233703598

13.2本地事務隔離級別&傳播行為等複習

事務的基本性質

資料庫事務的幾個特性:原子性(Atomicity )、一致性( Consistency )、隔離性或獨立性( Isolation) 和永續性(Durabilily),簡稱就是 ACID

  • 原子性:一系列的操作整體不可拆分,要麼同時成功,要麼同時失敗
  • 一致性:資料在事務的前後,業務整體一致。
  • 隔離性:事務之間互相隔離。
  • 永續性:一旦事務成功,資料一定會落盤在資料庫。

事務的隔離級別

  • READ UNCOMMITTED(讀未提交):該隔離級別的事務會讀到其它未提交事務的資料,此現象也稱之為髒讀。
  • READ COMMITTED(讀提交:一個事務可以讀取另一個已提交的事務,多次讀取會造成不一樣的結果,此現象稱為不可重 復讀問題,Oracle 和 SQL Server 的預設隔離級別。
  • REPEATABLE READ(可重複讀):該隔離級別是 MySQL 預設的隔離級別,在同一個事務裡,select 的結果是事務開始時時間 點的狀態,因此,同樣的 select 操作讀到的結果會是一致的,但是,會有幻讀現象。MySQL 的 InnoDB 引擎可以透過 next-key locks 機制(參考下文"行鎖的演算法"一節)來避免幻讀。
  • SERIALIZABLE(序列化):在該隔離級別下事務都是序列順序執行的,MySQL 資料庫的 InnoDB 引擎會給讀操作隱式 加一把讀共享鎖,從而避免了髒讀、不可重讀復讀和幻讀問題。

事務的傳播行為

  • 1、PROPAGATION_REQUIRED:如果當前沒有事務,就建立一個新事務,如果當前存在事務, 就加入該事務,該設定是最常用的設定。

  • 2、PROPAGATION_SUPPORTS:支援當前事務,如果當前存在事務,就加入該事務,如果當 前不存在事務,就以非事務執行。

  • 3、PROPAGATION_MANDATORY:支援當前事務,如果當前存在事務,就加入該事務,如果 當前不存在事務,就丟擲異常。

  • 4、PROPAGATION_REQUIRES_NEW:建立新事務,無論當前存不存在事務,都建立新事務。

  • 5、PROPAGATION_NOT_SUPPORTED:以非事務方式執行操作,如果當前存在事務,就把當 前事務掛起。

  • 6、PROPAGATION_NEVER:以非事務方式執行,如果當前存在事務,則丟擲異常。

  • 7、PROPAGATION_NESTED:如果當前存在事務,則在巢狀事務內執行。如果當前沒有事務, 則執行與 PROPAGATION_REQUIRED 類似的操作。

image-20240814235555288

SpringBoot 事務關鍵點

事務的自動配置:TransactionAutoConfiguration

事務的坑:在同一個類裡面,編寫兩個方法,內部呼叫的時候,會導致事務設定失效。原因是沒有用到代理物件的緣故。

解決:

  • 0)、匯入 spring-boot-starter-aop

  • 1)、@EnableTransactionManagement(proxyTargetClass = true)

  • 2)、@EnableAspectJAutoProxy(exposeProxy=true)

  • 3)、AopContext.currentProxy() 呼叫方法

使用代理物件來呼叫事務方法

image-20240814235456726

13.3分散式CAP&Raft原理

為什麼有分散式事務

分散式系統經常出現的異常 機器當機、網路異常、訊息丟失、訊息亂序、資料錯誤、不可靠的 TCP、儲存資料丟失...

image-20240815020859913

分散式事務是企業整合中的一個技術難點,也是每一個分散式系統架構中都會涉及到的一個 東西,特別是在微服務架構中,幾乎可以說是無法避免。

CAP 定理與 BASE 理論

CAP 定理

CAP 原則又稱 CAP 定理,指的是在一個分散式系統中

  • 一致性(Consistency):
    • 在分散式系統中的所有資料備份,在同一時刻是否同樣的值。(等同於所有節點訪 問同一份最新的資料副本)
  • 可用性(Availability):
    • 在叢集中一部分節點故障後,叢集整體是否還能響應客戶端的讀寫請求。(對資料 更新具備高可用性)
  • 分割槽容錯性(Partition tolerance):
    • 大多數分散式系統都分佈在多個子網路。每個子網路就叫做一個區(partition)。 分割槽容錯的意思是,區間通訊可能失敗。比如,一臺伺服器放在中國,另一臺服務 器放在美國,這就是兩個區,它們之間可能無法通訊。

CAP 原則指的是,這三個要素最多隻能同時實現兩點,不可能三者兼顧。

image-20240815021259457

一般來說,分割槽容錯無法避免,因此可以認為 CAP 的 P 總是成立。CAP 定理告訴我們, 剩下的 C 和 A 無法同時做到。

raft 演算法

分散式系統中實現一致性的 raft 演算法、paxos演算法

Raft (thesecretlivesofdata.com)

13.4BASE

面臨的問題

對於多數大型網際網路應用的場景,主機眾多、部署分散,而且現在的叢集規模越來越大,所 以節點故障、網路故障是常態,而且要保證服務可用性達到 99.99999%(N 個 9),即保證 P 和 A,捨棄 C

BASE 理論

是對 CAP 理論的延伸,思想是即使無法做到強一致性(CAP 的一致性就是強一致性),但可 以採用適當的採取弱一致性,即最終一致性。

BASE 是指

  • 基本可用(Basically Available)
    • 基本可用是指分散式系統在出現故障的時候,允許損失部分可用性(例如響應時間、 功能上的可用性),允許損失部分可用性。需要注意的是,基本可用絕不等價於系 統不可用。
      • 響應時間上的損失:正常情況下搜尋引擎需要在 0.5 秒之內返回給使用者相應的 查詢結果,但由於出現故障(比如系統部分機房發生斷電或斷網故障),查詢 結果的響應時間增加到了 1~2 秒
      • 功能上的損失:購物網站在購物高峰(如雙十一)時,為了保護系統的穩定性, 部分消費者可能會被引導到一個降級頁面。
  • 軟狀態( Soft State)
    • 軟狀態是指允許系統存在中間狀態,而該中間狀態不會影響系統整體可用性。分佈 式儲存中一般一份資料會有多個副本,允許不同副本同步的延時就是軟狀態的體 現。mysql replication 的非同步複製也是一種體現。
  • 最終一致性( Eventual Consistency)
    • 最終一致性是指系統中的所有資料副本經過一定時間後,最終能夠達到一致的狀 態。弱一致性和強一致性相反,最終一致性是弱一致性的一種特殊情況。

強一致性、弱一致性、最終一致性

從客戶端角度,多程序併發訪問時,更新過的資料在不同程序如何獲取的不同策略,決定了 不同的一致性。對於關係型資料庫,要求更新過的資料能被後續的訪問都能看到,這是強一 致性。如果能容忍後續的部分或者全部訪問不到,則是弱一致性。如果經過一段時間後要求 能訪問到更新後的資料,則是最終一致性。

13.5分散式事務常見解決方案

1.2PC 模式

資料庫支援的 2PC【2 phase commit 二階提交】,又叫做 XA Transactions。 MySQL 從 5.5 版本開始支援,SQL Server 2005 開始支援,Oracle 7 開始支援。 其中,XA 是一個兩階段提交協議,該協議分為以下兩個階段:

第一階段:事務協調器要求每個涉及到事務的資料庫預提交(precommit)此操作,並反映是 否可以提交.

第二階段:事務協調器要求每個資料庫提交資料。 其中,如果有任何一個資料庫否決此次提交,那麼所有資料庫都會被要求回滾它們在此事務 中的那部分資訊。

image-20240815031207781

  • XA 協議比較簡單,而且一旦商業資料庫實現了 XA 協議,使用分散式事務的成本也比較 低。

  • XA 效能不理想,特別是在交易下單鏈路,往往併發量很高,XA 無法滿足高併發場景

  • XA 目前在商業資料庫支援的比較理想,在 mysql 資料庫中支援的不太理想,mysql 的 XA 實現,沒有記錄 prepare 階段日誌,主備切換回導致主庫與備庫資料不一致。

  • 許多 nosql 也沒有支援 XA,這讓 XA 的應用場景變得非常狹隘。

  • 也有 3PC,引入了超時機制(無論協調者還是參與者,在向對方傳送請求後,若長時間 未收到回應則做出相應處理)

2.柔性事務-TCC 事務補償型

剛性事務:遵循 ACID 原則,強一致性。

柔性事務:遵循 BASE 理論,最終一致性;

與剛性事務不同,柔性事務允許一定時間內,不同節點的資料不一致,但要求最終一致。

image-20240815031513064

一階段 prepare 行為:呼叫 自定義 的 prepare 邏輯。

二階段 commit 行為:呼叫 自定義 的 commit 邏輯。

三階段 rollback 行為:呼叫 自定義 的 rollback 邏輯。

所謂 TCC 模式,是指支援把 自定義 的分支事務納入到全域性事務的管理

image-20240815031606759

3.柔性事務-最大努力通知型方案

按規律進行通知,不保證資料一定能通知成功,但會提供可查詢操作介面進行核對。這種 方案主要用在與第三方系統通訊時,比如:呼叫微信或支付寶支付後的支付結果通知。這種 方案也是結合 MQ 進行實現,例如:透過 MQ 傳送 http 請求,設定最大通知次數。達到通 知次數後即不再通知。 案例:銀行通知、商戶通知等(各大交易業務平臺間的商戶通知:多次通知、查詢校對、對 賬檔案),支付寶的支付成功非同步回撥。

4.柔性事務-可靠訊息+最終一致性方案(非同步確保型)

實現:業務處理服務在業務事務提交之前,向實時訊息服務請求傳送訊息,實時訊息服務只 記錄訊息資料,而不是真正的傳送。業務處理服務在業務事務提交之後,向實時訊息服務確 認傳送。只有在得到確認傳送指令後,實時訊息服務才會真正傳送。

防止訊息丟失:

/**

*1、做好訊息確認機制(pulisher,consumer【手動 ack】)

*2、每一個傳送的訊息都在資料庫做好記錄。定期將失敗的訊息再次傳送一 遍

*/

13.6Seata&環境準備

主要步驟:

  • 匯入seata 資料庫
  • 部署seata服務
  • 給所有的微服務建立undo_log

官網:https://seata.apache.org/zh-cn/

服務下載地址:https://seata.apache.org/zh-cn/unversioned/release-history/seata-server

原始碼地址:https://github.com/apache/incubator-seata/blob/1.5.2/server/src/main/resources/application.yml

配置application.yml

image-20240815220703982

匯入seata 資料庫

# 把sql檔案複製到mysql的docker容器裡
docker cp seata-tc.sql mysql:/seata-tc.sql
# 進入mysql容器
docker exec -it mysql bash
# 連線mysql
mysql -uroot -p
# 執行sql檔案
source /seata-tc.sql
# 執行完成的sql檔案可以刪除
rm -f seata-tc.sql

image-20240815220744274

部署seata

如果下載太慢,可以使用docker load -i從壓縮包載入 Docker 映象

# 載入映象
docker load -i seata-1.5.2.tar

我是使用docekrcompose部署的

  seata:
    image: seataio/seata-server:1.5.2
    container_name: seata
    privileged: true                     # 設定容器的特權模式為 true
    environment:
      SEATA_IP: 192.168.188.180
    ports:
      - "8099:8099"                      # 對映主機的7091埠到容器的7091埠
      - "7099:7099"                      # 對映主機的8091埠到容器的8091埠
    depends_on:
      - mysql                            # 啟動順序,先啟動 mysql 服務
      - nacos                            # 啟動順序,先啟動 nacos 服務
    volumes:
      - ./seata:/seata-server/resources  # 掛載本地 seata 目錄到容器的 /seata-server/resources
    networks:
      - mall-net                         # 指定連線的網路
    restart: always

image-20240815220840493

訪問http://192.168.188.180:7099/

使用者名稱密碼是application.yml配置的

image-20240527004546133

image-20240815220945016

給所有的微服務建立undo_log

image-20240815221331554

13.7Seata分散式事務體驗

主要步驟:

  • 訂單服務gulimall-order、購物車服務gulimall-cart、商品服務gulimall-product、庫存服務gulimall-ware都要匯入seata,並且新增seata配置
  • 測試下單服務

訂單服務gulimall-order、購物車服務gulimall-cart、商品服務gulimall-product、庫存服務gulimall-ware都要匯入seata,並且新增seata配置

匯入seata

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>

新增seata配置

seata:
  # data-source-proxy-mode: XA
  registry: # TC服務註冊中心的配置,微服務根據這些資訊去註冊中心獲取tc服務地址
    type: nacos # 註冊中心型別 nacos
    nacos:
      server-addr: 192.168.188.180:8848 # nacos地址
      namespace: "" # namespace,預設為空
      group: DEFAULT_GROUP # 分組,預設是DEFAULT_GROUP
      application: seata-server # seata服務名稱
      username: nacos
      password: nacos
  tx-service-group: peng # 事務組名稱
  service:
    vgroup-mapping: # 事務組與tc叢集的對映關係
      peng: "default"

image-20240815230526469

然後執行專案,seata日誌顯示gulimall-ordergulimall-productgulimall-waregulimall-cart加入成功

image-20240815231108872

再次新增購物車,我選了一個沒有庫存的商品,然後提交訂單

image-20240815231226918

沒有訂單生成,也沒有鎖定庫存,分散式事務回滾成功

image-20240815231330485

13.8最終一致性庫存解鎖邏輯

seata分散式事務不適合高併發場景

也不考慮2PC模式和TCC模式

建議最大努力通知和可靠訊息+最終一致性方案

image-20240815232234550

14.商城業務-訂單服務

14.1RabbitMQ延時佇列

RabbitMQ延時佇列(實現定時任務)

場景: 比如未付款訂單,超過一定時間後,系統自動取消訂單並釋放佔有物品。

常用解決方案: spring的 schedule

缺點: 消耗系統記憶體、增加了資料庫的壓力、存在較大的時間誤差

解決:rabbitmq的訊息TTL和死信Exchange結合

訊息的TTL(Time To Live)

  • 訊息的TTL就是訊息的存活時間。

  • RabbitMQ可以對佇列和訊息分別設定TTL。

    • 對佇列設定就是佇列沒有消費者連著的保留時間,也可以對每一個單獨的訊息做單獨的 設定。超過了這個時間,我們認為這個訊息就死了,稱之為死信。
    • 如果佇列設定了,訊息也設定了,那麼會取小的。所以一個訊息如果被路由到不同的隊 列中,這個訊息死亡的時間有可能不一樣(不同的佇列設定)。這裡單講單個訊息的 TTL,因為它才是實現延遲任務的關鍵。可以透過設定訊息的expiration欄位或者x- message-ttl屬性來設定時間,兩者是一樣的效果。

Dead Letter Exchanges(DLX)

  • 一個訊息在滿足如下條件下,會進死信路由,記住這裡是路由而不是佇列, 一個路由可以對應很多佇列。(什麼是死信)
  • 一個訊息被Consumer拒收了,並且reject方法的引數裡requeue是false。也就是說不 會被再次放在佇列裡,被其他消費者使用。(basic.reject/ basic.nack)requeue=false
  • 上面的訊息的TTL到了,訊息過期了。
  • 佇列的長度限制滿了。排在前面的訊息會被丟棄或者扔到死信路由上
  • Dead Letter Exchange其實就是一種普通的exchange,和建立其他 exchange沒有兩樣。只是在某一個設定Dead Letter Exchange的佇列中有 訊息過期了,會自動觸發訊息的轉發,傳送到Dead Letter Exchange中去。
  • 我們既可以控制訊息在一段時間後變成死信,又可以控制變成死信的訊息 被路由到某一個指定的交換機,結合二者,其實就可以實現一個延時佇列
  • 手動ack&異常訊息統一放在一個佇列處理建議的兩種方式
    • catch異常後,手動傳送到指定佇列,然後使用channel給rabbitmq確認訊息已消費
    • 給Queue繫結死信佇列,使用nack(requque為false)確認訊息消費失敗

image-20240816004625262

延時佇列實現-1

image-20240816004651245

延時佇列實現-2

image-20240816004711099

14.2延時佇列定時關單模擬

主要步驟:

  • 建立交換機order-event-exchange
  • 建立延時佇列order.delay.queue
    • arguments.put("x-dead-letter-exchange", "order-event-exchange");:繫結死信交換機
    • arguments.put("x-dead-letter-routing-key", "order.release.order");:設定死信路由routing-key
    • arguments.put("x-message-ttl", 20000):設定過期時間
  • 建立普通佇列order.release.order.queue
  • 兩個佇列都繫結order-event-exchange,當訊息過期帶上routing-key=order.release.order轉發給order-event-exchange,最後轉發給order.release.order.queue

第一種方式

設定訊息過期時間,1分鐘後轉發給user.order.exchange,客戶端監聽user.order.exchange進行消費

image-20240816010408280

第二種方式

生產者生產訊息設定訊息過期時間和routingkey=order.create.order轉發給order-event-exchange,1分鐘後帶上routingkey=order.release.order轉發給order-event-exchangeorder-event-exchange根據路由轉發給order.release.order.queue,客戶端監聽order.release.order.queue進行消費

image-20240816010435462

SpringBoot中使用延時佇列

使用Bean建立交換機、佇列,我這裡設定的過期時間是20s

image-20240816012834082

建立繫結關係

image-20240816012653776

監聽order.release.order.queue佇列

@RabbitListener(queues = "order.release.order.queue")
public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
    System.out.println("收到過期的訂單資訊:準備關閉訂單" + entity.getOrderSn());
    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}

image-20240816012721611

模擬建立訂單的訊息

@ResponseBody
@GetMapping("/test/createorder")
public String createOrderTest() {
    // 訂單下單成功
    OrderEntity entity = new OrderEntity();
    entity.setOrderSn(UUID.randomUUID().toString());
    entity.setModifyTime(new Date());
    // 給MQ傳送訊息。
    rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", entity);
    return "ok";
}

image-20240816012754319

傳送建立訂單請求

http://order.gulimall.com/test/createorder

image-20240816012931538

檢視控制檯,發現20s後order.delay.queue收到訊息並消費

image-20240816012443938

14.3建立業務交換機&佇列

主要步驟:

  • 建立交換機stock-event-exchange
  • 建立延時佇列stock.delay.queue
    • arguments.put("x-dead-letter-exchange", "stock-event-exchange"); :繫結死信交換機
      • arguments.put("x-dead-letter-routing-key", "stock.release");:設定死信路由routing-key
    • arguments.put("x-message-ttl", 120000);:設定過期時間2分鐘
  • 建立普通佇列stock.release.stock.queue
  • 兩個佇列都繫結stock-event-exchange,當訊息過期帶上routing-key=stock.release轉發給stock-event-exchange,最後轉發給stock.release.stock.queue

image-20240816015112739

image-20240816015347941

14.4監聽庫存解鎖

庫存解鎖場景

這裡主要解決第二種場景

  • 1.下訂單成功,訂單過期沒有支付被系統自動取消、被使用者手動取消。都要解鎖庫存
  • 2.下訂單成功,庫存鎖定成功,接下來的業務呼叫失敗,導致訂單回滾。
    • 之前鎖定的庫存就要自動解鎖。

主要步驟:

  • 資料庫mall_wms.wms_ware_order_task_detail新增欄位倉庫idware_id、鎖定狀態lock_status
  • 如果每一個商品都鎖定成功,將當前商品鎖定了幾件的工作單記錄發給MQ
  • 鎖定失敗。前面儲存的工作單資訊都回滾了。傳送出去的訊息,即使要解鎖庫存,由於在資料庫查不到指定的id,所有就不用解鎖
  • 監聽stock.release.stock.queue,成功解鎖傳送ack,不成功就reject,訊息重新放回佇列,讓別人消費

資料庫mall_wms.wms_ware_order_task_detail新增欄位倉庫idwareId、鎖定狀態lockStatus

image-20240816215233439

如果每一個商品都鎖定成功,將當前商品鎖定了幾件的工作單記錄發給MQ

image-20240816040248684

監聽stock.release.stock.queue,成功解鎖傳送ack,不成功就reject,訊息重新放回佇列,讓別人消費

image-20240816040331972

14.5庫存解鎖邏輯

主要步驟:

  • 遠端呼叫訂單服務gulimall-order根據orderSn查詢訂單資訊
  • 解鎖庫存
    • 訂單不存在
    • 訂單已關閉
    • 庫存工作單已鎖定

image-20240816042819352

解鎖

image-20240816042912908

14.6庫存自動解鎖完成

主要步驟:

  • gulimall-order攔截器呼叫/order/order/status/**時不需要進行登入驗證

  • 最佳化解鎖庫存程式碼,單獨建立監聽類StockReleaseListener

gulimall-order攔截器呼叫/order/order/status/**時不需要進行登入驗證

image-20240816210844935

最佳化解鎖庫存程式碼,單獨建立監聽類StockReleaseListener

image-20240816210719908

14.7測試庫存自動解鎖

清除相關表資料

truncate table mall_oms.oms_order;
truncate table mall_oms.oms_order_item;
truncate table mall_wms.wms_ware_order_task;
truncate table mall_wms.wms_ware_order_task_detail;

主要步驟:

  • 1.建立訂單submitOrder時,在呼叫gulimall-ware的方法orderLockStock鎖定庫存成功後,模擬異常,這樣建立訂單就會失敗,建立訂單會回滾(oms_orderoms_order_item),但是庫存已經成功扣除
  • 2.gulimall-ware的方法orderLockStock鎖定庫存方法成功時向mq延時佇列stock.delay.queue新增訊息,並新增溯源資料(wms_ware_order_taskwms_ware_order_task_detail),訊息會在20s(設定的過期時間)時轉發給普通佇列stock.release.stock.queue
  • 3.監聽普通佇列stock.release.stock.queue,根據orderSn獲取訂單狀態,在訂單狀態已關閉或者訂單不存在和當前庫存工作單詳情狀態已鎖定(1),可以進行解鎖,解鎖完成更新庫存工作單詳情狀態已解鎖

建立訂單submitOrder時,在呼叫gulimall-ware的方法orderLockStock鎖定庫存成功後,模擬異常,這樣建立訂單就會失敗,建立訂單會回滾(oms_orderoms_order_item),但是庫存已經成功扣除

image-20240816222713296

gulimall-ware的方法orderLockStock鎖定庫存方法成功時向mq延時佇列stock.delay.queue新增訊息,並新增溯源資料(wms_ware_order_taskwms_ware_order_task_detail),訊息會在20s(設定的過期時間)時轉發給普通佇列stock.release.stock.queue

image-20240816222818737

監聽普通佇列stock.release.stock.queue,根據orderSn獲取訂單狀態,在訂單狀態已關閉或者訂單不存在和當前庫存工作單詳情狀態已鎖定(1),可以進行解鎖

image-20240816222839514

解鎖完成更新庫存工作單詳情狀態已解鎖

image-20240816222851838

14.8定時關單完成

主要步驟:

  • order-event-exchange交換機和stock.release.stock.queue佇列建立繫結關係,關閉訂單後向stock.release.stock.queue傳送訊息解鎖庫存

  • gulimall-order建立訂單submitOrder完成時向延時佇列order.delay.queue傳送訊息,20s到期轉發給普通佇列order.delay.queue,模擬20s後關閉訂單

  • gulimall-ware監聽關閉訂單後傳送的訊息,然後解鎖庫存,這是主動解鎖

  • gulimall-ware解鎖庫存時傳送的延時訊息是被動解鎖

image-20240816234409117

order-event-exchange交換機和stock.release.stock.queue佇列建立繫結關係,關閉訂單後向stock.release.stock.queue傳送訊息解鎖庫存

image-20240816233830063

gulimall-order建立訂單submitOrder完成時向延時佇列order.delay.queue傳送訊息,30s到期轉發給普通佇列order.delay.queue,模擬30s後關閉訂單

image-20240816234446399

gulimall-ware監聽關閉訂單後傳送的訊息,然後解鎖庫存,這是主動解鎖

image-20240816234509950

gulimall-ware解鎖庫存時傳送的延時訊息是被動解鎖

image-20240816234535118

建立訂單30s收到延時佇列的訊息關閉訂單

image-20240816234609649

關閉訂單後向普通佇列stock.release.stock.queue傳送訊息,gulimall-ware進行主動解鎖

image-20240816234729968

鎖定庫存時向延時佇列stock.delay.queue傳送訊息,並設定過期時間30s,主動解鎖後再進行被動解鎖

image-20240816234841966

主動解鎖是建立訂單後20s,被動解鎖時鎖定庫存後30s

image-20240816235220006

14.9訊息丟失、積壓、重複等解決方案

訊息丟失

  • 訊息傳送出去,由於網路問題沒有抵達伺服器
    • 做好容錯方法(try-catch),傳送訊息可能會網路失敗,失敗後要有重試機 制,可記錄到資料庫,採用定期掃描重發的方式
    • 做好日誌記錄,每個訊息狀態是否都被伺服器收到都應該記錄
    • 做好定期重發,如果訊息沒有傳送成功,定期去資料庫掃描未成功的訊息進 行重發
  • 訊息抵達Broker,Broker要將訊息寫入磁碟(持久化)才算成功。此時Broker尚 未持久化完成,當機。
    • publisher也必須加入確認回撥機制,確認成功的訊息,修改資料庫訊息狀態。
  • 自動ACK的狀態下。消費者收到訊息,但沒來得及訊息然後當機
    • 一定開啟手動ACK,消費成功才移除,失敗或者沒來得及處理就noAck並重 新入隊

image-20240817001009436

image-20240817001104316

訊息重複

  • 訊息消費成功,事務已經提交,ack時,機器當機。導致沒有ack成功,Broker的訊息 重新由unack變為ready,併傳送給其他消費者
  • 訊息消費失敗,由於重試機制,自動又將訊息傳送出去
  • 成功消費,ack時當機,訊息由unack變為ready,Broker又重新傳送
    • 消費者的業務消費介面應該設計為冪等性的。比如扣庫存有 工作單的狀態標誌
    • 使用防重表(redis/mysql),傳送訊息每一個都有業務的唯 一標識,處理過就不用處理
    • rabbitMQ的每一個訊息都有redelivered欄位,可以獲取是否 是被重新投遞過來的,而不是第一次投遞過來的

沒有解鎖的庫存才進行解鎖,保證方法冪等性

image-20240817001322133

防重表

CREATE TABLE `mq_message` (
  `message_id` char(32) NOT NULL,
  `content` json,
  `to_exchane` varchar(255) DEFAULT NULL,
  `routing_key` varchar(255) DEFAULT NULL,
  `class_type` varchar(255) DEFAULT NULL,
  `message_status` int(1) DEFAULT '0' COMMENT '0-新建 1-已傳送 2-錯誤抵達 3-已抵達',
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

rabbitMQ的每一個訊息都有redelivered欄位,可以獲取是否 是被重新投遞過來的,而不是第一次投遞過來的

//當前訊息是否被第二次及以後(重新)派發過來了
Boolean redelivered = message.getMessageProperties().getRedelivered();

image-20240817001716714

訊息積壓

  • 消費者當機積壓
  • 消費者消費能力不足積壓
  • 傳送者傳送流量太大
    • 上線更多的消費者,進行正常消費
    • 上線專門的佇列消費服務,將訊息先批次取出來,記錄資料庫,離線慢慢處理

15.商城業務-支付

15.1支付寶沙箱&程式碼

文件地址 https://open.alipay.com/

開發者文件 https://openhome.alipay.com/docCenter/docCenter.htm

全部文件=>電腦網站支付文件;下載 demo https://opendocs.alipay.com/open/270/106291/

image-20240817004852479

15.2RSA、加密加簽、金鑰等

資料地址

沙箱環境:https://openhome.alipay.com/develop/sandbox/account

demo下載地址:https://opendocs.alipay.com/open/270/106291/

金鑰工具下載地址:https://opendocs.alipay.com/common/02kipk?pathHash=0d20b438

idea執行demo,參考https://blog.csdn.net/lovoo/article/details/118770354

買家賬號
kalnhn3954@sandbox.com
登入密碼
111111

公鑰、私鑰、加密、簽名和驗籤

公鑰私鑰

  • 公鑰和私鑰是一個相對概念
  • 它們的公私性是相對於生成者來說的。
  • 一對金鑰生成後,儲存在生成者手裡的就是私鑰,
  • 生成者釋出出去大家用的就是公鑰

加密和數字簽名

加密

  • 我們使用一對公私鑰中的一個金鑰來對資料進行加密,而使用另一個金鑰來進行解 密的技術。
  • 公鑰和私鑰都可以用來加密,也都可以用來解密。
  • 但這個加解密必須是一對金鑰之間的互相加解密,否則不能成功。
  • 加密的目的是: 為了確保資料傳輸過程中的不可讀性,就是不想讓別人看到。

簽名

  • 給我們將要傳送的資料,做上一個唯一簽名(類似於指紋)
  • 用來互相驗證接收方和傳送方的身份
  • 在驗證身份的基礎上再驗證一下傳遞的資料是否被篡改過。因此使用數字簽名可以 用來達到資料的明文傳輸。

驗籤

  • 支付寶為了驗證請求的資料是否商戶本人發的
  • 商戶為了驗證響應的資料是否支付寶發的

下載demo並開啟

下載demo

image-20240817025934994

然後解壓

image-20240817030007670

使用idea開啟

image-20240817021106020

選擇Eclipse,剩下一路下一步即可

image-20240817021134535

移除紅色模組

image-20240817030221189

Facet設定web.xml

image-20240817041959306

在工件新增war

image-20240817042117728

配置tomcat地址和埠

image-20240817042256204

配置tomcat訪問路徑

image-20240817042344013

下載支付寶開放平臺金鑰工具

安裝路徑記得不要有空格

image-20240817042603776

配置應用和支付寶的公鑰、私鑰、APPID,執行demo

生成金鑰

image-20240817042623966

在沙盒應用中選擇自定義金鑰,然後點選檢視

image-20240817042753982

請複製“應用公鑰”至支付寶開放平臺,進而獲取支付寶公鑰

image-20240817043135710

在專案中配置應用私鑰

image-20240817043032970

把沙盒裡支付公鑰配置到專案中

image-20240817043304088

配置APPID

image-20240817043416120

執行專案

點選付款,跳轉登入介面,輸入沙盒賬號裡的買家資訊賬號、密碼和支付密碼

image-20240817043803475

支付成功

image-20240817043948475

15.3內網穿透

內網穿透工具有很多,我這裡使用的是OpenFrp

首次註冊需要實名認證,可能話費1-2元

地址:https://console.openfrp.net/helpcenter

image-20240818000334523

下載客戶端並安裝

image-20240818000402872

首先建立一個隧道,本地埠就是你demo執行埠,然後隨機生成遠端埠即可

image-20240818000305871

建立完成後,開啟客戶端,開啟隧道,並複製域名用於訪問

image-20240818000449277

再次訪問支付demo,能正常執行即可

image-20240818000649492

16.商城業務-訂單服務

16.1整合支付前需要注意的問題

保證所有專案編碼都是utf-8

image-20240818002036866

16.2整合支付

主要步驟:

  • 匯入支付寶SDKapplication.yaml新增支付配置
  • 封裝支付寶支付幫助類
  • 根據orderSn查詢訂單,並把訂單資料傳入支付功能
  • 前端請求支付寶支付介面

匯入支付寶SDKapplication.yaml新增支付配置

image-20240819012523924

封裝支付寶支付幫助類

image-20240819012607048

根據orderSn查詢訂單,並把訂單資料傳入支付功能

image-20240819012639059

前端請求支付寶支付介面

image-20240819012705491

16.3支付成功同步回撥

主要步驟:

  • 修改gulimall-order支付成功頁面跳轉連結,跳轉到會員服務gulimall-member,顯示訂單列表
  • gulimall-member匯入SpringSession依賴,application.yaml新增thymeleafredissession的配置
  • gulimall-member新增SpringSession配置
  • gulimall-member新增登入攔截器,放行OpenFiegn遠端呼叫介面
  • 將訂單頁靜態資源上傳nginx
  • index.html複製到``gulimall-membersrc/main/resources/templates目錄下,並改名orderList.html,修改orderList.html頁面靜態資源地址,新增thymeleaf`名稱空間
  • 管理員啟動SwitchHosts,新增gulimall-member的域名對映
  • gulimall-gateway閘道器服務新增gulimall-member會員服務的閘道器地址
  • gulimall-member新增MemberWebController顯示orderList.html

修改gulimall-order支付成功頁面跳轉連結,跳轉到會員服務gulimall-member,顯示訂單列表

支付完成跳轉到訂單列表頁

image-20240819023508481

gulimall-member匯入SpringSession依賴,application.yaml新增thymeleafredissession的配置

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

image-20240819023602538

gulimall-member新增SpringSession配置

image-20240819024355870

gulimall-member新增登入攔截器,放行OpenFiegn遠端呼叫介面

image-20240819024217791

將訂單頁靜態資源上傳nginx

image-20240819014016893

index.html複製到``gulimall-membersrc/main/resources/templates目錄下,並改名orderList.html,修改orderList.html頁面靜態資源地址,新增thymeleaf`名稱空間

href="
href="/static/member/
src="
src="/static/member/

image-20240819023820744

管理員啟動SwitchHosts,新增gulimall-member的域名對映

192.168.188.180     member.gulimall.com

image-20240819023920849

gulimall-gateway閘道器服務新增gulimall-member會員服務的閘道器地址

- id: gulimall-member_route
  uri: lb://gulimall-member
  predicates:
    - Host=member.gulimall.com

image-20240819023953295

gulimall-member新增MemberWebController顯示orderList.html

@GetMapping(value = "/orderList.html")
public String memberOrderPage(@RequestParam(value = "pageNum",required = false,defaultValue = "0") Integer pageNum,
                              Model model, HttpServletRequest request) {
    return "orderList";
}

image-20240819024040999

16.4訂單列表頁渲染完成

主要步驟:

  • gulimall-member新增OpenFeign配置
  • gulimall-member新增gulimall-order的遠端呼叫,獲取使用者所有訂單列表
  • gulimall-order實現queryPageWithItem分頁獲取使用者所有訂單列表

gulimall-member新增OpenFeign配置

image-20240819035109700

gulimall-member新增gulimall-order的遠端呼叫,獲取使用者所有訂單列表

image-20240819035130374

gulimall-order實現queryPageWithItem分頁獲取使用者所有訂單列表

@Override
public PageUtils queryPageWithItem(Map<String, Object> params) {
    MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
    IPage<OrderEntity> page = this.page(
            new Query<OrderEntity>().getPage(params),
            new QueryWrapper<OrderEntity>()                  .eq("member_id",memberResponseVo.getId()).orderByDesc("create_time")
    );

    //遍歷所有訂單集合
    List<OrderEntity> orderEntityList = page.getRecords().stream().map(order -> {
        //根據訂單號查詢訂單項裡的資料
        List<OrderItemEntity> orderItemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>()
                .eq("order_sn", order.getOrderSn()));
        order.setOrderItemEntityList(orderItemEntities);
        return order;
    }).collect(Collectors.toList());
    page.setRecords(orderEntityList);
    return new PageUtils(page);
}

image-20240819035219344

16.5非同步通知內網穿透環境搭建

支付寶非同步通知

支付寶非同步通知文件:https://opendocs.alipay.com/open/270/105902/

image-20240819041027571

使用nps作內網穿透,無法使用域名必須使用IP:PORT,所以會造成nginx無法根據訪問的域名gulimall.com來匹配請求

解決:修改nginx配置檔案gulimall.conf監聽server_name 124.223.7.41(這是自己的遠端地址)

新增以上域名監聽後,訪問124.223.7.41:8888(這是自己的遠端地址))出現404異常

原因:

  • 閘道器88未攔截到請求

解決:

  • 方案一:在閘道器增加攔截規則,攔截124.223.7.41,將請求傳送到order.gulimall.com
  • 方案二:在nginx轉發時,設定host=order.gulimall.com,使閘道器可以正確攔截【推薦】
  • 方案三:內網穿透的地址直接配成192.168.56.1:9000【缺點:沒有負載均衡了】

image-20240819203615696

bug1:
	修改gulimall.conf
server {
    listen       80;
    server_name gulimall.com *.gulimall.com 124.223.7.41;

    location /static/ {
        root /usr/share/nginx/html;
    }

    location / {
        proxy_set_header Host $host;
        proxy_pass http://gulimall;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

bug2:
方案一:
        - id: gulimall_order_route2
          uri: lb://gulimall-order
          predicates:
            - Host=124.223.7.41

方案二:
	修改gulimall.conf
server {
    listen       80;
    server_name gulimall.com *.gulimall.com 124.223.7.41;

    location /static/ {
        root /usr/share/nginx/html;
    }

    location /payed/ {
        proxy_set_header Host order.gulimall.com;
        proxy_pass http://gulimall;
    }

    location / {
        proxy_set_header Host $host;
        proxy_pass http://gulimall;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

主要步驟:

  • 配置本地連結為自己的本地服務虛擬接ip,遠端埠可隨機
  • 修改nginx配置檔案gulimall.conf監聽server_name 111.199.237.178(自己的遠端內網穿透地址)
  • nginx轉發時,設定host=order.gulimall.com,使閘道器可以正確攔截【推薦】
  • gulimall-order配置支付成功非同步回撥地址
  • gulimall-order登入攔截放行支付寶的非同步回撥通知
  • gulimall-order新增支付寶的非同步回撥通知介面

配置本地連結為自己的本地服務虛擬接ip,遠端埠可隨機

image-20240819203733039

修改nginx配置檔案gulimall.conf監聽server_name 111.199.237.178(自己的遠端內網穿透地址)

nginx轉發時,設定host=order.gulimall.com,使閘道器可以正確攔截【推薦】

image-20240819204019187

gulimall-order配置支付成功非同步回撥地址

boolean match = antPathMatcher.match("/order/order/status/**", uri);
boolean match1 = antPathMatcher.match("/payed/notify", uri);
if (match || match1) {
    return true;
}

image-20240819204552494

gulimall-order登入攔截放行支付寶的非同步回撥通知

@RestController
public class OrderPayedListener {

    @PostMapping("/payed/notify")
    public String handleAlipayed(HttpServletRequest request) {
        // 只要我們收到了支付寶給我們非同步的通知,告訴我們訂單支付成功。返回success,支付寶就再也不通知
        Map<String, String[]> requestParams = request.getParameterMap();
        System.out.println("支付寶通知到位了..資料:" + requestParams);
        return "success";
    }

}

image-20240819204616195

gulimall-order新增支付寶的非同步回撥通知介面

image-20240819204730597

測試

image-20240819204324096

16.6支付完成

主要步驟:

  • 支付寶驗籤
  • 處理支付寶的支付結果
    • 儲存交易流水
    • 更新支付狀態
  • 配置spring.mvc日期格式化

支付寶驗籤

@PostMapping(value = "/payed/notify")
public String handleAlipayed(PayAsyncVo asyncVo, HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {
    // 只要收到支付寶的非同步通知,返回 success 支付寶便不再通知
    // 獲取支付寶POST過來反饋資訊
    //TODO 需要驗籤
    Map<String, String> params = new HashMap<>();
    Map<String, String[]> requestParams = request.getParameterMap();
    for (String name : requestParams.keySet()) {
        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(asyncVo);
        return result;
    } else {
        System.out.println("簽名驗證失敗...");
        return "error";
    }
}

處理支付寶的支付結果

  • 儲存交易流水
  • 更新支付狀態
/**
 * 處理支付寶的支付結果
 * @param asyncVo
 * @return
 */
@Override
public String handlePayResult(PayAsyncVo asyncVo) {
    //儲存交易流水資訊
    PaymentInfoEntity paymentInfo = new PaymentInfoEntity();
    paymentInfo.setOrderSn(asyncVo.getOut_trade_no());
    paymentInfo.setAlipayTradeNo(asyncVo.getTrade_no());
    paymentInfo.setTotalAmount(new BigDecimal(asyncVo.getBuyer_pay_amount()));
    paymentInfo.setSubject(asyncVo.getBody());
    paymentInfo.setPaymentStatus(asyncVo.getTrade_status());
    paymentInfo.setCreateTime(new Date());
    paymentInfo.setCallbackTime(asyncVo.getNotify_time());
    //新增到資料庫中
    this.paymentInfoService.save(paymentInfo);

    //修改訂單狀態
    //獲取當前狀態
    String tradeStatus = asyncVo.getTrade_status();

    if (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED")) {
        //支付成功狀態
        String orderSn = asyncVo.getOut_trade_no(); //獲取訂單號
        this.updateOrderStatus(orderSn,OrderStatusEnum.PAYED.getCode(), PayConstant.ALIPAY);
    }

    return "success";
}

/**
 * 修改訂單狀態
 * @param orderSn
 * @param code
 */
private void updateOrderStatus(String orderSn, Integer code,Integer payType) {

    this.baseMapper.updateOrderStatus(orderSn,code,payType);
}

mall_oms.oms_payment_infoorder_sn欄位改為char(64)

image-20240819220255221

配置spring.mvc日期格式化

spring:
  mvc:
    format:
      date: yyyy-MM-dd HH:mm:ss

image-20240820011311348

16.7收單

image-20240819215827573

支付寶支付介面

https://opendocs.alipay.com/open/cd12c885_alipay.trade.app.pay?pathHash=ab686e33&ref=api&scene=20

主要步驟:

  • 訂單超時,不允許支付
    • 解決:支付時設定超時時間:應該設定訂單絕對超時時間,而不是30m,按照建立訂單+30m來算截止時間 time_expire
  • 訂單解鎖完成,非同步通知才到
    • 解決:釋放庫存的時候,手動呼叫收單功能(參照官方demo的實現)
  • 對賬:每晚啟動定時任務和請求支付包介面進行對賬

訂單超時,不允許支付

image-20240819221024632

訂單解鎖完成,非同步通知才到

image-20240819221230868

關閉交易:https://opendocs.alipay.com/open/ce0b4954_alipay.trade.close?pathHash=7b0fdae1&ref=api&scene=common

image-20240819221253649

對賬:每晚啟動定時任務和請求支付包介面進行對賬

https://opendocs.alipay.com/open/82ea786a_alipay.trade.query?pathHash=0745ecea&ref=api&scene=23

image-20240819221409864

創作不易,感謝支援。

wxzf

相關文章