05谷粒商城-高级篇五

前言

知不足而奋进,望远山而前行

13.商城业务-分布式事务

13.1本地事务在分布式下的问题

主要步骤:

  • 远程服务其实成功了,由于网络故障没有返回,导致订单回滚,存库成功扣减
  • 远程服务执行完成,下面的其他方法出现问题,导致已执行的远程请求不能回滚

image-20240814233816585

image-20240814233703598

13.2本地事务隔离级别&传播行为等复习

事务的基本性质

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

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

事务的隔离级别

  • READ UNCOMMITTED(读未提交):该隔离级别的事务会读到其它未提交事务的数据,此现象也称之为脏读。
  • READ COMMITTED(读提交:一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重 复读问题,Oracle 和 SQL Server 的默认隔离级别。
  • REPEATABLE READ(可重复读):该隔离级别是 MySQL 默认的隔离级别,在同一个事务里,select 的结果是事务开始时时间 点的状态,因此,同样的 select 操作读到的结果会是一致的,但是,会有幻读现象。MySQL 的 InnoDB 引擎可以通过 next-key locks 机制(参考下文"行锁的算法"一节)来避免幻读。
  • SERIALIZABLE(序列化):在该隔离级别下事务都是串行顺序执行的,MySQL 数据库的 InnoDB 引擎会给读操作隐式 加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。

事务的传播行为

  • 1、PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务, 就加入该事务,该设置是最常用的设置。

  • 2、PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当 前不存在事务,就以非事务执行。

  • 3、PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果 当前不存在事务,就抛出异常。

  • 4、PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。

  • 5、PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当 前事务挂起。

  • 6、PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

  • 7、PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务, 则执行与 PROPAGATION_REQUIRED 类似的操作。

image-20240814235555288

SpringBoot 事务关键点

事务的自动配置:TransactionAutoConfiguration

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

解决:

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

  • 1)、@EnableTransactionManagement(proxyTargetClass = true)

  • 2)、@EnableAspectJAutoProxy(exposeProxy=true)

  • 3)、AopContext.currentProxy() 调用方法

使用代理对象来调用事务方法

image-20240814235456726

13.3分布式CAP&Raft原理

为什么有分布式事务

分布式系统经常出现的异常 机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的 TCP、存储数据丢失...

image-20240815020859913

分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个 东西,特别是在微服务架构中,几乎可以说是无法避免。

CAP 定理与 BASE 理论

CAP 定理

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

  • 一致性(Consistency):
    • 在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访 问同一份最新的数据副本)
  • 可用性(Availability):
    • 在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据 更新具备高可用性)
  • 分区容错性(Partition tolerance):
    • 大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。 分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务 器放在美国,这就是两个区,它们之间可能无法通信。

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

image-20240815021259457

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

raft 算法

分布式系统中实现一致性的 raft 算法、paxos算法

Raft (thesecretlivesofdata.com)

13.4BASE

面临的问题

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

BASE 理论

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

BASE 是指

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

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

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

13.5分布式事务常见解决方案

1.2PC 模式

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

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

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

image-20240815031207781

  • XA 协议比较简单,而且一旦商业数据库实现了 XA 协议,使用分布式事务的成本也比较 低。

  • XA 性能不理想,特别是在交易下单链路,往往并发量很高,XA 无法满足高并发场景

  • XA 目前在商业数据库支持的比较理想,在 mysql 数据库中支持的不太理想,mysql 的 XA 实现,没有记录 prepare 阶段日志,主备切换回导致主库与备库数据不一致。

  • 许多 nosql 也没有支持 XA,这让 XA 的应用场景变得非常狭隘。

  • 也有 3PC,引入了超时机制(无论协调者还是参与者,在向对方发送请求后,若长时间 未收到回应则做出相应处理)

2.柔性事务-TCC 事务补偿型

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

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

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

image-20240815031513064

一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。

二阶段 commit 行为:调用 自定义 的 commit 逻辑。

三阶段 rollback 行为:调用 自定义 的 rollback 逻辑。

所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理

image-20240815031606759

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

按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。这种 方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种 方案也是结合 MQ 进行实现,例如:通过 MQ 发送 http 请求,设置最大通知次数。达到通 知次数后即不再通知。 案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对 账文件),支付宝的支付成功异步回调。

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

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

防止消息丢失:

/**

*1、做好消息确认机制(pulisher,consumer【手动 ack】)

*2、每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一 遍

*/

13.6Seata&环境准备

主要步骤:

  • 导入seata 数据库
  • 部署seata服务
  • 给所有的微服务创建undo_log

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

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

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

配置application.yml

image-20240815220703982

导入seata 数据库

# 把sql文件拷贝到mysql的docker容器里
docker cp seata-tc.sql mysql:/seata-tc.sql
# 进入mysql容器
docker exec -it mysql bash
# 连接mysql
mysql -uroot -p
# 执行sql文件
source /seata-tc.sql
# 执行完成的sql文件可以删除
rm -f seata-tc.sql

image-20240815220744274

部署seata

如果下载太慢,可以使用docker load -i从压缩包加载 Docker 镜像

# 加载镜像
docker load -i seata-1.5.2.tar

我是使用docekrcompose部署的

  seata:
    image: seataio/seata-server:1.5.2
    container_name: seata
    privileged: true                     # 设置容器的特权模式为 true
    environment:
      SEATA_IP: 192.168.188.180
    ports:
      - "8099:8099"                      # 映射主机的7091端口到容器的7091端口
      - "7099:7099"                      # 映射主机的8091端口到容器的8091端口
    depends_on:
      - mysql                            # 启动顺序,先启动 mysql 服务
      - nacos                            # 启动顺序,先启动 nacos 服务
    volumes:
      - ./seata:/seata-server/resources  # 挂载本地 seata 目录到容器的 /seata-server/resources
    networks:
      - mall-net                         # 指定连接的网络
    restart: always

image-20240815220840493

访问http://192.168.188.180:7099/

用户名密码是application.yml配置的

image-20240527004546133

image-20240815220945016

给所有的微服务创建undo_log

image-20240815221331554

13.7Seata分布式事务体验

主要步骤:

  • 订单服务gulimall-order、购物车服务gulimall-cart、商品服务gulimall-product、库存服务gulimall-ware都要导入seata,并且添加seata配置
  • 测试下单服务

订单服务gulimall-order、购物车服务gulimall-cart、商品服务gulimall-product、库存服务gulimall-ware都要导入seata,并且添加seata配置

导入seata

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

添加seata配置

seata:
  # data-source-proxy-mode: XA
  registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
    type: nacos # 注册中心类型 nacos
    nacos:
      server-addr: 192.168.188.180:8848 # nacos地址
      namespace: "" # namespace,默认为空
      group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
      application: seata-server # seata服务名称
      username: nacos
      password: nacos
  tx-service-group: peng # 事务组名称
  service:
    vgroup-mapping: # 事务组与tc集群的映射关系
      peng: "default"

image-20240815230526469

然后运行项目,seata日志显示gulimall-ordergulimall-productgulimall-waregulimall-cart加入成功

image-20240815231108872

再次添加购物车,我选了一个没有库存的商品,然后提交订单

image-20240815231226918

没有订单生成,也没有锁定库存,分布式事务回滚成功

image-20240815231330485

13.8最终一致性库存解锁逻辑

seata分布式事务不适合高并发场景

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

建议最大努力通知和可靠消息+最终一致性方案

image-20240815232234550

14.商城业务-订单服务

14.1RabbitMQ延时队列

RabbitMQ延时队列(实现定时任务)

场景: 比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品。

常用解决方案: spring的 schedule

缺点: 消耗系统内存、增加了数据库的压力、存在较大的时间误差

解决:rabbitmq的消息TTL和死信Exchange结合

消息的TTL(Time To Live)

  • 消息的TTL就是消息的存活时间。

  • RabbitMQ可以对队列和消息分别设置TTL。

    • 对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的 设置。超过了这个时间,我们认为这个消息就死了,称之为死信。
    • 如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队 列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的 TTL,因为它才是实现延迟任务的关键。可以通过设置消息的expiration字段或者x- message-ttl属性来设置时间,两者是一样的效果。

