04穀粒商城-高階篇四

peng_boke發表於2024-10-14

前言

偏我來時不逢春

11.商城業務-訊息佇列

11.1MQ簡介

主要步驟:

  • 非同步處理
  • 應用解耦
  • 流量控制

非同步處理

image-20240807213730824

應用解耦

image-20240807213738527

流量控制

image-20240807213758027

11.2RabbitMQ簡介

11.2.1概述

image-20240807215047415

訊息代理(message broker)

訊息代理:指安裝了訊息中介軟體的伺服器,用於接收訊息和傳送訊息

目的地(destination)

通俗解釋:訊息代理接收到訊息後會將訊息繼續發給目的地(生產者傳送訊息)
目的地主要有兩種形式:佇列、主題

佇列(queue)

  • 點對點訊息通訊(point-to-point)
  • 1.訊息傳送者傳送訊息,訊息代理將其放入一個佇列中,訊息接受者從佇列中獲取訊息內容,訊息讀取後被移出佇列
  • 2.佇列可以被多個消費者監聽,但一條訊息只會被一個消費者成功消費

主題(topic)

  • 釋出(publish)/訂閱(subscribe)訊息通訊
  • 1.傳送者傳送訊息到主題,多個訂閱者訂閱該主題,多個消費者會同時收到訊息

11.2.3兩種規範

JMS(JAVA訊息服務)

JMS:(Java Message Service)
JAVA訊息服務,基於JVM資訊代理的規範。ActiveMQ、HornetMQ是JMS實現

AMQP(高階訊息佇列協議)

AMQP:(Advanced Message Queuing Protocol)
高階訊息佇列協議,也是一個訊息代理的規範,相容JMS
RabbitMQ是AMQP的實現

image-20240807215614554

Spring支援與SpringBoot自動裝配

image-20240807215728890

11.3RabbitMQ工作流程

RabbitMQ簡介: RabbitMQ是一個由erlang開發的AMQP(Advanved Message Queue Protocol)的開源實現。

image-20240807220813036

Message

訊息,訊息是不具名的,它由訊息頭和訊息體組成。訊息體是不透明的,而訊息頭則由一系列的可選屬性組成, 這些屬性包括routing-key(路由鍵)、priority(相對於其他訊息的優先權)、delivery-mode(指出該訊息可 能需要永續性儲存)等。

Publisher

訊息的生產者,也是一個向交換器釋出訊息的客戶端應用程式

Exchange

交換器,用來接收生產者傳送的訊息並將這些訊息路由給伺服器中的佇列。 Exchange有4種型別:direct(預設),fanout, topic, 和headers,不同型別的Exchange轉發訊息的策略有所區別

Queue

訊息佇列,用來儲存訊息直到傳送給消費者。它是訊息的容器,也是訊息的終點。一個訊息可投入一個或多個佇列。訊息一直 在佇列裡面,等待消費者連線到這個佇列將其取走。

Binding

繫結,用於訊息佇列和交換器之間的關聯。一個繫結就是基於路由鍵將交換器和訊息佇列連線起來的路由規則,所以可以將交 換器理解成一個由繫結構成的路由表。 Exchange 和Queue的繫結可以是多對多的關係。

Connection

網路連線,比如一個TCP連線。

Channel

通道,多路複用連線中的一條獨立的雙向資料流通道。通道是建立在真實的TCP連線內的虛擬連線,AMQP 命令都是透過通道 發出去的,不管是釋出訊息、訂閱佇列還是接收訊息,這些動作都是透過通道完成。因為對於作業系統來說建立和銷燬 TCP 都 是非常昂貴的開銷,所以引入了通道的概念,以複用一條 TCP 連線。

Consumer

訊息的消費者,表示一個從訊息佇列中取得訊息的客戶端應用程式。

Virtual Host

虛擬主機,表示一批交換器、訊息佇列和相關物件。虛擬主機是共享相同的身份認證和加 密環境的獨立伺服器域。每個 vhost 本質上就是一個 mini 版的 RabbitMQ 伺服器,擁 有自己的佇列、交換器、繫結和許可權機制。vhost 是 AMQP 概念的基礎,必須在連線時 指定,RabbitMQ 預設的 vhost 是 / 。

Broker

表示訊息佇列伺服器實體

image-20240807221501490

11.4RabbitMQ安裝

官網地址:https://www.rabbitmq.com/networking.html

埠對映:

  • **4369, 25672 (Erlang發現&叢集埠) **

  • **5672, 5671 (AMQP埠) **

  • **15672 (web管理後臺埠) **

  • **61613, 61614 (STOMP協議埠) **

  • **1883, 8883 (MQTT協議埠) **

11.5Exchange型別

RabbitMQ執行機制

AMQP 中的訊息路由

AMQP 中訊息的路由過程和 Java 開 發者熟悉的 JMS 存在一些差別, AMQP 中增加了 Exchange 和 Binding 的角色。生產者把訊息釋出 到 Exchange 上,訊息最終到達佇列 並被消費者接收,而 Binding 決定交 換器的訊息應該傳送到那個佇列

image-20240808160134659

Exchange 型別

Exchange分發訊息時根據型別的不同分發策略有區別,目前共四種型別:direct、 fanout、topic、headers 。headers 匹配 AMQP 訊息的 header 而不是路由鍵, headers 交換器和 direct 交換器完全一致,但效能差很多,目前幾乎用不到了,所以直接 看另外三種型別

Direct Exchange

訊息中的路由鍵(routing key)如果和 Binding 中的 binding key 一致, 交換器 就將訊息發到對應的佇列中。路由鍵與隊 列名完全匹配,如果一個佇列繫結到交換 機要求路由鍵為“dog”,則只轉發 routing key 標記為“dog”的訊息,不會轉發 “dog.puppy”,也不會轉發“dog.guard” 等等。它是完全匹配、單播的模式。

image-20240808160314900

Fanout Exchange

每個發到 fanout 型別交換器的訊息都 會分到所有繫結的佇列上去。fanout 交 換器不處理路由鍵,只是簡單的將佇列 繫結到交換器上,每個傳送到交換器的 訊息都會被轉發到與該交換器繫結的所 有佇列上。很像子網廣播,每臺子網內 的主機都獲得了一份複製的訊息。 fanout 型別轉發訊息是最快的。

