谷粒商城篇章10 -- P262-P291/P295-P310 -- 订单服务(支付)【分布式高级篇七】

发布于:2024-07-05 ⋅ 阅读:(12) ⋅ 点赞:(0)

目录

1 页面环境搭建

1.1 静态资源上传到nginx

1.2 SwitchHosts增加配置

1.3 网关配置

1.4 订单模块基础配置

1.4.1 引入 thymeleaf 依赖

1.4.2 application.yml配置

1.4.3 bootstrap.properties配置

1.4.4 开启nacos注册发现和远程调用

1.5 修改各个页面的静态资源路径

1.6 测试

1.6.1 订单确认页

1.6.2 订单详情页

1.6.3 订单列表页

1.6.4 订单支付页

2 整合SpringSession

2.1 引入依赖

2.2 开启SpringSession

2.3 session数据存储方式配置

2.4 修改订单相关页面同步用户登录信息

2.5 整合效果

3 自定义线程池

3.1 线程池属性配置类

3.2 yml中线程池相关配置

3.3 自定义线程池

4 订单基本概念

4.1 订单中心

4.1.1 订单构成

4.1.1.1 用户信息

4.1.1.2 订单基础信息

4.1.1.3 商品信息

4.1.1.4 优惠信息

4.1.1.5 支付信息

4.1.1.6 物流信息

4.1.2 订单状态

4.2 订单流程

4.2.1 订单创建与支付

4.2.2 逆向流程

5 订单实现

5.1 订单登录拦截

5.2 订单确认页

5.2.1 模型抽取

5.2.1.1 订单确认页Vo

5.2.1.2 订单页用户收货地址Vo

 5.2.1.3 订单页购物项Vo

5.2.2 订单确认页流程

5.2.3 功能实现

5.2.3.1 controller层

5.2.3.2 service层

5.2.3.3 远程调用接口

5.2.2.3.1 远程查询会员所有的收货地址

5.2.2.3.2 远程查询购物车中所有选中的购物项 

5.2.2.3.3 远程查询商品库存信息

5.2.4 Feign远程调用丢失请求头问题

5.2.5 Feign异步情况丢失上下文问题

5.2.6 创建防重令牌

5.2.7 模拟运费效果

5.2.7.1 后端实现

5.2.7.2 前端实现

5.2.8 提交订单

5.2.8.1 封装下单接口入参出参vo

5.2.8.1.1 订单提交接口入参vo

5.2.8.1.2 订单提交接口出参vo

5.2.8.2 原子验证防重令牌

5.2.8.3 创建订单、订单项等信息

5.2.8.4 验价

5.2.8.5 保存订单

5.2.8.6 锁定库存

5.2.8.7 提交订单完整代码

5.2.8.8 提交订单的问题

5.2.8.9 Seata分布式事务

5.2.8.9.1 Windows下安装Seata

5.2.8.9.2 服务端整合Seata(AT模式)

5.2.8.9.3 启用分布式事务

5.2.8.9.4 测试

5.2.8.10 最终一致性库存解锁逻辑:【可靠消息+最终一致性】【库存解锁】

5.2.8.10.1 库存服务整合RabbitMQ,创建交换机、队列、绑定

5.2.8.10.2 监听库存解锁

5.2.8.11 最终一致性库存解锁逻辑:【可靠消息+最终一致性】【定时关单】

5.2.8.11.1 订单服务整合RabbitMQ,创建交换机、队列、绑定

5.2.8.11.2 监听订单定时关单

5.2.8.11.3 订单创建成功,机器卡顿,消息延迟导致库存无法解锁(关单逻辑升级) 

5.2.8.12 消息丢失、积压、重复等解决方案(如何保证消息可靠性?)

5.2.8.12.1 消息丢失

5.2.8.12.2 消息重复

5.2.8.12.3 消息积压

5.3  订单支付页

5.3.1 加密分类

5.3.1.1 对称加密

5.3.1.2 非对称加密

5.3.2 支付宝支付

5.3.2.1 支付宝加密原理

5.3.2.1.1 什么是公钥、 私钥、 加密、 签名和验签

5.3.2.2 支付宝-电脑网站支付Demo测试

5.3.2.2.1 使用支付宝沙箱环境进行测试

5.3.2.2.2 系统默认密钥

5.3.2.2.3 修改Demo中配置AlipayConfig 

5.3.2.2.4 启动Demo测试

5.3.2.3 支付宝支付流程

5.3.3 内网穿透

5.3.3.1 简介

5.3.3.2 使用场景

5.3.3.3 内网穿透的几个常用软件

5.3.3.4 natapp内网穿透

5.3.4 整合支付

5.3.4.1 导入依赖

5.3.4.2 yml配置 

5.3.4.3 支付Vo

5.3.4.4 阿里云支付模板

5.3.4.5 订单支付宝支付接口

5.3.4.6 前端页面修改pay.html

5.3.4.7 支付测试

5.3.5 订单列表页渲染(member服务)

5.3.5.1 静态资源上传到nginx

5.3.5.2 会员服务整合thymeleaf

5.3.5.3 网关配置

5.3.5.4 SwitchHosts添加配置

5.3.5.5 整合SpringSession(登录后才可以查看订单信息)

5.3.5.6 配置拦截器

5.3.5.7 订单支付成功回调页面接口

5.3.5.8 设置支付宝支付成功回调url

5.3.5.9 订单列表页渲染 

5.3.6 接收支付宝异步通知

5.3.6.1 支付宝异步通知信息vo

5.3.6.2 接收支付宝异步通知接口

5.3.6.3 登录拦截器放行异步通知接口

5.3.6.4 设置支付宝异步通知路径

5.3.6.5 设置接收异步通知信息相关日期格式

5.3.6.6 解决订单号长度报错

5.3.7 异步通知内网穿透环境搭建 

5.3.7.1 修改内网穿透隧道配置  

5.3.7.2 修改nginx配置

5.3.8 支付测试

5.3.9 收单

6 接口幂等性 

6.1 什么是幂等性

6.2 哪些情况需要防止

6.3 什么情况下需要幂等

6.4 幂等解决方案

6.4.1 token机制

6.4.2 各种锁机制

6.4.2.1 数据库悲观锁

6.4.2.2 数据库乐观锁

6.4.2.3 业务层分布式锁

6.4.3 各种唯一约束

6.4.3.1 数据库唯一约束

6.4.3.2 redis set 防重

6.4.4 防重表

6.4.5 全局请求唯一id

7 本地事务与分布式事务

7.1 本地事务

7.1.1 事务的基本性质

7.1.2 事务的隔离级别

7.1.3 事务的七种传播行为

7.1.4 SpringBoot 事务关键点

7.1.4.1 事务的自动配置

7.1.4.2 事务的坑 

7.2 分布式事务

7.2.1 为什么有分布式事务

7.2.2 CAP定理与BASE理论

7.2.2.1 CAP定理

7.2.2.2 面临问题

7.2.2.3 BASE理论

7.2.2.4 强一致性、弱一致性、最终一致性

7.2.3 分布式事务的几种方案

7.2.3.1 2PC模式

7.2.3.2 柔性事务-TCC事务补偿型方案(手动补偿)

7.2.3.3 柔性事务-最大努力通知型方案

7.2.3.4 柔性事务-可靠消息+最终一致性方案(异步确保型)


1 页面环境搭建

1.1 静态资源上传到nginx

等待付款 -》detail

订单页    -》list

结算页    -》confirm

收银页    -》pay

1.2 SwitchHosts增加配置

添加订单服务的域名与ip映射:xxx.xxx.11.10 order.gulimall.com 

1.3 网关配置

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

- id: gulimall_order_route
  uri: lb://gulimall-order
  predicates:
    # 由以下的主机域名访问转发到订单服务
    - Host=order.gulimall.com

1.4 订单模块基础配置

1.4.1 引入 thymeleaf 依赖

gulimall-order/pom.xml

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

1.4.2 application.yml配置

开启nacos注册发现、关闭thymeleaf缓存。

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

spring:
  cloud:
    nacos:
      discovery:
        server-addr: xxx.xxx.xxx.10:8848
  thymeleaf:
    cache: false

1.4.3 bootstrap.properties配置

服务名、开启nacos配置中心(可以不开启)。

gulimall-order/src/main/resources/bootstrap.properties

spring.application.name=gulimall-order
spring.cloud.nacos.config.server-addr=xxx.xxx.xxx.10:8848
spring.cloud.nacos.config.namespace=de91d4bf-xxxx-xxxx-b095-d8ac87337d8a

1.4.4 开启nacos注册发现和远程调用

主类开启注册发现、远程调用。

@EnableFeignClients // 远程调用
@EnableDiscoveryClient // 注册发现

1.5 修改各个页面的静态资源路径

1. confirm.html

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

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

2. detail.html

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

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

3. list.html

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

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

4. pay.html

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

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

1.6 测试

测试代码

gulimall-order/src/main/java/com/wen/gulimall/order/web/HelloController.java

@Controller
public class HelloController {

    /**
     * 测试订单相关页面访问
     * @param page
     * @return
     */
    @GetMapping("/{page}.html")
    public String listPage(@PathVariable String page){

        return page;
    }
}

1.6.1 订单确认页

注意:如果页面展示不全,删除确认页面注释代码中的 /*。

http://order.gulimall.com/confirm.html

1.6.2 订单详情页

 http://order.gulimall.com/detail.html

1.6.3 订单列表页

http://order.gulimall.com/list.html

1.6.4 订单支付页

http://order.gulimall.com/pay.html

2 整合SpringSession

注意: SpringSession的配置类GulimallSessionConfig.java在公共模块(gulimall-common)已经配置,这里购物车模块直接引入公共模块即可。

公共模块(gulimall-common)可以参考我之前的博客:谷粒商城篇章7 ---- P211-P235 ---- 认证服务【分布式高级篇四】_谷粒商城p235-CSDN博客

2.1 引入依赖

<!--	整合SpringSession完成session共享问题	-->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<!--   redis     -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
        <exclusions>
            <exclusion>
                <groupId>io.lettuce</groupId>
                <artifactId>lettuce-core</artifactId>
            </exclusion>
        </exclusions>
</dependency>

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

2.2 开启SpringSession

gulimall-order/src/main/java/com/wen/gulimall/order/GulimallOrderApplication.java

主类上添加以下注解:

@EnableRedisHttpSession

2.3 session数据存储方式配置

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

spring:
  redis:
    host: 172.1.11.10
  session:
    store-type: redis

2.4 修改订单相关页面同步用户登录信息

1.订单详情页detail.html的134-140行,修改如下:

<li style="width: 100px">
    <a href="" th:if="${session.loginUser}!=null">欢迎:[[${session.loginUser==null?"":session.loginUser.nickname}]]</a>
    <a href="http://auth.gulimall.com/login.html" th:if="${session.loginUser}==null">欢迎,请登录</a>
</li>
<li th:if="${session.loginUser}==null">
    <a href="http://auth.gulimall.com/reg.html" class="li_2">免费注册</a>
</li>

2.订单确认页confirm.html的78行,修改如下:
<li>[[${session.loginUser==null?"":session.loginUser.nickname}]]<img src="/static/order/confirm/img/03.png" style="margin-bottom: 0px;margin-left3: 3px;" /><img src="/static/order/confirm/img/06.png" /></li>

3.订单支付页pay.html的14行,修改如下:
<li><span>[[${session.loginUser==null?"":session.loginUser.nickname}]]</span><span>退出</span></li>

2.5 整合效果

登录成功后用户信息共享,如下:

3 自定义线程池

3.1 线程池属性配置类

gulimall-order/src/main/java/com/wen/gulimall/order/config/ThreadPoolConfigProperties.java

@Data
@Component
@ConfigurationProperties(prefix = "gulimall.thread")
public class ThreadPoolConfigProperties {
    private Integer coreSize;
    private Integer maxSize;
    private Integer keepAliveTime;
}

3.2 yml中线程池相关配置

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

3.3 自定义线程池

gulimall-order/src/main/java/com/wen/gulimall/order/config/MyThreadConfig.java

@Configuration
public class MyThreadConfig {

    @Bean
    public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties poolProperties){
        return new ThreadPoolExecutor(poolProperties.getCoreSize(),
                poolProperties.getMaxSize(),
                poolProperties.getKeepAliveTime(),
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(100000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

    }
}

4 订单基本概念

4.1 订单中心

电商系统涉及到 3 流, 分别时信息流, 资金流, 物流, 而订单系统作为中枢将三者有机的集
合起来。
订单模块是电商系统的枢纽, 在订单这个环节上需求获取多个模块的数据和信息, 同时对这
些信息进行加工处理后流向下个环节, 这一系列就构成了订单的信息流通。

4.1.1 订单构成

4.1.1.1 用户信息

        用户信息包括用户账号、 用户等级、 用户的收货地址、 收货人、 收货人电话等组成, 用户账户需要绑定手机号码, 但是用户绑定的手机号码不一定是收货信息上的电话。 用户可以添加多个收货信息, 用户等级信息可以用来和促销系统进行匹配, 获取商品折扣, 同时用户等级还可以获取积分的奖励等。

4.1.1.2 订单基础信息

        订单基础信息是订单流转的核心, 其包括订单类型、 父/子订单、 订单编号、 订单状态、 订单流转的时间等。

(1) 订单类型包括实体商品订单和虚拟订单商品等, 这个根据商城商品和服务类型进行区分。

(2) 同时订单都需要做父子订单处理, 之前在初创公司一直只有一个订单, 没有做父子订单处理后期需要进行拆单的时候就比较麻烦, 尤其是多商户商场, 和不同仓库商品的时候,父子订单就是为后期做拆单准备的。

(3) 订单编号不多说了, 需要强调的一点是父子订单都需要有订单编号, 需要完善的时候可以对订单编号的每个字段进行统一定义和诠释。

(4) 订单状态记录订单每次流转过程, 后面会对订单状态进行单独的说明。

(5) 订单流转时间需要记录下单时间, 支付时间, 发货时间, 结束时间/关闭时间等等

4.1.1.3 商品信息

        商品信息从商品库中获取商品的 SKU 信息、 图片、 名称、 属性规格、 商品单价、 商户信息等, 从用户下单行为记录的用户下单数量, 商品合计价格等。

4.1.1.4 优惠信息

        优惠信息记录用户参与的优惠活动, 包括优惠促销活动, 比如满减、 满赠、 秒杀等, 用户使用的优惠券信息, 优惠券满足条件的优惠券需要默认展示出来, 具体方式已在之前的优惠券篇章做过详细介绍, 另外还虚拟币抵扣信息等进行记录。

为什么把优惠信息单独拿出来而不放在支付信息里面呢?

因为优惠信息只是记录用户使用的条目, 而支付信息需要加入数据进行计算, 所以做为区分。

4.1.1.5 支付信息

(1) 支付流水单号, 这个流水单号是在唤起网关支付后支付通道返回给电商业务平台的支付流水号, 财务通过订单号和流水单号与支付通道进行对账使用。

(2) 支付方式用户使用的支付方式, 比如微信支付、 支付宝支付、 钱包支付、 快捷支付等。支付方式有时候可能有两个——余额支付+第三方支付。

(3) 商品总金额, 每个商品加总后的金额; 运费, 物流产生的费用; 优惠总金额, 包括促销活动的优惠金额, 优惠券优惠金额, 虚拟积分或者虚拟币抵扣的金额, 会员折扣的金额等之和; 实付金额, 用户实际需要付款的金额。

        用户实付金额=商品总金额+运费-优惠总金额

4.1.1.6 物流信息

        物流信息包括配送方式, 物流公司, 物流单号, 物流状态, 物流状态可以通过第三方接口来获取和向用户展示物流每个状态节点。

4.1.2 订单状态

1. 待付款
        用户提交订单后, 订单进行预下单, 目前主流电商网站都会唤起支付, 便于用户快速完成支
付, 需要注意的是待付款状态下可以对库存进行锁定, 锁定库存需要配置支付超时时间, 超时后将自动取消订单, 订单变更关闭状态。
2. 已付款/待发货
        用户完成订单支付, 订单系统需要记录支付时间, 支付流水单号便于对账, 订单下放到 WMS系统, 仓库进行调拨, 配货, 分拣, 出库等操作。
3. 待收货/已发货
        仓储将商品出库后, 订单进入物流环节, 订单系统需要同步物流信息, 便于用户实时知悉物
品物流状态
4. 已完成
        用户确认收货后, 订单交易完成。 后续支付侧进行结算, 如果订单存在问题进入售后状态
5. 已取消
        付款之前取消订单。 包括超时未付款或用户商户取消订单都会产生这种订单状态。
6. 售后中
        用户在付款后申请退款, 或商家发货后用户申请退换货。
        售后也同样存在各种状态, 当发起售后申请后生成售后订单, 售后订单状态为待审核, 等待
商家审核, 商家审核通过后订单状态变更为待退货, 等待用户将商品寄回, 商家收货后订单状态更新为待退款状态, 退款到用户原账户后订单状态更新为售后成功。

4.2 订单流程

        订单流程是指从订单产生到完成整个流转的过程, 从而行程了一套标准流程规则。 而不同的产品类型或业务类型在系统中的流程会千差万别, 比如上面提到的线上实物订单和虚拟订单的流程, 线上实物订单与 O2O 订单等, 所以需要根据不同的类型进行构建订单流程。

        不管类型如何订单都包括正向流程和逆向流程, 对应的场景就是购买商品和退换货流程, 正向流程就是一个正常的网购步骤: 订单生成–>支付订单–>卖家发货–>确认收货–>交易成功。而每个步骤的背后, 订单是如何在多系统之间交互流转的, 可概括如下图

4.2.1 订单创建与支付

  1. 订单创建前需要预览订单, 选择收货信息等    
  2. 订单创建需要锁定库存, 库存有才可创建, 否则不能创建
  3. 订单创建后超时未支付需要解锁库存
  4. 支付成功后, 需要进行拆单, 根据商品打包方式, 所在仓库, 物流等进行拆单
  5. 支付的每笔流水都需要记录, 以待查账
  6. 订单创建, 支付成功等状态都需要给 MQ 发送消息, 方便其他系统感知订阅

4.2.2 逆向流程

  1. 修改订单, 用户没有提交订单, 可以对订单一些信息进行修改, 比如配送信息,优惠信息,及其他一些订单可修改范围的内容, 此时只需对数据进行变更即可。
  2. 订单取消, 用户主动取消订单和用户超时未支付, 两种情况下订单都会取消订单, 而超时情况是系统自动关闭订单, 所以在订单支付的响应机制上面要做支付的限时处理, 尤其是在前面说的下单减库存的情形下面, 可以保证快速的释放库存。另外需要需要处理的是促销优惠中使用的优惠券, 权益等视平台规则, 进行相应补回给用户。
  3. 退款, 在待发货订单状态下取消订单时, 分为缺货退款和用户申请退款。 如果是全部退款则订单更新为关闭状态, 若只是做部分退款则订单仍需进行进行, 同时生成一条退款的售后订单, 走退款流程。 退款金额需原路返回用户的账户。
  4. 发货后的退款, 发生在仓储货物配送, 在配送过程中商品遗失, 用户拒收, 用户收货后对商品不满意, 这样情况下用户发起退款的售后诉求后, 需要商户进行退款的审核, 双方达成一致后, 系统更新退款状态, 对订单进行退款操作, 金额原路返回用户的账户, 同时关闭原订单数据。 仅退款情况下暂不考虑仓库系统变化。 如果发生双方协调不一致情况下, 可以申请平台客服介入。 在退款订单商户不处理的情况下, 系统需要做限期判断, 比如 5 天商户不处理, 退款单自动变更同意退款 。

5 订单实现

5.1 订单登录拦截

        订单系统必然会涉及到用户相关信息,所以进入订单系统的所有请求必须是已登录的状态下。这里编写用户登录拦截器,对未登录情况下的订单请求进行拦截。

自定义拦截器

gulimall-order/src/main/java/com/wen/gulimall/order/interceptor/LoginUserInterceptor.java

/**
 * @author W
 * @createDate 2024/02/27
 * @description: 登录拦截器
 * 从session中(redis中)获取了登录信息,封装到ThreadLocal
 * 自定义拦截器需要添加到webmvc中,否则不起作用
 */
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
    // 同一个线程共享数据
    private static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
        if(attribute!=null){
            // 登录成功
            loginUser.set(attribute);
            return true;
        }else {
            // 没登录,去登录
            request.getSession().setAttribute("msg","请先进行登录");
            response.sendRedirect("http://auth.gulimall.com/login.html");
            return false;
        }
    }
}