Dead Letter Exchanges(DLX)

  • 一个消息在满足如下条件下,会进死信路由,记住这里是路由而不是队列, 一个路由可以对应很多队列。(什么是死信)
  • 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不 会被再次放在队列里,被其他消费者使用。(basic.reject/ basic.nack)requeue=false
  • 上面的消息的TTL到了,消息过期了。
  • 队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上
  • Dead Letter Exchange其实就是一种普通的exchange,和创建其他 exchange没有两样。只是在某一个设置Dead Letter Exchange的队列中有 消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去。
  • 我们既可以控制消息在一段时间后变成死信,又可以控制变成死信的消息 被路由到某一个指定的交换机,结合二者,其实就可以实现一个延时队列
  • 手动ack&异常消息统一放在一个队列处理建议的两种方式
    • catch异常后,手动发送到指定队列,然后使用channel给rabbitmq确认消息已消费
    • 给Queue绑定死信队列,使用nack(requque为false)确认消息消费失败

image-20240816004625262

延时队列实现-1

image-20240816004651245

延时队列实现-2

image-20240816004711099

14.2延时队列定时关单模拟

主要步骤:

  • 创建交换机order-event-exchange
  • 创建延时队列order.delay.queue
    • arguments.put("x-dead-letter-exchange", "order-event-exchange");:绑定死信交换机
    • arguments.put("x-dead-letter-routing-key", "order.release.order");:设置死信路由routing-key
    • arguments.put("x-message-ttl", 20000):设置过期时间
  • 创建普通队列order.release.order.queue
  • 两个队列都绑定order-event-exchange,当消息过期带上routing-key=order.release.order转发给order-event-exchange,最后转发给order.release.order.queue

第一种方式

设置消息过期时间,1分钟后转发给user.order.exchange,客户端监听user.order.exchange进行消费

image-20240816010408280

第二种方式

生产者生产消息设置消息过期时间和routingkey=order.create.order转发给order-event-exchange,1分钟后带上routingkey=order.release.order转发给order-event-exchangeorder-event-exchange根据路由转发给order.release.order.queue,客户端监听order.release.order.queue进行消费

image-20240816010435462

SpringBoot中使用延时队列

使用Bean创建交换机、队列,我这里设置的过期时间是20s

image-20240816012834082

创建绑定关系

image-20240816012653776

监听order.release.order.queue队列

@RabbitListener(queues = "order.release.order.queue")
public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
    System.out.println("收到过期的订单信息:准备关闭订单" + entity.getOrderSn());
    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}

image-20240816012721611

模拟创建订单的消息

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

image-20240816012754319

发送创建订单请求

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

image-20240816012931538

查看控制台,发现20s后order.delay.queue收到消息并消费

image-20240816012443938

14.3创建业务交换机&队列

主要步骤:

  • 创建交换机stock-event-exchange
  • 创建延时队列stock.delay.queue
    • arguments.put("x-dead-letter-exchange", "stock-event-exchange"); :绑定死信交换机
      • arguments.put("x-dead-letter-routing-key", "stock.release");:设置死信路由routing-key
    • arguments.put("x-message-ttl", 120000);:设置过期时间2分钟
  • 创建普通队列stock.release.stock.queue
  • 两个队列都绑定stock-event-exchange,当消息过期带上routing-key=stock.release转发给stock-event-exchange,最后转发给stock.release.stock.queue

image-20240816015112739

image-20240816015347941

14.4监听库存解锁

库存解锁场景

这里主要解决第二种场景

  • 1.下订单成功,订单过期没有支付被系统自动取消、被用户手动取消。都要解锁库存
  • 2.下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。
    • 之前锁定的库存就要自动解锁。

主要步骤:

  • 数据库mall_wms.wms_ware_order_task_detail添加字段仓库idware_id、锁定状态lock_status
  • 如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
  • 锁定失败。前面保存的工作单信息都回滚了。发送出去的消息,即使要解锁库存,由于在数据库查不到指定的id,所有就不用解锁
  • 监听stock.release.stock.queue,成功解锁发送ack,不成功就reject,消息重新放回队列,让别人消费

数据库mall_wms.wms_ware_order_task_detail添加字段仓库idwareId、锁定状态lockStatus

image-20240816215233439

如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ

image-20240816040248684

监听stock.release.stock.queue,成功解锁发送ack,不成功就reject,消息重新放回队列,让别人消费

image-20240816040331972

14.5库存解锁逻辑

主要步骤:

  • 远程调用订单服务gulimall-order根据orderSn查询订单信息
  • 解锁库存
    • 订单不存在
    • 订单已关闭
    • 库存工作单已锁定

image-20240816042819352

解锁

image-20240816042912908

14.6库存自动解锁完成

