日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 综合教程 >内容正文

综合教程

谷粒商城--订单服务--高级篇笔记十一

發(fā)布時(shí)間:2023/12/4 综合教程 40 生活家
生活随笔 收集整理的這篇文章主要介紹了 谷粒商城--订单服务--高级篇笔记十一 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

1.頁(yè)面環(huán)境搭建

1.1 靜態(tài)資源導(dǎo)入nginx

等待付款 --------->detail

訂單頁(yè) --------->list

結(jié)算頁(yè) --------->confirm

收銀頁(yè) ---------> pay

1.2 配置host

# gulimall
192.168.157.128 gulimall.com
# search
192.168.157.128 search.gulimall.com
# item 商品詳情
192.168.157.128 item.gulimall.com
#商城認(rèn)證
192.168.157.128 auth.gulimall.com
#購(gòu)物車
192.168.157.128 cart.gulimall.com
#訂單
192.168.157.128 order.gulimall.com
#單點(diǎn)登錄
127.0.0.1 ssoserver.com127.0.0.1 client1.com127.0.0.1 client2.com

1.3 配置網(wǎng)關(guān)

gulimall-gateway/src/main/resources/application.yml

        #訂單- id: gulimall_order_routeuri: lb://gulimall-orderpredicates:- Host=order.gulimall.com

1.4 開啟注冊(cè)發(fā)現(xiàn)

@EnableDiscoveryClient

1.5 新增依賴

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

1.6 修改各個(gè)頁(yè)面的靜態(tài)資源路徑

src=" ===>src="/static/order/xxx/

herf=" ===>herf="/static/order/xxx/

1.7 測(cè)試

1.7.1 訂單確認(rèn)頁(yè)

確認(rèn)頁(yè)前端代碼:https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/confirm.html

order.gulimall.com/confirm.html

1.7.2 訂單列表頁(yè)

訂單列表頁(yè)前端代碼:https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/list.html

谷粒商城訂單 (gulimall.com)

1.7.3 訂單詳情頁(yè)

訂單詳情頁(yè)前端代碼:https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/detail.html

order.gulimall.com/detail.html

1.7.4 訂單支付頁(yè)

訂單支付頁(yè)前端代碼:https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/pay.html

order.gulimall.com/pay.html

2. 整合Spring Session

2.1 導(dǎo)入依賴

        <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><!--jedis,redis客戶端--><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId></dependency>

2.2 開啟Spring Session

@EnableRedisHttpSession  //整合Redis作為session存儲(chǔ)

2.3 配置Spring Session存儲(chǔ)方式

  redis:host: 192.168.157.128session:store-type: redis

2.4 SpringSession 自定義

package site.zhourui.gulimall.order.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;/*** @author zr* @date 2021/12/12 10:29*/
@Configuration
public class GulimallSessionConfig {@Beanpublic CookieSerializer cookieSerializer() {DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();//放大作用域cookieSerializer.setDomainName("gulimall.com");cookieSerializer.setCookieName("GULISESSION");cookieSerializer.setCookieMaxAge(60*60*24*7);return cookieSerializer;}//session存儲(chǔ)對(duì)象方式j(luò)son,默認(rèn)jdk@Beanpublic RedisSerializer<Object> springSessionDefaultRedisSerializer() {return new GenericJackson2JsonRedisSerializer();}
}

2.5 整合后效果

可以實(shí)現(xiàn)登錄成功后用戶信息共享

3. 整合線程池

3.1 自定義線程池配置

gulimall-order/src/main/java/site/zhourui/gulimall/order/config/MyThreadConfig.java

package site.zhourui.gulimall.order.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;/*** @author zr* @date 2021/11/28 10:12*/@Configuration
public class MyThreadConfig {@Beanpublic ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool) {return new ThreadPoolExecutor(pool.getCoreSize(),pool.getMaxSize(),pool.getKeepAliveTime(),TimeUnit.SECONDS,new LinkedBlockingDeque<>(100000),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());}
}

gulimall-order/src/main/java/site/zhourui/gulimall/order/config/ThreadPoolConfigProperties.java

package site.zhourui.gulimall.order.config;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {private Integer coreSize;private Integer maxSize;private Integer keepAliveTime;
}

3.2 配置

gulimall:thread:core-size: 20max-size: 200keep-alive-time: 10

4. 訂單中心(理論)

電商系統(tǒng)涉及到 3 流, 分別時(shí)信息流, 資金流, 物流, 而訂單系統(tǒng)作為中樞將三者有機(jī)的集合起來(lái)。訂單模塊是電商系統(tǒng)的樞紐, 在訂單這個(gè)環(huán)節(jié)上需求獲取多個(gè)模塊的數(shù)據(jù)和信息, 同時(shí)對(duì)這些信息進(jìn)行加工處理后流向下個(gè)環(huán)節(jié), 這一系列就構(gòu)成了訂單的信息流通。

4.1 訂單的構(gòu)成

4.1.1 用戶信息

用戶信息包括用戶賬號(hào)、 用戶等級(jí)、 用戶的收貨地址、 收貨人、 收貨人電話等組成, 用戶賬戶需要綁定手機(jī)號(hào)碼, 但是用戶綁定的手機(jī)號(hào)碼不一定是收貨信息上的電話。 用戶可以添加多個(gè)收貨信息, 用戶等級(jí)信息可以用來(lái)和促銷系統(tǒng)進(jìn)行匹配, 獲取商品折扣, 同時(shí)用戶等級(jí)還可以獲取積分的獎(jiǎng)勵(lì)等

4.1.2 訂單基礎(chǔ)信息

訂單基礎(chǔ)信息是訂單流轉(zhuǎn)的核心, 其包括訂單類型、 父/子訂單、 訂單編號(hào)、 訂單狀態(tài)、 訂單流轉(zhuǎn)的時(shí)間等。

(1) 訂單類型包括實(shí)體商品訂單和虛擬訂單商品等, 這個(gè)根據(jù)商城商品和服務(wù)類型進(jìn)行區(qū)分。
(2) 同時(shí)訂單都需要做父子訂單處理, 之前在初創(chuàng)公司一直只有一個(gè)訂單, 沒有做父子訂單處理后期需要進(jìn)行拆單的時(shí)候就比較麻煩, 尤其是多商戶商場(chǎng), 和不同倉(cāng)庫(kù)商品的時(shí)候,父子訂單就是為后期做拆單準(zhǔn)備的。
(3) 訂單編號(hào)不多說(shuō)了, 需要強(qiáng)調(diào)的一點(diǎn)是父子訂單都需要有訂單編號(hào), 需要完善的時(shí)候可以對(duì)訂單編號(hào)的每個(gè)字段進(jìn)行統(tǒng)一定義和詮釋。
(4) 訂單狀態(tài)記錄訂單每次流轉(zhuǎn)過(guò)程, 后面會(huì)對(duì)訂單狀態(tài)進(jìn)行單獨(dú)的說(shuō)明。
(5) 訂單流轉(zhuǎn)時(shí)間需要記錄下單時(shí)間, 支付時(shí)間, 發(fā)貨時(shí)間, 結(jié)束時(shí)間/關(guān)閉時(shí)間等等

4.1.3 商品信息

商品信息從商品庫(kù)中獲取商品的 SKU 信息、 圖片、 名稱、 屬性規(guī)格、 商品單價(jià)、 商戶信息等, 從用戶下單行為記錄的用戶下單數(shù)量, 商品合計(jì)價(jià)格等。

4.1.4 優(yōu)惠信息

優(yōu)惠信息記錄用戶參與的優(yōu)惠活動(dòng), 包括優(yōu)惠促銷活動(dòng), 比如滿減、 滿贈(zèng)、 秒殺等, 用戶使用的優(yōu)惠券信息, 優(yōu)惠券滿足條件的優(yōu)惠券需要默認(rèn)展示出來(lái), 具體方式已在之前的優(yōu)惠券篇章做過(guò)詳細(xì)介紹, 另外還虛擬幣抵扣信息等進(jìn)行記錄。

4.1.4.1為什么把優(yōu)惠信息單獨(dú)拿出來(lái)而不放在支付信息里面呢?

因?yàn)閮?yōu)惠信息只是記錄用戶使用的條目, 而支付信息需要加入數(shù)據(jù)進(jìn)行計(jì)算, 所以做為區(qū)分。

4.1.5 支付信息

( 1) 支付流水單號(hào), 這個(gè)流水單號(hào)是在喚起網(wǎng)關(guān)支付后支付通道返回給電商業(yè)務(wù)平臺(tái)的支付流水號(hào), 財(cái)務(wù)通過(guò)訂單號(hào)和流水單號(hào)與支付通道進(jìn)行對(duì)賬使用。
( 2) 支付方式用戶使用的支付方式, 比如微信支付、 支付寶支付、 錢包支付、 快捷支付等。支付方式有時(shí)候可能有兩個(gè)——余額支付+第三方支付。
( 3) 商品總金額, 每個(gè)商品加總后的金額; 運(yùn)費(fèi), 物流產(chǎn)生的費(fèi)用; 優(yōu)惠總金額, 包括促銷活動(dòng)的優(yōu)惠金額, 優(yōu)惠券優(yōu)惠金額, 虛擬積分或者虛擬幣抵扣的金額, 會(huì)員折扣的金額等之和; 實(shí)付金額, 用戶實(shí)際需要付款的金額。用戶實(shí)付金額=商品總金額+運(yùn)費(fèi)-優(yōu)惠總金額

4.1.6 物流信息

物流信息包括配送方式, 物流公司, 物流單號(hào), 物流狀態(tài), 物流狀態(tài)可以通過(guò)第三方接口來(lái)獲取和向用戶展示物流每個(gè)狀態(tài)節(jié)點(diǎn)。

4.2 訂單狀態(tài)

  1. 待付款
    用戶提交訂單后, 訂單進(jìn)行預(yù)下單, 目前主流電商網(wǎng)站都會(huì)喚起支付, 便于用戶快速完成支付, 需要注意的是待付款狀態(tài)下可以對(duì)庫(kù)存進(jìn)行鎖定, 鎖定庫(kù)存需要配置支付超時(shí)時(shí)間, 超時(shí)后將自動(dòng)取消訂單, 訂單變更關(guān)閉狀態(tài)。
  2. 已付款/待發(fā)貨
    用戶完成訂單支付, 訂單系統(tǒng)需要記錄支付時(shí)間, 支付流水單號(hào)便于對(duì)賬, 訂單下放到 WMS系統(tǒng), 倉(cāng)庫(kù)進(jìn)行調(diào)撥, 配貨, 分揀, 出庫(kù)等操作。
  3. 待收貨/已發(fā)貨
    倉(cāng)儲(chǔ)將商品出庫(kù)后, 訂單進(jìn)入物流環(huán)節(jié), 訂單系統(tǒng)需要同步物流信息, 便于用戶實(shí)時(shí)知悉物品物流狀態(tài)
  4. 已完成
    用戶確認(rèn)收貨后, 訂單交易完成。 后續(xù)支付側(cè)進(jìn)行結(jié)算, 如果訂單存在問(wèn)題進(jìn)入售后狀態(tài)
  5. 已取消
    付款之前取消訂單。 包括超時(shí)未付款或用戶商戶取消訂單都會(huì)產(chǎn)生這種訂單狀態(tài)。
  6. 售后中
    用戶在付款后申請(qǐng)退款, 或商家發(fā)貨后用戶申請(qǐng)退換貨。售后也同樣存在各種狀態(tài), 當(dāng)發(fā)起售后申請(qǐng)后生成售后訂單, 售后訂單狀態(tài)為待審核, 等待商家審核, 商家審核通過(guò)后訂單狀態(tài)變更為待退貨, 等待用戶將商品寄回, 商家收貨后訂單狀態(tài)更新為待退款狀態(tài), 退款到用戶原賬戶后訂單狀態(tài)更新為售后成功。

4.3 訂單流程

訂單流程是指從訂單產(chǎn)生到完成整個(gè)流轉(zhuǎn)的過(guò)程, 從而行程了一套標(biāo)準(zhǔn)流程規(guī)則。 而不同的產(chǎn)品類型或業(yè)務(wù)類型在系統(tǒng)中的流程會(huì)千差萬(wàn)別, 比如上面提到的線上實(shí)物訂單和虛擬訂單的流程, 線上實(shí)物訂單與 O2O 訂單等, 所以需要根據(jù)不同的類型進(jìn)行構(gòu)建訂單流程。不管類型如何訂單都包括正向流程和逆向流程, 對(duì)應(yīng)的場(chǎng)景就是購(gòu)買商品和退換貨流程, 正向流程就是一個(gè)正常的網(wǎng)購(gòu)步驟: 訂單生成–>支付訂單–>賣家發(fā)貨–>確認(rèn)收貨–>交易成功。而每個(gè)步驟的背后, 訂單是如何在多系統(tǒng)之間交互流轉(zhuǎn)的, 可概括如下圖

4.3.1 訂單創(chuàng)建與支付 (重點(diǎn))

  1. 訂單創(chuàng)建前需要預(yù)覽訂單, 選擇收貨信息等
  2. 訂單創(chuàng)建需要鎖定庫(kù)存, 庫(kù)存有才可創(chuàng)建, 否則不能創(chuàng)建
  3. 訂單創(chuàng)建后超時(shí)未支付需要解鎖庫(kù)存
  4. 支付成功后, 需要進(jìn)行拆單, 根據(jù)商品打包方式, 所在倉(cāng)庫(kù), 物流等進(jìn)行拆單
  5. 支付的每筆流水都需要記錄, 以待查賬
  6. 訂單創(chuàng)建, 支付成功等狀態(tài)都需要給 MQ 發(fā)送消息, 方便其他系統(tǒng)感知訂閱

4.3.2 逆向流程

  1. 修改訂單, 用戶沒有提交訂單, 可以對(duì)訂單一些信息進(jìn)行修改, 比如配送信息,優(yōu)惠信息, 及其他一些訂單可修改范圍的內(nèi)容, 此時(shí)只需對(duì)數(shù)據(jù)進(jìn)行變更即可。
  2. 訂單取消, 用戶主動(dòng)取消訂單和用戶超時(shí)未支付, 兩種情況下訂單都會(huì)取消訂單, 而超時(shí)情況是系統(tǒng)自動(dòng)關(guān)閉訂單, 所以在訂單支付的響應(yīng)機(jī)制上面要做支付的限時(shí)處理, 尤其是在前面說(shuō)的下單減庫(kù)存的情形下面, 可以保證快速的釋放庫(kù)存。另外需要需要處理的是促銷優(yōu)惠中使用的優(yōu)惠券, 權(quán)益等視平臺(tái)規(guī)則, 進(jìn)行相應(yīng)補(bǔ)回給用戶。
  3. 退款, 在待發(fā)貨訂單狀態(tài)下取消訂單時(shí), 分為缺貨退款和用戶申請(qǐng)退款。 如果是全部退款則訂單更新為關(guān)閉狀態(tài), 若只是做部分退款則訂單仍需進(jìn)行進(jìn)行, 同時(shí)生成一條退款的售后訂單, 走退款流程。 退款金額需原路返回用戶的賬戶。
  4. 發(fā)貨后的退款, 發(fā)生在倉(cāng)儲(chǔ)貨物配送, 在配送過(guò)程中商品遺失, 用戶拒收, 用戶收貨后對(duì)商品不滿意, 這樣情況下用戶發(fā)起退款的售后訴求后, 需要商戶進(jìn)行退款的審核, 雙方達(dá)成一致后, 系統(tǒng)更新退款狀態(tài), 對(duì)訂單進(jìn)行退款操作, 金額原路返回用戶的賬戶, 同時(shí)關(guān)閉原訂單數(shù)據(jù)。 僅退款情況下暫不考慮倉(cāng)庫(kù)系統(tǒng)變化。 如果發(fā)生雙方協(xié)調(diào)不一致情況下, 可以申請(qǐng)平臺(tái)客服介入。 在退款訂單商戶不處理的情況下, 系統(tǒng)需要做限期判斷, 比如 5 天商戶不處理, 退款單自動(dòng)變更同意退款。

5. 訂單中心(代碼)

5.1 訂單登錄攔截

因?yàn)橛唵蜗到y(tǒng)必然涉及到用戶信息,因此進(jìn)入訂單系統(tǒng)的請(qǐng)求必須是已經(jīng)登錄的,所以我們需要通過(guò)攔截器對(duì)未登錄訂單請(qǐng)求進(jìn)行攔截

gulimall-order/src/main/java/site/zhourui/gulimall/order/interceptor/LoginUserInterceptor.java

package site.zhourui.gulimall.order.interceptor;/*** @author zr* @date 2021/12/21 22:04*/import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import site.zhourui.common.constant.AuthServerConstant;
import site.zhourui.common.vo.MemberResponseVo;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.PrintWriter;import static site.zhourui.common.constant.AuthServerConstant.LOGIN_USER;/*** 登錄攔截器* 從session中獲取了登錄信息(redis中),封裝到了ThreadLocal中*/
@Component
public class LoginUserInterceptor implements HandlerInterceptor {public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HttpSession session = request.getSession();MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);if (memberResponseVo != null) {loginUser.set(memberResponseVo);return true;}else {session.setAttribute("msg","請(qǐng)先登錄");response.sendRedirect("http://auth.gulimall.com/login.html");return false;}}
}

gulimall-order/src/main/java/site/zhourui/gulimall/order/config/OrderWebConfig.java

package site.zhourui.gulimall.order.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import site.zhourui.gulimall.order.interceptor.LoginUserInterceptor;/*** @author zr* @date 2021/12/21 22:05*/
@Configuration
public class OrderWebConfig implements WebMvcConfigurer {@Autowiredprivate LoginUserInterceptor loginUserInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");}
}

5.2 訂單確認(rèn)頁(yè)

5.2.1 模型抽取

確認(rèn)頁(yè)提交數(shù)據(jù)

gulimall-order/src/main/java/site/zhourui/gulimall/order/vo/OrderConfirmVo.java

package site.zhourui.gulimall.order.vo;import lombok.Getter;
import lombok.Setter;import java.math.BigDecimal;
import java.util.List;
import java.util.Map;/*** 訂單確認(rèn)頁(yè)需要用的數(shù)據(jù)* @author zr* @date 2021/12/21 22:22*/
public class OrderConfirmVo {@Getter@SetterList<MemberAddressVo> memberAddressVos;/** 會(huì)員收獲地址列表 **/@Getter @SetterList<OrderItemVo> items;    /** 所有選中的購(gòu)物項(xiàng)【購(gòu)物車中的所有項(xiàng)】 **/@Getter @Setterprivate Integer integration;/** 優(yōu)惠券(會(huì)員積分) **//** TODO 防止重復(fù)提交的令牌 冪等性**/@Getter @Setterprivate String orderToken;@Getter @SetterMap<Long,Boolean> stocks;public Integer getCount() {Integer count = 0;if (items != null && items.size() > 0) {for (OrderItemVo item : items) {count += item.getCount();}}return count;}/** 總商品金額 **///BigDecimal total;//計(jì)算訂單總額public BigDecimal getTotal() {BigDecimal totalNum = BigDecimal.ZERO;if (items != null && items.size() > 0) {for (OrderItemVo item : items) {//計(jì)算當(dāng)前商品的總價(jià)格BigDecimal itemPrice = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));//再計(jì)算全部商品的總價(jià)格totalNum = totalNum.add(itemPrice);}}return totalNum;}/** 應(yīng)付總額 **///BigDecimal payPrice;public BigDecimal getPayPrice() {return getTotal();}
}

確認(rèn)頁(yè)提交數(shù)據(jù)模型還需要地址信息

gulimall-order/src/main/java/site/zhourui/gulimall/order/vo/MemberAddressVo.java

package site.zhourui.gulimall.order.vo;import lombok.Data;/*** 地址信息* @author zr* @date 2021/12/21 22:24*/
@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;/*** 區(qū)*/private String region;/*** 詳細(xì)地址(街道)*/private String detailAddress;/*** 省市區(qū)代碼*/private String areacode;/*** 是否默認(rèn)*/private Integer defaultStatus;}

確認(rèn)頁(yè)提交數(shù)據(jù)模型還需要訂單行信息

gulimall-order/src/main/java/site/zhourui/gulimall/order/vo/OrderItemVo.java

package site.zhourui.gulimall.order.vo;import lombok.Data;import java.math.BigDecimal;
import java.util.List;/*** 購(gòu)物項(xiàng)內(nèi)容* @author zr* @date 2021/12/21 22:23*/
@Data
public class OrderItemVo {private Long skuId;             // skuIdprivate Boolean check = true;   // 是否選中private String title;           // 標(biāo)題private String image;           // 圖片private List<String> skuAttrValues;// 商品銷售屬性private BigDecimal price;       // 單價(jià)private Integer count;          // 當(dāng)前商品數(shù)量private BigDecimal totalPrice;  // 總價(jià)private BigDecimal weight = new BigDecimal("0.085");// 商品重量
}

5.2.2 提交確認(rèn)訂單

5.2.2.1 訂單確認(rèn)頁(yè)流程

1、遠(yuǎn)程調(diào)用:獲取所有收貨地址【member-ums表】
2、遠(yuǎn)程調(diào)用:所有選中的商品(最新價(jià)格-遠(yuǎn)程調(diào)用)【cart-redis中】【product-查詢最新價(jià)格】
3、查詢用戶積分【session的用戶信息中】
4、訂單總額【根據(jù)所有選中的價(jià)格之和 求得】
5、應(yīng)付總額【暫時(shí)跟訂單總額相等】【優(yōu)惠卡等功能不做,直接用積分】

6、查詢每個(gè)商品是否有貨【批量查詢ware服務(wù)】
7、收貨地址高亮【選中地址調(diào)用ajax直接遠(yuǎn)程調(diào)用ware計(jì)算運(yùn)費(fèi)【遠(yuǎn)程調(diào)用會(huì)員服務(wù)member傳入addrId獲取詳細(xì)地址】
WareInfoController /fare
接口返回運(yùn)費(fèi)信息,和地址信息
8、防重令牌【防止用戶多次 提交訂單】【點(diǎn)擊提交訂單后,數(shù)據(jù)庫(kù)只保存一條訂單信息(冪等性,提交1次和多次結(jié)果是一致的)】

5.2.2.2 去到訂單確認(rèn)頁(yè)面

返回訂單確認(rèn)頁(yè)所需要的數(shù)據(jù)OrderConfirmVo