将拦截器添加到webMvc中

gulimall-order/src/main/java/com/wen/gulimall/order/config/OrderWebConfiguration.java

@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {
    @Resource
    private LoginUserInterceptor interceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 订单服务的所有请求都要走登录拦截
        registry.addInterceptor(interceptor).addPathPatterns("/**");
    }
}

5.2 订单确认页

5.2.1 模型抽取

5.2.1.1 订单确认页Vo

gulimall-order/src/main/java/com/wen/gulimall/order/vo/OrderConfirmVo.java

public class OrderConfirmVo {
    // 收货地址
    @Setter @Getter
    private List<MemberAddressVo> address;
    // 所有选中的购物项
    @Setter @Getter
    private List<OrderItemVo> items;
    // 发票信息
    // 优惠券信息...,这里使用积分
    @Setter @Getter
    private Integer integration;

    // 订单的防重令牌
    @Setter @Getter
    private String orderToken;

    @Setter @Getter
    private Map<Long, Boolean> stocks;

    // 商品总数量
    public Integer getCount(){
        Integer i = 0;
        if(items!=null){
            for (OrderItemVo item : items) {
                i+=item.getCount();
            }
        }
        return i;
    }
    // 订单总额
    //private BigDecimal total;

    public BigDecimal getTotal() {
        BigDecimal sum = new BigDecimal("0");
        if(items!=null){
            for (OrderItemVo item : items) {
                BigDecimal multiply = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
                sum = sum.add(multiply);
            }
        }
        return sum;
    }

    // 应付价格
    //private BigDecimal payPrice;

    public BigDecimal getPayPrice() {
        return getTotal();
    }
}
5.2.1.2 订单页用户收货地址Vo

gulimall-order/src/main/java/com/wen/gulimall/order/vo/MemberAddressVo.java

@Data
public class MemberAddressVo {
    private Long id;
    /**
     * member_id
     */
    private Long memberId;
    /**
     * 收货人姓名
     */
    private String name;
    /**
     * 电话
     */
    private String phone;
    /**
     * 邮政编码
     */
    private String postCode;
    /**
     * 省份/直辖市
     */
    private String province;
    /**
     * 城市
     */
    private String city;
    /**
     * 区
     */
    private String region;
    /**
     * 详细地址(街道)
     */
    private String detailAddress;
    /**
     * 省市区代码
     */
    private String areacode;
    /**
     * 是否默认
     */
    private Integer defaultStatus;
}
 5.2.1.3 订单页购物项Vo

gulimall-order/src/main/java/com/wen/gulimall/order/vo/OrderItemVo.java

/**
 * @author W
 * @createDate 2024/02/28
 * @description: 订单确认页的购物项
 * 不需要选中标志
 */
@Data
public class OrderItemVo {
    private Long skuId; // 商品编号
    private String title; // 标题
    private String image;// 图片
    private List<String> skuAttr;// 商品销售属性集合
    private BigDecimal price; // 单价
    private Integer count; // 数量
    private BigDecimal totalPrice;// 总价
    private BigDecimal weight = new BigDecimal("0.085");// 商品重量
}

5.2.2 订单确认页流程

1. 判断是否登录,使用登录拦截器LoginUserInterceptor;

2. 远程获取用户所有的地址列表【gulimall_ums库=》ums_member_receive_address表】,字段default_status值1-表示默认地址,0-非默认地址,默认地址高亮;

3. 远程查询购物车中所有的购物项,【gulimall-cart调用商品服务,获取购物项的最新价格】;

4. 订单总额,根据购物项的价格和数量计算,然后求和;

5. 应付价格【暂时跟订单总额一样】,实际总额要减去优惠价格等;

6. 远程查询库存服务;

7. 根据选中的地址,通过ajax请求调用/ware/wareinfo/fare?addrId=addrIdValue接口获取运费信息和地址信息;

8. 防重令牌(幂等性:提交一次和多次的效果是一样的)防止订单重复提交,数据库只保存一条订单信息。

5.2.3 功能实现

5.2.3.1 controller层

gulimall-order/src/main/java/com/wen/gulimall/order/web/OrderWebController.java

@Controller
public class OrderWebController {
    @Resource
    private OrderService orderService;

    @GetMapping("/toTrade")
    public String toTrade(Model model, HttpServletRequest request) throws ExecutionException, InterruptedException {
        OrderConfirmVo orderConfirmVo = orderService.confirmOrder();
        model.addAttribute("orderConfirmData",orderConfirmVo);
        return "confirm";
    }

}
5.2.3.2 service层

 gulimall-order/src/main/java/com/wen/gulimall/order/service/OrderService.java

public interface OrderService extends IService<OrderEntity> {

    ...

    /**
     * 订单确认页返回需要的数据
     * @return
     */
    OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException;
}

 gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java

业务流程,如下:

(1)获取登录用户信息;

(2)远程查询登录用户所有的收货地址列表;

(3)远程查询购物车中所有的购物项列表;

(4)远程查询商品库存信息;

(5)查询用户积分;

(6)价格等信息自动计算;

(7)防重令牌(幂等性),防止订单重复提交。

@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {

    @Resource
    private MemberFeignService memberFeignService;
    @Resource
    private CartFeignService cartFeignService;
    @Resource
    private WmsFeignService wmsFeignService;
    @Resource
    private ThreadPoolExecutor executor;
   
    ...

   @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();

        // 获取之前的请求
        // RequestContextHolder是同一个线程共享请求数据
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            // 每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            // 1. 远程查询会员所有的收货地址
            List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
            orderConfirmVo.setAddress(address);
        }, executor);

        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes);
            // 2. 远程查询购物车中所有选中的购物项
            List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
            orderConfirmVo.setItems(currentUserCartItems);
            // feign在远程调用之前需要构造新的请求,调用很多拦截器
            // RequestInterceptor interceptor : RequestInterceptors
        }, executor).thenRunAsync(()->{
            // 远程查询库存
            List<OrderItemVo> items = orderConfirmVo.getItems();
            List<Long> collect = items.stream().map(OrderItemVo::getSkuId).collect(Collectors.toList());
            // 远程调用库存服务
            R skusHasStock = wmsFeignService.getSkusHasStock(collect);
            List<SkuStockVo> data = skusHasStock.getData(new TypeReference<List<SkuStockVo>>() {
            });
            if(data!=null) {
                // 以map的形式显示每个商品是否有库存
                Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                orderConfirmVo.setStocks(map);
            }
        },executor);

        // 3. 查询用户积分
        Integer integration = memberRespVo.getIntegration();
        orderConfirmVo.setIntegration(integration);

        // 4. 其他数据自动计算

        // TODO 5. 放重令牌(幂等性)防止订单重复提交
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        // 防重令牌设置30分钟的过期时间,存放在redis
        stringRedisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberRespVo.getId(),token,30, TimeUnit.MINUTES);
        orderConfirmVo.setOrderToken(token);
        CompletableFuture.anyOf(getAddressFuture, cartFuture).get();

        return orderConfirmVo;
    }
}
5.2.3.3 远程调用接口
5.2.2.3.1 远程查询会员所有的收货地址

1. 模拟数据

2. controller层

gulimall-member/src/main/java/com/wen/gulimall/member/controller/MemberReceiveAddressController.java

@RestController
@RequestMapping("member/memberreceiveaddress")
public class MemberReceiveAddressController {
    @Autowired
    private MemberReceiveAddressService memberReceiveAddressService;

    @GetMapping("/{memberId}/addresses")
    public List<MemberReceiveAddressEntity> getAddress(@PathVariable Long memberId){
        return memberReceiveAddressService.getAddress(memberId);
    }
    
    ...
}

3. service层

gulimall-member/src/main/java/com/wen/gulimall/member/service/MemberReceiveAddressService.java

public interface MemberReceiveAddressService extends IService<MemberReceiveAddressEntity> {

    ...

    List<MemberReceiveAddressEntity> getAddress(Long memberId);
}

gulimall-member/src/main/java/com/wen/gulimall/member/service/impl/MemberReceiveAddressServiceImpl.java 

@Service("memberReceiveAddressService")
public class MemberReceiveAddressServiceImpl extends ServiceImpl<MemberReceiveAddressDao, MemberReceiveAddressEntity> implements MemberReceiveAddressService {

    ...

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

}

4. Feign接口

gulimall-order/src/main/java/com/wen/gulimall/order/feign/MemberFeignService.java

@FeignClient("gulimall-member")
public interface MemberFeignService {
    @GetMapping("/member/memberreceiveaddress/{memberId}/addresses")
    List<MemberAddressVo> getAddress(@PathVariable Long memberId);
}
5.2.2.3.2 远程查询购物车中所有选中的购物项 

1. controller层

gulimall-cart/src/main/java/com/wen/gulimall/cart/controller/CartController.java

@Controller
public class CartController {
    @Resource
    private CartService cartService;

    @ResponseBody
    @GetMapping("/currentUserCartItems")
    public List<CartItemVo> getCurrentUserCartItems(){
        return cartService.getUserCartItems();
    }

    ...
}

2. service层

gulimall-cart/src/main/java/com/wen/gulimall/cart/service/CartService.java

public interface CartService {

    ...

    List<CartItemVo> getUserCartItems();
}

gulimall-cart/src/main/java/com/wen/gulimall/cart/service/impl/CartServiceImpl.java

@Service
public class CartServiceImpl implements CartService {
    
    @Resource
    private ProductFeignService productFeignService;
    @Resource
    private ThreadPoolExecutor executor;

    ...

    @Override
    public List<CartItemVo> getUserCartItems() {
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        if(userInfoTo.getUserId()==null){
            return null;
        }else {
            String cartKey = CartConstant.CART_PREFIX + userInfoTo.getUserId();
            List<CartItemVo> cartItems = getCartItems(cartKey);
            // 获取所有被选中的购物项
            List<CartItemVo> collect = cartItems.stream()
                    .filter(item -> item.getCheck())
                    .map(item ->{
                        BigDecimal price = productFeignService.getPrice(item.getSkuId());
                        // TODO 更新为最新价格
                        item.setPrice(price);
                        return item;
                    })
                    .collect(Collectors.toList());
            return collect;
        }
    }
}

3. Feign接口

 gulimall-order/src/main/java/com/wen/gulimall/order/feign/CartFeignService.java

@FeignClient("gulimall-cart")
public interface CartFeignService {
    @GetMapping("/currentUserCartItems")
    List<OrderItemVo> getCurrentUserCartItems();
}

4. 远程调用商品服务查询商品的最新价格

gulimall-product/src/main/java/com/wen/gulimall/product/app/SkuInfoController.java

@RestController
@RequestMapping("product/skuinfo")
public class SkuInfoController {
    @Autowired
    private SkuInfoService skuInfoService;

    /**
     * 订单确认页查询商品此时的价格
     * @param skuId
     * @return
     */
    @GetMapping("/{skuId}/price")
    public BigDecimal getPrice(@PathVariable Long skuId){
        SkuInfoEntity byId = skuInfoService.getById(skuId);
        return byId.getPrice();
    }

    ...
}

gulimall-cart/src/main/java/com/wen/gulimall/cart/feign/ProductFeignService.java 

@FeignClient("gulimall-product")
public interface ProductFeignService {

    ...

    @GetMapping("/product/skuinfo/{skuId}/price")
    BigDecimal getPrice(@PathVariable Long skuId);
}
5.2.2.3.3 远程查询商品库存信息

1. 库存vo

gulimall-order/src/main/java/com/wen/gulimall/order/vo/SkuStockVo.java

@Data
public class SkuStockVo {
    private Long skuId;
    private Boolean hasStock;
}

2. controller层

gulimall-ware/src/main/java/com/wen/gulimall/ware/controller/WareSkuController.java

@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
    @Autowired
    private WareSkuService wareSkuService;

    @PostMapping("/hasstock")
    public R getSkusHasStock(@RequestBody List<Long> skuIds){
        // sku_id, stock
        List<SkuHasStockVo> vos = wareSkuService.getSkusHasStock(skuIds);
        R ok = R.ok();
        ok.setData(vos);
        return ok;
    }
    
    ...
}

3. service层

gulimall-ware/src/main/java/com/wen/gulimall/ware/service/WareSkuService.java

public interface WareSkuService extends IService<WareSkuEntity> {

   
    ...

    List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds);
}

gulimall-ware/src/main/java/com/wen/gulimall/ware/service/impl/WareSkuServiceImpl.java 

@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {
    
    ...

    @Override
    public List<SkuHasStockVo> getSkusHasStock(List<Long> skuIds) {
        List<SkuHasStockVo> collect = skuIds.stream().map(skuId -> {
            SkuHasStockVo skuHasStockVo = new SkuHasStockVo();

            // 查询是否有库存 = (库存数-锁定库存)> 0
            Long count = this.baseMapper.getSkuStock(skuId);
            skuHasStockVo.setSkuId(skuId);
            skuHasStockVo.setHasStock(count==null?false:count > 0);
            return skuHasStockVo;
        }).collect(Collectors.toList());
        return collect;
    }

}

4. Feign接口

gulimall-order/src/main/java/com/wen/gulimall/order/feign/WmsFeignService.java

@FeignClient("gulimall-ware")
public interface WmsFeignService {

    @PostMapping("/ware/waresku/hasstock")
    R getSkusHasStock(@RequestBody List<Long> skuIds);
}

5.2.4 Feign远程调用丢失请求头问题

问题产生原因:通过feign进行远程调用时,会创建一个新的RequestTemplate,没有请求头,Cookie信息没有了,导致在远程调用cart服务时,购物车拦截器无法从session中获取登录信息,无法获取userId。

解决方案:将老请求头的信息同步给新请求头。

原理:feign进行远程调用时会创建新的请求,然后调用很多拦截器(debug了解原理),我们可以自定义拦截器设置请求头。

gulimall-order/src/main/java/com/wen/gulimall/order/config/GuliFeignConfig.java

/**
 * @author W
 * @createDate 2024/02/29
 * @description: 解决 Feign远程调用请求头丢失问题
 * 远程调用会创建一个新的请求,新的请求没有请求头
 * 使用RequestInterceptor拦截器为新构建的请求添加请求头
 */
@Configuration
public class GuliFeignConfig {