image-20240808160436196

Topic Exchange

topic 交換器透過模式匹配分配訊息的 路由鍵屬性,將路由鍵和某個模式進行 匹配,此時佇列需要繫結到一個模式上。 它將路由鍵和繫結鍵的字串切分成單 詞,這些單詞之間用點隔開。它同樣也 會識別兩個萬用字元:符號“#”和符號 “”。#匹配0個或多個單詞,匹配一 個單詞。

image-20240808160542338

11.6Direct-Exchange

建立佇列

pengmall
pengmall.news
pengmall.emps
other.news

image-20240809212612978

建立direct型別交換機,並繫結建立的4個佇列,Routing key就是佇列名

pengmall.exchange.direct

image-20240809212936508

Routing keypengmall傳送訊息,發現佇列pengmall收到訊息

image-20240809213019961

Get Message獲取訊息,Nack messahe requeue true獲取訊息但是訊息不丟失

image-20240809213139236

可以選擇Automatic ack,獲取訊息自動回覆ack

image-20240809213359668

11.7Fanout-Exchange

建立fanout型別交換機,並繫結4個佇列,Routing key就是佇列名

pengmall.exchange.fanout

傳送訊息,發現4個佇列都收到了訊息

image-20240809213950724

指定Routing key4個佇列依然可以收到訊息

image-20240809214053900

11.8Topic-Exchange

建立topic型別交換機,並繫結4個佇列,需要設定4個佇列的Routing key

pengmall.exchange.topic
*.news
pengmall.#

image-20240809214534602

pengmall.exchange.topic傳送訊息的時候設定Routing keypengmall.news,發現匹配4個的佇列的Routing key,4個佇列都收到了訊息

image-20240809214822903

pengmall.exchange.topic傳送訊息的時候設定Routing keyhello.news,只有other.news佇列匹配Routing key

image-20240809214956782

11.9Spring整合Fanout-Exchange

主要步驟:

  • 1.匯入依賴
  • 2.RabbitAutoConfiguration生效後自動注入多個容器
    • CachingConnectionFactory
    • RabbitTemplate
    • AmqpAdmin
    • RabbitMessagingTemplate
  • 3.配置application.yaml

匯入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

image-20240809220301566

配置application.yaml

spring:
  rabbitmq:
    host: 192.168.188.180
    port: 5672
    virtual-host: /
    username: guest
    password: guest

image-20240809220519231

11.10AmqpAdmin使用

主要步驟:

  • 建立交換機
  • 建立佇列
  • 建立繫結關係

程式碼

@Test
public void createExchange(){
    Exchange directExchange = new DirectExchange("hello.exchange.direct", true, false);
    amqpAdmin.declareExchange(directExchange);
    log.info("Exchange[{}]建立成功:","hello.exchange.direct");
}

@Test
public void createQueue() {
    Queue queue = new Queue("hello.queue",true,false,false);
    amqpAdmin.declareQueue(queue);
    log.info("Queue[{}]建立成功:","hello.queue");
}

@Test
public void createBinding() {
    Binding binding = new Binding("hello.queue",
            Binding.DestinationType.QUEUE,
            "hello.exchange.direct",
            "hello",
            null);
    amqpAdmin.declareBinding(binding);
    log.info("Binding[{}]建立成功:","hello.binding");

}

image-20240809222209350

11.11RabbitTemplate使用

主要步驟:

  • 傳送字串訊息
  • 傳送序列化訊息,物件必須實現Serializable介面
  • 傳送json訊息

傳送字串訊息

@Test
public void sendStringMessage() {
    String msg = "Hello World";
    rabbitTemplate.convertAndSend("hello.exchange.direct","hello.queue",
            msg,new CorrelationData(UUID.randomUUID().toString()));
    log.info("訊息傳送完成:{}",msg);
}

image-20240809223914176

傳送序列化訊息,物件必須實現Serializable介面

@Test
public void sendEntityMessage() {
    OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
    reasonEntity.setId(1L);
    reasonEntity.setCreateTime(new Date());
    reasonEntity.setName("reason");
    reasonEntity.setStatus(1);
    reasonEntity.setSort(2);
    String msg = "Hello World";
    //1、傳送訊息,如果傳送的訊息是個物件,會使用序列化機制,將物件寫出去,物件必須實現Serializable介面

    //2、傳送的物件型別的訊息,可以是一個json
    rabbitTemplate.convertAndSend("hello.exchange.direct","hello", reasonEntity);
    log.info("訊息傳送完成:{}",reasonEntity);
}

image-20240809224048867

新增rabbitmq配置

@Configuration
public class MyRabbitConfig {

    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

image-20240809224321619

再次傳送訊息,發現訊息變成了json字串

image-20240809224355217

11.12RabbitListener&RabbitHandler接收訊息

@RabbitListener

  • 1.用於標註在監聽類或監聽方法上,接收訊息,需要指定監聽的佇列(陣列)
  • 2.使用該註解之前,需要在啟動類加上該註解:@EnableRabbit
  • 3.@RabbitListener即可以標註在方法上又可以標註在類上
    • 標註在類上:表示該類是監聽類,使得@RabbitHandler註解生效
    • 標註在方法上:表示該方法時監聽方法,會監聽指定佇列獲得訊息
  • 4.一般只標註在方法上,並配合@RabbitHandler使用,過載的方式接收不同訊息物件

測試叢集多客戶端監聽接收訊息

  • 1.多個客戶端可以共同監聽同一佇列
  • 2.一條訊息同時只能被一個客戶端接收
  • 3.同一個客戶端接收訊息是序列的,revieveMessage方法執行完後才會繼續接收下一條訊息

@RabbitHandler

  • 1.用於標註在監聽方法上,接收訊息,不需要指定監聽的佇列
  • 2.使用該註解之前,需要在啟動類加上該註解:@EnableRabbit
  • 3.@RabbitListener只可以標註在方法,過載的方式接收不同訊息物件

主要步驟:

  • @RabbitListener接收訊息
  • @RabbitListenerMessage
  • 多個服務監聽同一個佇列,同一個訊息只能一個客戶端收到,只有一個訊息完全處理完,才可以接收下一個訊息
  • @RabbitHandler

@RabbitListener接收訊息

@RabbitListener(queues = {"hello.queue"})
public void receiveMsg(Object msg) {
    System.out.println("接收到訊息,內容:" + msg + ",型別:" + msg.getClass());
}

image-20240809234608974

@RabbitListenerMessage

@RabbitListener(queues = {"hello.queue"})
public void receiveMsg(Message msg, OrderReturnReasonEntity content) {
    byte[] body = msg.getBody();
    MessageProperties properties = msg.getMessageProperties();
    System.out.println("接收到訊息,內容:" + msg + ",型別:" + content);
}

image-20240809234301630

在啟動一個gulimall-order

image-20240810000524843

OrderItemServiceImpl新增@RabbitListener(queues = {"hello.queue"})

新增2個監聽方法,但接收訊息的型別不一樣

image-20240810000749016

多個服務監聽同一個佇列,同一個訊息只能一個客戶端收到,只有一個訊息完全處理完,才可以接收下一個訊息

image-20240809235123106

@RabbitHandler處理不同型別引數的訊息

image-20240810000427602

新增一個傳送訊息的RabbitMqController方便測試

@RestController
public class RabbitMqController {

    @Autowired
    RabbitTemplate rabbitTemplate;

    @GetMapping("/sendMq")
    public String sendMq(@RequestParam(value = "num", defaultValue = "10") Integer num) {

        // for (int i = 0; i < num; i++) {
        //     OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
        //     reasonEntity.setId(1L);
        //     reasonEntity.setCreateTime(new Date());
        //     reasonEntity.setName("hello" + i);
        //     rabbitTemplate.convertAndSend("hello.exchange.direct", "hello", reasonEntity);
        // }

        for (int i = 0; i < num; i++) {
            if (i % 2 == 0) {
                OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
                reasonEntity.setId(1L);
                reasonEntity.setCreateTime(new Date());
                reasonEntity.setName("hello"+i);
                rabbitTemplate.convertAndSend("hello.exchange.direct","hello", reasonEntity);
            }else{
                OrderEntity orderEntity = new OrderEntity();
                orderEntity.setOrderSn(UUID.randomUUID().toString());
                rabbitTemplate.convertAndSend("hello.exchange.direct","hello", orderEntity);
            }
        }

        return "ok";
    }
}

image-20240810000628531

11.13可靠投遞-傳送端確認

可靠抵達

  • 保證訊息不丟失,可靠抵達,可以使用事務訊息,效能下降250倍,為此引入確認機制
  • publisher confirmCallback 確認
  • publisher returnCallback 未投遞到 queue 退回模式
  • consumer ack機制

主要步驟:

  • 配置rabbitmq
  • 設定確認回撥setConfirmCallback
  • 傳送訊息時設定new CorrelationData(UUID.randomUUID().toString())
  • 設定失敗回撥setReturnCallback,只要訊息沒有投遞給指定的佇列,就觸發這個失敗回撥setReturnCallback

配置rabbitmq

spring:
  rabbitmq:
    host: 192.168.188.180
    port: 5672
    virtual-host: /
    username: guest
    password: guest
    # 開啟傳送端傳送確認,無論是否到達broker都會觸發回撥【傳送端確認機制+本地事務表】
    publisher-confirm-type: correlated
    # 開啟傳送端抵達佇列確認,訊息未被佇列接收時觸發回撥【傳送端確認機制+本地事務表】
    publisher-returns: true
    # 訊息在沒有被佇列接收時是否強行退回
    template:
        mandatory: true

設定確認回撥setConfirmCallback

設定失敗回撥setReturnCallback

image-20240810011740403

只要訊息抵達Broker就ack=true

  • correlationData:當前訊息的唯一關聯資料(這個是訊息的唯一id)
  • ack:訊息是否成功收到
  • cause:失敗的原因

image-20240810010841814

傳送訊息時設定new CorrelationData(UUID.randomUUID().toString())

並且設定設定錯誤的routingKey模擬未投遞到 queue 退回模式

image-20240810011120064

11.14可靠投遞-消費端確認

主要步驟:

消費端確認(保證每個訊息被正確消費,此時才可以broker刪除這個訊息)

  • 預設是自動確認的,只要訊息接收到,客戶端會自動確認,服務端就會移除這個訊息
    • 問題:我們收到很多訊息,自動回覆給伺服器ack,只有一個訊息處理成功,當機了。發生訊息丟失;
    • 手動確認模式。只要我們沒有明確告訴MQ,貨物被簽收。沒有Ack,消,息就一直是unacked狀態。即使consumer當機。訊息不會丟失,會重新變為Ready,下一次有新的consumer連線進來就發給他
  • 如何簽收
    • channel.basicAck(deliveryTag, false);:簽收;業務成功完成就應該簽收
    • channel.basicNack(deliveryTag, false,true);:拒籤

配置,手動簽收

rabbitmq:
  host: 192.168.188.180
  port: 5672
  virtual-host: /
  username: guest
  password: guest
  # 開啟傳送端傳送確認,無論是否到達broker都會觸發回撥【傳送端確認機制+本地事務表】
  publisher-confirm-type: correlated
  # 開啟傳送端抵達佇列確認,訊息未被佇列接收時觸發回撥【傳送端確認機制+本地事務表】
  publisher-returns: true
  # 訊息在沒有被佇列接收時是否強行退回
  template:
      mandatory: true
  listener:
    simple:
      acknowledge-mode: manual

image-20240812212055286

程式碼

@RabbitHandler
public void receiveMsg(Message message,
                       OrderReturnReasonEntity content,
                       Channel channel) throws InterruptedException, IOException {
    System.out.println("接收到訊息... " + content);
    byte[] body = message.getBody();
    // 訊息頭屬性資訊
    MessageProperties properties = message.getMessageProperties();
    Thread.sleep(3000);
    System.out.println("訊息處理完成=>" + content.getName());
    // channel內按順序自增的。
    long deliveryTag = message.getMessageProperties().getDeliveryTag();
    System.out.println("deliveryTag==>" + deliveryTag);
    // 簽收貨物,非批次模式
    try {
        if (deliveryTag % 2 == 0) {

            channel.basicAck(deliveryTag, false);
            System.out.println("簽收了貨物..." + deliveryTag);
        } else
            System.out.println("沒有收了貨物..." + deliveryTag);
    } catch (Exception e) {
        // 網路中斷
        // 拒籤
        channel.basicNack(deliveryTag, false,true);
    }
}

12.商城業務-訂單服務

12.1頁面環境搭建

主要步驟:

  • 上傳等待付款(detail)、訂單頁(list)、結算頁(confirm)、收銀頁(pay)靜態資源到/root/mall/nginx/html/static/order/
  • 把等待付款(detail)、訂單頁(list)、結算頁(confirm)、收銀頁(pay)的index頁面複製到訂單服務專案gulimall-ordersrc/main/resources/templates目錄下
  • 修改nginx配置,這裡之前都配置好了,只是檢查一下
  • 管理員啟動SwitchHosts,新增訂單服務gulimall-order的域名域名對映
  • 新增訂單服務gulimall-order的閘道器配置
  • gulimall-order新增thymeleaf的依賴,並配置取消頁面快取
  • 新增頁面測試Controller
  • 訪問頁面測試

上傳等待付款(detail)、訂單頁(list)、結算頁(confirm)、收銀頁(pay)靜態資源到/root/mall/nginx/html/static/order/

image-20240812214650096

上傳完成後檢視一下

image-20240812214738895

把等待付款(detail)、訂單頁(list)、結算頁(confirm)、收銀頁(pay)的index頁面複製到訂單服務專案gulimall-ordersrc/main/resources/templates目錄下

image-20240812222618228

修改nginx配置,這裡之前都配置好了,只是檢查一下

image-20240812214932937

管理員啟動SwitchHosts,新增訂單服務gulimall-order的域名域名對映

192.168.188.180     order.gulimall.com

image-20240812215332653

新增訂單服務gulimall-order的閘道器配置,注意不要有空格

image-20240812222704439

gulimall-order新增thymeleaf的依賴,並配置取消頁面快取

image-20240812215759909

新增靜態頁面thymeleaf名稱空間,修改頁面靜態資源地址

confirm頁面

href="
href="/static/order/confirm/

src="
src="/static/order/confirm/

image-20240812220026323

detail頁面

href="
href="/static/order/detail/

src="
src="/static/order/detail/

image-20240812220349543

list頁面

href="
href="/static/order/list/

src="
src="/static/order/list/

image-20240812220437698

pay頁面

href="
href="/static/order/pay/

src="
src="/static/order/pay/

image-20240812220605492

新增頁面測試Controller

@Controller
public class IndexController {

    @GetMapping("/{page}.html")
    public String getPage(@PathVariable("page") String page) {
        return page;
    }
}

image-20240812220959267

detail頁面的錯誤,搜尋xuanxiangka,然後刪除多餘"即可

image-20240812222247915

confirm頁面的錯誤,搜尋/*,然後刪除即可

image-20240812222146763

訪問頁面測試

  • http://order.gulimall.com/detail.html
  • http://order.gulimall.com/list.html
  • http://order.gulimall.com/confirm.html
  • http://order.gulimall.com/pay.html

image-20240812222854007

12.2整合SpringSession

主要步驟:

  • 匯入SpringSession依賴,EnableRedisHttpSession開啟EnableRedisHttpSession配置Session
  • application.yaml配置RedisSpringSession、執行緒池
  • 建立GulimallSessionConfigMyRedisConfigMyThreadConfigThreadPoolConfigProperties配置
  • 修改頁面登入資訊
  • 測試,登入之後檢視頁面登入資訊展示即可

匯入SpringSession依賴,EnableRedisHttpSession開啟EnableRedisHttpSession配置Session

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

image-20240812230450252

application.yaml配置RedisSpringSession、執行緒池

server:
  port: 8203
  servlet:
    session:
      timeout: 30m
spring:
  redis:
    host: 192.168.188.180
    port: 6379
  session:
    store-type: redis
gulimall:
  thread:
    coreSize: 20
    maxSize: 200
    keepAliveTime: 10   

image-20240812230602011

建立GulimallSessionConfigMyRedisConfigMyThreadConfigThreadPoolConfigProperties配置

之前gulimall-product服務都有,複製過來即可

image-20240812230621357

修改頁面登入資訊

confirm.html

image-20240812230713766

detail.html

image-20240812230733229

pay.html

image-20240812230756687

測試,登入之後檢視頁面登入資訊展示即可

image-20240812230844362

12.3訂單基本概念

訂單中心

電商系統涉及到 3 流,分別時資訊流,資金流,物流,而訂單系統作為中樞將三者有機的集 合起來。 訂單模組是電商系統的樞紐,在訂單這個環節上需求獲取多個模組的資料和資訊,同時對這 些資訊進行加工處理後流向下個環節,這一系列就構成了訂單的資訊流通。

訂單構成

image-20240812231841293

訂單狀態

  • 1.待付款 使用者提交訂單後,訂單進行預下單,目前主流電商網站都會喚起支付,便於使用者快速完成支 付,需要注意的是待付款狀態下可以對庫存進行鎖定,鎖定庫存需要配置支付超時時間,超 時後將自動取消訂單,訂單變更關閉狀態。
  • 2.已付款/待發貨 使用者完成訂單支付,訂單系統需要記錄支付時間,支付流水單號便於對賬,訂單下放到 WM系統,倉庫進行調撥,配貨,分揀,出庫等操作。
  • 3.待收貨/已發貨 倉儲將商品出庫後,訂單進入物流環節,訂單系5統需要同步物流資訊,便於使用者實時知悉物 品物流狀態
  • 4.已完成 使用者確認收貨後,訂單交易完成。後續支付側進行結算,如果訂單存在問題進入售後狀態
  • 5.已取消 付款之前取消訂單。包括超時未付款或使用者商戶取消訂單都會產生這種訂單狀態。
  • 6.售後中 使用者在付款後申請退款,或商家發貨後使用者申請退換貨。

訂單流程

image-20240812232225218

12.4訂單登入攔截

主要步驟:

  • 1.購物車結算時跳轉confirm.html
  • 2.購物車結算需要登入
    • 新增LoginUserInterceptor攔截器
    • OrderWebConfig使用LoginUserInterceptor攔截器處理所有gulimall-order的請求

gulimall-carttoTrade方法跳轉到gulimall-order服務

function toTrade() {
    // window.location.href = "http://cart.gmall.com:8087/toTrade";
    window.location.href = "http://order.gulimall.com/toTrade";
}

image-20240812235819769

OrderWebController處理toTrade請求,跳轉到確認頁confirm

@Controller
public class OrderWebController {
    /**
     * 去結算確認頁
     * @return
     * @throws ExecutionException
     * @throws InterruptedException
     */
    @GetMapping(value = "/toTrade")
    public String toTrade() {
        return "confirm";
    }
}