gulimall-order/src/main/java/site/zhourui/gulimall/order/web/OrderWebController.java

    /*** 去結(jié)算確認(rèn)頁(yè)* @param model* @param request* @return* @throws ExecutionException* @throws InterruptedException*/@GetMapping(value = "/toTrade")public String toTrade(Model model, HttpServletRequest request) throws ExecutionException, InterruptedException {OrderConfirmVo confirmVo = orderService.confirmOrder();model.addAttribute("confirmOrderData",confirmVo);//展示訂單確認(rèn)的數(shù)據(jù)return "confirm";}

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java

    OrderConfirmVo confirmOrder();

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

  1. 判斷用戶登錄信息
  2. 遠(yuǎn)程查詢所有的收獲地址列表
  3. 遠(yuǎn)程查詢購(gòu)物車所有選中的購(gòu)物項(xiàng)
  4. 遠(yuǎn)程查詢商品庫(kù)存信息
  5. 查詢用戶積分
  6. 價(jià)格數(shù)據(jù)自動(dòng)計(jì)算
  7. 防重令牌(防止表單重復(fù)提交)
/*** 訂單確認(rèn)頁(yè)返回需要用的數(shù)據(jù)* @return*/@Overridepublic OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {//構(gòu)建OrderConfirmVoOrderConfirmVo confirmVo = new OrderConfirmVo();//獲取當(dāng)前用戶登錄的信息MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();//TODO :獲取當(dāng)前線程請(qǐng)求頭信息(解決Feign異步調(diào)用丟失請(qǐng)求頭問(wèn)題)RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();//開啟第一個(gè)異步任務(wù)CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {//每一個(gè)線程都來(lái)共享之前的請(qǐng)求數(shù)據(jù)RequestContextHolder.setRequestAttributes(requestAttributes);//1、遠(yuǎn)程查詢所有的收獲地址列表List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());confirmVo.setMemberAddressVos(address);}, threadPoolExecutor);//開啟第二個(gè)異步任務(wù)CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {//每一個(gè)線程都來(lái)共享之前的請(qǐng)求數(shù)據(jù)【解決異步ThreadLocal 無(wú)法共享數(shù)據(jù)】RequestContextHolder.setRequestAttributes(requestAttributes);//2、遠(yuǎn)程查詢購(gòu)物車所有選中的購(gòu)物項(xiàng)List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();confirmVo.setItems(currentCartItems);//feign在遠(yuǎn)程調(diào)用之前要構(gòu)造請(qǐng)求,調(diào)用很多的攔截器}, threadPoolExecutor).thenRunAsync(() -> {List<OrderItemVo> items = confirmVo.getItems();//獲取全部商品的idList<Long> skuIds = items.stream().map((itemVo -> itemVo.getSkuId())).collect(Collectors.toList());//3、遠(yuǎn)程查詢商品庫(kù)存信息R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {});if (skuStockVos != null && skuStockVos.size() > 0) {//將skuStockVos集合轉(zhuǎn)換為mapMap<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));confirmVo.setStocks(skuHasStockMap);}},threadPoolExecutor);//4、、查詢用戶積分Integer integration = memberResponseVo.getIntegration();confirmVo.setIntegration(integration);//5、、價(jià)格數(shù)據(jù)自動(dòng)計(jì)算//TODO 5、防重令牌(防止表單重復(fù)提交)//為用戶設(shè)置一個(gè)token,三十分鐘過(guò)期時(shí)間(存在redis)String token = UUID.randomUUID().toString().replace("-", "");redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);confirmVo.setOrderToken(token);CompletableFuture.allOf(addressFuture,cartInfoFuture).get();return confirmVo;}
5.2.2.2.1 遠(yuǎn)程查詢所有的收獲地址列表

模擬創(chuàng)建兩條該用戶地址信息

gulimall-member/src/main/java/site/zhourui/gulimall/member/service/MemberReceiveAddressService.java

    List<MemberReceiveAddressEntity> getAddress(Long memberId);

gulimall-member/src/main/java/site/zhourui/gulimall/member/service/impl/MemberReceiveAddressServiceImpl.java

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

gulimall-member/src/main/java/site/zhourui/gulimall/member/controller/MemberReceiveAddressController.java

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

gulimall-order/src/main/java/site/zhourui/gulimall/order/feign/MemberFeignService.java

package site.zhourui.gulimall.order.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import site.zhourui.gulimall.order.vo.MemberAddressVo;import java.util.List;/*** @author zr* @date 2021/12/23 15:05*/
@FeignClient("gulimall-member")
public interface MemberFeignService {/*** 查詢當(dāng)前用戶的全部收貨地址* @param memberId* @return*/@GetMapping(value = "/member/memberreceiveaddress/{memberId}/address")List<MemberAddressVo> getAddress(@PathVariable("memberId") Long memberId);
}
5.2.2.2.2 遠(yuǎn)程查詢購(gòu)物車所有選中的購(gòu)物項(xiàng)

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/service/CartService.java

    /*** 獲取當(dāng)前用戶的購(gòu)物車所有商品項(xiàng)* @return*/List<CartItemVo> getUserCartItems();

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/service/Impl/CartServiceImpl.java

    /*** 遠(yuǎn)程調(diào)用:訂單服務(wù)調(diào)用【更新最新價(jià)格】* 獲取當(dāng)前用戶購(gòu)物車所有選中的商品項(xiàng)check=true【從redis中取】*/@Overridepublic List<CartItemVo> getUserCartItems() {List<CartItemVo> cartItemVoList = new ArrayList<>();//獲取當(dāng)前用戶登錄的信息UserInfoTo userInfoTo = CartInterceptor.toThreadLocal.get();//如果用戶未登錄直接返回nullif (userInfoTo.getUserId() == null) {return null;} else {//獲取購(gòu)物車項(xiàng)String cartKey =CartConstant.CART_PREFIX + userInfoTo.getUserId();//獲取所有的List<CartItemVo> cartItems = getCartItems(cartKey);if (cartItems == null) {throw new CartExceptionHandler();}//篩選出選中的cartItemVoList = cartItems.stream().filter(items -> items.getCheck()).map(item -> {//更新為最新的價(jià)格(查詢數(shù)據(jù)庫(kù))// redis中的價(jià)格不是最新的BigDecimal price = productFeignService.getPrice(item.getSkuId());item.setPrice(price);return item;}).collect(Collectors.toList());}return cartItemVoList;}

gulimall-cart/src/main/java/site/zhourui/gulimall/cart/controller/CartController.java

    /*** 訂單服務(wù)調(diào)用:【購(gòu)物車頁(yè)面點(diǎn)擊確認(rèn)訂單時(shí)】* 返回所有選中的商品項(xiàng)【從redis中取】* 并且要獲取最新的商品價(jià)格信息,而不是redis中的數(shù)據(jù)** 獲取當(dāng)前用戶的購(gòu)物車所有商品項(xiàng)*/@GetMapping(value = "/currentUserCartItems")@ResponseBodypublic List<CartItemVo> getCurrentCartItems() {List<CartItemVo> cartItemVoList = cartService.getUserCartItems();return cartItemVoList;}

gulimall-order/src/main/java/site/zhourui/gulimall/order/feign/CartFeignService.java

package site.zhourui.gulimall.order.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import site.zhourui.gulimall.order.vo.OrderItemVo;import java.util.List;/*** @author zr* @date 2021/12/23 15:06*/
@FeignClient("gulimall-cart")
public interface CartFeignService {/*** 查詢當(dāng)前用戶購(gòu)物車選中的商品項(xiàng)* @return*/@GetMapping(value = "/currentUserCartItems")List<OrderItemVo> getCurrentCartItems();}
5.2.2.2.3 遠(yuǎn)程查詢商品庫(kù)存信息

gulimall-order/src/main/java/site/zhourui/gulimall/order/vo/SkuStockVo.java

返回庫(kù)存信息的vo

	package site.zhourui.gulimall.order.vo;import lombok.Data;/*** 庫(kù)存vo* @author zr* @date 2021/12/23 15:53*/
@Data
public class SkuStockVo {private Long skuId;private Boolean hasStock;}

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareSkuService.java

    /*** 判斷是否有庫(kù)存*/List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds);

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareSkuServiceImpl.java

    /***  檢查sku 是否有庫(kù)存*/@Overridepublic List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds) {List<SkuHasStockVo> vos = skuIds.stream().map(skuId -> {SkuHasStockVo vo = new SkuHasStockVo();// 1、不止一個(gè)倉(cāng)庫(kù)有,多個(gè)倉(cāng)庫(kù)都有庫(kù)存 sum// 2、鎖定庫(kù)存是別人下單但是還沒下完Long count = baseMapper.getSkuStock(skuId);vo.setSkuId(skuId);vo.setHasStock(count == null ? false : count > 0);return vo;}).collect(Collectors.toList());return vos;}

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/controller/WareSkuController.java

    /*** 查詢sku是否有庫(kù)存*/@PostMapping("/hasstock")// @RequiresPermissions("ware:waresku:list")public R getSkusHasStock(@RequestBody List<Long> skuIds){// sku_id  stockList<SkuHasStockVo> vos = wareSkuService.getSkusHasStock(skuIds);return R.ok().setData(vos);}

gulimall-order/src/main/java/site/zhourui/gulimall/order/feign/WmsFeignService.java

package site.zhourui.gulimall.order.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import site.zhourui.common.utils.R;import java.util.List;/*** @author zr* @date 2021/12/23 15:06*/
@FeignClient("gulimall-ware")
public interface WmsFeignService {/*** 查詢sku是否有庫(kù)存*/@PostMapping(value = "/ware/waresku/hasstock")R getSkuHasStock(@RequestBody List<Long> skuIds);}
5.2.2.2.4 多線程異步編排

之前在章節(jié)3整合過(guò)線程池了,只需導(dǎo)入

    @Autowiredprivate ThreadPoolExecutor threadPoolExecutor;
5.2.2.3 feign遠(yuǎn)程調(diào)用丟失請(qǐng)求頭

原因:feign發(fā)送請(qǐng)求時(shí)構(gòu)造的RequestTemplate沒有請(qǐng)求頭(該請(qǐng)求頭為空),請(qǐng)求參數(shù)等信息【cookie沒了】

導(dǎo)致在cart服務(wù)中,攔截器攔截獲取session中的登錄信息,獲取不到userId【沒有cookie】

解決:同步新、老請(qǐng)求(老請(qǐng)求就是/toTrade請(qǐng)求,帶有Cookie數(shù)據(jù))的cookie

原理: feign在遠(yuǎn)程調(diào)用之前要構(gòu)造請(qǐng)求,調(diào)用很多的攔截器(DEBUG,查看到會(huì)調(diào)用 攔截器)

gulimall-order/src/main/java/site/zhourui/gulimall/order/config/GuliFeignConfig.java

package site.zhourui.gulimall.order.config;/*** @author zr* @date 2021/12/23 17:03*/import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;/*** feign攔截器功能* 解決feign 遠(yuǎn)程請(qǐng)求頭丟失問(wèn)題**/
@Configuration
public class GuliFeignConfig {@Bean("requestInterceptor")public RequestInterceptor requestInterceptor() {RequestInterceptor requestInterceptor = new RequestInterceptor() {@Overridepublic void apply(RequestTemplate template) {System.out.println("feign遠(yuǎn)程調(diào)用,攔截包裝請(qǐng)求頭");//1、使用RequestContextHolder拿到剛進(jìn)來(lái)的請(qǐng)求數(shù)據(jù)ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (requestAttributes != null) {HttpServletRequest request = requestAttributes.getRequest();//老請(qǐng)求if (request != null) {//2、同步請(qǐng)求頭的數(shù)據(jù)(主要是cookie)//把老請(qǐng)求的cookie值放到新請(qǐng)求上來(lái),進(jìn)行一個(gè)同步String cookie = request.getHeader("Cookie");template.header("Cookie", cookie);}}}};return requestInterceptor;}}
5.2.2.4 Feign異步情況丟失上下文問(wèn)題

導(dǎo)致攔截器中 空指針異常
1、先在主線程的ThreadLocal中獲取 請(qǐng)求頭數(shù)據(jù)

        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

2、再在新線程給ThreadLocal設(shè)置 請(qǐng)求頭數(shù)據(jù)【否則獲取不到數(shù)據(jù),不是同一個(gè)線程】

          //每一個(gè)線程都來(lái)共享之前的請(qǐng)求數(shù)據(jù)【解決異步ThreadLocal 無(wú)法共享數(shù)據(jù)】RequestContextHolder.setRequestAttributes(requestAttributes);
5.2.2.5 模擬運(yùn)費(fèi)效果

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareInfoService.java

    /*** 獲取運(yùn)費(fèi)和收貨地址信息* @param addrId* @return*/FareVo getFare(Long addrId);

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareInfoServiceImpl.java

模擬運(yùn)費(fèi),真實(shí)情況下需要計(jì)算得出

    /*** 計(jì)算運(yùn)費(fèi)* @param addrId* @return*/@Overridepublic FareVo getFare(Long addrId) {FareVo fareVo = new FareVo();//收獲地址的詳細(xì)信息R addrInfo = memberFeignService.info(addrId);MemberAddressVo memberAddressVo = addrInfo.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {});if (memberAddressVo != null) {String phone = memberAddressVo.getPhone();//截取用戶手機(jī)號(hào)碼最后一位作為我們的運(yùn)費(fèi)計(jì)算//1558022051String fare = phone.substring(phone.length() - 1);BigDecimal bigDecimal = new BigDecimal(fare);fareVo.setFare(bigDecimal);fareVo.setAddress(memberAddressVo);return fareVo;}return null;}

需要獲取用戶選擇的地址信息(遠(yuǎn)程調(diào)用)

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/feign/MemberFeignService.java

    /*** 根據(jù)id獲取用戶地址信息* @param id* @return*/@RequestMapping("/member/memberreceiveaddress/info/{id}")R info(@PathVariable("id") Long id);

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/controller/WareInfoController.java

    /*** 獲取運(yùn)費(fèi)信息,訂單服務(wù)遠(yuǎn)程調(diào)用* @return*/@GetMapping(value = "/fare")public R getFare(@RequestParam("addrId") Long addrId) {FareVo fare = wareInfoService.getFare(addrId);return R.ok().setData(fare);}

前端頁(yè)面當(dāng)用戶地址切換時(shí),查詢出運(yùn)費(fèi)及訂單總金額為用戶展示

測(cè)試需要?jiǎng)?chuàng)建兩條地址信息數(shù)據(jù)

5.2.2.6 創(chuàng)建防重令牌

令牌前綴常量

package site.zhourui.gulimall.order.constant;/*** @author zr* @date 2021/12/23 22:05*/
public class OrderConstant {public static final String USER_ORDER_TOKEN_PREFIX = "order:token";}

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

        //TODO 5、防重令牌(防止表單重復(fù)提交)//為用戶設(shè)置一個(gè)token,三十分鐘過(guò)期時(shí)間(存在redis)String token = UUID.randomUUID().toString().replace("-", "");redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);confirmVo.setOrderToken(token);
5.2.2.7 提交訂單

注意:創(chuàng)建的訂單號(hào)很長(zhǎng),注意將oms_orderoms_order_item數(shù)據(jù)庫(kù)表中的order_sn字段調(diào)大至50,否則會(huì)報(bào)錯(cuò)

  • 下單:去創(chuàng)建訂單,驗(yàn)令牌,驗(yàn)價(jià)格,鎖庫(kù)存
  • 提交訂單成功,則攜帶返回?cái)?shù)據(jù)轉(zhuǎn)發(fā)至支付頁(yè)面
  • 提交訂單失敗,則攜帶錯(cuò)誤信息重定向至確認(rèn)頁(yè)

gulimall-order/src/main/java/site/zhourui/gulimall/order/web/OrderWebController.java

    @RequestMapping("/submitOrder")public String submitOrder(OrderSubmitVo submitVo, Model model, RedirectAttributes attributes) {try{SubmitOrderResponseVo responseVo=orderService.submitOrder(submitVo);Integer code = responseVo.getCode();if (code==0){model.addAttribute("order", responseVo.getOrder());return "pay";}else {String msg = "下單失敗;";switch (code) {case 1:msg += "防重令牌校驗(yàn)失敗";break;case 2:msg += "商品價(jià)格發(fā)生變化";break;}attributes.addFlashAttribute("msg", msg);return "redirect:http://order.gulimall.com/toTrade";}}catch (Exception e){if (e instanceof NoStockException){String msg = "下單失敗,商品無(wú)庫(kù)存";attributes.addFlashAttribute("msg", msg);}return "redirect:http://order.gulimall.com/toTrade";}}

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 提交訂單* @param vo* @return*/// @Transactional(isolation = Isolation.READ_COMMITTED) 設(shè)置事務(wù)的隔離級(jí)別// @Transactional(propagation = Propagation.REQUIRED)   設(shè)置事務(wù)的傳播級(jí)別// @GlobalTransactional(rollbackFor = Exception.class)@Transactional@Overridepublic SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {confirmVoThreadLocal.set(vo);SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();//去創(chuàng)建、下訂單、驗(yàn)令牌、驗(yàn)價(jià)格、鎖定庫(kù)存...//獲取當(dāng)前用戶登錄的信息MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();responseVo.setCode(0);//1、驗(yàn)證令牌是否合法【令牌的對(duì)比和刪除必須保證原子性】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();//通過(guò)lure腳本原子驗(yàn)證令牌和刪除令牌Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),orderToken);if (result == 0L) {//令牌驗(yàn)證失敗responseVo.setCode(1);return responseVo;} else {//令牌驗(yàn)證成功//1、創(chuàng)建訂單、訂單項(xiàng)等信息OrderCreateTo order = createOrder();//2、驗(yàn)證價(jià)格BigDecimal payAmount = order.getOrder().getPayAmount();BigDecimal payPrice = vo.getPayPrice();if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {//金額對(duì)比//TODO 3、保存訂單saveOrder(order);//4、庫(kù)存鎖定,只要有異常,回滾訂單數(shù)據(jù)//訂單號(hào)、所有訂單項(xiàng)信息(skuId,skuNum,skuName)WareSkuLockVo lockVo = new WareSkuLockVo();lockVo.setOrderSn(order.getOrder().getOrderSn());//獲取出要鎖定的商品數(shù)據(jù)信息【order里面存儲(chǔ)的是Entity】List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> {OrderItemVo orderItemVo = new OrderItemVo();orderItemVo.setSkuId(item.getSkuId());orderItemVo.setCount(item.getSkuQuantity());orderItemVo.setTitle(item.getSkuName());return orderItemVo;}).collect(Collectors.toList());lockVo.setLocks(orderItemVos);//TODO 調(diào)用遠(yuǎn)程鎖定庫(kù)存的方法//出現(xiàn)的問(wèn)題:扣減庫(kù)存成功了,但是由于網(wǎng)絡(luò)原因超時(shí),出現(xiàn)異常,導(dǎo)致訂單事務(wù)回滾,庫(kù)存事務(wù)不回滾(解決方案:seata)//為了保證高并發(fā),不推薦使用seata,因?yàn)槭羌渔i,并行化,提升不了效率,可以發(fā)消息給庫(kù)存服務(wù)R r = wmsFeignService.orderLockStock(lockVo);if (r.getCode() == 0) {//鎖定成功responseVo.setOrder(order.getOrder());//int i = 10/0;//TODO 訂單創(chuàng)建成功,發(fā)送消息給MQ
//                    rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());//刪除購(gòu)物車?yán)锏臄?shù)據(jù)
//                    redisTemplate.delete(CART_PREFIX+memberResponseVo.getId());return responseVo;} else {//鎖定失敗String msg = (String) r.get("msg");throw new NoStockException(msg);// responseVo.setCode(3);// return responseVo;}} else {responseVo.setCode(2);return responseVo;}}}
5.2.2.7.1 驗(yàn)證防重令牌
        //1、驗(yàn)證令牌是否合法【令牌的對(duì)比和刪除必須保證原子性】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();//通過(guò)lure腳本原子驗(yàn)證令牌和刪除令牌Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),orderToken);if (result == 0L) {//令牌驗(yàn)證失敗responseVo.setCode(1);return responseVo;} else {//令牌驗(yàn)證成功//1、創(chuàng)建訂單、訂單項(xiàng)等信息//2、驗(yàn)證價(jià)格//3、保存訂單//4、庫(kù)存鎖定,只要有異常,回滾訂單數(shù)據(jù)}
5.2.2.7.2 創(chuàng)建訂單、訂單項(xiàng)等信息

需要遠(yuǎn)程調(diào)用獲取SPU信息

gulimall-order/src/main/java/site/zhourui/gulimall/order/feign/ProductFeignService.java

package site.zhourui.gulimall.order.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import site.zhourui.common.utils.R;/*** @author zr* @date 2021/12/24 9:58*/
@FeignClient("gulimall-product")
public interface ProductFeignService {/*** 根據(jù)skuId查詢spu的信息* @param skuId* @return*/@GetMapping(value = "/product/spuinfo/skuId/{skuId}")R getSpuInfoBySkuId(@PathVariable("skuId") Long skuId);}

gulimall-product/src/main/java/site/zhourui/gulimall/product/app/SpuInfoController.java

    /*** 提交訂單,遠(yuǎn)程接口* 根據(jù)skuId查詢spu的信息*/@GetMapping(value = "/skuId/{skuId}")public R getSpuInfoBySkuId(@PathVariable("skuId") Long skuId) {SpuInfoEntity spuInfoEntity = spuInfoService.getSpuInfoBySkuId(skuId);return R.ok().setData(spuInfoEntity);}

gulimall-product/src/main/java/site/zhourui/gulimall/product/service/SpuInfoService.java

    /*** 根據(jù)skuId查詢spu的信息* @param skuId* @return*/SpuInfoEntity getSpuInfoBySkuId(Long skuId);

gulimall-product/src/main/java/site/zhourui/gulimall/product/service/impl/SpuInfoServiceImpl.java

    /*** 根據(jù)skuId查詢spu的信息* @param skuId* @return*/@Overridepublic SpuInfoEntity getSpuInfoBySkuId(Long skuId) {//先查詢sku表里的數(shù)據(jù)SkuInfoEntity skuInfoEntity = skuInfoService.getById(skuId);//獲得spuIdLong spuId = skuInfoEntity.getSpuId();//再通過(guò)spuId查詢spuInfo信息表里的數(shù)據(jù)SpuInfoEntity spuInfoEntity = baseMapper.selectById(spuId);//查詢品牌表的數(shù)據(jù)獲取品牌名BrandEntity brandEntity = brandService.getById(spuInfoEntity.getBrandId());spuInfoEntity.setBrandName(brandEntity.getName());return spuInfoEntity;}

gulimall-product/src/main/java/site/zhourui/gulimall/product/entity/SpuInfoEntity.java

需要加上品牌名稱

	/*** 品牌名*/@TableField(exist = false)private String brandName;

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 創(chuàng)建訂單*/private OrderCreateTo createOrder() {OrderCreateTo createTo = new OrderCreateTo();//1、生成訂單號(hào)String orderSn = IdWorker.getTimeId();// 構(gòu)建訂單數(shù)據(jù)【封裝價(jià)格】OrderEntity orderEntity = builderOrder(orderSn);//2、獲取到所有的訂單項(xiàng)【封裝價(jià)格】List<OrderItemEntity> orderItemEntities = builderOrderItems(orderSn);//3、驗(yàn)價(jià)(計(jì)算價(jià)格、積分等信息)computePrice(orderEntity, orderItemEntities);createTo.setOrder(orderEntity);createTo.setOrderItems(orderItemEntities);return createTo;}

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 構(gòu)建訂單數(shù)據(jù)* @param orderSn* @return*/private OrderEntity builderOrder(String orderSn) {//獲取當(dāng)前用戶登錄信息MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();OrderEntity orderEntity = new OrderEntity();orderEntity.setMemberId(memberResponseVo.getId());orderEntity.setOrderSn(orderSn);orderEntity.setMemberUsername(memberResponseVo.getUsername());OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();//遠(yuǎn)程獲取收貨地址和運(yùn)費(fèi)信息R fareAddressVo = wmsFeignService.getFare(orderSubmitVo.getAddrId());FareVo fareResp = fareAddressVo.getData("data", new TypeReference<FareVo>() {});//獲取到運(yùn)費(fèi)信息BigDecimal fare = fareResp.getFare();orderEntity.setFreightAmount(fare);//獲取到收貨地址信息MemberAddressVo address = fareResp.getAddress();//設(shè)置收貨人信息orderEntity.setReceiverName(address.getName());orderEntity.setReceiverPhone(address.getPhone());orderEntity.setReceiverPostCode(address.getPostCode());orderEntity.setReceiverProvince(address.getProvince());orderEntity.setReceiverCity(address.getCity());orderEntity.setReceiverRegion(address.getRegion());orderEntity.setReceiverDetailAddress(address.getDetailAddress());//設(shè)置訂單相關(guān)的狀態(tài)信息orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());orderEntity.setAutoConfirmDay(7);orderEntity.setConfirmStatus(0);return orderEntity;}

訂單狀態(tài)枚舉

gulimall-order/src/main/java/site/zhourui/gulimall/order/enume/OrderStatusEnum.java

package site.zhourui.gulimall.order.enume;/*** @author zr* @date 2021/12/24 9:52*//*** 訂單狀態(tài)枚舉*/public enum OrderStatusEnum {CREATE_NEW(0,"待付款"),PAYED(1,"已付款"),SENDED(2,"已發(fā)貨"),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/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 構(gòu)建所有訂單項(xiàng)數(shù)據(jù)* @return*/public List<OrderItemEntity> builderOrderItems(String orderSn) {List<OrderItemEntity> orderItemEntityList = new ArrayList<>();//最后確定每個(gè)購(gòu)物項(xiàng)的價(jià)格List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();if (currentCartItems != null && currentCartItems.size() > 0) {orderItemEntityList = currentCartItems.stream().map((items) -> {//構(gòu)建訂單項(xiàng)數(shù)據(jù)OrderItemEntity orderItemEntity = builderOrderItem(items);orderItemEntity.setOrderSn(orderSn);return orderItemEntity;}).collect(Collectors.toList());}return orderItemEntityList;}
5.2.2.7.3 驗(yàn)價(jià)

將頁(yè)面提交的價(jià)格和后臺(tái)計(jì)算的價(jià)格進(jìn)行對(duì)比,若不同則提示用戶商品價(jià)格發(fā)生變化

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 計(jì)算價(jià)格的方法* @param orderEntity* @param orderItemEntities*/private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> orderItemEntities) {//總價(jià)BigDecimal total = new BigDecimal("0.0");//優(yōu)惠價(jià)BigDecimal coupon = new BigDecimal("0.0");BigDecimal intergration = new BigDecimal("0.0");BigDecimal promotion = new BigDecimal("0.0");//積分、成長(zhǎng)值Integer integrationTotal = 0;Integer growthTotal = 0;//訂單總額,疊加每一個(gè)訂單項(xiàng)的總額信息for (OrderItemEntity orderItem : orderItemEntities) {//優(yōu)惠價(jià)格信息coupon = coupon.add(orderItem.getCouponAmount());promotion = promotion.add(orderItem.getPromotionAmount());intergration = intergration.add(orderItem.getIntegrationAmount());//總價(jià)total = total.add(orderItem.getRealAmount());//積分信息和成長(zhǎng)值信息integrationTotal += orderItem.getGiftIntegration();growthTotal += orderItem.getGiftGrowth();}//1、訂單價(jià)格相關(guān)的orderEntity.setTotalAmount(total);//設(shè)置應(yīng)付總額(總額+運(yùn)費(fèi))orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));orderEntity.setCouponAmount(coupon);orderEntity.setPromotionAmount(promotion);orderEntity.setIntegrationAmount(intergration);//設(shè)置積分成長(zhǎng)值信息orderEntity.setIntegration(integrationTotal);orderEntity.setGrowth(growthTotal);//設(shè)置刪除狀態(tài)(0-未刪除,1-已刪除)orderEntity.setDeleteStatus(0);}
5.2.2.7.4 保存訂單

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 保存訂單所有數(shù)據(jù)*/private void saveOrder(OrderCreateTo orderCreateTo) {//獲取訂單信息OrderEntity order = orderCreateTo.getOrder();order.setModifyTime(new Date());order.setCreateTime(new Date());//保存訂單this.baseMapper.insert(order);//獲取訂單項(xiàng)信息List<OrderItemEntity> orderItems = orderCreateTo.getOrderItems();//批量保存訂單項(xiàng)數(shù)據(jù)orderItemService.saveBatch(orderItems);}
5.2.2.7.5 庫(kù)存鎖定

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

                //TODO 調(diào)用遠(yuǎn)程鎖定庫(kù)存的方法//出現(xiàn)的問(wèn)題:扣減庫(kù)存成功了,但是由于網(wǎng)絡(luò)原因超時(shí),出現(xiàn)異常,導(dǎo)致訂單事務(wù)回滾,庫(kù)存事務(wù)不回滾(解決方案:seata)//為了保證高并發(fā),不推薦使用seata,因?yàn)槭羌渔i,并行化,提升不了效率,可以發(fā)消息給庫(kù)存服務(wù)R r = wmsFeignService.orderLockStock(lockVo);

gulimall-order/src/main/java/site/zhourui/gulimall/order/feign/WmsFeignService.java

  • 找出所有庫(kù)存大于商品數(shù)的倉(cāng)庫(kù)
  • 遍歷所有滿足條件的倉(cāng)庫(kù),逐個(gè)嘗試鎖庫(kù)存,若鎖庫(kù)存成功則退出遍歷
    /*** 鎖定庫(kù)存*/@PostMapping(value = "/ware/waresku/lock/order")R orderLockStock(@RequestBody WareSkuLockVo vo);

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/controller/WareSkuController.java

    /*** 下訂單時(shí)鎖庫(kù)存* @param* @return*/@RequestMapping("/lock/order")public R orderLockStock(@RequestBody WareSkuLockVo lockVo) {try {Boolean lock = wareSkuService.orderLockStock(lockVo);return R.ok();} catch (NoStockException e) {return R.error(BizCodeEnume.NO_STOCK_EXCEPTION.getCode(), BizCodeEnume.NO_STOCK_EXCEPTION.getMsg());}}

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareSkuService.java

    /*** 鎖定庫(kù)存*/boolean orderLockStock(WareSkuLockVo vo);

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareSkuServiceImpl.java

    /*** 為某個(gè)訂單鎖定庫(kù)存*/@Transactional(rollbackFor = Exception.class)@Overridepublic boolean orderLockStock(WareSkuLockVo vo) {/*** 保存庫(kù)存工作單詳情信息* 追溯* 如果沒有庫(kù)存,就不會(huì)發(fā)送消息給mq* 【不會(huì)進(jìn)入save(WareOrderTaskDetailEntity)邏輯,也不會(huì)發(fā)送消息給mq,也不會(huì)鎖定庫(kù)存,也不會(huì)監(jiān)聽到解鎖服務(wù)】*/WareOrderTaskEntity wareOrderTaskEntity = new WareOrderTaskEntity();wareOrderTaskEntity.setOrderSn(vo.getOrderSn());wareOrderTaskEntity.setCreateTime(new Date());wareOrderTaskService.save(wareOrderTaskEntity);//1、按照下單的收貨地址,找到一個(gè)就近倉(cāng)庫(kù),鎖定庫(kù)存//2、找到每個(gè)商品在哪個(gè)倉(cāng)庫(kù)都有庫(kù)存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());//查詢這個(gè)商品在哪個(gè)倉(cāng)庫(kù)有庫(kù)存 stock-鎖定num > 0List<Long> wareIdList = wareSkuDao.listWareIdHasSkuStock(skuId);stock.setWareId(wareIdList);return stock;}).collect(Collectors.toList());//2、鎖定庫(kù)存for (SkuWareHasStock hasStock : collect) {boolean skuStocked = false;Long skuId = hasStock.getSkuId();List<Long> wareIds = hasStock.getWareId();if (CollectionUtils.isEmpty(wareIds)) {//沒有任何倉(cāng)庫(kù)有這個(gè)商品的庫(kù)存throw new NoStockException(skuId);}//1、如果每一個(gè)商品都鎖定成功,將當(dāng)前商品鎖定了幾件的工作單記錄發(fā)給MQ//2、鎖定失敗。前面保存的工作單信息都回滾了。發(fā)送出去的消息,即使要解鎖庫(kù)存,由于在數(shù)據(jù)庫(kù)查不到指定的id,所有就不用解鎖for (Long wareId : wareIds) {//鎖定成功就返回1,失敗就返回0Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());// count==1表示鎖定成功if (count == 1) {skuStocked = true;
//                    WareOrderTaskDetailEntity taskDetailEntity = WareOrderTaskDetailEntity.builder()
//                            .skuId(skuId)
//                            .skuName("")
//                            .skuNum(hasStock.getNum())
//                            .taskId(wareOrderTaskEntity.getId())
//                            .wareId(wareId)
//                            .lockStatus(1)
//                            .build();
//                    wareOrderTaskDetailService.save(taskDetailEntity);
//
//                    //TODO 告訴MQ庫(kù)存鎖定成功
//                    StockLockedTo lockedTo = new StockLockedTo();
//                    lockedTo.setId(wareOrderTaskEntity.getId());
//                    StockDetailTo detailTo = new StockDetailTo();
//                    BeanUtils.copyProperties(taskDetailEntity,detailTo);// 這里直接存entity。但是應(yīng)該存id更好,數(shù)據(jù)最好來(lái)自DB
//                    lockedTo.setDetailTo(detailTo);
//                    rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);// 鎖定成功返回break;} else {//當(dāng)前倉(cāng)庫(kù)鎖失敗,重試下一個(gè)倉(cāng)庫(kù)}}if (skuStocked == false) {//當(dāng)前商品所有倉(cāng)庫(kù)都沒有鎖住throw new NoStockException(skuId);}}//3、肯定全部都是鎖定成功的return true;}
5.2.2.8 訂單提交的問(wèn)題 (本地事務(wù)在分布式情況下出現(xiàn)的問(wèn)題)

分布式情況下,可能出現(xiàn)一些服務(wù)事務(wù)不一致的情況

  • 遠(yuǎn)程服務(wù)假失敗
  • 遠(yuǎn)程服務(wù)執(zhí)行完成后,下面其他方法出現(xiàn)異常

庫(kù)存扣減成功但是訂單業(yè)務(wù)執(zhí)行出錯(cuò),訂單業(yè)務(wù)可以回滾但遠(yuǎn)程調(diào)用的庫(kù)存服務(wù)是辦法回滾的

5.2.2.9 使用seata解決分布式事務(wù)

有多種模式:AT、TCC、SAGA 和 XA
doc:http://seata.io/zh-cn/docs/overview/what-is-seata.html

1、TC不會(huì)控制各RM回滾,而是調(diào)用補(bǔ)償方案,AT模式是根據(jù) 回滾日志表【每個(gè)數(shù)據(jù)庫(kù)都創(chuàng)建一個(gè)回滾日志表】
2、而TCC模式的回滾是根據(jù)補(bǔ)償方法 來(lái)回滾

AT模式:Auto Transiaction:自動(dòng)事務(wù)模式,根據(jù)回滾日志表自動(dòng)回滾
TCC模式:就是根據(jù)自己手寫的事務(wù)補(bǔ)償方法 來(lái)回滾

Seata術(shù)語(yǔ)

TC (Transaction Coordinator) - 事務(wù)協(xié)調(diào)者
維護(hù)全局和分支事務(wù)的狀態(tài),驅(qū)動(dòng)全局事務(wù)提交或回滾。

TM (Transaction Manager) - 事務(wù)管理器
定義全局事務(wù)的范圍:開始全局事務(wù)、提交或回滾全局事務(wù)。

RM (Resource Manager) - 資源管理器
管理分支事務(wù)處理的資源,與TC交談以注冊(cè)分支事務(wù)和報(bào)告分支事務(wù)的狀態(tài),并驅(qū)動(dòng)分支事務(wù)提交或回滾。

5.2.2.9.1 AT模式實(shí)現(xiàn)步驟【創(chuàng)建訂單+鎖定庫(kù)存】【不推薦使用】

不適用高并發(fā)場(chǎng)景,適用于商品服務(wù),保存商品的那個(gè)接口 SpuInfoController
/save
1、保存spu pms_spu_info
2、保存attr
3、保存描述圖片 pms_spu_info_desc
4、保存圖片集 pms_spu_images
5、保存當(dāng)前spu對(duì)應(yīng)的所有sku信息
6、優(yōu)惠券信息【遠(yuǎn)程調(diào)用】分布式事務(wù)【并發(fā)不高,可以使用AT模式,@GlobalTransactional】
7、保存積分信息【遠(yuǎn)程調(diào)用】分布式事務(wù)【并發(fā)不高,可以使用AT模式,@GlobalTransactional】

seata官方文檔:https://github.com/seata/seata-samples/blob/master/doc/quick-integration-with-spring-cloud.md

1.創(chuàng)建 UNDO_LOG 表

-- 注意此處0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`branch_id` bigint(20) NOT NULL,`xid` varchar(100) NOT NULL,`context` varchar(128) NOT NULL,`rollback_info` longblob NOT NULL,`log_status` int(11) NOT NULL,`log_created` datetime NOT NULL,`log_modified` datetime NOT NULL,`ext` varchar(100) DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

2.導(dǎo)入依賴

        <!--seata 分布式事務(wù)--><!--不使用的模塊要排除掉--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><exclusions><exclusion><groupId>io.seata</groupId><artifactId>seata-all</artifactId></exclusion></exclusions></dependency>
<!--        使用與事務(wù)協(xié)調(diào)器版本相同的--><dependency><groupId>io.seata</groupId><artifactId>seata-all</artifactId><version>0.7.1</version></dependency>

下載安裝事務(wù)協(xié)調(diào)器:seate-server0.7.1Release v0.7.1 · seata/seata · GitHub

版本與seata-all版本對(duì)應(yīng)

3.配置seata的注冊(cè)中心 registry.conf

4.啟動(dòng)?D:\environment\seata-server-0.7.1\bin\seata-server.bat

seata各種屬性配置 file.conf

5.所有想要用到分布式事務(wù)的微服務(wù)使用seata DataSourceProxy 代理自己的數(shù)據(jù)源

package site.zhourui.gulimall.order.config;/*** @author zr* @date 2021/12/28 10:57*/import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.util.StringUtils;import javax.sql.DataSource;/*** seata分布式事務(wù)* 配置代理數(shù)據(jù)源*/
//@Configuration
public class MySeataConfig {@AutowiredDataSourceProperties dataSourceProperties;/*** 自動(dòng)配置類,如果容器中存在數(shù)據(jù)源就不自動(dòng)配置數(shù)據(jù)源了*/@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);}
}

6.每個(gè)使用分布式事務(wù)的微服務(wù)都需要導(dǎo)入 file.conf registry.conf

注意file.conf:

需要注意的是 service.vgroup_mapping這個(gè)配置,在 Spring Cloud 中默認(rèn)是${spring.application.name}-fescar-service-group ,可以通過(guò)指定application.propertiesspring.cloud.alibaba.seata.tx-service-group這個(gè)屬性覆蓋,但是必須要和 file.conf中的一致,否則會(huì)提示 no available server to connect

7.加注解

  • 給分布式大事務(wù)的入口標(biāo)注@GlobalTransactional gulimall-order服務(wù)
  •  每一個(gè)遠(yuǎn)程的小事務(wù)用@Trabsactional                     gulimall-ware服務(wù)
    

重啟服務(wù)測(cè)試

測(cè)試完成后關(guān)閉seataGlobalTransactional,排除依賴 gulimall-order,gulimall-ware

5.2.2.10 最終一致性庫(kù)存解鎖邏輯:基于消息隊(duì)列的分布式事務(wù)+分布式表【庫(kù)存自動(dòng)解鎖】

5.2.2.10.1 為庫(kù)存模塊創(chuàng)建業(yè)務(wù)交換機(jī),隊(duì)列,綁定(整合Rabbitmq)

導(dǎo)入依賴

gulimall-ware/pom.xml

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

配置Rabbitmq地址端口(虛擬主機(jī),確認(rèn)發(fā)送,抵達(dá)確認(rèn),手動(dòng)ack)

spring:rabbitmq:host: 192.168.157.128port: 5672virtual-host: /#開啟發(fā)送端確認(rèn)publisher-confirms: true# 開啟發(fā)送端消息抵達(dá)Queue確認(rèn)publisher-returns: true# 只要消息抵達(dá)Queue,就會(huì)異步發(fā)送優(yōu)先回調(diào)returnfirmtemplate:mandatory: true#    使用手動(dòng)ack確認(rèn)模式,關(guān)閉自動(dòng)確認(rèn)【消息丟失】listener:simple:acknowledge-mode: manual

開啟RabbitMQ

主啟動(dòng)類加上

@EnableRabbit

配置確認(rèn)回調(diào),失敗回調(diào)

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/config/MyRabbitConfig.java

package site.zhourui.gulimall.ware.config;import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import javax.annotation.PostConstruct;/*** @author zr* @date 2021/12/15 9:56*/
@Configuration
public class MyRabbitConfig {@AutowiredRabbitTemplate rabbitTemplate;@Beanpublic MessageConverter messageConverter(){return new Jackson2JsonMessageConverter();}/*** 定制RabbitTemplate* 1、服務(wù)收到消息就會(huì)回調(diào)*      1、spring.rabbitmq.publisher-confirms: true*      2、設(shè)置確認(rèn)回調(diào)* 2、消息正確抵達(dá)隊(duì)列就會(huì)進(jìn)行回調(diào)*      1、spring.rabbitmq.publisher-returns: true*         spring.rabbitmq.template.mandatory: true*      2、設(shè)置確認(rèn)回調(diào)ReturnCallback** 3、消費(fèi)端確認(rèn)(保證每個(gè)消息都被正確消費(fèi),此時(shí)才可以broker刪除這個(gè)消息)**/@PostConstruct  //MyRabbitConfig對(duì)象創(chuàng)建完成以后,執(zhí)行這個(gè)方法public void initRabbitTemplate() {/*** 1、只要消息抵達(dá)Broker就ack=true* correlationData:當(dāng)前消息的唯一關(guān)聯(lián)數(shù)據(jù)(這個(gè)是消息的唯一id)* ack:消息是否成功收到* cause:失敗的原因*///設(shè)置確認(rèn)回調(diào)rabbitTemplate.setConfirmCallback((correlationData,ack,cause) -> {System.out.println("confirm...correlationData["+correlationData+"]==>ack:["+ack+"]==>cause:["+cause+"]");});/*** 只要消息沒有投遞給指定的隊(duì)列,就觸發(fā)這個(gè)失敗回調(diào)* message:投遞失敗的消息詳細(xì)信息* replyCode:回復(fù)的狀態(tài)碼* replyText:回復(fù)的文本內(nèi)容* exchange:當(dāng)時(shí)這個(gè)消息發(fā)給哪個(gè)交換機(jī)* routingKey:當(dāng)時(shí)這個(gè)消息用哪個(gè)路郵鍵*/rabbitTemplate.setReturnCallback((message,replyCode,replyText,exchange,routingKey) -> {System.out.println("Fail Message["+message+"]==>replyCode["+replyCode+"]" +"==>replyText["+replyText+"]==>exchange["+exchange+"]==>routingKey["+routingKey+"]");});}
}

創(chuàng)建庫(kù)存解鎖延時(shí)隊(duì)列及交換機(jī),綁定

package site.zhourui.gulimall.ware.config;/*** @author zr* @date 2021/12/28 16:47*/import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.io.IOException;
import java.util.HashMap;/*** 創(chuàng)建隊(duì)列,交換機(jī),延遲隊(duì)列,綁定關(guān)系 的configuration* 不會(huì)重復(fù)創(chuàng)建覆蓋* 1、第一次使用隊(duì)列【監(jiān)聽】的時(shí)候才會(huì)創(chuàng)建* 2、Broker沒有隊(duì)列、交換機(jī)才會(huì)創(chuàng)建*/
@Configuration
public class MyRabbitMQConfig {@RabbitListener(queues = "stock.release.stock.queue")public void listen( Channel channel, Message message) throws IOException {System.out.println("收到庫(kù)存解鎖消息,準(zhǔn)備解鎖庫(kù)存:------>");channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);}/*** 庫(kù)存服務(wù)默認(rèn)的交換機(jī)* @return*/@Beanpublic Exchange stockEventExchange() {//String name, boolean durable, boolean autoDelete, Map<String, Object> argumentsTopicExchange topicExchange = new TopicExchange("stock-event-exchange", true, false);return topicExchange;}/*** 普通隊(duì)列* @return*/@Beanpublic Queue stockReleaseStockQueue() {//String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> argumentsQueue queue = new Queue("stock.release.stock.queue", true, false, false);return queue;}/*** 延遲隊(duì)列* @return*/@Beanpublic Queue stockDelay() {HashMap<String, Object> arguments = new HashMap<>();arguments.put("x-dead-letter-exchange", "stock-event-exchange");arguments.put("x-dead-letter-routing-key", "stock.release");// 消息過(guò)期時(shí)間 2分鐘arguments.put("x-message-ttl", 120000);Queue queue = new Queue("stock.delay.queue", true, false, false,arguments);return queue;}/*** 交換機(jī)與普通隊(duì)列綁定* @return*/@Beanpublic Binding stockLocked() {//String destination, DestinationType destinationType, String exchange, String routingKey,// 			Map<String, Object> argumentsBinding binding = new Binding("stock.release.stock.queue",Binding.DestinationType.QUEUE,"stock-event-exchange","stock.release.#",null);return binding;}/*** 交換機(jī)與延遲隊(duì)列綁定* @return*/@Beanpublic Binding stockLockedBinding() {return new Binding("stock.delay.queue",Binding.DestinationType.QUEUE,"stock-event-exchange","stock.locked",null);}}

啟動(dòng)測(cè)試

stock.locked路郵鍵發(fā)送隊(duì)列,并監(jiān)聽死信隊(duì)列,兩分鐘后監(jiān)聽到消息說(shuō)明成功了

5.2.2.10.2 庫(kù)存解鎖

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareSkuService.java

    /*** 解鎖庫(kù)存* @param to*/void unlockStock(StockLockedTo to);

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareSkuServiceImpl.java

此處需要調(diào)用訂單服務(wù)遠(yuǎn)程服務(wù)根據(jù)訂單號(hào)查詢訂單信息

    /*** 解鎖庫(kù)存*/@Overridepublic void unlockStock(StockLockedTo to) {//庫(kù)存工作單的idStockDetailTo detail = to.getDetailTo();Long detailId = detail.getId();/*** 解鎖* 1、查詢數(shù)據(jù)庫(kù)關(guān)于這個(gè)訂單鎖定庫(kù)存信息*   有:證明庫(kù)存鎖定成功了*      解鎖:訂單狀況*          1、沒有這個(gè)訂單,必須解鎖庫(kù)存*          2、有這個(gè)訂單,不一定解鎖庫(kù)存*              訂單狀態(tài):已取消:解鎖庫(kù)存*                      已支付:不能解鎖庫(kù)存*/WareOrderTaskDetailEntity taskDetailInfo = wareOrderTaskDetailService.getById(detailId);if (taskDetailInfo != null) {//查出wms_ware_order_task工作單的信息Long id = to.getId();WareOrderTaskEntity orderTaskInfo = wareOrderTaskService.getById(id);//獲取訂單號(hào)查詢訂單狀態(tài)String orderSn = orderTaskInfo.getOrderSn();//遠(yuǎn)程查詢訂單信息R orderData = orderFeignService.getOrderStatus(orderSn);if (orderData.getCode() == 0) {//訂單數(shù)據(jù)返回成功OrderVo orderInfo = orderData.getData("data", new TypeReference<OrderVo>() {});//判斷訂單狀態(tài)是否已取消或者支付或者訂單不存在// 1、訂單不存在:解鎖// 2、訂單存在,且訂單狀態(tài)是取消狀態(tài):解鎖if (orderInfo == null || orderInfo.getStatus() == 4) {// 工作單狀態(tài)必須是 已鎖定 才可以解鎖【因?yàn)榻怄i方法沒有加事務(wù)】if (taskDetailInfo.getLockStatus() == 1) {unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);}}} else {//消息拒絕以后重新放在隊(duì)列里面,讓別人繼續(xù)消費(fèi)解鎖//遠(yuǎn)程調(diào)用服務(wù)失敗throw new RuntimeException("遠(yuǎn)程調(diào)用服務(wù)失敗");}} else {//無(wú)需解鎖【回滾狀態(tài)】}}

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/feign/OrderFeignService.java

package site.zhourui.gulimall.ware.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import site.zhourui.common.utils.R;/*** @author zr* @date 2021/12/29 15:29*/
@FeignClient("gulimall-order")
public interface OrderFeignService {@GetMapping(value = "/order/order/status/{orderSn}")R getOrderStatus(@PathVariable("orderSn") String orderSn);}

遠(yuǎn)程訂單服務(wù)

gulimall-order/src/main/java/site/zhourui/gulimall/order/controller/OrderController.java

    /*** 根據(jù)訂單編號(hào)查詢訂單狀態(tài)* @param orderSn* @return*/@GetMapping(value = "/status/{orderSn}")public R getOrderStatus(@PathVariable("orderSn") String orderSn) {OrderEntity orderEntity = orderService.getOrderByOrderSn(orderSn);return R.ok().setData(orderEntity);}

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java

    /*** 按照訂單號(hào)獲取訂單信息* @param orderSn* @return*/OrderEntity getOrderByOrderSn(String orderSn);

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 按照訂單號(hào)獲取訂單信息* @param orderSn* @return*/@Overridepublic OrderEntity getOrderByOrderSn(String orderSn) {OrderEntity orderEntity = this.baseMapper.selectOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));return orderEntity;}
5.2.2.10.3 監(jiān)聽?zhēng)齑娼怄i

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/entity/WareOrderTaskDetailEntity.java

增加兩個(gè)字段 倉(cāng)庫(kù)id,鎖定狀態(tài)使用@Builder

package site.zhourui.gulimall.ware.entity;import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;import java.io.Serializable;
import java.util.Date;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;/*** 庫(kù)存工作單** @author zr* @email 2437264464@qq.com* @date 2021-09-28 15:47:50*/
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
@TableName("wms_ware_order_task_detail")
public class WareOrderTaskDetailEntity implements Serializable {private static final long serialVersionUID = 1L;/*** 倉(cāng)庫(kù)id*/private Long wareId;/*** 鎖定狀態(tài)* 1-已鎖定 2-已解鎖 3-已扣減*/private Integer lockStatus;/*** id*/@TableIdprivate Long id;/*** sku_id*/private Long skuId;/*** sku_name*/private String skuName;/*** 購(gòu)買個(gè)數(shù)*/private Integer skuNum;/*** 工作單id*/private Long taskId;}

鎖定庫(kù)存時(shí)向庫(kù)存延時(shí)隊(duì)列發(fā)送一條庫(kù)存工作單記錄

庫(kù)存工作單

gulimall-common/src/main/java/site/zhourui/common/to/mq/StockLockedTo.java

package site.zhourui.common.to.mq;/***  鎖定庫(kù)存成功,往延時(shí)隊(duì)列存入 工作單to 對(duì)象*  wms_ware_order_task* @author zr* @date 2021/12/29 15:07*/import lombok.Data;/***/
@Data
public class StockLockedTo {/** 庫(kù)存工作單的id **/private Long id;/** 庫(kù)存單詳情 wms_ware_order_task_detail**/private StockDetailTo detailTo;
}

庫(kù)存詳情單

gulimall-common/src/main/java/site/zhourui/common/to/mq/StockDetailTo.java

package site.zhourui.common.to.mq;/*** 庫(kù)存單詳情* wms_ware_order_task_detail* @author zr* @date 2021/12/29 15:07*/import lombok.Data;@Data
public class StockDetailTo {private Long id;/*** sku_id*/private Long skuId;/*** sku_name*/private String skuName;/*** 購(gòu)買個(gè)數(shù)*/private Integer skuNum;/*** 工作單id*/private Long taskId;/*** 倉(cāng)庫(kù)id*/private Long wareId;/*** 鎖定狀態(tài)* 1-鎖定 2-解鎖 3-扣減*/private Integer lockStatus;}

監(jiān)聽?zhēng)齑嫠佬抨?duì)列,解鎖庫(kù)存

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/listener/StockReleaseListener.java

package site.zhourui.gulimall.ware.listener;import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import site.zhourui.common.to.mq.StockLockedTo;
import site.zhourui.gulimall.ware.service.WareSkuService;import java.io.IOException;/*** 監(jiān)聽死信隊(duì)列,解鎖庫(kù)存* @author zr* @date 2021/12/29 15:22*/
@Slf4j
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {@Autowiredprivate WareSkuService wareSkuService;/*** 這個(gè)是監(jiān)聽死信消息* 1、庫(kù)存自動(dòng)解鎖*  下訂單成功,庫(kù)存鎖定成功,接下來(lái)的業(yè)務(wù)調(diào)用失敗,導(dǎo)致訂單回滾。之前鎖定的庫(kù)存就要自動(dòng)解鎖**  2、訂單失敗*      庫(kù)存鎖定失敗**   只要解鎖庫(kù)存的消息失敗,一定要告訴服務(wù)解鎖失敗*/@RabbitHandlerpublic void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {System.out.println("******收到解鎖庫(kù)存的延時(shí)信息******,準(zhǔn)備解鎖" + to.getDetailTo().getId());try {//當(dāng)前消息是否被第二次及以后(重新)派發(fā)過(guò)來(lái)了// Boolean redelivered = message.getMessageProperties().getRedelivered();//解鎖庫(kù)存wareSkuService.unlockStock(to);// 手動(dòng)刪除消息channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);} catch (Exception e) {// 解鎖失敗 將消息重新放回隊(duì)列,讓別人消費(fèi)channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}/*** 客戶取消訂單,監(jiān)聽到消息*/@RabbitHandlerpublic void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {System.out.println("******收到訂單關(guān)閉,準(zhǔn)備解鎖庫(kù)存的信息******訂單號(hào):" + orderTo.getOrderSn());try {wareSkuService.unlockStock(orderTo);// 手動(dòng)刪除消息channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);} catch (Exception e) {// 解鎖失敗 將消息重新放回隊(duì)列,讓別人消費(fèi)channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}
}

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareSkuService.java

    /*** 解鎖庫(kù)存* @param to*/void unlockStock(StockLockedTo to);

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareSkuServiceImpl.java

此處需要調(diào)用訂單服務(wù)遠(yuǎn)程查詢訂單信息

    /*** 解鎖庫(kù)存*/@Overridepublic void unlockStock(StockLockedTo to) {//庫(kù)存工作單的idStockDetailTo detail = to.getDetailTo();Long detailId = detail.getId();/*** 解鎖* 1、查詢數(shù)據(jù)庫(kù)關(guān)于這個(gè)訂單鎖定庫(kù)存信息*   有:證明庫(kù)存鎖定成功了*      解鎖:訂單狀況*          1、沒有這個(gè)訂單,必須解鎖庫(kù)存*          2、有這個(gè)訂單,不一定解鎖庫(kù)存*              訂單狀態(tài):已取消:解鎖庫(kù)存*                      已支付:不能解鎖庫(kù)存*/WareOrderTaskDetailEntity taskDetailInfo = wareOrderTaskDetailService.getById(detailId);if (taskDetailInfo != null) {//查出wms_ware_order_task工作單的信息Long id = to.getId();WareOrderTaskEntity orderTaskInfo = wareOrderTaskService.getById(id);//獲取訂單號(hào)查詢訂單狀態(tài)String orderSn = orderTaskInfo.getOrderSn();//遠(yuǎn)程查詢訂單信息R orderData = orderFeignService.getOrderStatus(orderSn);if (orderData.getCode() == 0) {//訂單數(shù)據(jù)返回成功OrderVo orderInfo = orderData.getData("data", new TypeReference<OrderVo>() {});//判斷訂單狀態(tài)是否已取消或者支付或者訂單不存在// 1、訂單不存在:解鎖// 2、訂單存在,且訂單狀態(tài)是取消狀態(tài):解鎖if (orderInfo == null || orderInfo.getStatus() == 4) {// 工作單狀態(tài)必須是 已鎖定 才可以解鎖【因?yàn)榻怄i方法沒有加事務(wù)】if (taskDetailInfo.getLockStatus() == 1) {unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detailId);}}} else {//消息拒絕以后重新放在隊(duì)列里面,讓別人繼續(xù)消費(fèi)解鎖//遠(yuǎn)程調(diào)用服務(wù)失敗throw new RuntimeException("遠(yuǎn)程調(diào)用服務(wù)失敗");}} else {//無(wú)需解鎖【回滾狀態(tài)】}}/*** 解鎖庫(kù)存的方法【設(shè)計(jì)DB,沒加事務(wù)】*/public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId) {// 1、庫(kù)存解鎖wareSkuDao.unLockStock(skuId,wareId,num);// 2、更新工作單的狀態(tài) 為已解鎖 2WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();taskDetailEntity.setId(taskDetailId);taskDetailEntity.setLockStatus(2);wareOrderTaskDetailService.updateById(taskDetailEntity);}

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/feign/OrderFeignService.java

遠(yuǎn)程查詢訂單信息

package site.zhourui.gulimall.ware.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import site.zhourui.common.utils.R;/*** @author zr* @date 2021/12/29 15:29*/
@FeignClient("gulimall-order")
public interface OrderFeignService {@GetMapping(value = "/order/order/status/{orderSn}")R getOrderStatus(@PathVariable("orderSn") String orderSn);}

gulimall-order/src/main/java/site/zhourui/gulimall/order/controller/OrderController.java

    /*** 根據(jù)訂單編號(hào)查詢訂單狀態(tài)* @param orderSn* @return*/@GetMapping(value = "/status/{orderSn}")public R getOrderStatus(@PathVariable("orderSn") String orderSn) {OrderEntity orderEntity = orderService.getOrderByOrderSn(orderSn);return R.ok().setData(orderEntity);}

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java

    /*** 按照訂單號(hào)獲取訂單信息* @param orderSn* @return*/OrderEntity getOrderByOrderSn(String orderSn);

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 按照訂單號(hào)獲取訂單信息* @param orderSn* @return*/@Overridepublic OrderEntity getOrderByOrderSn(String orderSn) {OrderEntity orderEntity = this.baseMapper.selectOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));return orderEntity;}
5.2.2.10.4 遠(yuǎn)程服務(wù)order訂單服務(wù)登錄攔截跳轉(zhuǎn)login.html

gulimall-order/src/main/java/site/zhourui/gulimall/order/interceptor/LoginUserInterceptor.java

將該請(qǐng)求放行

        String uri = request.getRequestURI();AntPathMatcher antPathMatcher = new AntPathMatcher();boolean match = antPathMatcher.match("/order/order/status/**", uri);return match;
5.2.2.11 最終一致性庫(kù)存解鎖邏輯:基于消息隊(duì)列的分布式事務(wù)+分布式表【訂單自動(dòng)關(guān)單】
5.2.2.11.1 為訂單模塊創(chuàng)建業(yè)務(wù)交換機(jī),隊(duì)列綁定

谷粒商城--消息隊(duì)列--高級(jí)篇筆記十6.6 延時(shí)隊(duì)列定時(shí)關(guān)單模擬已經(jīng)創(chuàng)建了交換機(jī)隊(duì)列,綁定

https://blog.csdn.net/qq_31745863/article/details/122212434

5.2.2.11.2 訂單關(guān)閉

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java

    /*** 關(guān)閉訂單* @param orderEntity*/void closeOrder(OrderEntity orderEntity);

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 關(guān)閉訂單*/@Overridepublic void closeOrder(OrderEntity orderEntity) {//關(guān)閉訂單之前先查詢一下數(shù)據(jù)庫(kù),判斷此訂單狀態(tài)是否已支付OrderEntity orderInfo = this.getOne(new QueryWrapper<OrderEntity>().eq("order_sn",orderEntity.getOrderSn()));if (orderInfo.getStatus().equals(OrderStatusEnum.CREATE_NEW.getCode())) {//代付款狀態(tài)進(jìn)行關(guān)單OrderEntity orderUpdate = new OrderEntity();orderUpdate.setId(orderInfo.getId());orderUpdate.setStatus(OrderStatusEnum.CANCLED.getCode());this.updateById(orderUpdate);// 發(fā)送消息給MQOrderTo orderTo = new OrderTo();BeanUtils.copyProperties(orderInfo, orderTo);try {//TODO 確保每個(gè)消息發(fā)送成功,給每個(gè)消息做好日志記錄,(給數(shù)據(jù)庫(kù)保存每一個(gè)詳細(xì)信息)保存每個(gè)消息的詳細(xì)信息//TODO 定期掃描數(shù)據(jù)庫(kù),重新發(fā)送失敗的消息rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo);} catch (Exception e) {}}}
5.2.2.11.3 監(jiān)聽訂單自動(dòng)關(guān)單

gulimall-order/src/main/java/site/zhourui/gulimall/order/listener/OrderCloseListener.java

package site.zhourui.gulimall.order.interceptor;import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import site.zhourui.gulimall.order.entity.OrderEntity;
import site.zhourui.gulimall.order.service.OrderService;import java.io.IOException;/*** @author zr* @date 2021/12/29 17:22*/
/*** 定時(shí)關(guān)閉訂單**/
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {@Autowiredprivate OrderService orderService;@RabbitHandlerpublic void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException {System.out.println("收到過(guò)期的訂單信息,準(zhǔn)備關(guān)閉訂單" + orderEntity.getOrderSn());try {orderService.closeOrder(orderEntity);// 手動(dòng)調(diào)用支付寶收單【這里省略了,可以參照demo中的代碼】channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);} catch (Exception e) {channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}}
5.2.2.12 測(cè)試自動(dòng)關(guān)單,自動(dòng)解鎖庫(kù)存

清空之前的訂單與庫(kù)存鎖定,庫(kù)存工作單(也可以不清,但需要記住提交此時(shí)的狀態(tài),這樣好看一點(diǎn))

清空mq消息

下單

mq的訂單延時(shí)隊(duì)列(1分鐘),庫(kù)存延時(shí)隊(duì)列(2分鐘)

一分鐘之內(nèi)數(shù)據(jù)庫(kù)狀態(tài)

大于一分鐘小于兩分鐘,自動(dòng)關(guān)單

其他數(shù)據(jù)庫(kù)表與一分鐘一致

大于兩分鐘,庫(kù)存自動(dòng)解鎖

5.2.2.13 訂單卡頓導(dǎo)致的庫(kù)存無(wú)法解鎖

防止訂單服務(wù)卡頓,導(dǎo)致訂單狀態(tài)消息一直改不了,庫(kù)存優(yōu)先到期,查訂單狀態(tài)新建,什么都不處理
導(dǎo)致卡頓的訂單,永遠(yuǎn)都不能解鎖庫(kù)存

解決方案

再往訂單死信隊(duì)列發(fā)送消息時(shí),同時(shí)也往庫(kù)存死信隊(duì)列發(fā)送相同消息,通知庫(kù)存解鎖

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/listener/StockReleaseListener.java

    /*** 客戶取消訂單,監(jiān)聽到消息*/@RabbitHandlerpublic void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {System.out.println("******收到訂單關(guān)閉,準(zhǔn)備解鎖庫(kù)存的信息******訂單號(hào):" + orderTo.getOrderSn());try {wareSkuService.unlockStock(orderTo);// 手動(dòng)刪除消息channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);} catch (Exception e) {// 解鎖失敗 將消息重新放回隊(duì)列,讓別人消費(fèi)channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);}}

重載解鎖庫(kù)存

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareSkuService.java

    /*** 解鎖訂單*/void unlockStock(OrderTo orderTo);

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareSkuServiceImpl.java

    /*** 防止訂單服務(wù)卡頓,導(dǎo)致訂單狀態(tài)消息一直改不了,庫(kù)存優(yōu)先到期,查訂單狀態(tài)新建,什么都不處理* 導(dǎo)致卡頓的訂單,永遠(yuǎn)都不能解鎖庫(kù)存* @param orderTo*/@Transactional(rollbackFor = Exception.class)@Overridepublic void unlockStock(OrderTo orderTo) {String orderSn = orderTo.getOrderSn();//查一下最新的庫(kù)存解鎖狀態(tài),防止重復(fù)解鎖庫(kù)存WareOrderTaskEntity orderTaskEntity = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);//按照工作單的id找到所有 沒有解鎖的庫(kù)存,進(jìn)行解鎖Long id = orderTaskEntity.getId();List<WareOrderTaskDetailEntity> list = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>().eq("task_id", id).eq("lock_status", 1));for (WareOrderTaskDetailEntity taskDetailEntity : list) {unLockStock(taskDetailEntity.getSkuId(),taskDetailEntity.getWareId(),taskDetailEntity.getSkuNum(),taskDetailEntity.getId());}}

在庫(kù)存解鎖前查一下最新的庫(kù)存解鎖狀態(tài),防止重復(fù)解鎖庫(kù)存

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/WareOrderTaskService.java

    WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn);

gulimall-ware/src/main/java/site/zhourui/gulimall/ware/service/impl/WareOrderTaskServiceImpl.java

    @Overridepublic WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn) {WareOrderTaskEntity orderTaskEntity = this.baseMapper.selectOne(new QueryWrapper<WareOrderTaskEntity>().eq("order_sn", orderSn));return orderTaskEntity;}
5.2.2.14 如何保證消息可靠性
5.2.2.14.1 消息丟失
  • 消息發(fā)送出去,由于網(wǎng)絡(luò)問(wèn)題沒有抵達(dá)服務(wù)器
    • 做好容錯(cuò)方法(try-catch),發(fā)送消息可能會(huì)網(wǎng)絡(luò)失敗,失敗后要有重試機(jī)制,可記錄到數(shù)據(jù)庫(kù),采用定期掃描重發(fā)的方式
    • 做好日志記錄,每個(gè)消息狀態(tài)是否都被服務(wù)器收到都應(yīng)該記錄
    • 做好定期重發(fā),如果消息沒有發(fā)送成功,定期去數(shù)據(jù)庫(kù)掃描未成功的消息進(jìn)行重發(fā)
  • 消息抵達(dá)Broker,Broker要將消息寫入磁盤(持久化)才算成功。此時(shí)Broker尚未持久化完成,宕機(jī)。
    • publisher也必須加入確認(rèn)回調(diào)機(jī)制,確認(rèn)成功的消息,修改數(shù)據(jù)庫(kù)消息狀態(tài)。
  • 自動(dòng)ACK的狀態(tài)下。消費(fèi)者收到消息,但沒來(lái)得及消息然后宕機(jī)
    • 一定開啟手動(dòng)ACK,消費(fèi)成功才移除,失敗或者沒來(lái)得及處理就noAck并重新入隊(duì)
5.2.2.14.2 消息重復(fù)
  • 消息消費(fèi)成功,事務(wù)已經(jīng)提交,ack時(shí),機(jī)器宕機(jī)。導(dǎo)致沒有ack成功,Broker的消息
    重新由unack變?yōu)閞eady,并發(fā)送給其他消費(fèi)者

  • 消息消費(fèi)失敗,由于重試機(jī)制,自動(dòng)又將消息發(fā)送出去

  • 成功消費(fèi),ack時(shí)宕機(jī),消息由unack變?yōu)閞eady,Broker又重新發(fā)送

    • 消費(fèi)者的業(yè)務(wù)消費(fèi)接口應(yīng)該設(shè)計(jì)為冪等性的。比如扣庫(kù)存有工作單的狀態(tài)標(biāo)志

    • 使用防重表(redis/mysql),發(fā)送消息每一個(gè)都有業(yè)務(wù)的唯一標(biāo)識(shí),處理過(guò)就不用處理

      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-已發(fā)送 2-錯(cuò)誤抵達(dá) 3-已抵達(dá)',`create_time` datetime default null ,`update_time` datetime default null
      )
      

    • rabbitMQ的每一個(gè)消息都有redelivered字段,可以獲取是否是被重新投遞過(guò)來(lái)的,而不是第一次投遞過(guò)來(lái)的

5.2.2.14.3 消息積壓
  • 消費(fèi)者宕機(jī)積壓
  • 消費(fèi)者消費(fèi)能力不足積壓
  • 發(fā)送者發(fā)送流量太大
    • 上線更多的消費(fèi)者,進(jìn)行正常消費(fèi)
    • 上線專門的隊(duì)列消費(fèi)服務(wù),將消息先批量取出來(lái),記錄數(shù)據(jù)庫(kù),離線慢慢處理

5.3 訂單支付頁(yè)

5.3.1 加密

5.3.1.1 對(duì)稱加密

加密與解密用的秘鑰都是一樣的

5.3.1.2 非對(duì)稱加密

加密與解密用到的秘鑰不一致

5.3.2 支付寶加密原理

  • 支付寶加密采用RSA非對(duì)稱加密,分別在商戶端和支付寶端有兩對(duì)公鑰和私鑰
  • 在發(fā)送訂單數(shù)據(jù)時(shí),直接使用明文,但會(huì)使用商戶私鑰加一個(gè)對(duì)應(yīng)的簽名,支付寶端會(huì)使用商戶公鑰對(duì)簽名進(jìn)行驗(yàn)簽,只有數(shù)據(jù)明文和簽名對(duì)應(yīng)的時(shí)候才能說(shuō)明傳輸正確
  • 支付成功后,支付寶發(fā)送支付成功數(shù)據(jù)之外,還會(huì)使用支付寶私鑰加一個(gè)對(duì)應(yīng)的簽名,商戶端收到支付成功數(shù)據(jù)之后也會(huì)使用支付寶公鑰延簽,成功后才能確認(rèn)

5.3.2.1 什么是公鑰、 私鑰、 加密、 簽名和驗(yàn)簽?
5.3.2.1.1 公鑰私鑰

公鑰和私鑰是一個(gè)相對(duì)概念
它們的公私性是相對(duì)于生成者來(lái)說(shuō)的。
一對(duì)密鑰生成后, 保存在生成者手里的就是私鑰,生成者發(fā)布出去大家用的就是公鑰

5.3.2.1.2 加密和數(shù)字簽名
  • 加密是指:
    • 我們使用一對(duì)公私鑰中的一個(gè)密鑰來(lái)對(duì)數(shù)據(jù)進(jìn)行加密, 而使用另一個(gè)密鑰來(lái)進(jìn)行解密的技術(shù)。
    • 公鑰和私鑰都可以用來(lái)加密, 也都可以用來(lái)解密。
    • 但這個(gè)加解密必須是一對(duì)密鑰之間的互相加解密, 否則不能成功。
    • 加密的目的是:
      • 為了確保數(shù)據(jù)傳輸過(guò)程中的不可讀性, 就是不想讓別人看到。
  • 簽名:
    • 給我們將要發(fā)送的數(shù)據(jù), 做上一個(gè)唯一簽名(類似于指紋)
    • 用來(lái)互相驗(yàn)證接收方和發(fā)送方的身份;
    • 在驗(yàn)證身份的基礎(chǔ)上再驗(yàn)證一下傳遞的數(shù)據(jù)是否被篡改過(guò)。 因此使用數(shù)字簽名可以用來(lái)達(dá)到數(shù)據(jù)的明文傳輸。
  • 驗(yàn)簽
    支付寶為了驗(yàn)證請(qǐng)求的數(shù)據(jù)是否商戶本人發(fā)的,
    商戶為了驗(yàn)證響應(yīng)的數(shù)據(jù)是否支付寶發(fā)的

5.3.3 支付寶官方demo測(cè)試

官方demo下載地址:https://opendocs.alipay.com/open/54/106682

5.3.3.1 使用支付寶沙箱環(huán)境進(jìn)行測(cè)試:https://open.alipay.com/platform/appDaily.htm?tab=account

5.3.3.2 自定義秘鑰,點(diǎn)擊rsa2秘鑰后面的設(shè)置并啟用

5.3.3.3 利用秘鑰工具生成自己的公鑰,私鑰,拿到支付寶公鑰

5.3.3.4 配置demo中的AlipayConfig

5.3.3.5 啟動(dòng)demo

配置web目錄

添加archive

配置Tomcat

添加依賴

配置字符集

啟動(dòng)Tomcat,測(cè)試

能夠字符成功的話就說(shuō)明測(cè)試成功了

測(cè)試賬號(hào)密碼https://open.alipay.com/platform/appDaily.htm?tab=account

5.3.4 支付寶支付流程

5.3.5 內(nèi)網(wǎng)穿透

5.3.5.0 為什么使用內(nèi)網(wǎng)穿透?

支付寶需要回調(diào)我們的接口進(jìn)行異步通知

5.3.5.1 簡(jiǎn)介

內(nèi)網(wǎng)穿透功能可以允許我們使用外網(wǎng)的網(wǎng)址來(lái)訪問(wèn)主機(jī);
正常的外網(wǎng)需要訪問(wèn)我們項(xiàng)目的流程是:

  1. 買服務(wù)器并且有公網(wǎng)固定 IP
  2. 買域名映射到服務(wù)器的 IP
  3. 域名需要進(jìn)行備案和審核
5.3.5.2 使用場(chǎng)景

1、 開發(fā)測(cè)試(微信、 支付寶)
2、 智慧互聯(lián)
3、 遠(yuǎn)程控制
4、 私有云

5.3.5.3 內(nèi)網(wǎng)穿透的幾個(gè)常用軟件

1、 natapp: https://natapp.cn/ 優(yōu)惠碼: 022B93FD(9 折) [僅限第一次使用]
2、 續(xù)斷: www.zhexi.tech 優(yōu)惠碼: SBQMEA(95 折) [僅限第一次使用]
3、 花生殼: https://www.oray.com/

5.3.5.4 natapp內(nèi)網(wǎng)穿透

官方文檔NATAPP1分鐘快速新手圖文教程 - NATAPP-內(nèi)網(wǎng)穿透 基于ngrok的國(guó)內(nèi)高速內(nèi)網(wǎng)映射工具

  1. 注冊(cè),實(shí)名認(rèn)證

  2. 購(gòu)買免費(fèi)隧道后拿到authtoken

  3. window啟動(dòng)命令

    natapp -authtoken=你的authtoken
    

5.3.6 整合支付

5.3.6.1 導(dǎo)入依賴

gulimall-order/pom.xml

        <!-- 支付寶sdk --><!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-sdk-java --><dependency><groupId>com.alipay.sdk</groupId><artifactId>alipay-sdk-java</artifactId><version>4.10.111.ALL</version></dependency>
5.3.6.2 抽取阿里云支付模板

gulimall-order/src/main/java/site/zhourui/gulimall/order/config/AlipayTemplate.java

package site.zhourui.gulimall.order.config;import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;/*** @author zr* @date 2021/12/31 10:26*/
@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {// 應(yīng)用ID,您的APPID,收款賬號(hào)既是您的APPID對(duì)應(yīng)支付寶賬號(hào)public String app_id;// 商戶私鑰,您的PKCS8格式RSA2私鑰public String merchant_private_key;// 支付寶公鑰,查看地址:https://openhome.alipay.com/platform/keyManage.htm 對(duì)應(yīng)APPID下的支付寶公鑰。public String alipay_public_key;// 服務(wù)器[異步通知]頁(yè)面路徑  需http://格式的完整路徑,不能加?id=123這類自定義參數(shù),必須外網(wǎng)可以正常訪問(wèn)// 支付寶會(huì)悄悄的給我們發(fā)送一個(gè)請(qǐng)求,告訴我們支付成功的信息public String notify_url;// 頁(yè)面跳轉(zhuǎn)同步通知頁(yè)面路徑 需http://格式的完整路徑,不能加?id=123這類自定義參數(shù),必須外網(wǎng)可以正常訪問(wèn)//同步通知,支付成功,一般跳轉(zhuǎn)到成功頁(yè)public String return_url;// 簽名方式private  String sign_type;// 字符編碼格式private  String charset;//訂單超時(shí)時(shí)間private String timeout = "1m";// 支付寶網(wǎng)關(guān); https://openapi.alipaydev.com/gateway.dopublic 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、根據(jù)支付寶的配置生成一個(gè)支付客戶端AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,app_id, merchant_private_key, "json",charset, alipay_public_key, sign_type);//2、創(chuàng)建一個(gè)支付請(qǐng)求 //設(shè)置請(qǐng)求參數(shù)AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();alipayRequest.setReturnUrl(return_url);alipayRequest.setNotifyUrl(notify_url);//商戶訂單號(hào),商戶網(wǎng)站訂單系統(tǒng)中唯一訂單號(hào),必填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 +"\","+ "\"timeout_express\":\""+timeout+"\","+ "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");String result = alipayClient.pageExecute(alipayRequest).getBody();//會(huì)收到支付寶的響應(yīng),響應(yīng)的是一個(gè)頁(yè)面,只要瀏覽器顯示這個(gè)頁(yè)面,就會(huì)自動(dòng)來(lái)到支付寶的收銀臺(tái)頁(yè)面System.out.println("支付寶響應(yīng):登錄頁(yè)面的代碼\n"+result);return result;}@Datapublic static class PayVo {private String out_trade_no; // 商戶訂單號(hào) 必填private String subject; // 訂單名稱 必填private String total_amount;  // 付款金額 必填private String body; // 商品描述 可空}
}
5.3.6.3 配置模板所需相關(guān)配置
alipay:alipay_public_key: xxxapp_id: 2021000117672854charset: utf-8gatewayUrl: https://openapi.alipaydev.com/gateway.domerchant_private_key: xxxx#此處先使用demo的回調(diào)接口頁(yè)面notify_url: http://4wa8cx.natappfree.cc/alipay_trade_wap_pay_java_utf_8_Web_exploded/notify_url.jspreturn_url: http://4wa8cx.natappfree.cc/alipay_trade_wap_pay_java_utf_8_Web_exploded/return_url.jspsign_type: RSA2
5.3.6.4 支付寶支付接口

gulimall-order/src/main/java/site/zhourui/gulimall/order/web/PayWebController.java

package site.zhourui.gulimall.order.web;import com.alipay.api.AlipayApiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import site.zhourui.gulimall.order.config.AlipayTemplate;
import site.zhourui.gulimall.order.service.OrderService;/*** @author zr* @date 2021/12/31 10:57*/
@Slf4j
@Controller
public class PayWebController {@Autowiredprivate AlipayTemplate alipayTemplate;@Autowiredprivate OrderService orderService;/*** 用戶下單:支付寶支付* 1、讓支付頁(yè)讓瀏覽器展示* 2、支付成功以后,跳轉(zhuǎn)到用戶的訂單列表頁(yè)* @param orderSn* @return* @throws AlipayApiException*/@ResponseBody@GetMapping(value = "/aliPayOrder",produces = "text/html")public String aliPayOrder(@RequestParam("orderSn") String orderSn) throws AlipayApiException {AlipayTemplate.PayVo payVo = orderService.getOrderPay(orderSn);// 支付寶返回一個(gè)頁(yè)面【支付寶賬戶登錄的html頁(yè)面】String pay = alipayTemplate.pay(payVo);System.out.println(pay);return pay;}
}

獲取當(dāng)前訂單的支付信息

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java

    /*** 獲取當(dāng)前訂單的支付信息* @param orderSn* @return*/AlipayTemplate.PayVo getOrderPay(String orderSn);

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 獲取當(dāng)前訂單的支付信息*/@Overridepublic AlipayTemplate.PayVo getOrderPay(String orderSn) {AlipayTemplate.PayVo payVo = new AlipayTemplate.PayVo();OrderEntity orderInfo = this.getOrderByOrderSn(orderSn);//保留兩位小數(shù)點(diǎn),向上取值BigDecimal payAmount = orderInfo.getPayAmount().setScale(2, BigDecimal.ROUND_UP);payVo.setTotal_amount(payAmount.toString());payVo.setOut_trade_no(orderInfo.getOrderSn());//查詢訂單項(xiàng)的數(shù)據(jù)List<OrderItemEntity> orderItemInfo = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", orderSn));OrderItemEntity orderItemEntity = orderItemInfo.get(0);payVo.setBody(orderItemEntity.getSkuAttrsVals());payVo.setSubject(orderItemEntity.getSkuName());return payVo;}
5.3.6.5 前端支付頁(yè)面

https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/pay.html

5.3.6.6 支付測(cè)試

支付成功后跳轉(zhuǎn)頁(yè)面,正常情況下應(yīng)該跳轉(zhuǎn)到訂單列表頁(yè)面

5.3.6.7 訂單列表頁(yè)渲染
5.3.6.7.1 靜態(tài)資源上傳

5.3.6.7.2 配置host

5.3.6.7.3 配置網(wǎng)關(guān)

gulimall-gateway/src/main/resources/application.yml

- id: gulimall_member_routeuri: lb://gulimall-memberpredicates:- Host=member.gulimall.com
5.3.6.7.3 前端頁(yè)面

前端頁(yè)面https://gitee.com/zhourui815/gulimall/blob/master/gulimall-member/src/main/resources/templates/orderList.html

5.3.6.7.4 回調(diào)頁(yè)面接口

gulimall-member/src/main/java/site/zhourui/gulimall/member/web/MemberWebController.java

package site.zhourui.gulimall.member.web;import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import site.zhourui.common.utils.R;
import site.zhourui.gulimall.member.feign.OrderFeignService;import java.util.HashMap;
import java.util.Map;/*** @author zr* @date 2021/12/31 15:23*/
@Controller
public class MemberWebController {@Autowiredprivate OrderFeignService orderFeignService;@GetMapping(value = "/memberOrder.html")public String memberOrderPage(@RequestParam(value = "pageNum",required = false,defaultValue = "0") Integer pageNum,Model model) {//獲取到支付寶給我們轉(zhuǎn)來(lái)的所有請(qǐng)求數(shù)據(jù)//request,驗(yàn)證簽名//查出當(dāng)前登錄用戶的所有訂單列表數(shù)據(jù)Map<String,Object> page = new HashMap<>();page.put("page",pageNum.toString());//遠(yuǎn)程查詢訂單服務(wù)訂單數(shù)據(jù)R orderInfo = orderFeignService.listWithItem(page);System.out.println(JSON.toJSONString(orderInfo));model.addAttribute("orders",orderInfo);return "orderList";}}

需要調(diào)用訂單遠(yuǎn)程服務(wù)

gulimall-member/src/main/java/site/zhourui/gulimall/member/feign/OrderFeignService.java

package site.zhourui.gulimall.member.feign;import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import site.zhourui.common.utils.R;import java.util.Map;/*** @author zr* @date 2021/12/31 15:24*/
@FeignClient("gulimall-order")
public interface OrderFeignService {/*** 分頁(yè)查詢當(dāng)前登錄用戶的所有訂單信息*/@PostMapping("/order/order/listWithItem")R listWithItem(@RequestBody Map<String, Object> params);}

gulimall-order/src/main/java/site/zhourui/gulimall/order/controller/OrderController.java

    /*** member遠(yuǎn)程調(diào)用:分頁(yè)查詢當(dāng)前登錄用戶的所有訂單信息*/@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/site/zhourui/gulimall/order/service/OrderService.java

    /*** 查詢當(dāng)前用戶所有訂單數(shù)據(jù)* @param params* @return*/PageUtils queryPageWithItem(Map<String, Object> params);

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

此處需要設(shè)置訂單行信息

    /*** 查詢當(dāng)前用戶所有訂單數(shù)據(jù)*/@Overridepublic 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 -> {//根據(jù)訂單號(hào)查詢訂單項(xiàng)里的數(shù)據(jù)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);}

為OrderEntity 新增屬性

	@TableField(exist = false)private List<OrderItemEntity> orderItemEntityList;
5.3.6.8 會(huì)員服務(wù)整合spring session(需要登錄后查看訂單信息)
5.3.6.8.1 配置攔截器

gulimall-member/src/main/java/site/zhourui/gulimall/member/interceptor/LoginUserInterceptor.java

新增攔截器,放行member/**,遠(yuǎn)程調(diào)用接口

package site.zhourui.gulimall.member.interceptor;import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import site.zhourui.common.constant.AuthServerConstant;
import site.zhourui.common.vo.MemberResponseVo;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.PrintWriter;/*** @author zr* @date 2021/12/31 15:34*/
@Component
public class LoginUserInterceptor implements HandlerInterceptor {public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String uri = request.getRequestURI();boolean match = new AntPathMatcher().match("/member/**", uri);if (match) {return true;}HttpSession session = request.getSession();//獲取登錄的用戶信息MemberResponseVo attribute = (MemberResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);if (attribute != null) {//把登錄后用戶的信息放在ThreadLocal里面進(jìn)行保存loginUser.set(attribute);return true;} else {//未登錄,返回登錄頁(yè)面response.setContentType("text/html;charset=UTF-8");PrintWriter out = response.getWriter();out.println("<script>alert('請(qǐng)先進(jìn)行登錄,再進(jìn)行后續(xù)操作!');location.href='http://auth.gulimall.com/login.html'</script>");// session.setAttribute("msg", "請(qǐng)先進(jìn)行登錄");// response.sendRedirect("http://auth.gulimall.com/login.html");return false;}}
}

gulimall-member/src/main/java/site/zhourui/gulimall/member/config/MemberWebConfig.java

package site.zhourui.gulimall.member.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import site.zhourui.gulimall.member.interceptor.LoginUserInterceptor;/*** @author zr* @date 2021/12/31 15:36*/
@Configuration
public class MemberWebConfig implements WebMvcConfigurer {@Autowiredprivate LoginUserInterceptor loginUserInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");}}
5.3.6.8.2 整合springsession

依賴

        <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><!--jedis,redis客戶端--><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId></dependency>

配置文件

spring:application:name: gulimall-memberredis:port: 6379host: 192.168.157.128jackson:date-format: yyyy-MM-dd HH:mm:sssession:store-type: redis

session自定義配置

gulimall-member/src/main/java/site/zhourui/gulimall/member/config/GulimallSessionConfig.java

package site.zhourui.gulimall.member.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;/*** @author zr* @date 2021/12/12 10:29*/
@Configuration
public class GulimallSessionConfig {@Beanpublic CookieSerializer cookieSerializer() {DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();//放大作用域cookieSerializer.setDomainName("gulimall.com");cookieSerializer.setCookieName("GULISESSION");cookieSerializer.setCookieMaxAge(60*60*24*7);return cookieSerializer;}@Beanpublic RedisSerializer<Object> springSessionDefaultRedisSerializer() {return new GenericJackson2JsonRedisSerializer();}
}

解決feign遠(yuǎn)程調(diào)用請(qǐng)求頭丟失問(wèn)題

gulimall-member/src/main/java/site/zhourui/gulimall/member/config/GuliFeignConfig.java

package site.zhourui.gulimall.member.config;import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;/*** @author zr* @date 2021/12/31 15:36*/
@Configuration
public class GuliFeignConfig {@Bean("requestInterceptor")public RequestInterceptor requestInterceptor() {RequestInterceptor requestInterceptor = new RequestInterceptor() {@Overridepublic void apply(RequestTemplate template) {//1、使用RequestContextHolder拿到剛進(jìn)來(lái)的請(qǐng)求數(shù)據(jù)ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (requestAttributes != null) {//老請(qǐng)求HttpServletRequest request = requestAttributes.getRequest();if (request != null) {//2、同步請(qǐng)求頭的數(shù)據(jù)(主要是cookie)//把老請(qǐng)求的cookie值放到新請(qǐng)求上來(lái),進(jìn)行一個(gè)同步String cookie = request.getHeader("Cookie");template.header("Cookie", cookie);}}}};return requestInterceptor;}
}
5.3.6.9 設(shè)置支付成功回調(diào)接口

修改return_url地址為member服務(wù)的訂單列表頁(yè)請(qǐng)求地址

gulimall-order/src/main/resources/application.yaml

  return_url: http://member.gulimall.com/memberOrder.html
5.3.6.10 獲取支付寶異步通知
  • 訂單支付成功后支付寶會(huì)回調(diào)商戶接口,這個(gè)時(shí)候需要修改訂單狀態(tài)
  • 由于同步跳轉(zhuǎn)可能由于網(wǎng)絡(luò)問(wèn)題失敗,所以使用異步通知
  • 支付寶使用的是最大努力通知方案,保障數(shù)據(jù)一致性,隔一段時(shí)間會(huì)通知商戶支付成功,直到返回success
5.3.6.10.1 接收支付寶異步通知接口

gulimall-order/src/main/java/site/zhourui/gulimall/order/listener/OrderPayedListener.java

package site.zhourui.gulimall.order.listener;import com.alipay.api.AlipayApiException;
import com.alipay.api.internal.util.AlipaySignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import site.zhourui.gulimall.order.config.AlipayTemplate;
import site.zhourui.gulimall.order.service.OrderService;
import site.zhourui.gulimall.order.vo.PayAsyncVo;import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;/*** @author zr* @date 2021/12/31 17:30*/
@RestController
public class OrderPayedListener {@Autowiredprivate OrderService orderService;@Autowiredprivate AlipayTemplate alipayTemplate;@PostMapping(value = "/payed/notify")public String handleAlipayed(PayAsyncVo asyncVo, HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {// 只要收到支付寶的異步通知,返回 success 支付寶便不再通知// 獲取支付寶POST過(guò)來(lái)反饋信息//TODO 需要驗(yàn)簽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] + ",";}//亂碼解決,這段代碼在出現(xiàn)亂碼時(shí)使用// 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()); //調(diào)用SDK驗(yàn)證簽名if (signVerified) {System.out.println("簽名驗(yàn)證成功...");//去修改訂單狀態(tài)String result = orderService.handlePayResult(asyncVo);return result;} else {System.out.println("簽名驗(yàn)證失敗...");return "error";}}}

處理支付寶支付結(jié)果

gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java

    /*** 處理支付寶的支付結(jié)果**/@Transactional(rollbackFor = Exception.class)@Overridepublic 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());//添加到數(shù)據(jù)庫(kù)中this.paymentInfoService.save(paymentInfo);//修改訂單狀態(tài)//獲取當(dāng)前狀態(tài)String tradeStatus = asyncVo.getTrade_status();if (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED")) {//支付成功狀態(tài)String orderSn = asyncVo.getOut_trade_no(); //獲取訂單號(hào)this.updateOrderStatus(orderSn,OrderStatusEnum.PAYED.getCode(), PayConstant.ALIPAY);}return "success";}/*** 修改訂單狀態(tài)* @param orderSn* @param code*/private void updateOrderStatus(String orderSn, Integer code,Integer payType) {this.baseMapper.updateOrderStatus(orderSn,code,payType);}

修改訂單狀態(tài)

gulimall-order/src/main/java/site/zhourui/gulimall/order/dao/OrderDao.java

    /*** 修改訂單狀態(tài)* @param orderSn* @param code* @param payType*/void updateOrderStatus(@Param("orderSn") String orderSn,@Param("code") Integer code,@Param("payType") Integer payType);

gulimall-order/src/main/resources/mapper/order/OrderDao.xml

    <update id="updateOrderStatus">UPDATE oms_orderSET `status` = #{code},modify_time = NOW(),pay_type = #{payType},payment_time = NOW()WHERE order_sn = #{orderSn}</update>
5.3.6.11 設(shè)置支付寶異步通知接口地址

修改notify_url地址為訂單服務(wù)的回調(diào)接口地址

gulimall-order/src/main/resources/application.yaml

  notify_url: http://4wa8cx.natappfree.cc/payed/notify
5.3.6.12 是異步通知接口不被攔截

gulimall-order/src/main/java/site/zhourui/gulimall/order/interceptor/LoginUserInterceptor.java

package site.zhourui.gulimall.order.interceptor;/*** @author zr* @date 2021/12/21 22:04*/import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import site.zhourui.common.constant.AuthServerConstant;
import site.zhourui.common.vo.MemberResponseVo;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.PrintWriter;import static site.zhourui.common.constant.AuthServerConstant.LOGIN_USER;/*** 登錄攔截器* 從session中獲取了登錄信息(redis中),封裝到了ThreadLocal中*/
@Component
public class LoginUserInterceptor implements HandlerInterceptor {public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String uri = request.getRequestURI();AntPathMatcher antPathMatcher = new AntPathMatcher();boolean match = antPathMatcher.match("/order/order/status/**", uri);boolean match1 = antPathMatcher.match("/payed/notify", uri);if (match || match1) {return true;}HttpSession session = request.getSession();MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);if (memberResponseVo != null) {loginUser.set(memberResponseVo);return true;}else {session.setAttribute("msg","請(qǐng)先登錄");response.sendRedirect("http://auth.gulimall.com/login.html");return false;}}
}
5.3.6.13 內(nèi)網(wǎng)穿透設(shè)置異步通知地址
  • 將外網(wǎng)映射到本地的order.gulimall.com:80
  • 由于回調(diào)的請(qǐng)求頭不是order.gulimall.com,因此nginx轉(zhuǎn)發(fā)到網(wǎng)關(guān)后找不到對(duì)應(yīng)的服務(wù),所以需要對(duì)nginx進(jìn)行設(shè)置

修改內(nèi)網(wǎng)穿透接口

測(cè)試

nginx 配置域名轉(zhuǎn)發(fā)

    listen       80;server_name   gulimall.com *.gulimall.com *.natappfree.cc;#server_name  search.gulimall.com;#charset koi8-r;#access_log  /var/log/nginx/log/host.access.log  main;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;}

攔截器放行通知接口

gulimall-order/src/main/java/site/zhourui/gulimall/order/interceptor/LoginUserInterceptor.java

    @Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String uri = request.getRequestURI();AntPathMatcher antPathMatcher = new AntPathMatcher();boolean match = antPathMatcher.match("/order/order/status/**", uri);boolean match1 = antPathMatcher.match("/payed/notify", uri);if (match || match1) {return true;}HttpSession session = request.getSession();MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);if (memberResponseVo != null) {loginUser.set(memberResponseVo);return true;}else {session.setAttribute("msg","請(qǐng)先登錄");response.sendRedirect("http://auth.gulimall.com/login.html");return false;}}
5.3.6.13.1 日期格式問(wèn)題
Field error in object 'payAsyncVo' on field 'notify_time': rejected value [2022-01-02 10:50:06]; codes [typeMismatch.payAsyncVo.notify_time,typeMismatch.notify_time,typeMismatch.java.util.Date,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [payAsyncVo.notify_time,notify_time]; arguments []; default message [notify_time]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date' for property 'notify_time'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.util.Date] for value '2022-01-02 10:50:06'; nested exception is java.lang.IllegalArgumentException]]

解決方案

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

訂單號(hào)長(zhǎng)度報(bào)錯(cuò)

修改oms_payment_info 的訂單號(hào)長(zhǎng)度

5.3.6.9 支付測(cè)試

付款成功后自動(dòng)跳轉(zhuǎn)到訂單列表頁(yè)

5.3.6.10 收單
  1. 訂單在支付頁(yè),不支付,一直刷新,訂單過(guò)期了才支付,訂單狀態(tài)改為已支付了,但是庫(kù)存解鎖了。

    • 使用支付寶自動(dòng)收單功能解決。只要一段時(shí)間不支付,就不能支付了。

    • 效果

  2. 由于時(shí)延等問(wèn)題。訂單解鎖完成,正在解鎖庫(kù)存的時(shí)候,異步通知才到

    • 訂單解鎖,手動(dòng)調(diào)用收單
  3. 網(wǎng)絡(luò)阻塞問(wèn)題,訂單支付成功的異步通知一直不到達(dá)

    • 查詢訂單列表時(shí),ajax獲取當(dāng)前未支付的訂單狀態(tài),查詢訂單狀態(tài)時(shí),再獲取一下支付寶此訂單的狀態(tài)
  4. 其他各種問(wèn)題

    • 每天晚上閑時(shí)下載支付寶對(duì)賬單,一一進(jìn)行對(duì)賬

6 接口冪等性

6.1 什么是冪等性

接口冪等性就是用戶對(duì)于同一操作發(fā)起的一次請(qǐng)求或者多次請(qǐng)求的結(jié)果是一致的, 不會(huì)因?yàn)槎啻吸c(diǎn)擊而產(chǎn)生了副作用; 比如說(shuō)支付場(chǎng)景, 用戶購(gòu)買了商品支付扣款成功, 但是返回結(jié)果的時(shí)候網(wǎng)絡(luò)異常, 此時(shí)錢已經(jīng)扣了, 用戶再次點(diǎn)擊按鈕, 此時(shí)會(huì)進(jìn)行第二次扣款, 返回結(jié)果成功, 用戶查詢余額返發(fā)現(xiàn)多扣錢了, 流水記錄也變成了兩條. . . ,這就沒有保證接口的冪等性。

6.2 哪些情況需要防止

  • 用戶多次點(diǎn)擊按鈕
  • 用戶頁(yè)面回退再次提交
  • 微服務(wù)互相調(diào)用, 由于網(wǎng)絡(luò)問(wèn)題, 導(dǎo)致請(qǐng)求失敗。 feign 觸發(fā)重試機(jī)制
  • 其他業(yè)務(wù)情況

6.3 什么情況下需要冪等

以 SQL 為例, 有些操作是天然冪等的。

  • SELECT * FROM table WHER id=?, 無(wú)論執(zhí)行多少次都不會(huì)改變狀態(tài), 是天然的冪等。
  • UPDATE tab1 SET col1=1 WHERE col2=2, 無(wú)論執(zhí)行成功多少次狀態(tài)都是一致的, 也是冪等操作。
  • delete from user where userid=1, 多次操作, 結(jié)果一樣, 具備冪等性
  • insert into user(userid,name) values(1,‘a(chǎn)’) 如 userid 為唯一主鍵, 即重復(fù)操作上面的業(yè)務(wù), 只會(huì)插入一條用戶數(shù)據(jù), 具備冪等性。
  • UPDATE tab1 SET col1=col1+1 WHERE col2=2, 每次執(zhí)行的結(jié)果都會(huì)發(fā)生變化, 不是冪等的。
  • insert into user(userid,name) values(1,‘a(chǎn)’) 如 userid 不是主鍵, 可以重復(fù), 那上面業(yè)務(wù)多次操作, 數(shù)據(jù)都會(huì)新增多條, 不具備冪等性。

6.4 冪等解決方案

6.4.1 token 機(jī)制 (本次使用)

  1. 服務(wù)端提供了發(fā)送 token 的接口。 我們?cè)诜治鰳I(yè)務(wù)的時(shí)候, 哪些業(yè)務(wù)是存在冪等問(wèn)題的,就必須在執(zhí)行業(yè)務(wù)前, 先去獲取 token, 服務(wù)器會(huì)把 token 保存到 redis 中。
  2. 然后調(diào)用業(yè)務(wù)接口請(qǐng)求時(shí), 把 token 攜帶過(guò)去, 一般放在請(qǐng)求頭部。
  3. 服務(wù)器判斷 token 是否存在 redis 中, 存在表示第一次請(qǐng)求, 然后刪除 token,繼續(xù)執(zhí)行業(yè)務(wù)。
  4. 如果判斷 token 不存在 redis 中, 就表示是重復(fù)操作, 直接返回重復(fù)標(biāo)記給 client, 這樣就保證了業(yè)務(wù)代碼, 不被重復(fù)執(zhí)行。

危險(xiǎn)性:

  1. 刪除 token 還是后刪除 token;

    • 先刪除可能導(dǎo)致, 業(yè)務(wù)確實(shí)沒有執(zhí)行, 重試還帶上之前 token, 由于防重設(shè)計(jì)導(dǎo)致,請(qǐng)求還是不能執(zhí)行。
    • 后刪除可能導(dǎo)致, 業(yè)務(wù)處理成功, 但是服務(wù)閃斷, 出現(xiàn)超時(shí), 沒有刪除 token, 別人繼續(xù)重試, 導(dǎo)致業(yè)務(wù)被執(zhí)行兩邊
    • 我們最好設(shè)計(jì)為先刪除 token, 如果業(yè)務(wù)調(diào)用失敗, 就重新獲取 token 再次請(qǐng)求。
  2. Token 獲取、 比較和刪除必須是原子性

    • redis.get(token) 、 token.equals、 redis.del(token)如果這兩個(gè)操作不是原子, 可能導(dǎo)致, 高并發(fā)下, 都 get 到同樣的數(shù)據(jù), 判斷都成功, 繼續(xù)業(yè)務(wù)并發(fā)執(zhí)行
    • 可以在 redis 使用 lua 腳本完成這個(gè)操作
    if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end  
    

6.4.2 各種鎖機(jī)制

6.4.2.1 數(shù)據(jù)庫(kù)悲觀鎖

select * from xxxx where id = 1 for update;
悲觀鎖使用時(shí)一般伴隨事務(wù)一起使用, 數(shù)據(jù)鎖定時(shí)間可能會(huì)很長(zhǎng), 需要根據(jù)實(shí)際情況選用。另外要注意的是, id 字段一定是主鍵或者唯一索引, 不然可能造成鎖表的結(jié)果, 處理起來(lái)會(huì)非常麻煩。

6.4.2.2 數(shù)據(jù)庫(kù)樂觀鎖

這種方法適合在更新的場(chǎng)景中,
update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1根據(jù) version 版本, 也就是在操作庫(kù)存前先獲取當(dāng)前商品的 version 版本號(hào), 然后操作的時(shí)候帶上此 version 號(hào)。 我們梳理下, 我們第一次操作庫(kù)存時(shí), 得到 version 為 1, 調(diào)用庫(kù)存服務(wù)version 變成了 2; 但返回給訂單服務(wù)出現(xiàn)了問(wèn)題, 訂單服務(wù)又一次發(fā)起調(diào)用庫(kù)存服務(wù), 當(dāng)訂單服務(wù)傳如的 version 還是 1, 再執(zhí)行上面的 sql 語(yǔ)句時(shí), 就不會(huì)執(zhí)行; 因?yàn)?version 已經(jīng)變?yōu)?2 了, where 條件就不成立。 這樣就保證了不管調(diào)用幾次, 只會(huì)真正的處理一次。樂觀鎖主要使用于處理讀多寫少的問(wèn)題

6.4.2.3 業(yè)務(wù)層分布式鎖

如果多個(gè)機(jī)器可能在同一時(shí)間同時(shí)處理相同的數(shù)據(jù), 比如多臺(tái)機(jī)器定時(shí)任務(wù)都拿到了相同數(shù)據(jù)處理, 我們就可以加分布式鎖, 鎖定此數(shù)據(jù), 處理完成后釋放鎖。 獲取到鎖的必須先判斷這個(gè)數(shù)據(jù)是否被處理過(guò)。

6.4.3 各種唯一約束

6.4.3.1 數(shù)據(jù)庫(kù)唯一約束

插入數(shù)據(jù), 應(yīng)該按照唯一索引進(jìn)行插入, 比如訂單號(hào), 相同的訂單就不可能有兩條記錄插入。我們?cè)跀?shù)據(jù)庫(kù)層面防止重復(fù)。

這個(gè)機(jī)制是利用了數(shù)據(jù)庫(kù)的主鍵唯一約束的特性, 解決了在 insert 場(chǎng)景時(shí)冪等問(wèn)題。 但主鍵的要求不是自增的主鍵, 這樣就需要業(yè)務(wù)生成全局唯一的主鍵。

如果是分庫(kù)分表場(chǎng)景下, 路由規(guī)則要保證相同請(qǐng)求下, 落地在同一個(gè)數(shù)據(jù)庫(kù)和同一表中, 要不然數(shù)據(jù)庫(kù)主鍵約束就不起效果了, 因?yàn)槭遣煌臄?shù)據(jù)庫(kù)和表主鍵不相關(guān)。

6.4.3.2 redis set 防重

很多數(shù)據(jù)需要處理, 只能被處理一次, 比如我們可以計(jì)算數(shù)據(jù)的 MD5 將其放入 redis 的 set,每次處理數(shù)據(jù), 先看這個(gè) MD5 是否已經(jīng)存在, 存在就不處理。

6.4.4 防重表

使用訂單號(hào) orderNo 做為去重表的唯一索引, 把唯一索引插入去重表, 再進(jìn)行業(yè)務(wù)操作, 且他們?cè)谕粋€(gè)事務(wù)中。 這個(gè)保證了重復(fù)請(qǐng)求時(shí), 因?yàn)槿ブ乇碛形ㄒ患s束, 導(dǎo)致請(qǐng)求失敗, 避免了冪等問(wèn)題。 這里要注意的是, 去重表和業(yè)務(wù)表應(yīng)該在同一庫(kù)中, 這樣就保證了在同一個(gè)事務(wù), 即使業(yè)務(wù)操作失敗了, 也會(huì)把去重表的數(shù)據(jù)回滾。 這個(gè)很好的保證了數(shù)據(jù)一致性。之前說(shuō)的 redis 防重也算

6.4.5 全局請(qǐng)求唯一 id

調(diào)用接口時(shí), 生成一個(gè)唯一 id, redis 將數(shù)據(jù)保存到集合中(去重) , 存在即處理過(guò)。可以使用 nginx 設(shè)置每一個(gè)請(qǐng)求的唯一 id;

proxy_set_header X-Request-Id $request_id;  

7 本地事務(wù)與分布式事務(wù)

7.1 本地事務(wù)

7.1.1 事務(wù)的基本性質(zhì)

數(shù)據(jù)庫(kù)事務(wù)的幾個(gè)特性: 原子性(Atomicity )、 一致性( Consistency )、 隔離性或獨(dú)立性( Isolation)和持久性(Durabilily), 簡(jiǎn)稱就是 ACID;

  • 原子性: 一系列的操作整體不可拆分, 要么同時(shí)成功, 要么同時(shí)失敗
  • 一致性: 數(shù)據(jù)在事務(wù)的前后, 業(yè)務(wù)整體一致。
    • 轉(zhuǎn)賬。 A:1000; B:1000; 轉(zhuǎn) 200 事務(wù)成功; A: 800 B: 1200
  • 隔離性: 事務(wù)之間互相隔離。
  • 持久性: 一旦事務(wù)成功, 數(shù)據(jù)一定會(huì)落盤在數(shù)據(jù)庫(kù)。

在以往的單體應(yīng)用中, 我們多個(gè)業(yè)務(wù)操作使用同一條連接操作不同的數(shù)據(jù)表, 一旦有異常,我們可以很容易的整體回滾;

7.1.2 事務(wù)的隔離級(jí)別

  1. READ UNCOMMITTED(讀未提交)
    該隔離級(jí)別的事務(wù)會(huì)讀到其它未提交事務(wù)的數(shù)據(jù), 此現(xiàn)象也稱之為臟讀。
  2. READ COMMITTED( 讀提交)
    一個(gè)事務(wù)可以讀取另一個(gè)已提交的事務(wù), 多次讀取會(huì)造成不一樣的結(jié)果, 此現(xiàn)象稱為不可重復(fù)讀問(wèn)題, Oracle 和 SQL Server 的默認(rèn)隔離級(jí)別。
  3. REPEATABLE READ( 可重復(fù)讀)
    該隔離級(jí)別是 MySQL 默認(rèn)的隔離級(jí)別, 在同一個(gè)事務(wù)里, select 的結(jié)果是事務(wù)開始時(shí)時(shí)間點(diǎn)的狀態(tài), 因此, 同樣的 select 操作讀到的結(jié)果會(huì)是一致的, 但是, 會(huì)有幻讀現(xiàn)象。 MySQL的 InnoDB 引擎可以通過(guò) next-key locks 機(jī)制( 參考下文"行鎖的算法"一節(jié)) 來(lái)避免幻讀。
  4. SERIALIZABLE( 序列化)
    在該隔離級(jí)別下事務(wù)都是串行順序執(zhí)行的, MySQL 數(shù)據(jù)庫(kù)的 InnoDB 引擎會(huì)給讀操作隱式加一把讀共享鎖, 從而避免了臟讀、 不可重讀復(fù)讀和幻讀問(wèn)題。

7.1.3 事務(wù)的傳播行為

  1. PROPAGATION_REQUIRED: 如果當(dāng)前沒有事務(wù), 就創(chuàng)建一個(gè)新事務(wù), 如果當(dāng)前存在事務(wù),就加入該事務(wù), 該設(shè)置是最常用的設(shè)置。
  2. PROPAGATION_SUPPORTS: 支持當(dāng)前事務(wù), 如果當(dāng)前存在事務(wù), 就加入該事務(wù), 如果當(dāng)前不存在事務(wù), 就以非事務(wù)執(zhí)行。
  3. PROPAGATION_MANDATORY: 支持當(dāng)前事務(wù), 如果當(dāng)前存在事務(wù), 就加入該事務(wù), 如果當(dāng)前不存在事務(wù), 就拋出異常。
  4. PROPAGATION_REQUIRES_NEW: 創(chuàng)建新事務(wù), 無(wú)論當(dāng)前存不存在事務(wù), 都創(chuàng)建新事務(wù)。
  5. PROPAGATION_NOT_SUPPORTED: 以非事務(wù)方式執(zhí)行操作, 如果當(dāng)前存在事務(wù), 就把當(dāng)前事務(wù)掛起。
  6. PROPAGATION_NEVER: 以非事務(wù)方式執(zhí)行, 如果當(dāng)前存在事務(wù), 則拋出異常。
  7. PROPAGATION_NESTED: 如果當(dāng)前存在事務(wù), 則在嵌套事務(wù)內(nèi)執(zhí)行。 如果當(dāng)前沒有事務(wù),則執(zhí)行與 PROPAGATION_REQUIRED 類似的操作。

7.1.4 SpringBoot 事務(wù)關(guān)鍵點(diǎn)

7.1.4.1 事務(wù)的自動(dòng)配置

TransactionAutoConfiguration

7.1.4.2 本地事務(wù)的坑

在同一個(gè)類里面, 編寫兩個(gè)方法, 內(nèi)部調(diào)用的時(shí)候, 會(huì)導(dǎo)致事務(wù)設(shè)置失效。 原因是沒有用到代理對(duì)象的緣故。

解決辦法

  1. 導(dǎo)入 spring-boot-starter-aop
  2. @EnableTransactionManagement(proxyTargetClass = true)
  3. @EnableAspectJAutoProxy(exposeProxy=true)
  4. AopContext.currentProxy() 調(diào)用方法

示例:

1、如果方法a、b、c都在同一個(gè)service里面,事務(wù)傳播行為不生效,共享一個(gè)事務(wù)原理:事務(wù)是用代理對(duì)象來(lái)控制的,內(nèi)部調(diào)用b(),c(),就相當(dāng)于直接調(diào)用沒有經(jīng)過(guò)事務(wù)【繞過(guò)了代理對(duì)象】解決:不能使用this.b();也不能注入自己【要使用代理對(duì)象來(lái)調(diào)用事務(wù)方法】@Transactional(timeout=30)
public void a() {b();// a事務(wù)傳播給了b事務(wù),并且b事務(wù)的設(shè)置失效c();// c單獨(dú)創(chuàng)建一個(gè)新事務(wù)
}@Transactional(propagation = Propagation.REQUIRED, timeout=2)
public void b() {}@Transactional(propagation = Propagation.REQUIRES_NEW)
public void c() {}

解決步驟

具體步驟:
1、引入aop依賴<!-- 引入aop,解決本地事務(wù)失效問(wèn)題 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>2、開啟動(dòng)態(tài)代理【默認(rèn)使用jdk動(dòng)態(tài)代理,需要有接口】
@EnableAspectJAutoProxy(exposeProxy = true)     //開啟了aspect動(dòng)態(tài)代理模式,對(duì)外暴露代理對(duì)象
好處:cglib繼承的方式完成動(dòng)態(tài)代理
exposeProxy = true:對(duì)外暴露代理對(duì)象3、獲取動(dòng)態(tài)代理對(duì)象
OrderServiceImpl orderService = (OrderServiceImpl)AopContext.currentProxy();
orderService.b();
orderService.c();

7.2 分布式事務(wù)

7.2.1 為什么會(huì)有分布式事務(wù)?

分布式系統(tǒng)經(jīng)常出現(xiàn)的異常機(jī)器宕機(jī)、 網(wǎng)絡(luò)異常、 消息丟失、 消息亂序、 數(shù)據(jù)錯(cuò)誤、 不可靠的 TCP、 存儲(chǔ)數(shù)據(jù)丟失…

分布式事務(wù)是企業(yè)集成中的一個(gè)技術(shù)難點(diǎn), 也是每一個(gè)分布式系統(tǒng)架構(gòu)中都會(huì)涉及到的一個(gè)東西, 特別是在微服務(wù)架構(gòu)中, 幾乎可

以說(shuō)是無(wú)法避免。

7.2.2 CAP定理與base理論

7.2.2.1 CAP定理

CAP 原則又稱 CAP 定理, 指的是在一個(gè)分布式系統(tǒng)中

  • 一致性(Consistency) :
    • 在分布式系統(tǒng)中的所有數(shù)據(jù)備份, 在同一時(shí)刻是否同樣的值。 (等同于所有節(jié)點(diǎn)訪問(wèn)同一份最新的數(shù)據(jù)副本)
  • 可用性(Availability)
    • 在集群中一部分節(jié)點(diǎn)故障后, 集群整體是否還能響應(yīng)客戶端的讀寫請(qǐng)求。 (對(duì)數(shù)據(jù)更新具備高可用性)
  • 分區(qū)容錯(cuò)性(Partition tolerance)
    • 大多數(shù)分布式系統(tǒng)都分布在多個(gè)子網(wǎng)絡(luò)。 每個(gè)子網(wǎng)絡(luò)就叫做一個(gè)區(qū)(partition) 。分區(qū)容錯(cuò)的意思是, 區(qū)間通信可能失敗。 比如, 一臺(tái)服務(wù)器放在中國(guó), 另一臺(tái)服務(wù)器放在美國(guó), 這就是兩個(gè)區(qū), 它們之間可能無(wú)法通信。

CAP 原則指的是, 這三個(gè)要素最多只能同時(shí)實(shí)現(xiàn)兩點(diǎn), 不可能三者兼顧

一般來(lái)說(shuō), 分區(qū)容錯(cuò)無(wú)法避免, 因此可以認(rèn)為 CAP 的 P 總是成立。 CAP 定理告訴我們,剩下的 C 和 A 無(wú)法同時(shí)做到(CA沒有P就是單體應(yīng)用,沒有必要)。

如果滿足P,此時(shí)要滿足A(所有機(jī)器都可用包括通信故障那臺(tái)【數(shù)據(jù)未同步】),就不能保證一致性【同步數(shù)據(jù)的通信線故障,無(wú)法同步】
如果滿足P,此時(shí)要滿足C,那網(wǎng)絡(luò)通信故障的節(jié)點(diǎn)就不應(yīng)該繼續(xù)提供服務(wù)(因?yàn)樗臄?shù)據(jù)不一致)【宕機(jī)的那臺(tái)機(jī)器數(shù)據(jù) 無(wú)法同步】
AP:容易,就算未同步的數(shù)據(jù)也可用
CP:犧牲可用性

? 1、算法:raft和paxos算法:http://thesecretlivesofdata.com/raft/【raft算法演示】

7.2.2.2 raft算法
7.2.2.2.1 領(lǐng)導(dǎo)選舉機(jī)制

  1. 集群所有節(jié)點(diǎn)啟動(dòng)默認(rèn)都是隨從狀態(tài),在此期間每個(gè)隨從都會(huì)自旋,如果該節(jié)點(diǎn)滿足了自旋時(shí)間,那么該節(jié)點(diǎn)就會(huì)成為候選者,領(lǐng)導(dǎo)者需要在一定的時(shí)間內(nèi)為所有隨從者響應(yīng),告知自己還活著(心跳),隨從者收到響應(yīng)就會(huì)終止本次自旋開始新一輪的自旋
  2. 如果沒有監(jiān)聽到到領(lǐng)導(dǎo)者的心跳,滿足自旋時(shí)間的節(jié)點(diǎn)變成候選者,同時(shí)向其他節(jié)點(diǎn)發(fā)送投票請(qǐng)求(同時(shí)終止其自旋),終止其他節(jié)點(diǎn)自旋,成為候選者的可能有多個(gè),此時(shí)候選者發(fā)起投票,直到有一個(gè)候選者獲勝
  3. 最終成為領(lǐng)導(dǎo)

具體步驟:
1、選舉超時(shí) election timeout
隨從變成候選者的時(shí)間【150ms and 300ms隨機(jī)的】【自旋時(shí)間,如果沒有收到領(lǐng)導(dǎo)的命令變成候選者】
例如:啟動(dòng)集群,3個(gè)節(jié)點(diǎn)獲得隨機(jī)自旋時(shí)間,自旋時(shí)間到了就成為候選節(jié)點(diǎn)
2、成為候選節(jié)點(diǎn),并給自己投票1,然后給其他隨從節(jié)點(diǎn)發(fā)送選舉請(qǐng)求【隨從節(jié)點(diǎn)的票可能投給更快的候選者】
隨從節(jié)點(diǎn)的票一旦投出便重新自旋

3、心跳時(shí)間(heartbeat timeout):每隔一段時(shí)間發(fā)送一個(gè)心跳,然后隨從節(jié)點(diǎn)刷新自旋時(shí)間【小于300ms,否則大家都成為候選者了】
此時(shí)領(lǐng)導(dǎo)網(wǎng)絡(luò)延時(shí),自旋結(jié)束產(chǎn)生候選者,產(chǎn)生新領(lǐng)導(dǎo)

4、有多個(gè)候選者,并且票數(shù)一樣,就自旋重新投

7.2.2.2.2 領(lǐng)導(dǎo)日志復(fù)制(可保證數(shù)據(jù)一致性)

所有節(jié)點(diǎn)修改數(shù)據(jù),都要通過(guò)領(lǐng)導(dǎo)來(lái)修改

  1. 客戶端通知領(lǐng)導(dǎo)修改一個(gè)數(shù)據(jù),領(lǐng)導(dǎo)先創(chuàng)建一個(gè) 節(jié)點(diǎn)日志
  2. 領(lǐng)導(dǎo)將這條日志 發(fā)送給所有所有隨從節(jié)點(diǎn)【隨從節(jié)點(diǎn)收到并返回確認(rèn)消息給領(lǐng)導(dǎo)】
  3. 3、領(lǐng)導(dǎo)等待大多數(shù)隨從節(jié)點(diǎn)的確認(rèn)消息,領(lǐng)導(dǎo)提交數(shù)據(jù),然后通知隨從節(jié)點(diǎn)可以提交了
  4. 隨從節(jié)點(diǎn)也提交數(shù)據(jù)。最后領(lǐng)導(dǎo)節(jié)點(diǎn)給請(qǐng)求返回提交成功

具體步驟:
1、領(lǐng)導(dǎo)收到后并不會(huì)馬上給隨從節(jié)點(diǎn)發(fā)送 日志,等待下一次心跳時(shí)發(fā)送日志
2、然后領(lǐng)導(dǎo)提交并馬上返回請(qǐng)求提交成功。然后跟隨下一個(gè)心跳發(fā)送隨從 告訴其提交
3、可保證數(shù)據(jù)一致性【例如選出來(lái)兩個(gè)領(lǐng)導(dǎo),不同機(jī)房。2個(gè)和3個(gè)組成兩個(gè)群】

demo:此時(shí)2個(gè)的那個(gè)客戶端發(fā)請(qǐng)求,一直保存失敗,因?yàn)椴皇谴蠖鄶?shù)人成功【所以數(shù)據(jù)未提交】,但是另外一邊3個(gè)節(jié)點(diǎn)組成的集群可以保存成功【大多數(shù)節(jié)點(diǎn)】
如果此時(shí)兩個(gè)集群恢復(fù)了數(shù)據(jù)通信,舊領(lǐng)導(dǎo)退位,并且跟著舊領(lǐng)導(dǎo)未提交的數(shù)據(jù)需要回滾【低輪領(lǐng)導(dǎo)退位,新領(lǐng)導(dǎo)上位】
然后匹配上新領(lǐng)導(dǎo)的日志

7.2.2.3 CP的缺點(diǎn)

對(duì)于多數(shù)大型互聯(lián)網(wǎng)應(yīng)用的場(chǎng)景, 主機(jī)眾多、 部署分散, 而且現(xiàn)在的集群規(guī)模越來(lái)越大, 所以節(jié)點(diǎn)故障、 網(wǎng)絡(luò)故障是常態(tài), 而且要保證服務(wù)可用性達(dá)到 99.99999%(N 個(gè) 9) , 即保證P 和 A, 舍棄 C。

7.2.2.4 BASE 理論

是對(duì) CAP 理論的延伸, 思想是即使無(wú)法做到強(qiáng)一致性(CAP 的一致性就是強(qiáng)一致性) , 但可以采用適當(dāng)?shù)牟扇∪跻恢滦?#xff0c; 即最終一致性

BASE 是指

  • 基本可用(Basically Available)
    • 基本可用是指分布式系統(tǒng)在出現(xiàn)故障的時(shí)候, 允許損失部分可用性(例如響應(yīng)時(shí)間、功能上的可用性) , 允許損失部分可用性。 需要注意的是, 基本可用絕不等價(jià)于系統(tǒng)不可用
    • 響應(yīng)時(shí)間上的損失: 正常情況下搜索引擎需要在 0.5 秒之內(nèi)返回給用戶相應(yīng)的查詢結(jié)果, 但由于出現(xiàn)故障(比如系統(tǒng)部分機(jī)房發(fā)生斷電或斷網(wǎng)故障) , 查詢結(jié)果的響應(yīng)時(shí)間增加到了 1~2 秒。
    • 功能上的損失: 購(gòu)物網(wǎng)站在購(gòu)物高峰(如雙十一) 時(shí), 為了保護(hù)系統(tǒng)的穩(wěn)定性,部分消費(fèi)者可能會(huì)被引導(dǎo)到一個(gè)降級(jí)頁(yè)面。
  • 軟狀態(tài)( Soft State)
    • 軟狀態(tài)是指允許系統(tǒng)存在中間狀態(tài), 而該中間狀態(tài)不會(huì)影響系統(tǒng)整體可用性。 分布式存儲(chǔ)中一般一份數(shù)據(jù)會(huì)有多個(gè)副本, 允許不同副本同步的延時(shí)就是軟狀態(tài)的體現(xiàn)。 mysql replication 的異步復(fù)制也是一種體現(xiàn)。
  • 最終一致性( Eventual Consistency)
    • 最終一致性是指系統(tǒng)中的所有數(shù)據(jù)副本經(jīng)過(guò)一定時(shí)間后, 最終能夠達(dá)到一致的狀態(tài)。 弱一致性和強(qiáng)一致性相反, 最終一致性是弱一致性的一種特殊情況。
7.2.2.5 強(qiáng)一致性、 弱一致性、 最終一致性

從客戶端角度, 多進(jìn)程并發(fā)訪問(wèn)時(shí), 更新過(guò)的數(shù)據(jù)在不同進(jìn)程如何獲取的不同策略, 決定了不同的一致性。 對(duì)于關(guān)系型數(shù)據(jù)庫(kù), 要求更新過(guò)的數(shù)據(jù)能被后續(xù)的訪問(wèn)都能看到, 這是強(qiáng)一致性。 如果能容忍后續(xù)的部分或者全部訪問(wèn)不到, 則是弱一致性。 如果經(jīng)過(guò)一段時(shí)間后要求能訪問(wèn)到更新后的數(shù)據(jù), 則是最終一致性

7.2.3 分布式事務(wù)幾種方案

7.2.3.1 2PC 模式

數(shù)據(jù)庫(kù)支持的 2PC【 2 phase commit 二階提交】 , 又叫做 XA Transactions。MySQL 從 5.5 版本開始支持, SQL Server 2005 開始

支持, Oracle 7 開始支持。其中, XA 是一個(gè)兩階段提交協(xié)議, 該協(xié)議分為以下兩個(gè)階段:

第一階段: 事務(wù)協(xié)調(diào)器要求每個(gè)涉及到事務(wù)的數(shù)據(jù)庫(kù)預(yù)提交(precommit)此操作, 并反映是否可以提交.

第二階段: 事務(wù)協(xié)調(diào)器要求每個(gè)數(shù)據(jù)庫(kù)提交數(shù)據(jù)。

其中, 如果有任何一個(gè)數(shù)據(jù)庫(kù)否決此次提交, 那么所有數(shù)據(jù)庫(kù)都會(huì)被要求回滾它們?cè)诖耸聞?wù)中的那部分信息。

  • XA 協(xié)議比較簡(jiǎn)單, 而且一旦商業(yè)數(shù)據(jù)庫(kù)實(shí)現(xiàn)了 XA 協(xié)議, 使用分布式事務(wù)的成本也比較低。
  • XA 性能不理想, 特別是在交易下單鏈路, 往往并發(fā)量很高, XA 無(wú)法滿足高并發(fā)場(chǎng)景
  • XA 目前在商業(yè)數(shù)據(jù)庫(kù)支持的比較理想, 在 mysql 數(shù)據(jù)庫(kù)中支持的不太理想, mysql 的XA 實(shí)現(xiàn), 沒有記錄 prepare 階段日志, 主備切換回導(dǎo)致主庫(kù)與備庫(kù)數(shù)據(jù)不一致。
  • 許多 nosql 也沒有支持 XA, 這讓 XA 的應(yīng)用場(chǎng)景變得非常狹隘。
  • 也有 3PC, 引入了超時(shí)機(jī)制( 無(wú)論協(xié)調(diào)者還是參與者, 在向?qū)Ψ桨l(fā)送請(qǐng)求后, 若長(zhǎng)時(shí)間未收到回應(yīng)則做出相應(yīng)處理)
7.2.3.2 柔性事務(wù)-TCC 事務(wù)補(bǔ)償型方案 (seata)

剛性事務(wù): 遵循 ACID 原則, 強(qiáng)一致性。
柔性事務(wù): 遵循 BASE 理論, 最終一致性;
與剛性事務(wù)不同, 柔性事務(wù)允許一定時(shí)間內(nèi), 不同節(jié)點(diǎn)的數(shù)據(jù)不一致, 但要求最終一致。

一階段 prepare 行為: 調(diào)用 自定義 的 prepare 邏輯。
二階段 commit 行為: 調(diào)用 自定義 的 commit 邏輯。
二階段 rollback 行為: 調(diào)用 自定義 的 rollback 邏輯。
所謂 TCC 模式, 是指支持把 自定義 的分支事務(wù)納入到全局事務(wù)的管理中(seata)。

實(shí)現(xiàn):
將業(yè)務(wù)代碼拆成三部分。
1、try鎖庫(kù)存
2、confirm提交數(shù)據(jù)
3、事務(wù)補(bǔ)償邏輯:一旦出現(xiàn)異常執(zhí)行cancel來(lái)回滾【取消鎖定庫(kù)存】

其實(shí)就是2PC的手動(dòng)實(shí)現(xiàn)

7.2.3.3 柔性事務(wù)-最大努力通知型方案【支付寶支付】【多,高并發(fā)場(chǎng)景】【基于消息服務(wù)mq】

按規(guī)律進(jìn)行通知, 不保證數(shù)據(jù)一定能通知成功, 但會(huì)提供可查詢操作接口進(jìn)行核對(duì)。 這種方案主要用在與第三方系統(tǒng)通訊時(shí), 比如: 調(diào)用微信或支付寶支付后的支付結(jié)果通知。 這種方案也是結(jié)合 MQ 進(jìn)行實(shí)現(xiàn), 例如: 通過(guò) MQ 發(fā)送 http 請(qǐng)求, 設(shè)置最大通知次數(shù)。 達(dá)到通知次數(shù)后即不再通知。

案例: 銀行通知、 商戶通知等( 各大交易業(yè)務(wù)平臺(tái)間的商戶通知: 多次通知、 查詢校對(duì)、 對(duì)賬文件) , 支付寶的支付成功異步回調(diào)

例如支付寶支付成功,往MQ發(fā)送消息【隔幾秒發(fā)一個(gè)】
訂單訂閱topic,一旦訂單確認(rèn)消息,給支付寶發(fā)送確認(rèn),支付寶就不再通知了

總結(jié)

以上是生活随笔為你收集整理的谷粒商城--订单服务--高级篇笔记十一的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。

91亚瑟视频| 四虎影视www | 国产美女免费视频 | 激情视频一区二区三区 | 揉bbb玩bbb少妇bbb | 欧美小视频在线观看 | 亚洲人成免费网站 | 中文在线免费看视频 | 久影院| 人人狠狠综合久久亚洲婷 | 黄色免费网战 | 日韩精品一区二区三区外面 | 91久久电影| 人人爽人人插 | 欧美视频18 | 天天干天天做天天爱 | 在线观看v片 | 激情av一区二区 | 日韩精品中文字幕在线观看 | 99re久久资源最新地址 | 国产一线二线三线性视频 | 久久国产免 | 精品在线免费观看 | 最近高清中文在线字幕在线观看 | 精品视频专区 | 欧美日韩一区二区在线 | 亚洲aⅴ乱码精品成人区 | 久久国产精品免费 | 久久九九网站 | 97视频在线免费观看 | 波多野结衣在线播放视频 | 欧美一级特黄aaaaaa大片在线观看 | 亚洲精品美女免费 | 黄色小网站免费看 | 国产精品99久久久久的智能播放 | 综合色婷婷 | 国产黄色片在线 | 欧美性护士 | 蜜桃视频在线视频 | av无限看| 欧美一级xxxx | 一区二区三区在线看 | 波多野结衣在线播放一区 | 国产精品尤物视频 | 日韩网站视频 | 中文字幕一区在线观看视频 | 国产日韩欧美中文 | 亚洲国产成人精品久久 | 一区二区电影网 | 在线观看中文字幕第一页 | 色射爱| 久久夜色电影 | 婷婷色中文网 | 中文字幕在线观看日本 | 欧美日韩xxx | 美女视频黄在线 | 欧美一区成人 | 国产亚洲在线视频 | 国产精品欧美激情在线观看 | 国产精品九九久久久久久久 | www.xxx.性狂虐| 亚洲精品ww | 国产麻豆精品95视频 | 久久久久国产视频 | av色图天堂网 | 亚洲精品福利视频 | 国产精品12 | 国产三级av在线 | av 在线观看 | 国产视频一区二区在线观看 | 9草在线| 久久久福利 | 日日夜夜天天久久 | 国产精品久久av | 日韩精品中文字幕在线不卡尤物 | 中文字幕av在线免费 | 午夜999| 在线观看久 | 91视频免费视频 | 午夜婷婷在线观看 | 日韩av一区二区三区在线观看 | 成人av在线观| 国产精品视频在线观看 | 久久精品一区二区三 | 九色精品在线 | 久久免费视频在线观看30 | 2024av在线播放| 国内精品一区二区 | 99精品视频在线 | 日韩字幕在线 | 五月天狠狠操 | 精品二区视频 | 97久久久免费福利网址 | 国产一级视频在线 | 免费网站黄 | 久久久受www免费人成 | 日韩网站在线观看 | 婷婷久久网 | 中文字幕欧美日韩va免费视频 | 在线免费观看国产视频 | 婷婷久久一区 | 在线观看v片 | 在线观看的a站 | 伊人亚洲精品 | 日本久久久久久久久 | 午夜av免费 | 天天色天天干天天色 | 高清美女视频 | 片网站| 懂色av懂色av粉嫩av分享吧 | 日本中文在线播放 | 高清av免费看 | 精品96久久久久久中文字幕无 | 久久刺激视频 | 久久99精品久久久久婷婷 | 麻豆一二三精选视频 | 国产精品99久久久久的智能播放 | 国产精品96久久久久久吹潮 | 六月婷操 | 欧美精品xxx | 成人精品国产 | 亚洲欧美日韩国产 | 婷婷综合av | 久久伊人精品一区二区三区 | 成人av高清在线 | 2021国产精品 | 在线视频一区观看 | 奇米777777 | 免费成人短视频 | 在线色亚洲 | 久久公开免费视频 | 国产精品成人免费精品自在线观看 | 国产精品99久久久久人中文网介绍 | 国产精品一区二区三区在线 | 欧美日韩91 | 亚洲 欧美 变态 国产 另类 | 国产色影院| 久久久亚洲电影 | 国产精品日韩久久久久 | 视频 天天草 | 中文字幕一区在线 | 日韩欧美在线观看一区二区三区 | 视频一区二区三区视频 | 成人黄色在线 | 免费观看xxxx9999片 | 啪一啪在线 | 精品国内| 成人午夜电影在线观看 | 色先锋av资源中文字幕 | 91麻豆精品国产91久久久更新时间 | 激情深爱.com | 黄色av电影在线 | 国产成人av在线影院 | 久久亚洲综合国产精品99麻豆的功能介绍 | 中文字幕一区二区三区乱码在线 | 精品理论片 | 亚洲色图av | 亚洲激情一区二区三区 | 啪啪激情网 | 91黄视频在线观看 | 国产视频2021| 天天曰 | 欧美在线观看视频一区二区 | 日本精品久久久久影院 | 视频福利在线观看 | 婷婷午夜激情 | 亚洲天堂在线观看完整版 | 天天插视频 | 免费高清在线观看电视网站 | 波多野结衣在线播放视频 | 国产精品久久99 | 日韩电影一区二区三区在线观看 | 日韩精品一区二 | 超碰97中文 | 欧美少妇影院 | 美女网站在线播放 | 91九色精品| 久久午夜免费观看 | 国产精品免费久久久久 | 国产精品久久久久久久久久久久 | 天天综合网 天天综合色 | 国产精品久久久久久久久免费看 | 亚洲日本黄色 | 最新的av网站 | 91av在线免费观看 | 婷婷久久五月天 | 69精品人人人人 | 亚洲精品国产品国语在线 | 啪啪精品| 色噜噜在线观看视频 | 国产91成人 | 成人禁用看黄a在线 | 国产精品久久久久免费 | 国产精品中文字幕av | 一区二区精品 | 草免费视频 | 正在播放国产精品 | 亚洲精品久久久久999中文字幕 | 日韩三级一区 | 国产精品1区2区3区在线观看 | 四虎成人精品永久免费av | 国内精品久久久久久久久久久久 | 日韩视频在线播放 | 黄色av电影免费观看 | 天天夜夜狠狠操 | 啪啪免费观看网站 | 美女一二三区 | 人人干干人人 | 亚洲在线视频免费观看 | 在线观看黄色国产 | 视频精品一区二区三区 | 色综合久久99 | 亚洲精品视频在线免费播放 | 最近中文字幕免费观看 | 黄色a级片在线观看 | 亚洲成人资源网 | 亚洲国产高清在线观看视频 | 亚洲毛片在线观看. | 国产伦精品一区二区三区照片91 | 日韩在线免费高清视频 | 欧美精品久久久久久久久久白贞 | 国产精品中文 | 日本久久成人中文字幕电影 | 狠狠狠色丁香综合久久天下网 | 国产日韩精品一区二区三区在线 | 日韩在线看片 | 精品亚洲成a人在线观看 | 超级碰碰碰免费视频 | 中文区中文字幕免费看 | 久久看毛片 | 欧美aaa一级 | 婷婷久久综合网 | 国产色在线视频 | 国产精品免费麻豆入口 | 日韩av福利在线 | 精品视频专区 | 精品美女久久 | 91传媒免费在线观看 | 成年人天堂com | av 一区 二区 久久 | 极品国产91在线网站 | aⅴ精品av导航 | 91精品国自产在线 | 五月婷婷天堂 | 免费精品在线 | 国产成人精品亚洲日本在线观看 | 日韩精品字幕 | 亚洲妇女av | 久久国精品 | 国产视频资源在线观看 | 亚洲一区av | 国产一区二区三区免费观看视频 | 国产精品欧美久久久久天天影视 | 在线a人v观看视频 | 五月天综合网 | 涩涩在线| 国产精品区一区 | 欧美日韩99 | 天天射天天舔天天干 | av大全免费在线观看 | 18网站在线观看 | 亚洲一本视频 | av在线播放观看 | 亚洲香蕉视频 | 日韩1级片| 99超碰在线观看 | 亚洲精品视频在线观看免费 | 免费国产在线观看 | 久久久久高清 | 2021国产在线视频 | 中文字幕黄色 | 综合天天久久 | 五月天六月丁香 | 狠狠狠的干| 日韩草比 | 麻豆精品视频 | 玖玖爱在线观看 | 久久久久久国产精品亚洲78 | 亚洲精区二区三区四区麻豆 | 日韩免| 色婷婷视频在线 | 久99久在线视频 | 亚洲精品日韩一区二区电影 | 欧美日韩视频精品 | 久久精品一区 | 麻豆视频在线免费看 | 免费99视频 | 成人免费xxx在线观看 | 国产成人在线播放 | 乱男乱女www7788 | 免费高清在线一区 | 97精产国品一二三产区在线 | 日韩激情影院 | 最新中文在线视频 | 在线观看韩国av | 五月婷婷伊人网 | 国产福利专区 | 高清有码中文字幕 | 日韩女同一区二区三区在线观看 | 91亚洲精品在线 | 久久99精品久久久久久秒播蜜臀 | 最近中文字幕 | 国产精品午夜久久久久久99热 | 日韩在线免费小视频 | 丁香婷婷深情五月亚洲 | 中文字幕乱码在线播放 | 人人干狠狠干 | 中文字幕免费在线看 | 激情五月***国产精品 | 欧美精品久久久久久久久久久 | 成人午夜av电影 | 国产精品欧美激情在线观看 | 亚洲视频久久久 | 欧美日韩国产二区三区 | 蜜臀av性久久久久av蜜臀三区 | 日本中文字幕在线观看 | 欧美日韩性生活 | 激情综合色图 | 欧美a级免费视频 | 狠狠躁夜夜躁人人爽视频 | av福利超碰网站 | 久久乐九色婷婷综合色狠狠182 | 一区二区三区中文字幕在线 | 国产 字幕 制服 中文 在线 | 五月激情电影 | 久产久精国产品 | 亚洲人人av | 亚洲精品国产精品国自产 | 欧美成人精品在线 | 最新婷婷色 | 香蕉久久久久久久 | 91亚洲在线观看 | 日韩在线小视频 | 久久人人添人人爽添人人88v | 激情五月激情综合网 | 制服丝袜一区二区 | 成人a在线观看 | 99热精品在线观看 | 中文成人字幕 | 精品一区二区免费在线观看 | 日本三级不卡 | 国产精品一区二区免费视频 | 俺要去色综合狠狠 | 国产精品美女久久久久久久久 | 91精品免费在线视频 | 久久电影色 | 亚洲欧美日韩在线看 | 久久爱影视i | 日韩 精品 一区 国产 麻豆 | 人人澡人人舔 | 99激情网 | 精品久久久久久亚洲综合网站 | 国产69精品久久99不卡的观看体验 | 日日操网 | 日本中文字幕一二区观 | 久草视频免费观 | 成人久久亚洲 | 992tv成人免费看片 | 久久99视频精品 | 免费手机黄色网址 | 中文字幕在线观看视频一区二区三区 | 黄色高清视频在线观看 | 国产成人精品一区二区三区免费 | 久草视频免费播放 | 国产在线a | 黄色小说免费在线观看 | 激情视频免费在线观看 | www.人人草| 免费看黄的 | 狠狠干夜夜爽 | 精品久久久久久国产91 | 午夜精品一区二区三区在线观看 | 中文免费在线观看 | 日韩高清dvd | 日日爱影视| 天天操天天操天天操 | 国产精品乱码久久久久 | 日韩精品免费一区二区三区 | 四虎国产精品免费 | 国产精品久久久久久久免费 | 日韩欧美网址 | 欧美一级特黄aaaaaa大片在线观看 | 黄色精品久久 | 亚洲一二区视频 | 日本大尺码专区mv | 激情五月亚洲 | 日日干日日操 | 一区二区三区国产欧美 | 99精品视频网 | 日本精品视频在线播放 | 91自拍视频在线观看 | 日日日爽爽爽 | 区一区二区三区中文字幕 | 一区中文字幕在线观看 | 日本中文乱码卡一卡二新区 | 天天拍天天干 | 精品久久一 | 狠狠色丁香久久婷婷综合五月 | 国产一区福利 | 日韩欧美在线国产 | 五月天高清欧美mv | 狠狠狠色丁香综合久久天下网 | 2019中文在线观看 | 亚洲国产精品人久久电影 | 国产第一福利 | 99久久精品午夜一区二区小说 | 91香蕉嫩草 | 91成人在线观看喷潮 | 国产精品大片免费观看 | 天堂av一区二区 | 久久久久看片 | 97视频精品| 黄a在线看 | 免费国产在线精品 | 免费av在线| 日韩在线看片 | 午夜视频一区二区三区 | 欧美日韩精品在线播放 | 久久久久久久av | 一级黄色片在线播放 | 久久久久高清毛片一级 | 欧美日韩免费看 | 在线观看中文字幕网站 | 96av在线视频 | 日日夜夜精品免费观看 | 狠狠躁夜夜躁人人爽超碰91 | 国产精品一区二区免费 | 99精品视频免费观看 | 香蕉视频免费看 | 精品久久久国产 | 999视频在线播放 | 国产精品精品国产婷婷这里av | 色婷婷www | 国产精品毛片完整版 | 最近日韩免费视频 | 91网站在线视频 | 最近高清中文字幕在线国语5 | 9999激情 | 青青河边草免费视频 | 日韩免费av在线 | 久草国产视频 | 中文字幕电影一区 | 日韩欧美视频二区 | 四虎8848免费高清在线观看 | 久久精品综合网 | 伊人天堂网| 成年人在线观看视频免费 | 亚洲闷骚少妇在线观看网站 | www.五月婷婷.com | 一区三区视频 | 中文字幕日韩一区二区三区不卡 | 欧美日韩精品在线观看视频 | 欧美大香线蕉线伊人久久 | 午夜久久久精品 | 91片网| 久久综合亚洲鲁鲁五月久久 | 天堂av免费观看 | 看片一区二区三区 | 99人久久精品视频最新地址 | 五月婷婷综合在线视频 | 色多多视频在线 | 国产精品久久久久三级 | 亚洲成人网av | 欧美不卡视频在线 | 精品国产一区二区三区日日嗨 | 在线观看一区二区视频 | 中文字幕在线观看完整 | 久久久久国产精品午夜一区 | 天天艹天天干天天 | 婷婷在线免费观看 | 久久久久久网址 | 中文字幕免费 | 99热精品在线 | 久久久www成人免费精品张筱雨 | 亚洲欧美视频在线播放 | 久99久精品视频免费观看 | 500部大龄熟乱视频 欧美日本三级 | 免费成人av在线 | 黄av免费在线观看 | 四虎影视成人精品国库在线观看 | 欧美日韩国产一二 | 91在线视频播放 | 天天操福利视频 | 丝袜美腿在线播放 | av免费在线观看1 | 国产一区免费观看 | 一级a性色生活片久久毛片波多野 | 又黄又刺激视频 | 午夜av一区二区三区 | 日本 在线 视频 中文 有码 | 中文字幕丝袜一区二区 | 亚洲电影图片小说 | 超级碰碰碰视频 | 精品久久中文 | 99免费在线观看视频 | 丁香 婷婷 激情 | 亚洲成人精品在线 | 嫩草伊人久久精品少妇av | 国产精品99页 | 激情xxxx| 在线成人免费av | 一级片视频免费观看 | 日本久久久亚洲精品 | 免费成人在线视频网站 | 亚洲黄色免费观看 | 国产97在线观看 | 久久图 | 亚洲美女久久 | 探花视频免费在线观看 | 福利一区二区三区四区 | 久久精品视频在线看 | 久久中国精品 | 欧美激情第一页xxx 午夜性福利 | 伊人色综合久久天天网 | 国产精品剧情在线亚洲 | 日韩成人精品一区二区三区 | 四虎亚洲精品 | www免费黄色 | 国内精品免费久久影院 | 国产黄色免费电影 | 在线免费国产视频 | 色婷婷综合久色 | 亚洲精品综合欧美二区变态 | 99久久www免费 | 天天夜夜狠狠操 | 国产美女视频 | 精品国产成人在线影院 | 日韩一级电影在线观看 | 国产人在线成免费视频 | 日韩理论片在线观看 | 国产精品久久艹 | 91亚洲精品久久久蜜桃网站 | 国产二区视频在线 | 五月婷婷六月综合 | 干综合网 | 精品九九九九 | 在线免费观看黄色 | 蜜臀久久99精品久久久无需会员 | www.com久久久| 四虎影视成人精品 | 日韩黄色av网站 | 日韩xxxbbb | 99超碰在线播放 | 国内精品久久久久 | 91麻豆精品国产91 | 国产精品美女久久久 | 午夜久久视频 | 国产性xxxx| 992tv又爽又黄的免费视频 | 亚洲最新毛片 | 国产成人精品综合久久久久99 | 99久久婷婷国产综合亚洲 | www.久久色 | av电影免费在线看 | 三上悠亚一区二区在线观看 | 天天操夜夜曰 | 国产一区在线视频播放 | 天天操天天射天天爽 | 美女精品在线观看 | 久久婷婷激情 | 久久国产美女视频 | 久久综合九色综合97_ 久久久 | 99热在线这里只有精品 | 久久激情五月激情 | 亚洲精品激情 | 国产成人一区二区三区影院在线 | 久久99最新地址 | 在线观看韩国av | 亚洲日日日 | 首页国产精品 | 亚洲电影毛片 | 日日干天天射 | 欧美a影视| 久久久激情网 | 在线观看中文字幕dvd播放 | 九九久久在线看 | 在线视频观看成人 | 成全免费观看视频 | 欧美日韩亚洲精品在线 | 免费69视频 | 在线电影a | 在线97| 激情五月五月婷婷 | 中文字幕免费观看全部电影 | 天天天天色射综合 | 日本视频不卡 | 国产视频一区在线免费观看 | 97在线观视频免费观看 | 亚洲精品系列 | 亚洲人成在线观看 | 日精品| 国产黄免费看 | 激情综合啪 | 激情视频一区二区三区 | 国产自产高清不卡 | 日韩精品播放 | 亚洲高清不卡av | 精品资源在线 | 韩日精品中文字幕 | 伊色综合久久之综合久久 | 观看免费av | www.亚洲视频.com | 99精品国产在热久久下载 | 久久精品精品电影网 | 国产成人精品一区二区在线 | 波多野结衣亚洲一区二区 | 欧美日韩视频在线观看一区二区 | 国产伦理久久 | 激情综合中文娱乐网 | 97视频免费 | 99中文字幕在线观看 | 欧美精品亚洲二区 | 国产精品久久久久久一区二区 | 人人玩人人添人人 | 77国产精品 | 久久精品国产免费 | av电影不卡在线 | 国产小视频你懂的在线 | 在线观看视频h | 超碰公开在线 | h网站免费在线观看 | 最近日韩中文字幕中文 | 久久久网址 | 欧美色图东方 | 久久久久欧美精品999 | 久久久麻豆视频 | 99精品视频在线观看视频 | 最新国产福利 | 欧美一级片免费 | 久久99精品国产麻豆宅宅 | 国产第一页精品 | 免费福利小视频 | 久久精品国产v日韩v亚洲 | 国产91免费在线观看 | 精品一区二区久久久久久久网站 | 国产成人黄色 | 精品uu| 西西人体www444 | 一区二区久久久久 | 婷婷六月综合亚洲 | 日韩av影视| 91免费版成人 | 日韩视频一区二区三区在线播放免费观看 | 97电影在线看视频 | 亚洲伊人天堂 | 2021av在线| 一区二区三区久久 | 亚洲欧美国产精品18p | 久久国内精品 | 黄色软件在线看 | 天天干天天草天天爽 | a'aaa级片在线观看 | 日韩成人看片 | 日韩欧美国产精品 | 国产美女永久免费 | 国产涩涩网站 | 99久久超碰中文字幕伊人 | 欧美日韩在线电影 | 日本最新高清不卡中文字幕 | 久久成人麻豆午夜电影 | 在线视频中文字幕一区 | 久久精品一区二 | 久久久99精品免费观看app | 西西44人体做爰大胆视频 | 欧美一区,二区 | 国产va饥渴难耐女保洁员在线观看 | 在线激情网 | 97**国产露脸精品国产 | 97碰碰视频 | 国产精品久久久久久久久搜平片 | 日本最大色倩网站www | 中文字幕在线成人 | 国产精品涩涩屋www在线观看 | 超碰在线1 | 成人av地址| 婷婷日 | 国产伦精品一区二区三区照片91 | 黄p在线播放 | 黄色毛片视频免费 | 日韩精品视频在线观看网址 | 毛片激情永久免费 | 欧美成人影音 | 日韩专区av | 欧洲一区精品 | 欧美色婷 | 国产精品综合久久久久久 | 探花国产在线 | 国产大陆亚洲精品国产 | 国产亚洲精品久久久久久久久久久久 | 成人av地址 | 日韩欧美在线中文字幕 | 午夜精品剧场 | 九九综合九九 | 天天综合狠狠精品 | 免费h精品视频在线播放 | 网站在线观看日韩 | 黄色三级在线看 | 91精品麻豆 | 成人在线观看av | 制服丝袜一区二区 | 欧美日韩国产一二三区 | 99久久久国产精品免费99 | 一区中文字幕在线观看 | 香蕉国产91 | 人操人| 亚洲综合色丁香婷婷六月图片 | 深爱五月激情网 | 在线观看mv的中文字幕网站 | 青青看片| 日韩在线观看网站 | 久久久国产精品视频 | 亚洲国内精品视频 | 国产美女免费视频 | 欧美在线观看视频一区二区 | 九九精品在线观看 | 中文字幕精品一区二区三区电影 | 免费日韩一区二区三区 | 91麻豆文化传媒在线观看 | 久久99久久99精品中文字幕 | 麻豆视屏 | 欧美日韩国产一区二区三区 | 亚洲精品99 | 成人97人人超碰人人99 | 色婷婷激情 | 天天操欧美 | 日韩av不卡在线播放 | 三级av免费观看 | 亚洲免费a | 经典三级一区 | 欧美午夜剧场 | 最新精品视频在线 | 少妇精品久久久一区二区免费 | 国产玖玖精品视频 | 国产日韩精品欧美 | 色.com| 亚洲一级黄色大片 | 亚洲午夜精品久久久久久久久久久久 | 国产午夜免费视频 | 色福利网 | 97成人免费| 99热最新网址 | 狠狠狠狠狠色综合 | 国产资源精品 | 综合久久精品 | 亚洲一级二级三级 | 精品美女视频 | 国产一区二区手机在线观看 | 很污的网站| av在线电影网站 | 福利视频一区二区 | av资源网在线播放 | 国产不卡高清 | 综合网av| 国产精品一区二区av影院萌芽 | 美女黄频网站 | 国产精品久久 | av免费看电影 | 成人网在线免费视频 | 欧美日韩不卡在线视频 | 美女网站视频免费都是黄 | 成人h在线观看 | 99精品视频网站 | www.在线观看av | 久久久久久国产精品 | 久久看片网站 | 97在线影院 | 欧美日韩视频免费 | 91久久黄色 | 国产美女主播精品一区二区三区 | 免费观看一级视频 | 国产剧情亚洲 | 日本丶国产丶欧美色综合 | 亚洲一区尤物 | 国产一区视频在线 | 在线观看免费版高清版 | 久久国产女人 | 成人网色 | 天堂在线视频免费观看 | 日日弄天天弄美女bbbb | 日韩精品一区二区三区不卡 | 久久美女免费视频 | 久久免费观看少妇a级毛片 久久久久成人免费 | 91一区在线观看 | 色香蕉网 | 国产精品欧美久久 | 日产乱码一二三区别免费 | 色中文字幕在线观看 | 91成人精品一区在线播放69 | 久久这里只有精品首页 | 午夜久久成人 | 国产喷水在线 | 婷婷色在线播放 | 免费国产一区二区视频 | 制服丝袜在线 | 亚洲免费不卡 | 91视频电影 | 91黄在线看 | 久久视频国产精品免费视频在线 | 人人要人人澡人人爽人人dvd | 久草资源在线观看 | 97电影网手机版 | 免费特级黄毛片 | 黄色成人影视 | 日本免费久久高清视频 | 国产日韩精品一区二区三区在线 | 色婷婷狠狠操 | 韩国三级一区 | 91成人欧美 | 在线播放 日韩专区 | 久久er99热精品一区二区三区 | 最近2019好看的中文字幕免费 | 久久久人人人 | 日本中文字幕网址 | 伊人首页 | 亚洲激情国产精品 | 久精品视频在线观看 | 国产日韩在线视频 | 香蕉影视在线观看 | 欧美日韩高清一区二区 国产亚洲免费看 | av黄网站 | 国产手机视频精品 | 国产麻豆精品95视频 | 精品人人人 | 一区二区三区手机在线观看 | 狠狠色丁香久久婷婷综合五月 | 亚洲中字幕 | 波多野结衣在线观看一区二区三区 | 免费男女羞羞的视频网站中文字幕 | 日韩亚洲在线观看 | 亚洲免费小视频 | av成人黄色 | 欧美另类性 | 99久久久国产免费 | 欧美国产一区二区 | 视频一区视频二区在线观看 | 91看片在线观看 | 狠狠干五月天 | 一区二区三区在线观看免费 | 国产又黄又爽又猛视频日本 | 久热av在线 | 天天天天天天干 | 91香蕉视频在线下载 | 国产精品网红直播 | 国产99久久久国产精品免费二区 | 高清av免费看 | 日韩一区二区三区高清在线观看 | 韩国精品视频在线观看 | 波多野结衣电影一区二区 | 狠狠色噜噜狠狠狠狠2021天天 | 日韩欧美一级二级 | 偷拍久久久 | 开心激情五月婷婷 | 国产精品久久久久久麻豆一区 | 久久精品区| 麻豆高清免费国产一区 | 911久久| 日日干美女 | 日本午夜在线亚洲.国产 | 成人毛片一区二区三区 | 伊香蕉大综综综合久久啪 | 色婷婷综合久久久久 | 欧美一进一出抽搐大尺度视频 | 成人免费观看在线视频 | 一区二区三区在线观看中文字幕 | 网址你懂的在线观看 | 成人在线一区二区三区 | 欧美日韩电影在线播放 | 国产亚洲视频在线观看 | 中文字幕日韩精品有码视频 | 天天天干天天射天天天操 | 免费黄色在线播放 | 99成人免费视频 | 91丨九色丨蝌蚪丨老版 | 91手机电影| 国产aaa毛片| 免费在线观看av网站 | 四虎在线观看精品视频 | 色老板在线 | 在线天堂8√ | 色操插| 天天操,夜夜操 | 日韩在线播放av | 亚洲最新av网址 | 国产一卡二卡在线 | 成人一区二区在线 | 男女视频久久久 | 亚洲综合婷婷 | 超碰个人在线 | 免费在线观看亚洲视频 | 一区二区三区四区在线 | av播放在线| 色综合久久精品 | 欧美亚洲免费在线一区 | 亚洲精品在线视频观看 | 亚洲精品在线观看免费 | 久久成人免费视频 | 国产精品久久久久久久婷婷 | 亚洲精品视频大全 | 9999精品视频 | 天天摸日日摸人人看 | 五月天亚洲精品 | 免费视频一二三区 | 91av中文字幕 | 美国人与动物xxxx | 人人干天天干 | 成人亚洲精品国产www | 欧美一区免费观看 | 久久曰视频| 国产精品免费观看久久 | 亚洲视频 一区 | 天天色天天操天天爽 | 日本中文字幕在线电影 | av免费看av| 久久久免费精品国产一区二区 | 色资源网免费观看视频 | 亚洲区精品 | 免费在线激情电影 | 日韩无在线 | 精品一区二区在线免费观看 | 免费成人黄色 | 黄色免费网站 | 国产一区二区久久精品 | 国产一区二区在线播放视频 | 亚洲精品在线视频播放 | 操久在线 | 精品久久久亚洲 | 国产手机视频在线播放 | 亚洲最新av网站 | 成人毛片一区二区三区 | 超级碰碰免费视频 | 国产精品久一 | 国产裸体视频bbbbb | 免费在线观看日韩 | 久草免费在线视频 | av一二三区 | 在线观看视频国产一区 | 婷婷久久综合网 | 久久久久久久久国产 | 欧美色综合久久 | 国产黄色av网站 | 国产精品手机视频 | 高清av影院| 午夜视频在线观看一区二区 | 日韩视频在线不卡 | 91精品国产综合久久久久久久 | 精品爱爱| 久久久久免费观看 | a黄色大片 | 免费人成网ww44kk44 | 日日夜夜狠狠操 | 天天伊人网 | 在线观看免费一级片 | 国产成人av在线影院 | 亚洲日日夜夜 | 九九免费精品视频在线观看 | 欧美日韩一区二区免费在线观看 | 美女久久久久 | 国产成人综合图片 | 91av看片| 亚洲成人网av | 国产视频久 | 久久午夜精品影院一区 | 国产一区二区三区四区大秀 | 99久久99久国产黄毛片 | 99精品免费在线观看 | 国产成人精品一二三区 | 福利视频第一页 | 久久久91精品国产一区二区三区 | 欧美做受高潮 | av中文字幕剧情 | 亚洲理论在线观看 | 最近2019年日本中文免费字幕 | 日女人免费视频 | 国产首页 | 夜夜躁狠狠躁日日躁 | 国产精久久久 | 亚洲一区黄色 | 国模视频一区二区三区 | 欧美日韩精品在线观看视频 | 黄色成人在线观看 | 婷婷色网视频在线播放 | 99视频久 | 婷婷六月综合亚洲 | 伊人宗合| 欧美成人xxxx | 亚洲精品在线观看网站 | 在线观看国产成人av片 | 成人国产一区二区 | 91精品入口 | 免费观看特级毛片 | 69亚洲精品| 天天色天天干天天 | 成人在线播放免费观看 | 免费看国产精品 | 91观看视频 | 麻豆小视频在线观看 | 99re6热在线精品视频 | 国产精品国产亚洲精品看不卡 |