主要步骤:

  • gulimall-order拦截器调用/order/order/status/**时不需要进行登录验证

  • 优化解锁库存代码,单独创建监听类StockReleaseListener

gulimall-order拦截器调用/order/order/status/**时不需要进行登录验证

image-20240816210844935

优化解锁库存代码,单独创建监听类StockReleaseListener

image-20240816210719908

14.7测试库存自动解锁

清除相关表数据

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

主要步骤:

  • 1.创建订单submitOrder时,在调用gulimall-ware的方法orderLockStock锁定库存成功后,模拟异常,这样创建订单就会失败,创建订单会回滚(oms_orderoms_order_item),但是库存已经成功扣除
  • 2.gulimall-ware的方法orderLockStock锁定库存方法成功时向mq延时队列stock.delay.queue添加消息,并添加溯源数据(wms_ware_order_taskwms_ware_order_task_detail),消息会在20s(设置的过期时间)时转发给普通队列stock.release.stock.queue
  • 3.监听普通队列stock.release.stock.queue,根据orderSn获取订单状态,在订单状态已关闭或者订单不存在和当前库存工作单详情状态已锁定(1),可以进行解锁,解锁完成更新库存工作单详情状态已解锁

创建订单submitOrder时,在调用gulimall-ware的方法orderLockStock锁定库存成功后,模拟异常,这样创建订单就会失败,创建订单会回滚(oms_orderoms_order_item),但是库存已经成功扣除

image-20240816222713296

gulimall-ware的方法orderLockStock锁定库存方法成功时向mq延时队列stock.delay.queue添加消息,并添加溯源数据(wms_ware_order_taskwms_ware_order_task_detail),消息会在20s(设置的过期时间)时转发给普通队列stock.release.stock.queue

image-20240816222818737

监听普通队列stock.release.stock.queue,根据orderSn获取订单状态,在订单状态已关闭或者订单不存在和当前库存工作单详情状态已锁定(1),可以进行解锁

image-20240816222839514

解锁完成更新库存工作单详情状态已解锁

image-20240816222851838

14.8定时关单完成

主要步骤:

  • order-event-exchange交换机和stock.release.stock.queue队列建立绑定关系,关闭订单后向stock.release.stock.queue发送消息解锁库存

  • gulimall-order创建订单submitOrder完成时向延时队列order.delay.queue发送消息,20s到期转发给普通队列order.delay.queue,模拟20s后关闭订单

  • gulimall-ware监听关闭订单后发送的消息,然后解锁库存,这是主动解锁

  • gulimall-ware解锁库存时发送的延时消息是被动解锁

image-20240816234409117

order-event-exchange交换机和stock.release.stock.queue队列建立绑定关系,关闭订单后向stock.release.stock.queue发送消息解锁库存

image-20240816233830063

gulimall-order创建订单submitOrder完成时向延时队列order.delay.queue发送消息,30s到期转发给普通队列order.delay.queue,模拟30s后关闭订单

image-20240816234446399

gulimall-ware监听关闭订单后发送的消息,然后解锁库存,这是主动解锁

image-20240816234509950

gulimall-ware解锁库存时发送的延时消息是被动解锁

image-20240816234535118

创建订单30s收到延时队列的消息关闭订单

image-20240816234609649

关闭订单后向普通队列stock.release.stock.queue发送消息,gulimall-ware进行主动解锁

image-20240816234729968

锁定库存时向延时队列stock.delay.queue发送消息,并设定过期时间30s,主动解锁后再进行被动解锁

image-20240816234841966

主动解锁是创建订单后20s,被动解锁时锁定库存后30s

image-20240816235220006

14.9消息丢失、积压、重复等解决方案

消息丢失

  • 消息发送出去,由于网络问题没有抵达服务器
    • 做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机 制,可记录到数据库,采用定期扫描重发的方式
    • 做好日志记录,每个消息状态是否都被服务器收到都应该记录
    • 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进 行重发
  • 消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚 未持久化完成,宕机。
    • publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。
  • 自动ACK的状态下。消费者收到消息,但没来得及消息然后宕机
    • 一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重 新入队

image-20240817001009436

image-20240817001104316

消息重复

  • 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息 重新由unack变为ready,并发送给其他消费者
  • 消息消费失败,由于重试机制,自动又将消息发送出去
  • 成功消费,ack时宕机,消息由unack变为ready,Broker又重新发送
    • 消费者的业务消费接口应该设计为幂等性的。比如扣库存有 工作单的状态标志
    • 使用防重表(redis/mysql),发送消息每一个都有业务的唯 一标识,处理过就不用处理
    • rabbitMQ的每一个消息都有redelivered字段,可以获取是否 是被重新投递过来的,而不是第一次投递过来的

没有解锁的库存才进行解锁,保证方法幂等性

image-20240817001322133

防重表

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

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

//当前消息是否被第二次及以后(重新)派发过来了
Boolean redelivered = message.getMessageProperties().getRedelivered();

image-20240817001716714

消息积压

  • 消费者宕机积压
  • 消费者消费能力不足积压
  • 发送者发送流量太大
    • 上线更多的消费者,进行正常消费
    • 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理

15.商城业务-支付

15.1支付宝沙箱&代码

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

开发者文档 https://openhome.alipay.com/docCenter/docCenter.htm

全部文档=>电脑网站支付文档;下载 demo https://opendocs.alipay.com/open/270/106291/

image-20240817004852479

15.2RSA、加密加签、密钥等

资料地址

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

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

密钥工具下载地址:https://opendocs.alipay.com/common/02kipk?pathHash=0d20b438

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

买家账号
kalnhn3954@sandbox.com
登录密码
111111

公钥、私钥、加密、签名和验签

公钥私钥

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

加密和数字签名

加密

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

签名

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

验签

  • 支付宝为了验证请求的数据是否商户本人发的
  • 商户为了验证响应的数据是否支付宝发的

下载demo并打开

下载demo

image-20240817025934994

然后解压

image-20240817030007670

使用idea打开

image-20240817021106020

选择Eclipse,剩下一路下一步即可

image-20240817021134535

移除红色模块

image-20240817030221189

Facet设置web.xml

image-20240817041959306

在工件添加war

image-20240817042117728

配置tomcat地址和端口

image-20240817042256204

配置tomcat访问路径

image-20240817042344013

下载支付宝开放平台密钥工具

安装路径记得不要有空格

image-20240817042603776

配置应用和支付宝的公钥、私钥、APPID,运行demo

生成密钥

image-20240817042623966

在沙盒应用中选择自定义密钥,然后点击查看

image-20240817042753982

请复制“应用公钥”至支付宝开放平台,进而获取支付宝公钥

image-20240817043135710

在项目中配置应用私钥

image-20240817043032970

把沙盒里支付公钥配置到项目中

image-20240817043304088

配置APPID

image-20240817043416120

运行项目

点击付款,跳转登录界面,输入沙盒账号里的买家信息账号、密码和支付密码

image-20240817043803475

支付成功

image-20240817043948475

15.3内网穿透

内网穿透工具有很多,我这里使用的是OpenFrp

首次注册需要实名认证,可能话费1-2元

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

image-20240818000334523

下载客户端并安装

image-20240818000402872

首先创建一个隧道,本地端口就是你demo运行端口,然后随机生成远程端口即可

image-20240818000305871

创建完成后,打开客户端,开启隧道,并复制域名用于访问

image-20240818000449277

再次访问支付demo,能正常运行即可

image-20240818000649492

16.商城业务-订单服务

16.1整合支付前需要注意的问题

保证所有项目编码都是utf-8

image-20240818002036866

16.2整合支付

主要步骤:

  • 导入支付宝SDKapplication.yaml添加支付配置
  • 封装支付宝支付帮助类
  • 根据orderSn查询订单,并把订单数据传入支付功能
  • 前端请求支付宝支付接口

导入支付宝SDKapplication.yaml添加支付配置

image-20240819012523924

封装支付宝支付帮助类

image-20240819012607048

根据orderSn查询订单,并把订单数据传入支付功能

image-20240819012639059

前端请求支付宝支付接口

image-20240819012705491

16.3支付成功同步回调

主要步骤:

  • 修改gulimall-order支付成功页面跳转链接,跳转到会员服务gulimall-member,显示订单列表
  • gulimall-member导入SpringSession依赖,application.yaml添加thymeleafredissession的配置
  • gulimall-member添加SpringSession配置
  • gulimall-member添加登录拦截器,放行OpenFiegn远程调用接口
  • 将订单页静态资源上传nginx
  • index.html拷贝到``gulimall-membersrc/main/resources/templates目录下,并改名orderList.html,修改orderList.html页面静态资源地址,添加thymeleaf`命名空间
  • 管理员启动SwitchHosts,添加gulimall-member的域名映射
  • gulimall-gateway网关服务添加gulimall-member会员服务的网关地址
  • gulimall-member添加MemberWebController显示orderList.html

修改gulimall-order支付成功页面跳转链接,跳转到会员服务gulimall-member,显示订单列表

支付完成跳转到订单列表页

image-20240819023508481

gulimall-member导入SpringSession依赖,application.yaml添加thymeleafredissession的配置

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

image-20240819023602538

gulimall-member添加SpringSession配置

image-20240819024355870

gulimall-member添加登录拦截器,放行OpenFiegn远程调用接口

image-20240819024217791

将订单页静态资源上传nginx

image-20240819014016893

index.html拷贝到``gulimall-membersrc/main/resources/templates目录下,并改名orderList.html,修改orderList.html页面静态资源地址,添加thymeleaf`命名空间

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

image-20240819023820744

管理员启动SwitchHosts,添加gulimall-member的域名映射

192.168.188.180     member.gulimall.com

image-20240819023920849

gulimall-gateway网关服务添加gulimall-member会员服务的网关地址

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

image-20240819023953295

gulimall-member添加MemberWebController显示orderList.html

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

image-20240819024040999

16.4订单列表页渲染完成

主要步骤:

  • gulimall-member添加OpenFeign配置
  • gulimall-member添加gulimall-order的远程调用,获取用户所有订单列表
  • gulimall-order实现queryPageWithItem分页获取用户所有订单列表

gulimall-member添加OpenFeign配置

image-20240819035109700

gulimall-member添加gulimall-order的远程调用,获取用户所有订单列表

image-20240819035130374

gulimall-order实现queryPageWithItem分页获取用户所有订单列表

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

    //遍历所有订单集合
    List<OrderEntity> orderEntityList = page.getRecords().stream().map(order -> {
        //根据订单号查询订单项里的数据
        List<OrderItemEntity> orderItemEntities = orderItemService.list(new QueryWrapper<OrderItemEntity>()
                .eq("order_sn", order.getOrderSn()));
        order.setOrderItemEntityList(orderItemEntities);
        return order;
    }).collect(Collectors.toList());
    page.setRecords(orderEntityList);
    return new PageUtils(page);
}

image-20240819035219344

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

支付宝异步通知

支付宝异步通知文档:https://opendocs.alipay.com/open/270/105902/

image-20240819041027571

使用nps作内网穿透,无法使用域名必须使用IP:PORT,所以会造成nginx无法根据访问的域名gulimall.com来匹配请求

解决:修改nginx配置文件gulimall.conf监听server_name 124.223.7.41(这是自己的远程地址)

添加以上域名监听后,访问124.223.7.41:8888(这是自己的远程地址))出现404异常

原因:

  • 网关88未拦截到请求

解决:

  • 方案一:在网关增加拦截规则,拦截124.223.7.41,将请求发送到order.gulimall.com
  • 方案二:在nginx转发时,设置host=order.gulimall.com,使网关可以正确拦截【推荐】
  • 方案三:内网穿透的地址直接配成192.168.56.1:9000【缺点:没有负载均衡了】

image-20240819203615696

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

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

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

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

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

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

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

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

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

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

主要步骤:

  • 配置本地链接为自己的本地服务虚拟接ip,远程端口可随机
  • 修改nginx配置文件gulimall.conf监听server_name 111.199.237.178(自己的远程内网穿透地址)
  • nginx转发时,设置host=order.gulimall.com,使网关可以正确拦截【推荐】
  • gulimall-order配置支付成功异步回调地址
  • gulimall-order登录拦截放行支付宝的异步回调通知
  • gulimall-order添加支付宝的异步回调通知接口

配置本地链接为自己的本地服务虚拟接ip,远程端口可随机

image-20240819203733039

修改nginx配置文件gulimall.conf监听server_name 111.199.237.178(自己的远程内网穿透地址)

nginx转发时,设置host=order.gulimall.com,使网关可以正确拦截【推荐】

image-20240819204019187

gulimall-order配置支付成功异步回调地址

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

image-20240819204552494

gulimall-order登录拦截放行支付宝的异步回调通知

@RestController
public class OrderPayedListener {

    @PostMapping("/payed/notify")
    public String handleAlipayed(HttpServletRequest request) {
        // 只要我们收到了支付宝给我们异步的通知,告诉我们订单支付成功。返回success,支付宝就再也不通知
        Map<String, String[]> requestParams = request.getParameterMap();
        System.out.println("支付宝通知到位了..数据:" + requestParams);
        return "success";
    }

}

image-20240819204616195

gulimall-order添加支付宝的异步回调通知接口

image-20240819204730597

测试

image-20240819204324096

16.6支付完成

主要步骤:

  • 支付宝验签
  • 处理支付宝的支付结果
    • 保存交易流水
    • 更新支付状态
  • 配置spring.mvc日期格式化

支付宝验签

@PostMapping(value = "/payed/notify")
public String handleAlipayed(PayAsyncVo asyncVo, HttpServletRequest request) throws AlipayApiException, UnsupportedEncodingException {
    // 只要收到支付宝的异步通知,返回 success 支付宝便不再通知
    // 获取支付宝POST过来反馈信息
    //TODO 需要验签
    Map<String, String> params = new HashMap<>();
    Map<String, String[]> requestParams = request.getParameterMap();
    for (String name : requestParams.keySet()) {
        String[] values = requestParams.get(name);
        String valueStr = "";
        for (int i = 0; i < values.length; i++) {
            valueStr = (i == values.length - 1) ? valueStr + values[i]
                    : valueStr + values[i] + ",";
        }
        //乱码解决,这段代码在出现乱码时使用
        // valueStr = new String(valueStr.getBytes("ISO-8859-1"), "utf-8");
        params.put(name, valueStr);
    }

    boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(),
            alipayTemplate.getCharset(), alipayTemplate.getSign_type()); //调用SDK验证签名

    if (signVerified) {
        System.out.println("签名验证成功...");
        //去修改订单状态
        String result = orderService.handlePayResult(asyncVo);
        return result;
    } else {
        System.out.println("签名验证失败...");
        return "error";
    }
}

处理支付宝的支付结果

  • 保存交易流水
  • 更新支付状态
/**
 * 处理支付宝的支付结果
 * @param asyncVo
 * @return
 */