image-20240812235918196

新增LoginUserInterceptor攔截器

OrderWebConfig使用LoginUserInterceptor攔截器處理所有gulimall-order的請求

@Component
public class LoginUserInterceptor implements HandlerInterceptor {

    public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        MemberResponseVo attribute = (MemberResponseVo) request.getSession().getAttribute(LOGIN_USER);
        if (attribute != null) {
            // 把登入後使用者的資訊放在ThreadLocal裡面進行儲存
            loginUser.set(attribute);
            return true;
        } else {
            // 未登入,返回登入頁面
            response.setContentType("text/html;charset=UTF-8");
            PrintWriter out = response.getWriter();
            out.println("<script>alert('請先進行登入,再進行後續操作!');location.href='http://auth.gulimall.com/login.html'</script>");
            return false;
        }   
    }
}

image-20240813000048234

12.5訂單確認頁模型抽取

主要步驟:

  • 收貨地址
  • 所有選中的購物項
  • 發票記錄
  • 優惠券資訊
  • 訂單總額
  • 應付價格
//訂單確認頁需要用的資料
@Data
public class OrderConfirmVo {
    //// 收貨地址,ums member receive address表
    List<MemberAddressVo>address;
    //所有選中的購物項
    List<OrderItemVo> items;
    //發票記錄....
    //優惠券資訊.
    Integer integration;
    BigDecimal total;//訂單總額
    BigDecimal payPrice;//應付價格
}

12.6訂單確認頁資料獲取

主要步驟:

  • 1.遠端查詢使用者的收貨地址列表
  • 2.遠端查詢購物車裡所有選中的購物項
    • 獲取購物車選中購物項時需要查詢最新價格
  • 3.查詢使用者積分
  • 4.其他資料自動計算
  • 5.防重令牌
@Override
public OrderConfirmVo confirmOrder(){
OrderConfirmVo confirmVo =new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
//1、遠端查詢所有的收貨地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddress(address):
//2、遠端查詢購物車所有選中的購物項
List<OrderItemVo>items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//3、查詢使用者積分
Integer integration= memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
//4、其他資料自動計算
//TOD0 |5、防重令牌
return confirmVo;
}

遠端查詢使用者的收貨地址列表
MemberReceiveAddressController

     /**
     * 根據會員id查詢會員的所有地址
     * @param memberId
     * @return
     */
    @GetMapping(value = "/{memberId}/address")
    public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memberId") Long memberId) {
        List<MemberReceiveAddressEntity> addressList = memberReceiveAddressService.getAddress(memberId);
        return addressList;
}

MemberReceiveAddressServiceImpl

@Override
public List<MemberReceiveAddressEntity> getAddress(Long memberId) {
    return this.baseMapper.selectList
            (new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id", memberId));
}

image-20240813003300608

遠端查詢購物車裡所有選中的購物項

CartController

/**
 * 獲取當前使用者的購物車商品項
 * @return
 */
@GetMapping(value = "/currentUserCartItems")
@ResponseBody
public List<CartItemVo> getCurrentCartItems() {
    List<CartItemVo> cartItemVoList = cartService.getUserCartItems();
    return cartItemVoList;
}

CartServiceImpl

@Override
public List<CartItemVo> getUserCartItems() {

    List<CartItemVo> cartItemVoList = new ArrayList<>();
    //獲取當前使用者登入的資訊
    UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();
    //如果使用者未登入直接返回null
    if (userInfoTo.getUserId() == null) {
        return null;
    } else {
        //獲取購物車項
        String cartKey = CART_PREFIX + userInfoTo.getUserId();
        //獲取所有的
        List<CartItemVo> cartItems = getCartItems(cartKey);
        if (cartItems == null) {
            throw new CartExceptionHandler();
        }
        //篩選出選中的
        cartItemVoList = cartItems.stream()
                .filter(items -> items.getCheck())
                .map(item -> {
                    //更新為最新的價格(查詢資料庫)
                    BigDecimal price = productFeignService.getPrice(item.getSkuId());
                    item.setPrice(price);
                    return item;
                })
                .collect(Collectors.toList());
    }

    return cartItemVoList;
}

image-20240813004105739

獲取購物車選中購物項時需要查詢最新價格

/**
 * 根據skuId查詢當前商品的價格
 * @param skuId
 * @return
 */
@GetMapping(value = "/{skuId}/price")
public BigDecimal getPrice(@PathVariable("skuId") Long skuId) {
    //獲取當前商品的資訊
    SkuInfoEntity skuInfo = skuInfoService.getById(skuId);
    //獲取商品的價格
    BigDecimal price = skuInfo.getPrice();
    return price;
}

image-20240813004126000

結算確認頁

@Override
public OrderConfirmVo confirmOrder() {
    OrderConfirmVo confirmVo = new OrderConfirmVo();
    MemberResponseVo memberRespVo = LoginUserInterceptor.loginUser.get();
    // 1、遠端查詢所有的收貨地址列表
    List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
    confirmVo.setMemberAddressVos(address);
    // 2、遠端查詢購物車所有選中的購物項
    List<OrderItemVo> items = cartFeignService.getCurrentCartItems();
    confirmVo.setItems(items);
    // 3、查詢使用者積分
    Integer integration = memberRespVo.getIntegration();
    confirmVo.setIntegration(integration);
    // 4、其他資料自動計算
    // TOD0 |5、防重令牌
    return confirmVo;
}

12.7Feign遠端呼叫丟失請求頭問題

主要步驟:

  • Feign遠端呼叫時需要同步老請求的Cookie等請求頭

原因:

  • 瀏覽器請求時會帶上Cookie: GULISESSION
  • 預設使用feign呼叫時,會根據攔截器構造請求引數RequestTemplate,而此時請求頭沒有帶上Cookie,導致springsession無法獲取使用者資訊

解決:
攔截器構造請求頭

image-20240813005709058

@Configuration
public class GuliFeignConfig {

    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor() {

        RequestInterceptor requestInterceptor = new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                //1、使用RequestContextHolder拿到剛進來的請求資料
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

                if (requestAttributes != null) {
                    //老請求
                    HttpServletRequest request = requestAttributes.getRequest();

                    if (request != null) {
                        //2、同步請求頭的資料(主要是cookie)
                        //把老請求的cookie值放到新請求上來,進行一個同步
                        String cookie = request.getHeader("Cookie");
                        template.header("Cookie", cookie);
                    }
                }
            }
        };
        return requestInterceptor;
    }
}