    @Bean
    public RequestInterceptor requestInterceptor(){
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                // 1.RequestContextHolder拿到刚在进来的请求
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                assert requestAttributes != null;
                HttpServletRequest request = requestAttributes.getRequest(); // 老请求
                if(request!=null) {
                    // 同步请求头数据,Cookie
                    String cookie = request.getHeader("Cookie");
                    // 给构建的新请求同步老请求的Cookie
                    template.header("Cookie", cookie);
                }
            }
        };
    }
}

5.2.5 Feign异步情况丢失上下文问题

产生问题:RequestInterceptor拦截器报空指针异常,获取当前请求上下文的RequestContextHolder类本质上是一个ThreadLocal,同一个线程共享请求数据,异步线程无法共享之前的请求数据。

解决方案:获取主线程的请求数据设置到子线程的请求上下文RequestContextHolder中,如下。

5.2.6 创建防重令牌

        使用令牌机制实现下订单的幂等性,在订单确认页到来之前为订单确认页生成一个令牌,提交订单时带上这个令牌。令牌存在两个地方,服务器一个,页面一个。

        订单幂等性做了两种:防重令牌和数据库订单编号唯一性。

        订单防重复提交=》订单的幂等性,提交一次和多次的效果是一样的。(接口幂等性详解见)

令牌前缀常量

gulimall-order/src/main/java/com/wen/gulimall/order/constant/OrderConstant.java

public class OrderConstant {
    /** 订单确认也防重复提交令牌前缀 */
    public static final String USER_ORDER_TOKEN_PREFIX = "order:token:";
}

 gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java

// TODO 5. 放重令牌(幂等性)防止订单重复提交
String token = UUID.randomUUID().toString().replaceAll("-", "");
// 防重令牌设置30分钟的过期时间,存放在redis
stringRedisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberRespVo.getId(),token,30, TimeUnit.MINUTES);
orderConfirmVo.setOrderToken(token);

 订单表(oms_order),订单编号添加唯一索引。

5.2.7 模拟运费效果

5.2.7.1 后端实现

运费vo

gulimall-ware/src/main/java/com/wen/gulimall/ware/vo/FareVo.java

@Data
public class FareVo {
    private MemberAddressVo address;
    private BigDecimal fare;
}

gulimall-ware/src/main/java/com/wen/gulimall/ware/controller/WareInfoController.java

@RestController
@RequestMapping("ware/wareinfo")
public class WareInfoController {
    @Autowired
    private WareInfoService wareInfoService;

    @GetMapping("/fare")
    public R getFare(@RequestParam("addrId") Long addrId){
        FareVo fare = wareInfoService.getFare(addrId);
        return R.ok().setData(fare);
    }

    ...
}

 gulimall-ware/src/main/java/com/wen/gulimall/ware/service/WareInfoService.java

public interface WareInfoService extends IService<WareInfoEntity> {

   ...

    /**
     * 根据用户的收货地址计算运费
     * @param addrId
     * @return
     */
    FareVo getFare(Long addrId);
}

 gulimall-ware/src/main/java/com/wen/gulimall/ware/service/impl/WareInfoServiceImpl.java

@Service("wareInfoService")
public class WareInfoServiceImpl extends ServiceImpl<WareInfoDao, WareInfoEntity> implements WareInfoService {

    @Resource
    private MemberFeignService memberFeignService;

    ...

    @Override
    public FareVo getFare(Long addrId) {
        FareVo fareVo = new FareVo();
        // 获取地址的详细信息
        R r = memberFeignService.attrInfo(addrId);
        MemberAddressVo data = r.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {
        });
        if(data!=null){
            // 调用第三方运费计算系统
            // 这里截取用户手机号码最后一位作为我们的运费
            String phone = data.getPhone();
            String fare = phone.substring(phone.length() - 1);
            BigDecimal bigDecimal = new BigDecimal(fare);
            fareVo.setFare(bigDecimal);
            fareVo.setAddress(data);

            return fareVo;
        }
        return null;
    }

}

 远程获取地址详细信息

地址vo

gulimall-ware/src/main/java/com/wen/gulimall/ware/vo/MemberAddressVo.java 

@Data
public class MemberAddressVo {
    private Long id;
    /**
     * member_id
     */
    private Long memberId;
    /**
     * 收货人姓名
     */
    private String name;
    /**
     * 电话
     */
    private String phone;
    /**
     * 邮政编码
     */
    private String postCode;
    /**
     * 省份/直辖市
     */
    private String province;
    /**
     * 城市
     */
    private String city;
    /**
     * 区
     */
    private String region;
    /**
     * 详细地址(街道)
     */
    private String detailAddress;
    /**
     * 省市区代码
     */
    private String areacode;
    /**
     * 是否默认
     */
    private Integer defaultStatus;
}

远程调用接口

gulimall-ware/src/main/java/com/wen/gulimall/ware/feign/MemberFeignService.java

@FeignClient("gulimall-member")
public interface MemberFeignService {
    @RequestMapping("/member/memberreceiveaddress/info/{id}")
    R attrInfo(@PathVariable("id") Long id);
}
5.2.7.2 前端实现

5.2.8 提交订单

下单流程:

  • 下单:去创建订单、验令牌、验价格、锁库存...。
  • 提交订单成功,携带返回数据转发到支付选项页。
  • 提交订单失败,携带错误信息重定向到订单确认页,重新确认订单信息。
5.2.8.1 封装下单接口入参出参vo
5.2.8.1.1 订单提交接口入参vo

入参vo

gulimall-order/src/main/java/com/wen/gulimall/order/vo/OrderSubmitVo.java

/**
 * @author W
 * @createDate 2024/03/08
 * @description: 封装订单提交的数据
 */
@Data
public class OrderSubmitVo {
    private Long addrId;// 收货地址的id
    private Integer payType;// 支付方式
    // 无需提交需要购买的商品,去购物车在获取一遍
    // 优惠、发票
    private String orderToken;// 防重令牌
    private BigDecimal payPrice;// 应付价格 验价 可能加入购物车时的价格和提交订单时商品的价格不一样
    private String note;// 订单备注
    // 用户相关信息,直接去session取出登录的用户
}

订单确认页,构造订单提交数据的表单

gulimall-order/src/main/resources/templates/confirm.html

给表单的addrId和应付价payPrice赋值,如下

5.2.8.1.2 订单提交接口出参vo

gulimall-order/src/main/java/com/wen/gulimall/order/vo/SubmitOrderResponseVo.java

@Data
public class SubmitOrderResponseVo {
    private OrderEntity order;
    private Integer code; // 0-成功  下单错误状态码
}
5.2.8.2 原子验证防重令牌

通过redis的Lua脚本保证验证令牌和删除令牌的原子性,防止订单重复提交。

gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java

// 1. 验证令牌【令牌的对比和删除必须保证原子性】传统写法可能会存在重复提交
// 0-令牌失败;1-删除成功
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String orderToken = submitVo.getOrderToken();
// 通过Lua脚本原子验证令牌和删除令牌
Long result = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
if(result==0L){
    // 令牌验证失败
    responseVo.setCode(1);
    return responseVo;
}else {
    // 令牌验证成功
    // 下单:去创建订单,验令牌,验价格,锁库存...
    // 1.创建订单、订单项等信息
    // 2.订单验价
    // 3.保存订单
    // 4.锁定库存
}
5.2.8.3 创建订单、订单项等信息

1. 创建订单

gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java

/**
 * 创建订单
 * @return
 */
private OrderCreateTo createOrder(){
    OrderCreateTo orderCreateTo = new OrderCreateTo();
    // 1. 生成订单号,36位【注意数据库订单表订单号的长度】
    String orderSn = IdWorker.getTimeId();
    // 创建订单
    OrderEntity orderEntity = buildOrder(orderSn);
    // 2. 获取所有的订单项
    List<OrderItemEntity> orderItemEntities = buildOrderItems(orderSn);

    // 3. 验价,计算相关价格、积分等信息
    computePrice(orderEntity,orderItemEntities);

    orderCreateTo.setOrder(orderEntity);
    orderCreateTo.setOrderItems(orderItemEntities);
    return orderCreateTo;
}

2. 根据订单号构建订单

将订单提交vo放到本地线程中便于使用

gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java

远程调用库存服务,获取收货地址和运费信息

gulimall-order/src/main/java/com/wen/gulimall/order/feign/WmsFeignService.java

@FeignClient("gulimall-ware")
public interface WmsFeignService {

    ...

    @GetMapping("/ware/wareinfo/fare")
    R getFare(@RequestParam("addrId") Long addrId);
}

订单状态的枚举

gulimall-order/src/main/java/com/wen/gulimall/order/enume/OrderStatusEnum.java 

/**
 * @author W
 * @createDate 2024/03/12
 * @description: 订单状态的枚举
 */
public enum OrderStatusEnum {
    CREATE_NEW(0,"待付款"),
    PAYED(1,"已付款"),
    SENDED(2,"已发货"),
    RECIEVED(3,"已完成"),
    CANCLED(4,"已取消"),
    SERVICING(5,"售后中"),
    SERVICED(6,"售后完成");

    private Integer code;
    private String msg;

    OrderStatusEnum(Integer code,String msg){
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

构建订单

gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java

/**
 * 构建订单
 * @param orderSn
 * @return
 */
private OrderEntity buildOrder(String orderSn) {
    // 获取当前用户登录信息
    MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
    // 创建订单
    OrderEntity orderEntity = new OrderEntity();
    orderEntity.setMemberId(memberRespVo.getId());
    orderEntity.setOrderSn(orderSn);

    OrderSubmitVo orderSubmitVo = confirmVoThreadLocal.get();
    // 远程调用库存服务,获取收货地址和运费信息
    R fare = wmsFeignService.getFare(orderSubmitVo.getAddrId());
    FareVo fareResp = fare.getData(new TypeReference<FareVo>() {
    });

    // 设置运费信息
    orderEntity.setFreightAmount(fareResp.getFare());
    // 设置收货人信息
    orderEntity.setReceiverName(fareResp.getAddress().getName());
    orderEntity.setReceiverPhone(fareResp.getAddress().getPhone());
    orderEntity.setReceiverPostCode(fareResp.getAddress().getPostCode());
    orderEntity.setReceiverProvince(fareResp.getAddress().getProvince());
    orderEntity.setReceiverCity(fareResp.getAddress().getCity());
    orderEntity.setReceiverRegion(fareResp.getAddress().getRegion());
    orderEntity.setReceiverDetailAddress(fareResp.getAddress().getDetailAddress());
    // 设置订单的状态
    orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
    orderEntity.setAutoConfirmDay(7); // 自动确认收货时间 (天)
    return orderEntity;
}

3. 构建所有订单项数据

gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java

/**
 * 构建所有订单项数据
 * @return
 */
public List<OrderItemEntity> buildOrderItems(String orderSn){
    // 最后确定每个购物项的价格
    List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
    if(currentUserCartItems!=null && currentUserCartItems.size()>0){
        return currentUserCartItems.stream().map(cartItem -> {
            OrderItemEntity itemEntity = buildOrderItem(cartItem);
            itemEntity.setOrderSn(orderSn);
            return itemEntity;
        }).collect(Collectors.toList());
    }
    return null;
}

4. 构建每一个订单项数据  

远程调用商品服务,获取商品spu信息

gulimall-order/src/main/java/com/wen/gulimall/order/feign/ProductFeignService.java

/**
 * @description: 根据skuId获取spu信息
 */
@FeignClient("gulimall-product")
public interface ProductFeignService {

    @GetMapping("/product/spuinfo/skuId/{skuId}")
    R getSpuInfoBySkuId(@PathVariable Long skuId);
}

 gulimall-product/src/main/java/com/wen/gulimall/product/app/SpuInfoController.java

@RestController
@RequestMapping("product/spuinfo")
public class SpuInfoController {
    @Autowired
    private SpuInfoService spuInfoService;

    ...

    @GetMapping("/skuId/{skuId}")
    public R getSpuInfoBySkuId(@PathVariable Long skuId){
        SpuInfoEntity spuInfo = spuInfoService.getSpuInfoBySkuId(skuId);
        return R.ok().setData(spuInfo);
    }

    ...
}

 gulimall-product/src/main/java/com/wen/gulimall/product/service/SpuInfoService.java

public interface SpuInfoService extends IService<SpuInfoEntity> {

    ...

    /**
     * 根据skuId获取Spu信息
     * @param skuId
     * @return
     */
    SpuInfoEntity getSpuInfoBySkuId(Long skuId);
}

gulimall-product/src/main/java/com/wen/gulimall/product/service/impl/SpuInfoServiceImpl.java 

@Service("spuInfoService")
public class SpuInfoServiceImpl extends ServiceImpl<SpuInfoDao, SpuInfoEntity> implements SpuInfoService {
    
    ...

    @Resource
    private SkuInfoService skuInfoService;
    
    @Resource
    private BrandService brandService;
    
    ...
    
    @Override
    public SpuInfoEntity getSpuInfoBySkuId(Long skuId) {
        SkuInfoEntity byId = skuInfoService.getById(skuId);
        SpuInfoEntity spuInfo = getById(byId.getSpuId());
        // 查询品牌名称
        BrandEntity brand = brandService.getById(spuInfo.getBrandId());
        spuInfo.setBrandName(brand.getName());
        return spuInfo;
    }

}

给SpuInfoEntity添加一个属性brandName,如下

@TableField(exist = false)
private String brandName;

创建SpuInfoVo接收远程获取的商品SPU信息

gulimall-order/src/main/java/com/wen/gulimall/order/vo/SpuInfoVo.java

@Data
public class SpuInfoVo {
    private Long id;
    /**
     * 商品名称
     */
    private String spuName;
    /**
     * 商品描述
     */
    private String spuDescription;
    /**
     * 所属分类id
     */
    private Long catalogId;
    /**
     * 品牌id
     */
    private Long brandId;
    /**
     * 品牌名称
     */
    private String brandName;
    /**
     *
     */
    private BigDecimal weight;
    /**
     * 上架状态[0 - 下架,1 - 上架]
     */
    private Integer publishStatus;
    /**
     *
     */
    private Date createTime;
    /**
     *
     */
    private Date updateTime;
}

构建每一个订单项

gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java

/**
 * 构建每一个订单项数据
 * @param cartItem
 * @return
 */
private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
    OrderItemEntity orderItemEntity = new OrderItemEntity();
    // 1. 订单信息:订单号
    // 2. 商品的spu信息
    Long skuId = cartItem.getSkuId();
    R r = productFeignService.getSpuInfoBySkuId(skuId);
    SpuInfoVo spuInfo = r.getData(new TypeReference<SpuInfoVo>() {
    });
    orderItemEntity.setSpuId(spuInfo.getId());
    orderItemEntity.setSpuName(spuInfo.getSpuName());
    orderItemEntity.setSpuBrand(spuInfo.getBrandName());
    // 3. 商品的sku信息
    orderItemEntity.setSkuId(skuId);
    orderItemEntity.setSkuName(cartItem.getTitle());
    orderItemEntity.setSkuPic(cartItem.getImage());
    orderItemEntity.setSkuPrice(cartItem.getPrice());
    orderItemEntity.setSkuQuantity(cartItem.getCount());
    String skuAttr = StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(), ";");
    orderItemEntity.setSkuAttrsVals(skuAttr);

    // 4. 优惠信息【不做】
    // 5. 积分信息
    orderItemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
    orderItemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
    // 6. 订单项的价格信息
    orderItemEntity.setPromotionAmount(new BigDecimal("0"));
    orderItemEntity.setCouponAmount(new BigDecimal("0"));
    orderItemEntity.setIntegrationAmount(new BigDecimal("0"));
    // 当前订单项的实际金额
    BigDecimal orign = orderItemEntity.getSkuPrice().multiply(new BigDecimal(orderItemEntity.getSkuQuantity().toString()));
    BigDecimal subtract = orign.subtract(orderItemEntity.getPromotionAmount())
            .subtract(orderItemEntity.getCouponAmount())
            .subtract(orderItemEntity.getIntegrationAmount());
    orderItemEntity.setRealAmount(subtract);
    return orderItemEntity;
}
5.2.8.4 验价

将订单确认页提交的价格和后台计算的价格,进行对比,如果不一致,则提醒用户商品的价格发生变化

gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java

/**
 * 计算价格、积分等信息
 * @param orderEntity
 * @param orderItemEntities
 */
private void computePrice(OrderEntity orderEntity, List<OrderItemEntity> orderItemEntities) {
    BigDecimal total = new BigDecimal("0");
    BigDecimal promotion = new BigDecimal("0");
    BigDecimal integration = new BigDecimal("0");
    BigDecimal coupon = new BigDecimal("0");
    BigDecimal growth = new BigDecimal("0");
    BigDecimal gift = new BigDecimal("0");
    // 订单总额,叠加每个订单项的总金额
    for (OrderItemEntity orderItemEntity : orderItemEntities) {
        total = total.add(orderItemEntity.getRealAmount());
        promotion = promotion.add(orderItemEntity.getPromotionAmount());
        integration = integration.add(orderItemEntity.getIntegrationAmount());
        coupon = coupon.add(orderItemEntity.getCouponAmount());
        growth = growth.add(new BigDecimal(orderItemEntity.getGiftGrowth().toString()));
        gift = gift.add(new BigDecimal(orderItemEntity.getGiftIntegration().toString()));
    }
    // 1. 订单相关价格
    orderEntity.setTotalAmount(total);
    // 应付总额 = 订单总额 + 运费
    orderEntity.setPayAmount(total.add(orderEntity.getFreightAmount()));
    orderEntity.setPromotionAmount(promotion);
    orderEntity.setIntegrationAmount(integration);
    orderEntity.setCouponAmount(coupon);
    // 设置积分等信息
    orderEntity.setIntegration(gift.intValue());
    orderEntity.setGrowth(growth.intValue());
    orderEntity.setDeleteStatus(0);// 订单未删除
}
5.2.8.5 保存订单

gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java

/**
 * 保存订单数据
 * @param orderCreateTo
 */
private void saveOrder(OrderCreateTo orderCreateTo) {
    // 获取订单
    OrderEntity order = orderCreateTo.getOrder();
    // 保存订单
    this.save(order);
    // 获取订单项信息
    List<OrderItemEntity> orderItems = orderCreateTo.getOrderItems();
    // 批量保存订单项
    orderItemService.saveBatch(orderItems);
}
5.2.8.6 锁定库存

1. 找到每个商品在那个仓库都有库存

2. 锁定库存

        1)有商品在任何仓库都没有库存抛异常

        2)修改数据库wms_ware_sku表中锁定库存字段stock_locked默认值为0,将原来为null的都改为0.

3.修改数据库订单表中订单号的长度,由之前的32变成36。

4.如果扣库存失败,订单应该也不能下成功,查看数据库订单相关表中有订单信息,说明事务没有生效

1. 远程调用库存系统,为每个订单所库存 

gulimall-order/src/main/java/com/wen/gulimall/order/feign/WmsFeignService.java

@FeignClient("gulimall-ware")
public interface WmsFeignService {

   ...

    @PostMapping("/ware/waresku/lock/order")
    R orderLockStock(@RequestBody WareSkuLockVo vo);
}

gulimall-ware/src/main/java/com/wen/gulimall/ware/controller/WareSkuController.java 

@RestController
@RequestMapping("ware/waresku")
public class WareSkuController {
    @Autowired
    private WareSkuService wareSkuService;

    @PostMapping("/lock/order")
    public R orderLockStock(@RequestBody WareSkuLockVo vo){
        try {
            Boolean stock = wareSkuService.orderLockStock(vo);
            return R.ok();
        } catch (NoStockException e) {
            return R.error(BizCodeEnum.NO_STOCK_EXCEPTION.getCode(),BizCodeEnum.NO_STOCK_EXCEPTION.getMsg());
        }
    }

    ...
}

 gulimall-ware/src/main/java/com/wen/gulimall/ware/service/WareSkuService.java

public interface WareSkuService extends IService<WareSkuEntity> {

    ...

    Boolean orderLockStock(WareSkuLockVo vo);
}

 gulimall-ware/src/main/java/com/wen/gulimall/ware/service/impl/WareSkuServiceImpl.java

  • 找出所有库存大于要锁定商品数的仓库;
  • 遍历所有满足条件的仓库,逐个尝试进行锁库存,如果锁库存成功退出遍历;如果锁库存失败抛异常,一个商品锁失败就等于所有商品锁库存都不成功。
@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {

    @Resource
    private ProductFeignService productFeignService;
    @Resource
    private WareSkuDao wareSkuDao;

    ...

   /**
     * 为某个订单锁库存
     *
     * (rollbackFor = NoStockException.class) 默认运行时异常都会回滚
     * @param vo
     * @return
     */
    @Transactional
    @Override
    public Boolean orderLockStock(WareSkuLockVo vo) {
        // 1. 按照下单的收货地址,找到一个就近的仓库,锁定库存

        // 1. 找到每个商品在哪个仓库都有库存
        List<OrderItemVo> locks = vo.getLocks();
        List<SkuWareHasStock> collect = locks.stream().map(item -> {
            SkuWareHasStock skuWareHasStock = new SkuWareHasStock();
            Long skuId = item.getSkuId();
            skuWareHasStock.setSkuId(skuId);
            // 买几件
            skuWareHasStock.setNum(item.getCount());
            // 查询这个商品在哪里有库存 stock-锁定num>0
            List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
            skuWareHasStock.setWareId(wareIds);
            return skuWareHasStock;
        }).collect(Collectors.toList());
        // 2. 锁定库存
        for (SkuWareHasStock stock : collect) {
            Boolean skuStocked = false;
            Long skuId = stock.getSkuId();
            List<Long> wareIds = stock.getWareId();
            if(wareIds == null || wareIds.size()==0){
                // 有一个商品在任何仓库都没有库存
                throw new NoStockException(skuId);
            }
            for (Long wareId : wareIds) {
                // 成功返回1,否则就是0
                Long count = wareSkuDao.lockSkuStock(skuId,wareId,stock.getNum());
                if(count == 1){
                    // 当前商品锁定库存成功
                    skuStocked = true;
                    break;
                }else {
                    // 当前仓库锁失败,重试下一个仓库
                }
            }
            if(!skuStocked){
                // 当前商品所有的仓库都没有锁住
                throw new NoStockException(skuId);
            }
        }
        // 3. 肯定所有的商品库存都锁定成功
        return true;
    }

    @Data
    class SkuWareHasStock {
        private Long skuId;
        private Integer num;
        private List<Long> wareId;
    }
}
5.2.8.7 提交订单完整代码

提交订单问题提示:

(1)修改数据库wms_ware_sku表中锁定库存字段stock_locked默认值为0,将原来为null的都改为0;

(2)修改数据库订单号(order_sn)所在表oms_order、oms_order_item、oms_order_return_apply、oms_payment_info中order_sn的长度,由32改为36;

(3)把没有库存异常NoStockException移动至common模块,便于订单服务和库存服务使用;

(4)如果库存没有所成功,订单也不能下成功,使用事务(分布式事务)进行处理。

1. controller层

gulimall-order/src/main/java/com/wen/gulimall/order/web/OrderWebController.java

@Controller
public class OrderWebController {
    @Resource
    private OrderService orderService;

    ...

    /**
     * 下单功能
     * @param submitVo
     * @return
     */
    @PostMapping("/submitOrder")
    public String submitOrder(OrderSubmitVo submitVo, Model model, RedirectAttributes redirectAttributes){
        try {
            // 下单:去创建订单,验令牌,验价格,锁库存...
            SubmitOrderResponseVo responseVo = orderService.submitOrder(submitVo);


            System.out.println("订单提交的价格:"+submitVo);
            if(responseVo.getCode()==0){
                // 下单成功来到支付选项页
                model.addAttribute("submitOrderResp",responseVo);
                return "pay";
            }else {
                String msg = "下单失败;";
                switch (responseVo.getCode()){
                    case 1: msg += "订单信息过期,请刷新后再次提交" ;break;
                    case 2: msg += "订单商品价格发生变化,请确认后再次提交" ;break;
                    case 3: msg += "库存锁定失败,商品库存不足" ;break;
                }
                redirectAttributes.addFlashAttribute("msg",msg);
                // 下单失败回到订单确认页重新确认订单信息
                return "redirect:http://order.gulimall.com/toTrade";
            }
        } catch (Exception e) {
            if(e instanceof NoStockException){
                String msg = e.getMessage();
                redirectAttributes.addFlashAttribute("msg",msg);
            }
            return "redirect:http://order.gulimall.com/toTrade";
        }
    }
}

2. service层

gulimall-order/src/main/java/com/wen/gulimall/order/service/OrderService.java

public interface OrderService extends IService<OrderEntity> {

   ...

    /**
     * 下单
     * @param submitVo
     * @return
     */
    SubmitOrderResponseVo submitOrder(OrderSubmitVo submitVo);
}

 gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java

@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {

    private ThreadLocal<OrderSubmitVo> confirmVoThreadLocal = new ThreadLocal<>();
    @Resource
    private MemberFeignService memberFeignService;
    @Resource
    private CartFeignService cartFeignService;
    @Resource
    private WmsFeignService wmsFeignService;
    @Resource
    private ProductFeignService productFeignService;
    @Resource
    private ThreadPoolExecutor executor;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private OrderItemService orderItemService;
   
    ...

    @Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo submitVo) {
        confirmVoThreadLocal.set(submitVo);
        SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        responseVo.setCode(0);
        // 下单流程:去创建订单,验令牌,验价格,锁库存...
        // 1. 验证令牌【令牌的对比和删除必须保证原子性】传统写法可能会存在重复提交
        // 0-令牌失败;1-删除成功
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        String orderToken = submitVo.getOrderToken();
        // 通过Lua脚本原子验证令牌和删除令牌
        Long result = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
        if(result==0L){
            // 令牌验证失败
            responseVo.setCode(1);
            return responseVo;
        }else {
            // 令牌验证成功
            // 下单:去创建订单,验令牌,验价格,锁库存...
            // 1.创建订单、订单项等信息
            OrderCreateTo orderCreateTo = createOrder();
            // 2.订单验价
            BigDecimal payAmount = orderCreateTo.getOrder().getPayAmount();
            BigDecimal payPrice = submitVo.getPayPrice();
            if(Math.abs(payAmount.subtract(payPrice).doubleValue())<0.01){
                // 金额对比
                // 3.保存订单
                saveOrder(orderCreateTo);
                // 4.锁定库存。只要有异常回滚订单数据
                // 订单号,所有订单项(skuId,skuName,num)
                WareSkuLockVo lockVo = new WareSkuLockVo();
                lockVo.setOrderSn(orderCreateTo.getOrder().getOrderSn());
                List<OrderItemVo> locks = orderCreateTo.getOrderItems().stream().map(item -> {
                    OrderItemVo orderItemVo = new OrderItemVo();
                    orderItemVo.setSkuId(item.getSkuId());
                    orderItemVo.setTitle(item.getSkuName());
                    orderItemVo.setCount(item.getSkuQuantity());
                    return orderItemVo;
                }).collect(Collectors.toList());
                lockVo.setLocks(locks);
                R r = wmsFeignService.orderLockStock(lockVo);
                if (r.getCode() == 0) {
                    // 锁成功
                    responseVo.setOrder(orderCreateTo.getOrder());
                    return responseVo;
                }else {
                    String msg = (String) r.get("msg");
                    throw new NoStockException(msg);
                    // 锁定失败
                    //responseVo.setCode(3);
                    //return responseVo;
                }

            }else {
                responseVo.setCode(2);
                return responseVo;
            }
        }
        //String redisToken = stringRedisTemplate.opsForValue().get(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId());
        //if(orderToken!=null && orderToken.equals(redisToken)){
        //    // 令牌验证通过
        //}else {
        //    // 不通过
        //}
    }
    
    ...
}
5.2.8.8 提交订单的问题

1. 本地事务在分布式情况下出现的问题

分布式情况下,其他服务事务不一致问题:

  • 远程服务假失败

                远程服务其实成功了,由于网络故障等没有返回,导致:订单回滚,库存扣减

  • 远程服务执行完成,下面的其他方法出现问题

                导致:已经执行的远程请求,肯定不能回滚(本地事务只针对同一个数据库连接)

5.2.8.9 Seata分布式事务

1. Seata术语

(1)TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。

(2)TM (Transaction Manager) - 事务管理器: 定义全局事务的范围:开始全局事务、提交或回滚全局事务。

(3)RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

2. Seata事务模式

Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式。

  • AT模式:自动事务模式,根据回滚日志表undo_log自动回滚。
  • TCC模式:自己手写事务补偿回滚。

3. Seata分布式交易解决方案 

Seata官方手册:快速启动 | Apache Seata 

5.2.8.9.1 Windows下安装Seata

 说明:我使用的的hispring-cloud-alibaba:2021.0.4.0 对应 seata-all:1.5.2,所以选择 Seata1.5.2 版本,这里和老师的部署方式不一样。我这里使用store.mode.type=db,老师的store.mode.type=file。

部署步骤参考以下博客:

Windows下部署Seata1.5.2,解决Seata无法启动问题-CSDN博客

5.2.8.9.2 服务端整合Seata(AT模式)

说明:

1. Seata的AT模式不适用于高并发场景,因为AT模式实现的过程用了大量的锁机制。所以订单服务的下单功能不适用AT模式,适用于商品服务的保存商品接口(并发不高),流程如下:

/product/spuinfo/save:

(1) 保存spu的基本信息、保存spu的描述图片、保存spu的规格参数、保存当前spu对应的所有sku信息;

(2) 远程保存spu积分信息

(3) 远程保存sku优惠券信息

2. 下面使用AT模式实现【创建订单+锁定库存】,不推荐使用(原因:下单属于高并发场景)

1. 引入依赖

在公共模块引入spring-cloud-starter-alibaba-seata方便其他模块使用。

gulimall-common/pom.xml

<!--   分布式事务     -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

2. application.yml中添加配置

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

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

seata:
  tx-service-group: default_tx_group # 事务分组,必须和服务器配置一样
  service:
    # can not get cluster name in registry config 'service.vgroupMapping.default_tx_group', please make sure registry config correct
    vgroup-mapping:
      default_tx_group: default
    grouplist:
      default: localhost:8091

注意:事务分组必须和Seata服务端配置一样;grouplist.default必须配置,端口默认为 7091+1000,端口可在Seata的application.yml中自定义。

3. 使用seata的DataSourceProxy代理自己的数据源

gulimall-order/src/main/java/com/wen/gulimall/order/config/MySeataConfig.java

@Configuration
public class MySeataConfig {

    @Bean
    public DataSource dataSource(DataSourceProperties dataSourceProperties){
        HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if (StringUtils.hasText(dataSourceProperties.getName())) {
            dataSource.setPoolName(dataSourceProperties.getName());
        }
        return new DataSourceProxy(dataSource);
    }
}

gulimall-ware/src/main/java/com/wen/gulimall/ware/config/WareMybatisPlusConfig.java

@EnableTransactionManagement // 开启事务
@MapperScan("com.wen.gulimall.ware.dao")
@Configuration
public class WareMybatisPlusConfig {
    /**
     * 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
    // Seata数据源代理
    @Bean
    public DataSource dataSource(DataSourceProperties dataSourceProperties){
        HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        if (StringUtils.hasText(dataSourceProperties.getName())) {
            dataSource.setPoolName(dataSourceProperties.getName());
        }
        return new DataSourceProxy(dataSource);
    }
}
5.2.8.9.3 启用分布式事务

在分布式事务入口使用@GlobalTransactional,每个远程的小事务用@Transactional。

5.2.8.9.4 测试

重启服务进行测试。

注意:测试完成,删除订单服务下单接口Seata分布式入口注解@GlobalTransactional。

5.2.8.10 最终一致性库存解锁逻辑:【可靠消息+最终一致性】【库存解锁】

注意:对于高并发场景的分布式事务,不考虑2PC模式和TCC模式。

下单异常发消息解锁库存。

库存工作单详情表wms_ware_order_task_detail添加ware_id和lock_status字段

5.2.8.10.1 库存服务整合RabbitMQ,创建交换机、队列、绑定

1. 引入依赖

gulimall-ware/pom.xml

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

2. yml配置(设置发送端确认、抵达确认、手动ack) 

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

spring:
  rabbitmq:
    host: 172.1.11.10
    port: 5672
    virtual-host: /
    # 开启发送端确认
    publisher-confirm-type: correlated
    # 开启发送端消息抵达队列的确认,默认是false
    publisher-returns: true
    # 只要消息抵达队列,以异步发送优先回调我们的returnConfirm
    template:
      mandatory: true
    # 开启消费端手动ack确认
    listener:
      simple:
        acknowledge-mode: manual

3. 设置消息抵达确认回调

问题:MyRabbitConfig中尝试注入RabbitTemplate,同时也需要由MyRabbitConfig创建的MessageConverter注入到RabbitTemplate会导致循环依赖。

解决方案:将自定义消息转换器对象的创建messageConverter()移到另外一个配置类中。

gulimall-ware/src/main/java/com/wen/gulimall/ware/config/MyRabbitConfig.java

@Configuration
public class MyRabbitConfig {
    @Resource
    private RabbitTemplate rabbitTemplate;

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

    @PostConstruct //MyRabbitConfig对象创建完成之后,执行这个方法
    public void initRabbitTemplate(){
        // 设置确认回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             *  只要消息抵达Broker就ack=true
             * @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id)
             * @param b (ack) 消息是否成功收到
             * @param s (cause) 失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean b, String s) {
                System.out.println("confirm...correlationData["+correlationData+"]==>ack["+b+"]==>cause["+s+"]");
            }
        });
        // 设置消息抵达队列的确认回调
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /**
             * 只要消息没有投递给指定的队列,就触发这个失败回调
             * @param message the returned message. 投递失败的消息详细信息
             * @param replyCode the reply code.     回复的状态码
             * @param replyText the reply text.     回复的文本内容
             * @param exchange the exchange.        当时这个消息接收的交换机
             * @param routingKey the routing key.   当时这个消息用的哪个路由键
             */
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("Fail Message["+message+"]==>replyCode["+replyCode+"]==>replyText["+replyText+"]==>exchange["+exchange+"]==>routingKey["+routingKey+"]");
            }
        });
    }
}

4. 自定义消息转换器(使用JSON序列化机制)

gulimall-ware/src/main/java/com/wen/gulimall/ware/config/MyMessageConverter.java

@Configuration
public class MyMessageConverter {
    /**
     * 使用JSON序列化机制,进行消息转换
     * @return
     */
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

5. 创建库存解锁所需延时队列、交换机、绑定

gulimall-ware/src/main/java/com/wen/gulimall/ware/config/MyRabbitMQConfig.java

@Configuration
public class MyRabbitMQConfig {