@Override
public String handlePayResult(PayAsyncVo asyncVo) {
    //保存交易流水信息
    PaymentInfoEntity paymentInfo = new PaymentInfoEntity();
    paymentInfo.setOrderSn(asyncVo.getOut_trade_no());
    paymentInfo.setAlipayTradeNo(asyncVo.getTrade_no());
    paymentInfo.setTotalAmount(new BigDecimal(asyncVo.getBuyer_pay_amount()));
    paymentInfo.setSubject(asyncVo.getBody());
    paymentInfo.setPaymentStatus(asyncVo.getTrade_status());
    paymentInfo.setCreateTime(new Date());
    paymentInfo.setCallbackTime(asyncVo.getNotify_time());
    //添加到数据库中
    this.paymentInfoService.save(paymentInfo);

    //修改订单状态
    //获取当前状态
    String tradeStatus = asyncVo.getTrade_status();

    if (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED")) {
        //支付成功状态
        String orderSn = asyncVo.getOut_trade_no(); //获取订单号
        this.updateOrderStatus(orderSn,OrderStatusEnum.PAYED.getCode(), PayConstant.ALIPAY);
    }

    return "success";
}

/**
 * 修改订单状态
 * @param orderSn
 * @param code
 */
private void updateOrderStatus(String orderSn, Integer code,Integer payType) {

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

mall_oms.oms_payment_infoorder_sn字段改为char(64)

image-20240819220255221

配置spring.mvc日期格式化

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

image-20240820011311348

16.7收单

image-20240819215827573

支付宝支付接口

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

主要步骤:

  • 订单超时,不允许支付
    • 解决:支付时设置超时时间:应该设置订单绝对超时时间,而不是30m,按照创建订单+30m来算截止时间 time_expire
  • 订单解锁完成,异步通知才到
    • 解决:释放库存的时候,手动调用收单功能(参照官方demo的实现)
  • 对账:每晚启动定时任务和请求支付包接口进行对账

订单超时,不允许支付

image-20240819221024632

订单解锁完成,异步通知才到

image-20240819221230868

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

image-20240819221253649

对账:每晚启动定时任务和请求支付包接口进行对账

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

image-20240819221409864

创作不易,感谢支持。

wxzf
posted @ 2024-10-14 00:59  peng_boke  阅读(10)  评论(0编辑  收藏  举报