12.8Feign非同步呼叫丟失請求頭問題

主要步驟:

原因:

  • 使用非同步編排時,非同一執行緒無法取到RequestContextHolder(上下文環境保持器)
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();// 獲取controller請求物件
空指標異常

解決:

  • 獲取主執行緒ServletRequestAttributes,給每個非同步執行緒複製一份

image-20240813010721908

程式碼

 /**
     * 獲取結算頁(confirm.html)VO資料
     */
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo result = new OrderConfirmVo();
        // 獲取當前使用者
        MemberResponseVo member = LoginUserInterceptor.loginUser.get();

        // 獲取當前執行緒上下文環境器
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
            // 1.查詢封裝當前使用者收貨列表
            // 同步上下文環境器,解決非同步無法從ThreadLocal獲取RequestAttributes
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<MemberAddressVo> address = memberFeignService.getAddress(member.getId());
            result.setMemberAddressVos(address);
        }, executor);

        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            // 2.查詢購物車所有選中的商品
            // 同步上下文環境器,解決非同步無法從ThreadLocal獲取RequestAttributes
            RequestContextHolder.setRequestAttributes(requestAttributes);
            // 請求頭應該放入GULIMALLSESSION(feign請求會根據requestInterceptors構建請求頭)
            List<OrderItemVo> items = cartFeignService.getCurrentCartItems();
            result.setItems(items);
        }, executor);

        // 3.查詢使用者積分
        Integer integration = member.getIntegration();// 積分
        result.setIntegration(integration);

        // 4.金額資料自動計算

        // 5.TODO 防重令牌

        // 阻塞等待所有非同步任務返回
        CompletableFuture.allOf(addressFuture, cartFuture).get();

        return result;
    }

image-20240813014442045

12.9bug修改

獲取價格介面返回物件改為R

SkuInfoController

/**
 * 根據skuId查詢當前商品的價格
 * @param skuId
 * @return
 */
@GetMapping(value = "/{skuId}/price")
public R getPrice(@PathVariable("skuId") Long skuId) {
    //獲取當前商品的資訊
    SkuInfoEntity skuInfo = skuInfoService.getById(skuId);
    //獲取商品的價格
   return R.ok().setData(skuInfo.getPrice().toString());
}

image-20240813015909061

CartServiceImpl

R price = productFeignService.getPrice(item.getSkuId());
String data = (String)price.get("data");
item.setPrice(new BigDecimal(data));
return item;

image-20240813015713621

getCurrentCartItems返回json資料,所以加上ResponseBody註解

image-20240813014906157

12.10訂單確認頁渲染

有貨這裡可以先固定

image-20240813022716359

12.11訂單確認頁庫存查詢

主要步驟:

  • gulimall-order新增遠端呼叫庫存介面(之前gulimall-ware寫過,這裡Feign直接呼叫即可)
  • gulimall-orderconfirmOrder方法在查詢完購物車之後使用thenRunAsync在查詢庫存
  • confirm.html頁面渲染

gulimall-order新增遠端呼叫庫存介面(之前gulimall-ware寫過,這裡Feign直接呼叫即可)

image-20240813213516838

gulimall-orderconfirmOrder方法在查詢完購物車之後使用thenRunAsync在查詢庫存

.thenRunAsync(() -> {
    // 3.批次查詢庫存(有貨/無貨)
    List<Long> skuIds = result.getItems().stream().map(item -> item.getSkuId()).collect(Collectors.toList());
    R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
    List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {
    });

    if (skuStockVos != null && skuStockVos.size() > 0) {
        // 將skuStockVos集合轉換為map
        Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
        result.setStocks(skuHasStockMap);
    }
}, executor);

image-20240813213558861

confirm.html頁面渲染

image-20240813213706388

介面

image-20240813213731581

12.12訂單確認頁模擬運費效果

主要步驟:

  • mall_umsums_member_receive_address新增測試地址資料,預設設定一個預設地址
  • gulimall-ware新增獲取運費介面
    • 遠端呼叫gulimall-member獲取收貨地址資訊
    • 計算運費,我這裡是隨機5-15作為運費
  • 前端渲染
    • 運費p標籤自定義特性,繫結地址id和預設地址defaultStatus
    • 運費p標籤預設選中狀態,並在點選時計算運費

mall_umsums_member_receive_address新增測試地址資料,預設設定一個預設地址

INSERT INTO mall_ums.ums_member_receive_address (member_id,name,phone,post_code,province,city,region,detail_address,areacode,default_status) VALUES
	 (2,'peng','15766668888',NULL,NULL,'上海市',NULL,'上海市黃浦區',NULL,1);
INSERT INTO mall_ums.ums_member_receive_address (member_id,name,phone,post_code,province,city,region,detail_address,areacode,default_status) VALUES
	 (2,'peng','15766668888',NULL,NULL,'上海市',NULL,'上海市黃浦區',NULL,1);

image-20240813221056560

gulimall-ware新增獲取運費介面

  • 遠端呼叫gulimall-member獲取收貨地址資訊
  • 計算運費,我這裡是隨機5-15作為運費