    @RabbitListener(queues = "stock.release.stock.queue")
    public void listen(Message message, Channel channel) throws IOException {
        System.out.println("收到库存解锁消息,准备解锁");
        // 确认收到消息
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
    }

    /**
     * 延时队列
     * @return
     */
    @Bean
    public Queue stockDelayQueue(){
        Map<String, Object> arguments  = new HashMap<>();
        arguments.put("x-dead-letter-exchange","stock-event-exchange");
        arguments.put("x-dead-letter-routing-key","stock.release");
        arguments.put("x-message-ttl",60000);
        // String name,       【队列名称】
        // boolean durable,   【是否持久化】
        // boolean exclusive, 【是否排它】
        // boolean autoDelete,【是否自动删除】
        // @Nullable Map<String, Object> arguments 【自定义参数,死信路由、死信路由键、消息存活时间】
        return new Queue("stock.delay.queue",true,false,false,arguments);
    }

    /**
     * 普通队列/死信队列(接收过期消息(死信)的队列)
     * @return
     */
    @Bean
    public Queue stockReleaseStockQueue(){
        return new Queue("stock.release.stock.queue",true,false,false);
    }

    /**
     * 交换机
     * @return
     */
    @Bean
    public Exchange stockEventExchange(){
        return new TopicExchange("stock-event-exchange",true,false);
    }

    /**
     * 交换机与延时队列绑定
     * @return
     */
    @Bean
    public Binding stockLockedDelay(){
        return new Binding(
                "stock.delay.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.locked",
                null
        );
    }

    /**
     * 死信路由绑定死信队列
     * @return
     */
    @Bean
    public Binding stockLockedRelease(){
        return new Binding(
                "stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "stock-event-exchange",
                "stock.release.#",
                null
        );
    }

}

启动库存服务进行测试,使用路由键stock.locked发送消息至延时队列,下单两个商品,看到延时队列中有两条消息,一分钟后可以监听到消息说明成功。

5.2.8.10.2 监听库存解锁

库存锁定成功,向MQ延时队列中发送一条库存工作单记录

库存工作单记录,包含:哪件商品、哪个仓库、锁定了多少库存。

库存工作单记录实体类

gulimall-common/src/main/java/com/wen/common/to/mq/StockLockedTo.java 

@Data
public class StockLockedTo {
    private Long id; // 库存工作单id
    private StockDetailTo detail;// 库存工作单详情
}

库存工作单详情

gulimall-common/src/main/java/com/wen/common/to/mq/StockDetailTo.java 

@Data
public class StockDetailTo {
    private Long id;
    /**
     * sku_id
     */
    private Long skuId;
    /**
     * sku_name
     */
    private String skuName;
    /**
     * 购买个数
     */
    private Integer skuNum;
    /**
     * 工作单id
     */
    private Long taskId;
    /**
     * 仓库id
     */
    private Long wareId;
    /**
     * 1-已锁定  2-已解锁  3-扣减
     */
    private Integer lockStatus;
}

监听库存死信队列,解锁库存

gulimall-ware/src/main/java/com/wen/gulimall/ware/listener/StockReleaseListener.java

@RabbitListener(queues = {"stock.release.stock.queue"})
@Service
public class StockReleaseListener {
    @Resource
    private WareSkuService wareSkuService;

    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTo lockedTo, Message message, Channel channel) throws IOException {
        System.out.println("收到解锁库存的消息....");
        try {
            wareSkuService.unlockStock(lockedTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (IOException e) {
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}

解锁库存

gulimall-ware/src/main/java/com/wen/gulimall/ware/service/impl/WareSkuService.java

public interface WareSkuService extends IService<WareSkuEntity> {

    ...

    void unlockStock(StockLockedTo lockedTo);
}

gulimall-ware/src/main/java/com/wen/gulimall/ware/service/impl/WareSkuServiceImpl.java  

@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {

    @Resource
    private ProductFeignService productFeignService;
    @Resource
    private OrderFeignService orderFeignService;
    @Resource
    private WareSkuDao wareSkuDao;
    @Resource
    private WareOrderTaskService wareOrderTaskService;
    @Resource
    private WareOrderTaskDetailService wareOrderTaskDetailService;
    @Resource
    private RabbitTemplate rabbitTemplate;

    /**
     * 1. 库存自动解锁。
     *      下单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁。
     * 2. 订单失败
     *      锁库存失败
     *
     * 只要解锁库存失败,一定要告诉mq服务端解锁失败。
     */

    ...

    @Override
    public void unlockStock(StockLockedTo lockedTo) {
        StockDetailTo detail = lockedTo.getDetail();
        Long detailId = detail.getId();
        WareOrderTaskDetailEntity byId = wareOrderTaskDetailService.getById(detailId);
        // 解锁
        // 1. 查询数据库关于这个订单的锁定库存信息。
        //     有:证明库存锁定成功了
        //        解锁:订单情况。
        //             1. 没有这个订单。必须解锁
        //             2. 有这个订单。不是解锁库存。
        //                  订单状态:已取消,解锁库存
        //                           没取消,不能解锁
        //     没有:库存锁定失败,库存回滚了。这种情况无需解锁
        if(byId!=null){
            // 库存锁定成功,解锁
            Long id = lockedTo.getId();// 库存工作单id
            WareOrderTaskEntity taskEntity = wareOrderTaskService.getById(id);
            R r = orderFeignService.getOrderStatus(taskEntity.getOrderSn());
            if(r.getCode()==0){
                OrderVo orderVo = r.getData(new TypeReference<OrderVo>() {
                });
                if(orderVo == null || orderVo.getStatus() == 4){// 订单不存在或已取消,解锁库存
                    if(byId.getLockStatus() == 1) {
                        // 当前库存工作单详情,状态1-已锁定 ,未解锁才能解锁
                        unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);
                        // 手动确认 DeliveryTag-投递标签
                    }
                }
            }else {
                // 消息拒绝后重新放到队列里面,让别人继续消费解锁
                throw new RuntimeException("远程解锁失败");
            }
        }else {
            // 库存锁定失败,回滚,无需解锁
        }

    }

    public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId){
        // 库存解锁
        wareSkuDao.unLockStock(skuId,wareId,num);
        // 更新库存工作单的状态
        WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();
        taskDetailEntity.setId(taskDetailId);
        taskDetailEntity.setLockStatus(2);// 变为已解锁
        wareOrderTaskDetailService.updateById(taskDetailEntity);
    }

    ...

}

远程调用订单服务,查询订单状态

gulimall-ware/src/main/java/com/wen/gulimall/ware/feign/OrderFeignService.java

@FeignClient("gulimall-order")
public interface OrderFeignService {
    @GetMapping("/order/order/status/{orderSn}")
    R getOrderStatus(@PathVariable("orderSn") String orderSn);
}

gulimall-order/src/main/java/com/wen/gulimall/order/controller/OrderController.java

@RestController
@RequestMapping("order/order")
public class OrderController {
    @Autowired
    private OrderService orderService;

    @GetMapping("/status/{orderSn}")
    public R getOrderStatus(@PathVariable("orderSn") String orderSn){
        OrderEntity order = orderService.getOrderByOrderSn(orderSn);
        return R.ok().setData(order);
    }

    ...
}

 gulimall-order/src/main/java/com/wen/gulimall/order/service/OrderService.java

public interface OrderService extends IService<OrderEntity> {

    ....

    /**
     * 库存服务根据订单号查询订单信息
     * @param orderSn
     * @return
     */
    OrderEntity getOrderByOrderSn(String orderSn);
}

gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java 

@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {

    private ThreadLocal<OrderSubmitVo> confirmVoThreadLocal = new ThreadLocal<>();
    @Resource
    private MemberFeignService memberFeignService;
    @Resource
    private CartFeignService cartFeignService;
    @Resource
    private WmsFeignService wmsFeignService;
    @Resource
    private ProductFeignService productFeignService;
    @Resource
    private ThreadPoolExecutor executor;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private OrderItemService orderItemService;
    
    ...

    @Override
    public OrderEntity getOrderByOrderSn(String orderSn) {
        OrderEntity entity = this.getOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));
        return entity;
    }
    ...
}

库存服务远程调用订单服务被登录拦截器拦截跳转到login.html,修改订单服务登录拦截器,放行。

gulimall-order/src/main/java/com/wen/gulimall/order/interceptor/LoginUserInterceptor.java

5.2.8.11 最终一致性库存解锁逻辑:【可靠消息+最终一致性】【定时关单】

关单成功给库存服务发送一个消息通知解锁库存

5.2.8.11.1 订单服务整合RabbitMQ,创建交换机、队列、绑定

说明:在谷粒商城篇章9----消息队列【分不式高级篇六】4.6 延时队列定时关单模拟中已经创建交换机、队列、绑定关系。地址如下:

谷粒商城篇章9 ---- P248-P261/P292-P294 ---- 消息队列【分布式高级篇六】_谷粒商城 292-CSDN博客

5.2.8.11.2 监听订单定时关单

1. 订单创建成功发消息给MQ

gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java

2. 监听订单关单

下单成功,超过规定时间未支付,关单

gulimall-order/src/main/java/com/wen/gulimall/order/listener/OrderCloseListener.java

@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {
    @Resource
    private OrderService orderService;
    /**
     * 监听接收死信队列消息
     * @param message
     * @param channel
     * @param orderEntity
     * @throws IOException
     */
    @RabbitHandler
    public void listen(OrderEntity orderEntity, Message message, Channel channel) throws IOException {
        System.out.println("当前时间"+new Date() +"收到过期订单消息,准备关闭订单:------->"+orderEntity.getOrderSn());
        try {
            // 确认收到消息
            orderService.closeOrder(orderEntity);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        } catch (IOException e) {
            // 重回队列
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}

关闭订单

gulimall-order/src/main/java/com/wen/gulimall/order/service/OrderService.java 

public interface OrderService extends IService<OrderEntity> {

   ...

    /**
     * 关闭订单
     * @param orderEntity
     */
    void closeOrder(OrderEntity orderEntity);
}
@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {

    private ThreadLocal<OrderSubmitVo> confirmVoThreadLocal = new ThreadLocal<>();
    @Resource
    private RabbitTemplate rabbitTemplate;
    @Resource
    private MemberFeignService memberFeignService;
    @Resource
    private CartFeignService cartFeignService;
    @Resource
    private WmsFeignService wmsFeignService;
    @Resource
    private ProductFeignService productFeignService;
    @Resource
    private ThreadPoolExecutor executor;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private OrderItemService orderItemService;
   
    ...

    @Override
    public void closeOrder(OrderEntity orderEntity) {
        // 查询当前这个订单的最新状态
        OrderEntity order = this.getById(orderEntity.getId());
        if(order.getStatus().equals(OrderStatusEnum.CREATE_NEW.getCode())){
            // 关单,重新创建订单,30分钟前的订单信息可能与当前数据库不一致
            OrderEntity entity = new OrderEntity();
            entity.setId(orderEntity.getId());
            entity.setStatus(OrderStatusEnum.CANCLED.getCode());
            this.updateById(entity);
            
        }
    }
    ...
}

3. 测试自动关单、解锁库存

测试之前清空订单、库存锁定、库存工作单、库存工作单详情,便于测试查看,也可不清。

清空mq中消息,重启订单、库存服务,下单后,等待 

 

提交订单后,

订单的延时队列收到一条消息,未支付,一分钟后自动关单;

库存的延时队列收到两条消息(下单了两种商品),未支付,两分钟后解锁库存。

下单成功,未支付,一分钟内,数据库数据,如下:

下单成功,未支付,一分钟后(订单关闭),两分钟内,数据库数据,如下:

 下单成功,未支付,两分钟后(库存解锁),数据库数据,如下:

5.2.8.11.3 订单创建成功,机器卡顿,消息延迟导致库存无法解锁(关单逻辑升级) 

防止订单服务卡顿,导致订单状态消息一直改变不了,库存消息优先到期,查询订单状态是新建状态,消费消息不解锁库存。 导致卡顿的订单,永远不能解锁。 

解决方案:

订单关闭成功后发送消息至库存队列stock.release.stock.queue,库存服务监听关单成功消息,解锁库存。

1. 关单成功发送消息解锁库存

消息to = OrderEntity

gulimall-common/src/main/java/com/wen/common/to/mq/OrderTo.java

@Data
public class OrderTo {
    private Long id;
    /**
     * member_id
     */
    private Long memberId;
    /**
     * 订单号
     */
    private String orderSn;
    /**
     * 使用的优惠券
     */
    private Long couponId;
    /**
     * create_time
     */
    private Date createTime;
    /**
     * 用户名
     */
    private String memberUsername;
    /**
     * 订单总额
     */
    private BigDecimal totalAmount;
    /**
     * 应付总额
     */
    private BigDecimal payAmount;
    /**
     * 运费金额
     */
    private BigDecimal freightAmount;
    /**
     * 促销优化金额(促销价、满减、阶梯价)
     */
    private BigDecimal promotionAmount;
    /**
     * 积分抵扣金额
     */
    private BigDecimal integrationAmount;
    /**
     * 优惠券抵扣金额
     */
    private BigDecimal couponAmount;
    /**
     * 后台调整订单使用的折扣金额
     */
    private BigDecimal discountAmount;
    /**
     * 支付方式【1->支付宝;2->微信;3->银联; 4->货到付款;】
     */
    private Integer payType;
    /**
     * 订单来源[0->PC订单;1->app订单]
     */
    private Integer sourceType;
    /**
     * 订单状态【0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单】
     */
    private Integer status;
    /**
     * 物流公司(配送方式)
     */
    private String deliveryCompany;
    /**
     * 物流单号
     */
    private String deliverySn;
    /**
     * 自动确认时间(天)
     */
    private Integer autoConfirmDay;
    /**
     * 可以获得的积分
     */
    private Integer integration;
    /**
     * 可以获得的成长值
     */
    private Integer growth;
    /**
     * 发票类型[0->不开发票;1->电子发票;2->纸质发票]
     */
    private Integer billType;
    /**
     * 发票抬头
     */
    private String billHeader;
    /**
     * 发票内容
     */
    private String billContent;
    /**
     * 收票人电话
     */
    private String billReceiverPhone;
    /**
     * 收票人邮箱
     */
    private String billReceiverEmail;
    /**
     * 收货人姓名
     */
    private String receiverName;
    /**
     * 收货人电话
     */
    private String receiverPhone;
    /**
     * 收货人邮编
     */
    private String receiverPostCode;
    /**
     * 省份/直辖市
     */
    private String receiverProvince;
    /**
     * 城市
     */
    private String receiverCity;
    /**
     * 区
     */
    private String receiverRegion;
    /**
     * 详细地址
     */
    private String receiverDetailAddress;
    /**
     * 订单备注
     */
    private String note;
    /**
     * 确认收货状态[0->未确认;1->已确认]
     */
    private Integer confirmStatus;
    /**
     * 删除状态【0->未删除;1->已删除】
     */
    private Integer deleteStatus;
    /**
     * 下单时使用的积分
     */
    private Integer useIntegration;
    /**
     * 支付时间
     */
    private Date paymentTime;
    /**
     * 发货时间
     */
    private Date deliveryTime;
    /**
     * 确认收货时间
     */
    private Date receiveTime;
    /**
     * 评价时间
     */
    private Date commentTime;
    /**
     * 修改时间
     */
    private Date modifyTime;
}

gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java

2. 监听关单成功,解锁库存

监听关单

gulimall-ware/src/main/java/com/wen/gulimall/ware/listener/StockReleaseListener.java

解锁库存,方法重载

gulimall-ware/src/main/java/com/wen/gulimall/ware/service/WareSkuService.java 

public interface WareSkuService extends IService<WareSkuEntity> {

    ...

    void unlockStock(OrderTo orderTo);
}

库存解锁前,查询库存工单详情,防止重复解锁库存

gulimall-ware/src/main/java/com/wen/gulimall/ware/service/impl/WareSkuServiceImpl.java

@Service("wareSkuService")
public class WareSkuServiceImpl extends ServiceImpl<WareSkuDao, WareSkuEntity> implements WareSkuService {

    @Resource
    private ProductFeignService productFeignService;
    @Resource
    private OrderFeignService orderFeignService;
    @Resource
    private WareSkuDao wareSkuDao;
    @Resource
    private WareOrderTaskService wareOrderTaskService;
    @Resource
    private WareOrderTaskDetailService wareOrderTaskDetailService;
    @Resource
    private RabbitTemplate rabbitTemplate;
    
    ...

    /**
     * 防止订单服务卡顿,导致订单状态消息一直改变不了,库存消息优先到期,查询订单状态是新建状态,消费消息不解锁库存。
     * 导致卡顿的订单,永远不能解锁
     * @param orderTo
     */
    @Override
    public void unlockStock(OrderTo orderTo) {
        String orderSn = orderTo.getOrderSn();
        // 查一下最新库存的状态,防止重复解锁库存
        WareOrderTaskEntity task = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);
        Long id = task.getId();
        // 按照工作单找到所有没有解锁的库存,进行解锁
        List<WareOrderTaskDetailEntity> list = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>()
                .eq("task_id", id)
                .eq("lock_status", 1));
        for (WareOrderTaskDetailEntity entity : list) {
            unLockStock(entity.getSkuId(), entity.getWareId(), entity.getSkuNum(),entity.getId());
        }
    }
}