@Override
public FareVo getFare(Long addrId) {
    FareVo fareVo = new FareVo();
    //收穫地址的詳細資訊
    R addrInfo = memberFeignService.info(addrId);
    MemberAddressVo memberAddressVo = addrInfo.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {});
    if (memberAddressVo != null) {
        String phone = memberAddressVo.getPhone();
        //擷取使用者手機號碼最後一位作為我們的運費計算
        //1558022051
        // String fare = phone.substring(phone.length() - 10, phone.length()-8);
        // BigDecimal bigDecimal = new BigDecimal(fare);
        //
        // fareVo.setFare(bigDecimal);

        // 隨機5-15作為運費
        Random random = new Random();
        // 生成 5 到 15 之間的隨機數
        int randomNumber = 5 + random.nextInt(11);
        BigDecimal bigDecimal = new BigDecimal(randomNumber);
        fareVo.setFare(bigDecimal);

        fareVo.setAddress(memberAddressVo);
        return fareVo;
    }
    return null;
}

image-20240813221140652

前端渲染

  • 運費p標籤自定義特性,繫結地址id和預設地址defaultStatus
  • 運費p標籤預設選中狀態,並在點選時計算運費
			<div class="top-3 addr-item" th:each="addr:${confirmOrderData.memberAddressVos}">
				<p th:attr="def=${addr.defaultStatus},addrId=${addr.id}">[[${addr.name}]]</p><span>[[${addr.name}]]  [[${addr.province}]]  [[${addr.city}]] [[${addr.region}]] [[${addr.detailAddress}]]  [[${addr.phone}]]</span>
			</div>

image-20240813221231148

12.13訂單確認頁細節顯示

主要步驟:

  • 介面新增收貨地址詳細資訊
  • 介面載入渲染
  • 介面獲取資料後渲染介面

介面新增收貨地址詳細資訊

image-20240813221908595

介面載入渲染

image-20240813221946201

介面獲取資料後渲染介面

image-20240813222000127

12.14介面冪等性討論

什麼是冪等性

介面冪等性就是使用者對於同一操作發起的一次請求或者多次請求的結果是一致的,不會因 為多次點選而產生了副作用;比如說支付場景,使用者購買了商品支付扣款成功,但是返回結 果的時候網路異常,此時錢已經扣了,使用者再次點選按鈕,此時會進行第二次扣款,返回結 果成功,使用者查詢餘額返發現多扣錢了,流水記錄也變成了兩條...,這就沒有保證介面 的冪等性。

哪些情況需要防止

使用者多次點選按鈕

使用者頁面回退再次提交

微服務互相呼叫,由於網路問題,導致請求失敗。feign 觸發重試機制

其他業務情況

什麼情況下需要冪等

以 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 不是主鍵,可以重複,那上面業務多次操 作,資料都會新增多條,不具備冪等性。

冪等解決方案

token 機制

  • 服務端提供了傳送 token 的介面。我們在分析業務的時候,哪些業務是存在冪等問題的, 就必須在執行業務前,先去獲取 token,伺服器會把 token 儲存到 redis中。
  • 然後呼叫業務介面請求時,把 token 攜帶過去,一般放在請求頭部。
  • 伺服器判斷 token 是否存在 redis 中,存在表示第一次請求,然後刪除 token,繼續執行業 務。
  • 如果判斷 token 不存在 redis 中,就表示是重複操作,直接返回重複標記給 client,這樣 就保證了業務程式碼,不被重複執行。
  • 我們最好設計為先刪除 token,如果業務呼叫失敗,就重新獲取 token 再次請求。
  • Token 獲取、比較和刪除必須是原子性,可以在 redis 使用 lua 指令碼完成。if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

各種鎖機制

  • 資料庫悲觀鎖
    • select * from xxxx where id = 1 for update;
    • 悲觀鎖使用時一般伴隨事務一起使用,資料鎖定時間可能會很長,需要根據實際情況選用。 另外要注意的是,id 欄位一定是主鍵或者唯一索引,不然可能造成鎖表的結果,處理起來會 非常麻煩。
  • 資料庫樂觀鎖
    • 這種方法適合在更新的場景中
    • 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 條件就不成立。這樣就保證了不管呼叫幾次,只會真正的處理一次。 樂觀鎖主要使用於處理讀多寫少的問題

業務層分散式鎖

  • 如果多個機器可能在同一時間同時處理相同的資料,比如多臺機器定時任務都拿到了相同數 據處理,我們就可以加分散式鎖,鎖定此資料,處理完成後釋放鎖。獲取到鎖的必須先判斷 這個資料是否被處理過。

各種唯一約束

  • 資料庫唯一約束
    • 插入資料,應該按照唯一索引進行插入,比如訂單號,相同的訂單就不可能有兩條記錄插入。 我們在資料庫層面防止重複。
    • 這個機制是利用了資料庫的主鍵唯一約束的特性,解決了在 insert 場景時冪等問題。但主鍵 的要求不是自增的主鍵,這樣就需要業務生成全域性唯一的主鍵。
    • 如果是分庫分表場景下,路由規則要保證相同請求下,落地在同一個資料庫和同一表中,要 不然資料庫主鍵約束就不起效果了,因為是不同的資料庫和表主鍵不相關。
  • redis set 防重
    • 很多資料需要處理,只能被處理一次,比如我們可以計算資料的 MD5 將其放入 redis 的 set, 每次處理資料,先看這個 MD5 是否已經存在,存在就不處理。

防重表

  • 使用訂單號 orderNo 做為去重表的唯一索引,把唯一索引插入去重表,再進行業務操作,且 他們在同一個事務中。這個保證了重複請求時,因為去重表有唯一約束,導致請求失敗,避 免了冪等問題。這裡要注意的是,去重表和業務表應該在同一庫中,這樣就保證了在同一個 事務,即使業務操作失敗了,也會把去重表的資料回滾。這個很好的保證了資料一致性。 之前說的 redis

全域性請求唯一 id

  • 呼叫介面時,生成一個唯一 id,redis 將資料儲存到集合中(去重),存在即處理過。
  • 可以使用 nginx 設定每一個請求的唯一 id; proxy_set_header X-Request-Id $request_id

12.15訂單確認頁完成

主要步驟:

  • 服務生成防重令牌,並儲存在redis中,然後返回給前端,前端下單時帶上防重令牌
  • 渲染提交訂單的表單,主要包含地址、應付總額、防重令牌
  • 需要在選擇地址和計算總額時重新賦值表單資料

服務生成防重令牌,並儲存在redis中,然後返回給前端,前端下單時帶上防重令牌

String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+member.getId(),token,30, TimeUnit.MINUTES);
result.setOrderToken(token);

image-20240813233858109

渲染提交訂單的表單,主要包含地址、應付總額、防重令牌

image-20240813233930037

需要在選擇地址和計算總額時重新賦值表單資料

image-20240813234127661

12.16原子驗令牌

主要步驟:

  • 使用redis執行lue指令碼保證令牌的對比和刪除的原子性
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {

    // 登入資訊
    MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
    // 返回結果
    SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();

    // 1、驗證令牌是否合法【令牌的對比和刪除必須保證原子性】
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    String orderToken = vo.getOrderToken();

    // 透過lure指令碼原子驗證令牌和刪除令牌
    Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()), orderToken);
    if (result == 0L) {
        // 令牌驗證失敗
        responseVo.setCode(1);
        return responseVo;
    } else {
        // 令牌驗證成功
    }

    return null;
}

image-20240813235431291

12.17構造訂單資料

主要步驟:

  • createOrder生成訂單,生成唯一訂單號
  • builderOrderItems生成訂單項
    • 遠端呼叫gulimall-cart獲取當前使用者購物車
    • 遍歷購物車,遠端呼叫gulimall-ware獲取收貨地址資訊和運費資訊

createOrder生成訂單,生成唯一訂單號

遠端呼叫gulimall-cart獲取當前使用者購物車

image-20240814001834669

遍歷購物車,遠端呼叫gulimall-ware獲取收貨地址資訊和運費資訊

image-20240814001921772

12.18構造訂單項資料

主要步驟:

  • spu資訊,遠端呼叫gulimall-product根據skuId獲取spu資訊
  • sku資訊
  • 商品的優惠資訊
  • 商品的積分資訊
  • 訂單項的價格資訊
  • 當前訂單項的實際金額

image-20240814003622534

12.19訂單驗價

主要步驟:

  • 每個訂單項優惠卷分解金額
  • 每個訂單項商品促銷分解金額
  • 每個訂單項積分優惠分解金額
  • 每個訂單項優惠後的分解金額
  • 應付總額 = 總額 + 運費
  • 如果計算後的應付金額和前端傳來的應付金額相減取絕對值小於0.01驗價成功

遍歷訂單項,獲取每個訂單項優惠後的價格

image-20240814013330418

如果計算後的應付金額和前端傳來的應付金額相減取絕對值小於0.01驗價成功

image-20240814013443392

12.20儲存訂單資料

主要步驟:

  • 儲存訂單資料
  • 遠端呼叫gulimall-ware鎖定庫存

儲存訂單資料

image-20240814014937441

遠端呼叫gulimall-ware鎖定庫存

image-20240814015125700

12.21鎖定庫存

主要步驟:

  • 找到每個商品在哪個倉庫都有庫存
    • 庫存 - 鎖定庫存 > 0 代表當前倉庫有該商品庫存
  • 鎖定商品庫存
    • 庫存 - 鎖定庫存 > 要鎖定數量 代表當前倉庫可以鎖定該商品庫存
//2、找到每個商品在哪個倉庫都有庫存
        List<OrderItemVo> locks = vo.getLocks();

        List<SkuWareHasStock> collect = locks.stream().map((item) -> {
            SkuWareHasStock stock = new SkuWareHasStock();
            Long skuId = item.getSkuId();
            stock.setSkuId(skuId);
            stock.setNum(item.getCount());
            //查詢這個商品在哪個倉庫有庫存
            List<Long> wareIdList = wareSkuDao.listWareIdHasSkuStock(skuId);
            stock.setWareId(wareIdList);

            return stock;
        }).collect(Collectors.toList());

        //2、鎖定庫存
        for (SkuWareHasStock hasStock : collect) {
            boolean skuStocked = false;
            Long skuId = hasStock.getSkuId();
            List<Long> wareIds = hasStock.getWareId();

            if (org.springframework.util.StringUtils.isEmpty(wareIds)) {
                //沒有任何倉庫有這個商品的庫存
                throw new NotStockException(skuId.toString());
            }

            //1、如果每一個商品都鎖定成功,將當前商品鎖定了幾件的工作單記錄發給MQ
            //2、鎖定失敗。前面儲存的工作單資訊都回滾了。傳送出去的訊息,即使要解鎖庫存,由於在資料庫查不到指定的id,所有就不用解鎖
            for (Long wareId : wareIds) {
                //鎖定成功就返回1,失敗就返回0
                Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
                if (count == 1) {
                    skuStocked = true;        
                    break;
                } else {
                    //當前倉庫鎖失敗,重試下一個倉庫
                }
            }

            if (skuStocked == false) {
                //當前商品所有倉庫都沒有鎖住
                throw new NotStockException(skuId.toString());
            }

查詢有商品庫存的倉庫和鎖定庫存sql

庫存 - 鎖定庫存 > 0 代表當前倉庫有該商品庫存

庫存 - 鎖定庫存 > 要鎖定數量 代表當前倉庫可以鎖定該商品庫存

image-20240814021611892

12.22提交訂單的問題

主要步驟:

  • 建立完訂單後複製訂單和訂單項

  • 鎖定庫存失敗後丟擲異常,保證當前方法的事務回滾,鎖定庫存失敗不能建立訂單

  • submitOrder方法根據狀態碼回覆前端訊息

  • 計算運費介面改為手機號最後一位為運費

  • 訂單提交成功,跳到支付介面

建立完訂單後複製訂單和訂單項

image-20240814223424949

鎖定庫存失敗後丟擲異常,保證當前方法的事務回滾,鎖定庫存失敗不能建立訂單

image-20240814223455522

submitOrder方法根據狀態碼回覆前端訊息

image-20240814223608313

order_sn改為char(64)

oms_order

image-20240814223849612

oms_order_item

image-20240814223925851

計算運費介面改為手機號最後一位為運費

image-20240814224615234

訂單提交成功,跳到支付介面

image-20240814224809280

相關文章