查一下最新库存的状态,防止重复解锁库存 

gulimall-ware/src/main/java/com/wen/gulimall/ware/service/WareOrderTaskService.java

public interface WareOrderTaskService extends IService<WareOrderTaskEntity> {

    ...

    WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn);
}

gulimall-ware/src/main/java/com/wen/gulimall/ware/service/impl/WareOrderTaskServiceImpl.java 

@Service("wareOrderTaskService")
public class WareOrderTaskServiceImpl extends ServiceImpl<WareOrderTaskDao, WareOrderTaskEntity> implements WareOrderTaskService {

    ...

    @Override
    public WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn) {
        WareOrderTaskEntity task = this.getOne(new QueryWrapper<WareOrderTaskEntity>().eq("order_sn", orderSn));
        return task;
    }

}
5.2.8.12 消息丢失、积压、重复等解决方案(如何保证消息可靠性?)

gulimall_oms库中,添加mq消息表,记录消息的状态,便于重新发送、防重等

CREATE TABLE `mq_message`(
    `message_id` CHAR(32) NOT NULL ,
    `content` TEXT, #json
    `to_exchange` CHAR(255) DEFAULT NULL ,
    `routing_key` CHAR(255) DEFAULT NULL ,
    `class_type` CHAR(255) DEFAULT NULL ,
    `message_status` INT(1) DEFAULT '0' COMMENT '0-新建 1-已发送 2-错误抵达 3-已抵达',
    `create_time` DATETIME DEFAULT NULL ,
    `update_time` DATETIME DEFAULT NULL
)
5.2.8.12.1 消息丢失

(1)消息发送出去,由于网络问题没有抵达服务器

        • 做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机 制,可记录到数据库,采用定期扫描重发的方式

        • 做好日志记录,每个消息状态是否都被服务器收到都应该记录

        • 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进 行重发

(2)消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚 未持久化完成,宕机。

        • publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。

(3)自动ACK的状态下。消费者收到消息,但没来得及消息然后宕机

        • 一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重 新入队。

5.2.8.12.2 消息重复

(1)消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息 重新由unack变为ready,并发送给其他消费者

(2)消息消费失败,由于重试机制,自动又将消息发送出去

(3)成功消费,ack时宕机,消息由unack变为ready,Broker又重新发送

        • 消费者的业务消费接口应该设计为幂等性的。比如扣库存有 工作单的状态标志。

        • 使用防重表(redis/mysql),发送消息每一个都有业务的唯 一标识,处理过就不用处理。

        • rabbitMQ的每一个消息都有redelivered字段,可以获取是否 是被重新投递过来的,而不是第一次投递过来的。

5.2.8.12.3 消息积压

(1)消费者宕机积压

(2)消费者消费能力不足积压

(3)发送者发送流量太大

        • 上线更多的消费者,进行正常消费

        • 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理。

总结:保证消息的可靠性,最重要的是防止消息丢失。

注意:可将处理消息单独提出一个服务,发送消息,失败重试等。

5.3  订单支付页

5.3.1 加密分类

5.3.1.1 对称加密

        加密和解密使用同一把钥匙。

5.3.1.2 非对称加密

        加密和解密使用不同钥匙。

5.3.2 支付宝支付

1. 完成支付功能的流程,如下:

        支付宝开放平台-》API-》支付-》电脑网站支付-》创建应用-》绑定应用-》配置密钥-》上线应用-》开通产品。

说明:审核比较麻烦,需要营业执照等,所以下面使用支付宝的沙箱环境完成支付功能测试。

2. 沙箱环境:

      支付宝开放平台-》API-》AIT-API集成工具-》沙箱-》沙箱环境-》快速接入-》沙箱控制台。

3. 电脑网站支付开发工具包SDK&Demo获取,地址如下:

小程序文档 - 支付宝文档中心  

下载Java版本的Demo并运行,看一下支付的整体流程。

5.3.2.1 支付宝加密原理
  • 支付宝采用RSA非对称加密对信息进行签名。
  • 支付宝的加密原理主要依赖于RSA非对称加密技术,这种技术使用一对密钥:公钥和私钥。公钥用于加密数据,而私钥用于解密数据。
  • 在发送订单数据时,商户端直接使用明文,但会使用商户私钥对数据进行签名,支付宝使用商户公钥对签名进行验签,明文和签名对应则验签成功。
  • 支付成功后,支付宝发送支付成功数据时也会使用支付宝私钥加一个对应的签名,商户端收到支付成功数据后使用支付宝公钥进行验签,成功后才能确认支付成功。

5.3.2.1.1 什么是公钥、 私钥、 加密、 签名和验签

1. 公钥、私钥

        公钥和私钥是一个相对概念 它们的公私性是相对于生成者来说的。 一对密钥生成后, 保存在生成者手里的就是私钥, 生成者发布出去大家用的就是公钥。

2. 加密和数字签名

(1)加密是指:

  • 我们使用一对公私钥中的一个密钥来对数据进行加密, 而使用另一个密钥来进行解密的技术。
  • 公钥和私钥都可以用来加密, 也都可以用来解密。
  • 但这个加解密必须是一对密钥之间的互相加解密, 否则不能成功。
  • 加密的目的是:为了确保数据传输过程中的不可读性, 就是不想让别人看到。

(2)签名:

  • 给我们将要发送的数据, 做上一个唯一签名(类似于指纹)。
  • 用来互相验证接收方和发送方的身份;
  • 在验证身份的基础上再验证一下传递的数据是否被篡改过。 因此使用数字签名可以用来达到数据的明文传输。

(3)验签:

  • 支付宝为了验证请求的数据是否商户本人发的;
  • 商户为了验证响应的数据是否支付宝发的。
5.3.2.2 支付宝-电脑网站支付Demo测试

支付宝开放平台Demo下载地址:支付宝-电脑网站支付-SDK&Demo获取

5.3.2.2.1 使用支付宝沙箱环境进行测试

沙箱地址:支付宝-沙箱

5.3.2.2.2 系统默认密钥

也可以自定义密钥。

5.3.2.2.3 修改Demo中配置AlipayConfig 

5.3.2.2.4 启动Demo测试

说明:我这里新建了一个JavaWeb项目,然后将Demo中的相关代码等复制到新建的项目zhifu-demo.

看到以下界面说明启动成功

测试账号密码获取:支付宝-沙箱账号

支付成功,可以看到以下界面:

trade_no:交易编号

out_trade_no:商户订单号

total_amount:付款金额

5.3.2.3 支付宝支付流程

5.3.3 内网穿透

        如果别人想要调用我们的系统进行支付,那么AlipayConfig中的服务器异步通知页面路径和支付成功后要跳转的页面地址,必须外网可以正常访问。localhost:8080是不可以的。

可以使用内网穿透解决。

没有内网穿透: 

内网穿透的实现:

5.3.3.1 简介

内网穿透功能可以允许我们使用外网的网址来访问主机;

正常的外网需要访问我们项目的流程是:

        1、 买服务器并且有公网固定 IP

        2、 买域名映射到服务器的 IP

        3、 域名需要进行备案和审核 

5.3.3.2 使用场景

1、 开发测试(微信、 支付宝)

2、 智慧互联

3、 远程控制

4、 私有云

5.3.3.3 内网穿透的几个常用软件

1、 natapp: https://natapp.cn/ 优惠码: 022B93FD(9 折) [仅限第一次使用]

2、 续断: www.zhexi.tech 优惠码: SBQMEA(95 折) [仅限第一次使用]

3、 花生壳: https://www.oray.com/

5.3.3.4 natapp内网穿透

natapp有免费的隧道可以使用。

natapp官方操作文档:NATAPP1分钟快速新手图文教程 - NATAPP-内网穿透 基于ngrok的国内高速内网映射工具 

1. 注册、实名认证;

2. 购买免费隧道后,在我的隧道配置内网地址和端口;

3. windows启动命令

        windows ,点击开始->运行->命令行提示符,后进入 natapp.exe的目录,运行

natapp -authtoken=自己的authtoken

 启动成功后,可以看到natapp分配的网址:

将支付Demo中AlipayConfig里的服务器异步通知路径和页面跳转同步通知页面路径的http://localhost:8080改为以上域名地址。

5.3.4 整合支付

5.3.4.1 导入依赖

gulimall-order/pom.xml

<!--   支付宝的SDK     -->
<dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-sdk-java</artifactId>
    <version>4.39.86.ALL</version>
</dependency>
5.3.4.2 yml配置 

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

alipay:
  app_id: 9021000137684866
  merchant_private_key: xxxx
  alipay_public_key: xxxx
  # 这里先使用Demo的支付回调页
  notify_url: http://skb4tx.natappfree.cc/zhifu_demo_war_exploded/notify_url.jsp
  return_url: http://skb4tx.natappfree.cc/zhifu_demo_war_exploded/return_url.jsp
  sign_type: RSA2
  charset: utf-8
  gatewayUrl: https://openapi-sandbox.dl.alipaydev.com/gateway.do
5.3.4.3 支付Vo

gulimall-order/src/main/java/com/wen/gulimall/order/vo/PayVo.java

@Data
public class PayVo {
    private String out_trade_no; // 商户订单号 必填
    private String subject; // 订单名称 必填
    private String total_amount;  // 付款金额 必填
    private String body; // 商品描述 可空
}
5.3.4.4 阿里云支付模板

gulimall-order/src/main/java/com/wen/gulimall/order/config/AlipayTemplate.java

@ConfigurationProperties(prefix = "alipay")
@Component
@Data
public class AlipayTemplate {

    //在支付宝创建的应用的id
    private   String app_id;

    // 商户私钥,您的PKCS8格式RSA2私钥
    private  String merchant_private_key;
    // 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
    private  String alipay_public_key;
    // 服务器[异步通知]页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    // 支付宝会悄悄的给我们发送一个请求,告诉我们支付成功的信息
    private  String notify_url;

    // 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
    //同步通知,支付成功,一般跳转到成功页
    private  String return_url;

    // 签名方式
    private  String sign_type;

    // 字符编码格式
    private  String charset;

    // 支付宝网关; https://openapi.alipaydev.com/gateway.do
    private  String gatewayUrl;

    public  String pay(PayVo vo) throws AlipayApiException {

        //AlipayClient alipayClient = new DefaultAlipayClient(AlipayTemplate.gatewayUrl, AlipayTemplate.app_id, AlipayTemplate.merchant_private_key, "json", AlipayTemplate.charset, AlipayTemplate.alipay_public_key, AlipayTemplate.sign_type);
        //1、根据支付宝的配置生成一个支付客户端
        AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,
                app_id, merchant_private_key, "json",
                charset, alipay_public_key, sign_type);

        //2、创建一个支付请求 //设置请求参数
        AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
        alipayRequest.setReturnUrl(return_url);
        alipayRequest.setNotifyUrl(notify_url);

        //商户订单号,商户网站订单系统中唯一订单号,必填
        String out_trade_no = vo.getOut_trade_no();
        //付款金额,必填
        String total_amount = vo.getTotal_amount();
        //订单名称,必填
        String subject = vo.getSubject();
        //商品描述,可空
        String body = vo.getBody();

        alipayRequest.setBizContent("{\"out_trade_no\":\""+ out_trade_no +"\","
                + "\"total_amount\":\""+ total_amount +"\","
                + "\"subject\":\""+ subject +"\","
                + "\"body\":\""+ body +"\","
                + "\"product_code\":\"FAST_INSTANT_TRADE_PAY\"}");

        String result = alipayClient.pageExecute(alipayRequest).getBody();

        //会收到支付宝的响应,响应的是一个页面,只要浏览器显示这个页面,就会自动来到支付宝的收银台页面
        System.out.println("支付宝的响应:"+result);

        return result;

    }
}
5.3.4.5 订单支付宝支付接口

@Getmapping中属性produces指定控制器支持的响应媒体类型。text/html以页面的形式响应。

gulimall-order/src/main/java/com/wen/gulimall/order/web/PayWebController.java

@Controller
public class PayWebController {
    @Resource
    private AlipayTemplate alipayTemplate;
    @Resource
    private OrderService orderService;

    /**
     * 1.将支付页让浏览器展示。
     * 2.支付成功后,我们要跳到用户的订单列表页
     * @param orderSn
     * @return
     * @throws AlipayApiException
     */
    @ResponseBody
    @GetMapping(value = "/payOrder",produces = {"text/html"})
    public String payOrder(String orderSn) throws AlipayApiException {
        PayVo payVo = orderService.getOrderPay(orderSn);
        String pay = alipayTemplate.pay(payVo);
        return pay;
    }
}

获取当前订单的支付信息

gulimall-order/src/main/java/com/wen/gulimall/order/service/OrderService.java

public interface OrderService extends IService<OrderEntity> {

    ...

    /**
     * 获取当前订单的支付信息
     * @param orderSn
     * @return
     */
    PayVo getOrderPay(String orderSn);
}

gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java 

@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {

    private ThreadLocal<OrderSubmitVo> confirmVoThreadLocal = new ThreadLocal<>();
    @Resource
    private RabbitTemplate rabbitTemplate;
    @Resource
    private MemberFeignService memberFeignService;
    @Resource
    private CartFeignService cartFeignService;
    @Resource
    private WmsFeignService wmsFeignService;
    @Resource
    private ProductFeignService productFeignService;
    @Resource
    private ThreadPoolExecutor executor;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private OrderItemService orderItemService;

    ...

    @Override
    public PayVo getOrderPay(String orderSn) {
        PayVo payVo = new PayVo();
        OrderEntity order = this.getOrderByOrderSn(orderSn);
        // 保留两位小数,向上取值
        BigDecimal bigDecimal = order.getPayAmount().setScale(2, BigDecimal.ROUND_UP);
        payVo.setTotal_amount(bigDecimal.toString());
        payVo.setOut_trade_no(orderSn);
        // 获取订单项数据
        List<OrderItemEntity> orderItems = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", orderSn));
        OrderItemEntity orderItemEntity = orderItems.get(0);
        payVo.setSubject(orderItemEntity.getSkuName());
        payVo.setSubject(orderItemEntity.getSkuAttrsVals());
        return payVo;
    }
}
5.3.4.6 前端页面修改pay.html

gulimall-order/src/main/resources/templates/pay.html

5.3.4.7 支付测试

支付成功,跳转以下页面:

5.3.5 订单列表页渲染(member服务)

5.3.5.1 静态资源上传到nginx

1. 将 资料-》静态资源-》html-》订单页中的index.html复制到会员服务gulimall-member/src/main/resources/templates下,改名为orderList.html

2. 静态资源上传至nginx

3. 将orderList.html中的 静态资源路径,如下

        src="    ->  src="/static/member/

        href="   ->   href="/static/member/

5.3.5.2 会员服务整合thymeleaf

1. 引入依赖

gulimall-member/pom.xml

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

2. 关闭thymeleaf缓存

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

spring:
  thymeleaf:
    cache: false

3. orderList.html页面引入thymeleaf

5.3.5.3 网关配置

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

- id: gulimall_member_route
  uri: lb://gulimall-member
  predicates:
    # 由以下的主机域名访问转发到会员服务
    - Host=member.gulimall.com
5.3.5.4 SwitchHosts添加配置

添加会员服务的域名与ip映射:xxx.xxx.11.10 order.gulimall.com

商城首页我的订单跳转修改,跳转到会员服务。

gulimall-product的index.html页面搜索我的订单

5.3.5.5 整合SpringSession(登录后才可以查看订单信息)

 注意: SpringSession的配置类GulimallSessionConfig.java在公共模块(gulimall-common)已经配置,这里购物车模块直接引入公共模块即可。

公共模块(gulimall-common)可以参考我之前的博客:谷粒商城篇章7 ---- P211-P235 ---- 认证服务【分布式高级篇四】_谷粒商城p235-CSDN博客

1. 引入依赖

<!--	整合SpringSession完成session共享问题	-->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<!--   redis     -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
        <exclusions>
            <exclusion>
                <groupId>io.lettuce</groupId>
                <artifactId>lettuce-core</artifactId>
            </exclusion>
        </exclusions>
</dependency>

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

2. 开启SpringSession

在会员服务的启动类上,添加如下注解:

gulimall-member/src/main/java/com/wen/gulimall/member/GulimallMemberApplication.java

@EnableRedisHttpSession

3. session数据存储方式配置

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

spring:
  redis:
    host: 172.1.11.10
  session:
    store-type: redis
5.3.5.6 配置拦截器

登录拦截器

gulimall-member/src/main/java/com/wen/gulimall/member/interceptor/LoginUserInterceptor.java

@Component
public class LoginUserInterceptor implements HandlerInterceptor {
    // 同一个线程共享数据
    public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        boolean match = new AntPathMatcher().match("/member/**", requestURI);
        if(match){
            return true;
        }
        MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
        if(attribute!=null){
            // 登录成功
            loginUser.set(attribute);
            return true;
        }else {
            // 没登录,去登录
            request.getSession().setAttribute("msg","请先进行登录");
            response.sendRedirect("http://auth.gulimall.com/login.html");
            return false;
        }
    }
}

添加登录拦截器

gulimall-order/src/main/java/com/wen/gulimall/order/config/OrderWebConfiguration.java 

@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {
    @Resource
    private LoginUserInterceptor interceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 订单服务的所有请求都要走登录拦截
        registry.addInterceptor(interceptor).addPathPatterns("/**");
    }
}
5.3.5.7 订单支付成功回调页面接口

gulimall-member/src/main/java/com/wen/gulimall/member/web/MemberWebController.java

@Controller
public class MemberWebController {
    @Resource
    private OrderFeignService orderFeignService;

    @GetMapping("memberOrder.html")
    public String memberOrderPage(@RequestParam(value = "pageNum",defaultValue = "1") Integer pageNum, Model model){
        // 查询当前登录用户的所有订单页表数据
        Map<String,Object> params = new HashMap<>();
        params.put("page",pageNum.toString());
        R r = orderFeignService.listWithItem(params);
        System.out.println(JSON.toJSONString(r));
        model.addAttribute("orders",r);
        return "orderList";
    }
}

远程调用订单服务接口

gulimall-member/src/main/java/com/wen/gulimall/member/feign/OrderFeignService.java 

@FeignClient("gulimall-order")
public interface OrderFeignService {
    @PostMapping("/order/order/listWithItem")
    R listWithItem(@RequestBody Map<String, Object> params);
}

gulimall-order/src/main/java/com/wen/gulimall/order/controller/OrderController.java 

@RestController
@RequestMapping("order/order")
public class OrderController {
    @Autowired
    private OrderService orderService;

    ...

    /**
     * 分页查询当前登录用户的所有订单
     * @param params
     * @return
     */
    @PostMapping("/listWithItem")
    //@RequiresPermissions("order:order:list")
    public R listWithItem(@RequestBody Map<String, Object> params){
        PageUtils page = orderService.queryPageWithItem(params);

        return R.ok().put("page", page);
    }

    ...
}

gulimall-order/src/main/java/com/wen/gulimall/order/service/OrderService.java 

public interface OrderService extends IService<OrderEntity> {

    ...

    PageUtils queryPageWithItem(Map<String, Object> params);
}

 gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java

@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {

    private ThreadLocal<OrderSubmitVo> confirmVoThreadLocal = new ThreadLocal<>();
    @Resource
    private RabbitTemplate rabbitTemplate;
    @Resource
    private MemberFeignService memberFeignService;
    @Resource
    private CartFeignService cartFeignService;
    @Resource
    private WmsFeignService wmsFeignService;
    @Resource
    private ProductFeignService productFeignService;
    @Resource
    private ThreadPoolExecutor executor;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private OrderItemService orderItemService;

    ...

    @Override
    public PageUtils queryPageWithItem(Map<String, Object> params) {
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        IPage<OrderEntity> page = this.page(
                new Query<OrderEntity>().getPage(params),
                new QueryWrapper<OrderEntity>().eq("member_id",memberRespVo.getId())
        );
        List<OrderEntity> orderSn = page.getRecords().stream().map(order -> {
            List<OrderItemEntity> itemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>().eq("order_sn", order.getOrderSn()));
            // 设置订单项
            order.setItemEntities(itemEntities);
            return order;
        }).collect(Collectors.toList());
        page.setRecords(orderSn);
        return new PageUtils(page);
    }

    ...
}

 给OrderEntity添加订单项属性,如下:

/** 订单项 */
@TableField(exist = false)
private List<OrderItemEntity> itemEntities;

解决Feign远程调用请求头丢失问题

gulimall-member/src/main/java/com/wen/gulimall/member/config/GuliFeignConfig.java

/**
 * @author W
 * @createDate 2024/02/29
 * @description: 解决 Feign远程调用请求头丢失问题
 * 远程调用会创建一个新的请求,新的请求没有请求头
 * 使用RequestInterceptor拦截器为新构建的请求添加请求头
 */
@Configuration
public class GuliFeignConfig {

    @Bean
    public RequestInterceptor requestInterceptor(){
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                // 1.RequestContextHolder拿到刚在进来的请求
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                assert requestAttributes != null;
                HttpServletRequest request = requestAttributes.getRequest(); // 老请求
                if(request!=null) {
                    // 同步请求头数据,Cookie
                    String cookie = request.getHeader("Cookie");
                    // 给构建的新请求同步老请求的Cookie
                    template.header("Cookie", cookie);
                }
            }
        };
    }
}
5.3.5.8 设置支付宝支付成功回调url

修改return_url为member服务的订单列表页请求地址

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

return_url: http://member.gulimall.com/memberOrder.html
5.3.5.9 订单列表页渲染 

gulimall-member/src/main/resources/templates/orderList.html

<table class="table" th:each="order:${orders.page.list}">
  <tr>
    <td colspan="7" style="background:#F7F7F7" >
      <span style="color:#AAAAAA">2017-12-09 20:50:10</span>
      <span><ruby style="color:#AAAAAA">订单号:</ruby> [[${order.orderSn}]]</span>
      <span>谷粒商城<i class="table_i"></i></span>
      <i class="table_i5 isShow"></i>
    </td>
  </tr>
  <tr class="tr" th:each="item,itemStatus:${order.itemEntities}">
    <td colspan="3" style="border-right: 1px solid #ccc">
      <img style="width: 60px;height: 60px" th:src="${item.skuPic}" alt="" class="img">
      <div>
        <p style="width: 242px;height: auto;overflow: auto">
            [[${item.skuName}]]
        </p>
        <div><i class="table_i4"></i>找搭配</div>
      </div>
      <div style="margin-left:15px;">x[[${item.skuQuantity}]]</div>
      <div style="clear:both"></div>
    </td >
    <td th:if="${itemStatus.index}==0" th:rowspan="${itemStatus.size}">[[${order.receiverName}]]<i><i class="table_i1"></i></i></td>
    <td th:if="${itemStatus.index}==0" th:rowspan="${itemStatus.size}" style="padding-left:10px;color:#AAAAB1;">
      <p style="margin-bottom:5px;">总额 ¥[[${order.payAmount}]]</p>
      <hr style="width:90%;">
      <p>在线支付</p>
    </td>
    <td th:if="${itemStatus.index}==0" th:rowspan="${itemStatus.size}">
      <ul>
        <li style="color:#71B247;" th:if="${order.status}==0">待付款</li>
        <li style="color:#71B247;" th:if="${order.status}==1">已付款</li>
        <li style="color:#71B247;" th:if="${order.status}==2">已发货</li>
        <li style="color:#71B247;" th:if="${order.status}==3">已完成</li>
        <li style="color:#71B247;" th:if="${order.status}==4">已取消</li>
        <li style="color:#71B247;" th:if="${order.status}==5">售后中</li>
        <li style="color:#71B247;" th:if="${order.status}==6">售后完成</li>

        <li style="margin:4px 0;" class="hide"><i class="table_i2"></i>跟踪<i class="table_i3"></i>
            <div class="hi">
              <div class="p-tit">
                普通快递   运单号:390085324974
              </div>
              <div class="hideList">
                <ul>
                  <li>
                    [北京市] 在北京昌平区南口公司进行签收扫描,快件已被拍照(您
                    的快件已签收,感谢您使用韵达快递)签收
                  </li>
                  <li>
                    [北京市] 在北京昌平区南口公司进行签收扫描,快件已被拍照(您
                    的快件已签收,感谢您使用韵达快递)签收
                  </li>
                  <li>
                    [北京昌平区南口公司] 在北京昌平区南口公司进行派件扫描
                  </li>
                  <li>
                    [北京市] 在北京昌平区南口公司进行派件扫描;派送业务员:业务员;联系电话:17319268636
                  </li>
                </ul>
              </div>
            </div>
        </li>
        <li class="tdLi">订单详情</li>
      </ul>
    </td>
    <td th:if="${itemStatus.index}==0" th:rowspan="${itemStatus.size}">
      <button>确认收货</button>
      <p style="margin:4px 0; ">取消订单</p>
      <p>催单</p>
    </td>
  </tr>
</table>

5.3.6 接收支付宝异步通知

  • 订单支付成功后,支付宝会回调商户接口,这时候需要修改订单状态;
  • 为了确保订单真的支付成功,需要对支付宝支付成功回调带来的签名进行验签,验签通过修改订单状态;
  • 由于同步跳转可能由于网络问题导致失败,所以使用异步通知;
  • 支付宝使用的最大努力通知型方案,确保商户与其之间的数据一致性,隔一段时间会通知商户支付成功,直到返回 success.

支付宝异步通知说明文档:电脑网站支付-异步通知说明

5.3.6.1 支付宝异步通知信息vo

gulimall-order/src/main/java/com/wen/gulimall/order/vo/PayAsyncVo.java

@ToString
@Data
public class PayAsyncVo {

    private String gmt_create;
    private String charset;
    private String gmt_payment;
    private Date notify_time;
    private String subject;
    private String sign;
    private String buyer_id;//支付者的id
    private String body;//订单的信息
    private String invoice_amount;//支付金额
    private String version;
    private String notify_id;//通知id
    private String fund_bill_list;
    private String notify_type;//通知类型; trade_status_sync
    private String out_trade_no;//订单号
    private String total_amount;//支付的总额
    private String trade_status;//交易状态  TRADE_SUCCESS
    private String trade_no;//流水号
    private String auth_app_id;//
    private String receipt_amount;//商家收到的款
    private String point_amount;//
    private String app_id;//应用id
    private String buyer_pay_amount;//最终支付的金额
    private String sign_type;//签名类型
    private String seller_id;//商家的id

}
5.3.6.2 接收支付宝异步通知接口

gulimall-order/src/main/java/com/wen/gulimall/order/listener/OrderPayedListener.java

@RestController
public class OrderPayedListener {
    @Resource
    private OrderService orderService;
    @Resource
    private AlipayTemplate alipayTemplate;
    /**
     * 支付宝支付成功异步通知
     * @param request
     * @return
     */
    @PostMapping("/payed/notify")
    public String handleAliPayed(PayAsyncVo vo, HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {
        // 只要我们收到支付宝给我们的异步通知,告诉我们订单支付成功。返回success,支付宝就再也不通知
        //Map<String, String[]> map = request.getParameterMap();
        //for (String key : map.keySet()) {
        //    String value = request.getParameter(key);
        //    System.out.println("参数名:"+key+"=》参数值"+value);
        //}
        // 验签,验证是否是支付宝返回的信息
        Map<String,String> params = new HashMap<String,String>();
        Map<String,String[]> requestParams = request.getParameterMap();
        for (Iterator<String> iter = requestParams.keySet().iterator(); iter.hasNext();) {
            String name = iter.next();
            String[] values = requestParams.get(name);
            String valueStr = "";
            for (int i = 0; i < values.length; i++) {
                valueStr = (i == values.length - 1) ? valueStr + values[i]
                        : valueStr + values[i] + ",";
            }
            //乱码解决,这段代码在出现乱码时使用
            //valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
            params.put(name, valueStr);
        }

        boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(), alipayTemplate.getCharset(), alipayTemplate.getSign_type()); //调用SDK验证签名
        if(signVerified) {
            // 验签成功
            System.out.println("签名验证成功。。。。。");
            String result = orderService.handlePayResult(vo);
            return result;
        }else {
            System.out.println("签名验证失败。。。。。");
            return "error";
        }
    }
}

处理支付结果

gulimall-order/src/main/java/com/wen/gulimall/order/service/OrderService.java

public interface OrderService extends IService<OrderEntity> {
    
    ...
    String handlePayResult(PayAsyncVo vo);
}

 gulimall-order/src/main/java/com/wen/gulimall/order/service/impl/OrderServiceImpl.java

@Slf4j
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {

    private ThreadLocal<OrderSubmitVo> confirmVoThreadLocal = new ThreadLocal<>();
    @Resource
    private RabbitTemplate rabbitTemplate;
    @Resource
    private MemberFeignService memberFeignService;
    @Resource
    private CartFeignService cartFeignService;
    @Resource
    private WmsFeignService wmsFeignService;
    @Resource
    private ProductFeignService productFeignService;
    @Resource
    private ThreadPoolExecutor executor;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private OrderItemService orderItemService;
    @Resource
    private PaymentInfoService paymentInfoService;

    ...

    /**
     * 处理支付宝的支付结果
     * @param vo
     * @return
     */
    @Override
    public String handlePayResult(PayAsyncVo vo) {
        // 1. 保存交易流水
        PaymentInfoEntity infoEntity = new PaymentInfoEntity();
        infoEntity.setAlipayTradeNo(vo.getTrade_no());
        infoEntity.setOrderSn(vo.getOut_trade_no());
        infoEntity.setPaymentStatus(vo.getTrade_status());
        infoEntity.setCallbackTime(vo.getNotify_time());
        paymentInfoService.save(infoEntity);

        // 2. 修改订单状态
        if(vo.getTrade_status().equals("TRADE_SUCCESS")||vo.getTrade_status().equals("TRADE_FINISHED")){
            String outTradeNo = vo.getOut_trade_no();
            this.baseMapper.updateOrderStatus(outTradeNo,OrderStatusEnum.PAYED.getCode());
        }
        return "success";
    }
}

修改订单状态

gulimall-order/src/main/java/com/wen/gulimall/order/dao/OrderDao.java 

@Mapper
public interface OrderDao extends BaseMapper<OrderEntity> {

    void updateOrderStatus(@Param("outTradeNo") String outTradeNo, @Param("code") Integer code);
}

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

<update id="updateOrderStatus">
    update oms_order set `status`=#{code} where order_sn = #{outTradeNo}
</update>
5.3.6.3 登录拦截器放行异步通知接口

gulimall-order/src/main/java/com/wen/gulimall/order/interceptor/LoginUserInterceptor.java

/**
 * @author W
 * @createDate 2024/02/27
 * @description: 登录拦截器
 * 从session中(redis中)获取了登录信息,封装到ThreadLocal
 * 自定义拦截器需要添加到webmvc中,否则不起作用
 */
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
    // 同一个线程共享数据
    public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String requestURI = request.getRequestURI();
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        boolean match = antPathMatcher.match("/order/order/status/**", requestURI);
        boolean match1 = antPathMatcher.match("/payed/notify", requestURI);
        if(match || match1){
            return true;
        }
        MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
        if(attribute!=null){
            // 登录成功
            loginUser.set(attribute);
            return true;
        }else {
            // 没登录,去登录
            request.getSession().setAttribute("msg","请先进行登录");
            response.sendRedirect("http://auth.gulimall.com/login.html");
            return false;
        }
    }
}
5.3.6.4 设置支付宝异步通知路径

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

notify_url: http://2vgqx5.natappfree.cc/payed/notify
5.3.6.5 设置接收异步通知信息相关日期格式

注意:如果日期格式不正确,调用异步通知接口失败。(这里和老师的报错不同,与Spring版本相关)

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

spring:
  mvc:
    format:
      date: yyyy-MM-dd HH:mm:ss
5.3.6.6 解决订单号长度报错

修改表oms_payment_info中订单号order_sn的长度

5.3.7 异步通知内网穿透环境搭建 

  • 外网允许访问的内网地址:order.gulimall.com:80;
  • 由于支付宝支付成功,异步回调的请求头不是order.gulimall.com,因此nginx转发到网关后找不到对应的服务,所以需要修改nginx配置手动设置header为订单服务域名。

5.3.7.1 修改内网穿透隧道配置  

5.3.7.2 修改nginx配置
    listen       80;
    listen  [::]:80;
    server_name gulimall.com *.gulimall.com *.natappfree.cc;

    #charset koi8-r;
    #access_log  /var/log/nginx/host.access.log  main;
    location /static/ {
         root   /usr/share/nginx/html;
    }

    location / {
        proxy_pass http://gulimall;
        proxy_set_header Host $host; # 解决nginx反向代理host信息丢失问题
    }
    location /payed/ {
        proxy_pass http://gulimall;
        proxy_set_header Host order.gulimall.com; # 解决nginx反向代理host信息丢失问题
    }

5.3.8 支付测试

支付宝支付成功,同步跳转到订单列表页,异步修改订单状态。

5.3.9 收单

1、订单在支付页,不支付,一直刷新,订单过期了才支付,订单状态改为已支付了,但是库存解锁了。

  • 使用支付宝自动收单功能解决。只要一段时间不支付,就不能支付了。

设置订单超时时间

gulimall-order/src/main/java/com/wen/gulimall/order/config/AlipayTemplate.java

订单超过1m(分钟)不支付,效果:

2、 由于时延等问题。订单解锁完成,正在解锁库存的时候,异步通知才到

  • 订单解锁,手动调用收单。

3、 网络阻塞问题,订单支付成功的异步通知一直不到达

  • 查询订单列表时,ajax获取当前未支付的订单状态,查询订单状态时,再获取一下支付宝
    此订单的状态

4、其他各种问题

  • 每天晚上闲时下载支付宝对账单,一一进行对账

6 接口幂等性 

6.1 什么是幂等性

        接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的, 不会因
为多次点击而产生了副作用; 比如说支付场景, 用户购买了商品支付扣款成功, 但是返回结
果的时候网络异常, 此时钱已经扣了, 用户再次点击按钮, 此时会进行第二次扣款, 返回结
果成功, 用户查询余额返发现多扣钱了, 流水记录也变成了两条. . . ,这就没有保证接口
的幂等性。

6.2 哪些情况需要防止

  1. 用户多次点击按钮
  2. 用户界面回退再次提交
  3. 微服务互相调用,由于网络问题,导致请求失败。feign触发重试机制
  4. 其他业务情况

6.3 什么情况下需要幂等

以 SQL 为例, 有些操作是天然幂等的。

  • SELECT * FROM table WHER id=?, 无论执行多少次都不会改变状态, 是天然的幂等
  • UPDATE tab1 SET col1=1 WHERE col2=2, 无论执行成功多少次状态都是一致的, 也是幂等操作。
  • delete from user where userid=1, 多次操作, 结果一样, 具备幂等
  • insert into user(userid,name) values(1,'a') 如 userid 为唯一主键, 即重复操作上面的业务, 只会插入一条用户数据, 具备幂等性。
  • UPDATE tab1 SET col1=col1+1 WHERE col2=2, 每次执行的结果都会发生变化, 不是幂等的。
  • insert into user(userid,name) values(1,'a') 如 userid 不是主键, 可以重复, 那上面业务多次操作, 数据都会新增多条, 不具备幂等性。

6.4 幂等解决方案

6.4.1 token机制

  1. 服务端提供了发送 token 的接口。 我们在分析业务的时候, 哪些业务是存在幂等问题的,就必须在执行业务前, 先去获取 token, 服务器会把 token 保存到 redis 中。
  2.  然后调用业务接口请求时, 把 token 携带过去, 一般放在请求头部。
  3. 服务器判断 token 是否存在 redis 中, 存在表示第一次请求, 然后删除 token,继续执行业务。
  4. 如果判断 token 不存在 redis 中, 就表示是重复操作, 直接返回重复标记给 client, 这样就保证了业务代码, 不被重复执行。

危险性:
1、 先删除 token 还是后删除 token;
        (1) 先删除可能导致, 业务确实没有执行, 重试还带上之前 token, 由于防重设计导致,
请求还是不能执行。
        (2) 后删除可能导致, 业务处理成功, 但是服务闪断, 出现超时, 没有删除 token, 别
人继续重试, 导致业务被执行两遍
        (3) 我们最好设计为先删除 token, 如果业务调用失败, 就重新获取 token 再次请求。
2、 Token 获取、 比较和删除必须是原子性
        (1) redis.get(token) 、 token.equals、 redis.del(token)如果这两个操作不是原子, 可能导
致, 高并发下, 都 get 到同样的数据, 判断都成功, 继续业务并发执行
        (2) 可以在 redis 使用 lua 脚本完成这个操作

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

6.4.2 各种锁机制

6.4.2.1 数据库悲观锁

select * from xxxx where id = 1 for update;

悲观锁使用时一般伴随事务一起使用, 数据锁定时间可能会很长, 需要根据实际情况选用。另外要注意的是, id 字段一定是主键或者唯一索引, 不然可能造成锁表的结果, 处理起来会非常麻烦。

6.4.2.2 数据库乐观锁

这种方法适合在更新的场景中,

update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1

根据 version 版本, 也就是在操作库存前先获取当前商品的 version 版本号, 然后操作的时候带上此 version 号。 我们梳理下, 我们第一次操作库存时, 得到 version 为 1, 调用库存服务version 变成了 2; 但返回给订单服务出现了问题, 订单服务又一次发起调用库存服务, 当订单服务传如的 version 还是 1, 再执行上面的 sql 语句时, 就不会执行; 因为 version 已经变为 2 了, where 条件就不成立。 这样就保证了不管调用几次, 只会真正的处理一次。

乐观锁主要使用于处理读多写少的问题。

6.4.2.3 业务层分布式锁

如果多个机器可能在同一时间同时处理相同的数据, 比如多台机器定时任务都拿到了相同数据处理, 我们就可以加分布式锁, 锁定此数据, 处理完成后释放锁。 获取到锁的必须先判断这个数据是否被处理过。

6.4.3 各种唯一约束

6.4.3.1 数据库唯一约束

插入数据, 应该按照唯一索引进行插入, 比如订单号, 相同的订单就不可能有两条记录插入。我们在数据库层面防止重复。

这个机制是利用了数据库的主键唯一约束的特性, 解决了在 insert 场景时幂等问题。 但主键的要求不是自增的主键, 这样就需要业务生成全局唯一的主键。

如果是分库分表场景下, 路由规则要保证相同请求下, 落地在同一个数据库和同一表中, 要不然数据库主键约束就不起效果了, 因为是不同的数据库和表主键不相关。

6.4.3.2 redis set 防重

很多数据需要处理, 只能被处理一次, 比如我们可以计算数据的 MD5 将其放入 redis 的 set,每次处理数据, 先看这个 MD5 是否已经存在, 存在就不处理。

6.4.4 防重表

使用订单号 orderNo 做为去重表的唯一索引, 把唯一索引插入去重表, 再进行业务操作, 且他们在同一个事务中。 这个保证了重复请求时, 因为去重表有唯一约束, 导致请求失败, 避免了幂等问题。 这里要注意的是, 去重表和业务表应该在同一库中, 这样就保证了在同一个事务, 即使业务操作失败了, 也会把去重表的数据回滚。 这个很好的保证了数据一致性。

之前说的 redis 防重也算

6.4.5 全局请求唯一id

调用接口时, 生成一个唯一 id, redis 将数据保存到集合中(去重) , 存在即处理过。可以使用 nginx 设置每一个请求的唯一 id;

proxy_set_header X-Request-Id $request_id;

7 本地事务与分布式事务

7.1 本地事务

7.1.1 事务的基本性质

        数据库事务的几个特性:原子性(Atomicity)、一致性(Consistency)、隔离性或独立性(lsolation)和持久性(Durabilily),简称就是ACID;

  • 原子性:一系列的操作整体不可拆分, 要么同时成功, 要么同时失败。
  • 一致性:数据在事务的前后, 业务整体一致。
    •  A:1000; B:1000; 转 200 事务成功; A: 800 B: 1200
  • 隔离性: 事务之间互相隔离。
  • 持久性: 一旦事务成功, 数据一定会落盘在数据库。

在以往的单体应用中, 我们多个业务操作使用同一条连接操作不同的数据表, 一旦有异常, 我们可以很容易的整体回滚;

        Business: 我们具体的业务代码

        Storage: 库存业务代码; 扣库存

        Order: 订单业务代码; 保存订单

        Account: 账号业务代码; 减账户余额

比如买东西业务, 扣库存, 下订单, 账户扣款, 是一个整体; 必须同时成功或者失败。

一个事务开始, 代表以下的所有操作都在同一个连接里面。

7.1.2 事务的隔离级别

  • READ UNCOMMITTED(读未提交)

        该隔离级别的事务会读到其它未提交事务的数据, 此现象也称之为脏读。

  • READ COMMITTED( 读已提交)

        一个事务可以读取另一个已提交的事务, 多次读取会造成不一样的结果, 此现象称为不可重复读问题, Oracle 和 SQL Server 的默认隔离级别。

  • REPEATABLE READ( 可重复读)

        该隔离级别是 MySQL 默认的隔离级别, 在同一个事务里, select 的结果是事务开始时时间
点的状态, 因此, 同样的 select 操作读到的结果会是一致的, 但是, 会有幻读现象。 MySQL
的 InnoDB 引擎可以通过 next-key locks 机制( 参考下文"行锁的算法"一节) 来避免幻读。

  • SERIALIZABLE( 序列化)

        在该隔离级别下事务都是串行顺序执行的, MySQL 数据库的 InnoDB 引擎会给读操作隐式
加一把读共享锁, 从而避免了脏读、 不可重读复读和幻读问题。

7.1.3 事务的七种传播行为

  • PROPAGATION_REQUIRED: 如果当前没有事务, 就创建一个新事务, 如果当前存在事务, 就加入该事务, 该设置是最常用的设置。
  • PROPAGATION_SUPPORTS: 支持当前事务, 如果当前存在事务, 就加入该事务, 如果当 前不存在事务, 就以非事务执行。
  • PROPAGATION_MANDATORY: 支持当前事务, 如果当前存在事务, 就加入该事务, 如果 当前不存在事务, 就抛出异常。
  • PROPAGATION_REQUIRES_NEW: 创建新事务, 无论当前存不存在事务, 都创建新事务。  PROPAGATION_NOT_SUPPORTED: 以非事务方式执行操作, 如果当前存在事务, 就把当 前事务挂起。
  • PROPAGATION_NEVER: 以非事务方式执行, 如果当前存在事务, 则抛出异常。
  • PROPAGATION_NESTED: 如果当前存在事务, 则在嵌套事务内执行。 如果当前没有事务, 则执行与 PROPAGATION_REQUIRED 类似的操作。

7.1.4 SpringBoot 事务关键点

7.1.4.1 事务的自动配置

        TransactionAutoConfiguration

7.1.4.2 事务的坑 

        在同一个类里面, 编写两个方法, 内部调用的时候, 会导致事务设置失效。 原因是没有用到代理对象的缘故。事务是使用代理对象控制的

解决方案:

(1)导入 spring-boot-starter-aop

(2)@EnableTransactionManagement(proxyTargetClass = true)

(3)@EnableAspectJAutoProxy(exposeProxy=true)

(4) AopContext.currentProxy() 调用方法  

示例:

/**
 * 方法a、b、c在同一个Service类中,同一个对象内事务方法互调默认失效
 * 原因:绕过了代理对象,事务是使用代理对象控制的
 * 注意:不能使用this.b()、this.c();也不能注入自己,会导致循环依赖
 * 
*/
@Transactional(timeout = 30) // a事务所有的设置就传播到了和他公用的所有事务
public void a(){
    // b和c做任何设置都是没用的。都是和a公用一个事务
    //this.b(); 没用,原因没有使用代理对象进行调用
    //this.c(); 没用

    // bService.b(); // a事务
    // cService.c(); // 新事务(不回滚)
    int i = 10/0;
}

@Transactional(propagation = Propagation.REQUIRED,timeout = 30)
public void b(){

}
@Transactional(propagation = Propagation.REQUIRES_NEW,timeout = 20)
public void c(){

}

解决方案:

1. 引入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2. 开启aspectj动态代理
启动类上添加@EnableAspectJAutoProxy(exposeProxy = true)注解,暴露代理对象
(1)使用cglib继承的方式完成动态代理
(2)exposeProxy = true 暴露代理对象

3. 在同一类中获取动态代理对象
    OrderServiceImpl orderService = (OrderServiceImpl) AopContext.currentProxy();
    orderService.b();
    orderService.c();

7.2 分布式事务

7.2.1 为什么有分布式事务

7.2.2 CAP定理与BASE理论

7.2.2.1 CAP定理

CAP 原则又称 CAP 定理, 指的是在一个分布式系统中

  •  一致性(Consistency):

          在分布式系统中的所有数据备份, 在同一时刻是否同样的值。 (等同于所有节点访 问同一份最新的数据副本)

  • 可用性(Availability)

         在集群中一部分节点故障后, 集群整体是否还能响应客户端的读写请求。 (对数据 更新具备高可用性)

  • 分区容错性(Partition tolerance)

         大多数分布式系统都分布在多个子网络。 每个子网络就叫做一个区(partition) 。 分区容错的意思是, 区间通信可能失败。 比如, 一台服务器放在中国, 另一台服务 器放在美国, 这就是两个区, 它们之间可能无法通信。

CAP 原则指的是, 这三个要素最多只能同时实现两点, 不可能三者兼顾

        一般来说, 分区容错无法避免, 因此可以认为 CAP 的 P 总是成立。 CAP 定理告诉我们, 剩下的 C 和 A 无法同时做到。

两种方案:

(1)CP:

       网络通信故障(或宕机)的节点不在继续提供服务(数据不一致),不保证可用性。

(2)AP:

         所有的机器都可以用,包括故障的那台机器(因为网络等问题数据未同步),就不能保证一致性了。

        分布式系统中实现一致性的 raft 算法、 paxos算法:http://thesecretlivesofdata.com/raft/【raft算法演示】。

Raft算法简介:

(1)基本原理:共识算法就是保证一个集群的多台机器协同工作,在遇到请求时,数据能够保持一致。即使遇到机器宕机,整个系统仍然能够对外保持服务的可用性。

(2)Raft将共识问题分解三个子问题:

  • Leader election 领导选举:有且仅有一个leader节点,如果leader宕机,通过选举机制选出新的leader;

  • Log replication 日志复制:leader从客户端接收数据更新/删除请求,然后日志复制到follower节点,从而保证集群数据的一致性;

  • Safety 安全性:通过安全性原则来处理一些特殊case,保证Raft算法的完备性。

(3)核心流程:

  • 首先选出leader,leader节点负责接收外部的数据更新/删除请求;

  • 然后日志复制到其他follower节点,同时通过安全性的准则来保证整个日志复制的一致性;

  • 如果遇到leader故障,followers会重新发起选举出新的leader。

参考文献:分布式一致性算法Raft

在线演示:Raft Consensus Algorithm

7.2.2.2 面临问题

        对于多数大型互联网应用的场景, 主机众多、 部署分散, 而且现在的集群规模越来越大, 所 以节点故障、 网络故障是常态, 而且要保证服务可用性达到 99.99999%(N 个 9) , 即保证 P 和 A, 舍弃 C。

7.2.2.3 BASE理论

        是对 CAP 理论的延伸, 思想是即使无法做到强一致性(CAP 的一致性就是强一致性) , 但可以采用适当的采取弱一致性, 即最终一致性。

BASE 是指

  • 基本可用(Basically Available)
    • 基本可用是指分布式系统在出现故障的时候, 允许损失部分可用性(例如响应时间、功能上的可用性) , 允许损失部分可用性。 需要注意的是, 基本可用绝不等价于系统不可用。
      •  响应时间上的损失: 正常情况下搜索引擎需要在 0.5 秒之内返回给用户相应的查询结果, 但由于出现故障(比如系统部分机房发生断电或断网故障), 查询结果的响应时间增加到了 1~2 秒。
      • 功能上的损失: 购物网站在购物高峰(如双十一) 时, 为了保护系统的稳定性,部分消费者可能会被引导到一个降级页面。
  • 软状态( Soft State)
    • 软状态是指允许系统存在中间状态, 而该中间状态不会影响系统整体可用性。 分布式存储中一般一份数据会有多个副本, 允许不同副本同步的延时就是软状态的体现。 mysql replication 的异步复制也是一种体现。
  • 最终一致性( Eventual Consistency)
    • 最终一致性是指系统中的所有数据副本经过一定时间后, 最终能够达到一致的状态。 弱一致性和强一致性相反, 最终一致性是弱一致性的一种特殊情况。
7.2.2.4 强一致性、弱一致性、最终一致性

        从客户端角度, 多进程并发访问时, 更新过的数据在不同进程如何获取的不同策略, 决定了
不同的一致性。 对于关系型数据库, 要求更新过的数据能被后续的访问都能看到, 这是强一
致性。 如果能容忍后续的部分或者全部访问不到, 则是弱一致性。 如果经过一段时间后要求
能访问到更新后的数据, 则是最终一致性。

7.2.3 分布式事务的几种方案

7.2.3.1 2PC模式

        数据库支持的 2PC【 2 phase commit 二阶提交】 , 又叫做 XA Transactions。 MySQL 从 5.5 版本开始支持, SQL Server 2005 开始支持, Oracle 7 开始支持。 其中, XA 是一个两阶段提交协议, 该协议分为以下两个阶段:

第一阶段: 事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作, 并反映是 否可以提交。

第二阶段: 事务协调器要求每个数据库提交数据。 其中, 如果有任何一个数据库否决此次提交, 那么所有数据库都会被要求回滚它们在此事务 中的那部分信息。

  • XA 协议比较简单, 而且一旦商业数据库实现了 XA 协议, 使用分布式事务的成本也比较低。
  • XA 性能不理想, 特别是在交易下单链路, 往往并发量很高, XA 无法满足高并发场景。
  • XA 目前在商业数据库支持的比较理想, 在 mysql 数据库中支持的不太理想, mysql 的XA 实现, 没有记录 prepare 阶段日志, 主备切换回导致主库与备库数据不一致。
  • 许多 nosql 也没有支持 XA, 这让 XA 的应用场景变得非常狭隘。
  • 也有 3PC, 引入了超时机制( 无论协调者还是参与者, 在向对方发送请求后, 若长时间未收到回应则做出相应处理)。
7.2.3.2 柔性事务-TCC事务补偿型方案(手动补偿)

刚性事务: 遵循 ACID 原则, 强一致性。

柔性事务: 遵循 BASE 理论, 最终一致性;

与刚性事务不同, 柔性事务允许一定时间内, 不同节点的数据不一致, 但要求最终一致。

7.2.3.3 柔性事务-最大努力通知型方案

        按规律进行通知, 不保证数据一定能通知成功, 但会提供可查询操作接口进行核对。 这种 方案主要用在与第三方系统通讯时, 比如: 调用微信或支付宝支付后的支付结果通知。 这种 方案也是结合 MQ 进行实现, 例如: 通过 MQ 发送 http 请求, 设置最大通知次数。 达到通 知次数后即不再通知。

案例: 银行通知、 商户通知等( 各大交易业务平台间的商户通知: 多次通知、 查询校对、 对 账文件) , 支付宝的支付成功异步回调。

7.2.3.4 柔性事务-可靠消息+最终一致性方案(异步确保型)

        实现: 业务处理服务在业务事务提交之前, 向实时消息服务请求发送消息, 实时消息服务只记录消息数据, 而不是真正的发送。 业务处理服务在业务事务提交之后, 向实时消息服务确认发送。 只有在得到确认发送指令后, 实时消息服务才会真正发送。

防止消息丢失:

/**
* 1 、做好消息确认机制( pulisher consumer 【手动 ack 】)
* 2 、每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一
*/

网站公告

今日签到

点亮在社区的每一天
去签到