247、商城业务-消息队列-MQ简介
1、概述
Message Queue 简称 MQ
MQ全称为Message Queue
,消息队列是应用程序和应用程序之间的通信方法。多用于分布式系统之间进行通信。
先进先出:MQ
先进后出:栈
两边都可以取数据:双端队列
2、应用场景
2.1 异步处理
2.2 应用解耦
2.3 流量控制
2、概述
-
大多应用中,可通过消息服务中间件来提升系统异步通信、扩展解耦能力
-
消息服务中两个重要概念:
消息代理(message broker)和目的地(destination) 当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地。
• 消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获
取消息内容,消息读取后被移出队列
• 消息只有唯一的发送者和接受者,但并不是说只能有一个接收者
• 发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个
主题,那么就会在消息到达时同时收到消息
• 基于JVM消息代理的规范。ActiveMQ、HornetMQ是JMS实现
• 高级消息队列协议,也是一个消息代理的规范,兼容JMS
• RabbitMQ是AMQP的实现
JMS(Java Message Service) | AMQP(Advanced Message Queuing Protocol) | |
---|---|---|
定义 | Java api | 网络线级协议 |
跨语言 | 否 | 是 |
跨平台 | 否 | 是 |
Model | 提供两种消息模型: (1)、Peer-2-Peer (2)、Pub/sub | 提供了五种消息模型: (1)、direct exchange (2)、fanout exchange (3)、topic change (4)、headers exchange (5)、system exchange 本质来讲,后四种和JMS的pub/sub模型没有太大差别, 仅是在路由机制上做了更详细的划分; |
支持消息类型 | 多种消息类型: TextMessage,MapMessage,StreamMessage,ObjectMessage,ObjectMessage,BytesMessage,Message (只有消息头和属性) | byte[] 当实际应用时,有复杂的消息,可以将消息序列化后发 送。 |
综合评价 | JMS 定义了JAVA API层面的标准;在java体系中, 多个client均可以通过JMS进行交互,不需要应用修 改代码,但是其对跨平台的支持较差; | AMQP定义了wire-level层的协议标准;天然具有跨平 台、跨语言特性。 |
8、Spring支持
• spring-jms 提供了对 JMS 的支持
• spring-rabbit 提供了对 AMQP* 的支持
• 需要 ConnectionFactory 的实现来连接消息代理
• 提供 JmsTemplate 、 RabbitTemplate 来发送消息
• @JmsListener ( JMS )、 @RabbitListener ( AMQP )注解在方法上监听消息
代理发布的消息
• @EnableJms 、 @EnableRabbit 开启支持
9. Spring Boot自动配置
• JmsAutoConfiguration
• RabbitAutoConfiguration
• 10 、市面的 MQ 产品
• ActiveMQ 、 RabbitMQ 、 RocketMQ 、 Kafka
MQ是消息通信的模型;实现MQ的大致有两种主流方式:AMQP、JMS。
1.2.1. AMQP
AMQP,即 Advanced Message Queuing Protocol(高级消息队列协议),是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。2006年,AMQP 规范发布。类比HTTP。
AMQP是一种协议,更准确的说是一种binary wire-level protocol(链接协议)。这是其和JMS的本质差别,AMQP不从API层进行限定,而是直接定义网络交换的数据格式。
1.2.2. JMS
JMS即Java消息服务(JavaMessage Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。
1.2.3. AMQP 与 JMS 区别
JMS是定义了统一的接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式
JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。
JMS规定了两种消息模式;而AMQP的消息模式更加丰富
1.3. 消息队列产品
市场上常见的消息队列有如下:
ActiveMQ:基于JMS
ZeroMQ:基于C语言开发
RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好
RocketMQ:基于JMS,阿里巴巴产品
Kafka:类似MQ的产品;分布式消息系统,高吞吐量
RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
---|---|---|---|---|
公司/社区 | Rabbit | Apache | 阿里 | Apache |
开发语言 | Erlang | java | java | 自定义协议,社区封装了http协议支持 |
协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,STOMP,REST,XMPP,AMQP | 自定义 | 自定义协议,社区封装了http协议支持 |
客户端支持语言 | 官方支持Erlang,Java,Ruby等,社区产出多种API,几乎支持所有语言 | Java,C,C++,Python,PHP,Perl,.net等 | Java,C++(不成熟) | 官方支持Java,社区产出多种API,如PHP,Python等 |
单机吞吐量 | 万级(其次) | 万级(最差) | 十万级(最好 | 十万级(次之) |
消息延迟 | 微妙级 | 毫秒级 | 毫秒级 | 毫秒以内 |
功能特性 | 并发能力强,性能极其好,延时低,社区活跃,管理界面丰富 | 老牌产品,成熟度高,文档较多 | MQ功能比较完备,扩展性佳 | 只支持主要的MQ功能,毕竟是为大数据领域准备的。 |
248、商城业务-消息队列-RabbitMQ简介
1、RabbitMQ 简介:
RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现。
核心概念
Message
消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,
这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可
能需要持久性存储)等。
Publisher
消息的生产者,也是一个向交换器发布消息的客户端应用程序。
Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
Exchange有4种类型:direct(默认),fanout, topic, 和headers,不同类型的Exchange转发消息的策略有所区别
Queue
消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直
在队列里面,等待消费者连接到这个队列将其取走。
Binding
绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交
换器理解成一个由绑定构成的路由表。
Exchange 和Queue的绑定可以是多对多的关系。
Connection
网络连接,比如一个TCP连接。
Channel
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP 命令都是通过信道
发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都
是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
Consumer
消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
Virtual Host
虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加
密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥
有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时
指定,RabbitMQ 默认的 vhost 是 / 。
Broker
表示消息队列服务器实体
249、商城业务-消息队列-RabbitMQ工作流程
一个客户端只会建立上一条连接,是一个长连接
RabbitMQ运转流程
在入门案例中
在入门案例中:
生产者发送消息
1、生产者创建连接(Connection),开启一个信道(Channel),连接到RabbitMQ Broker;
2、声明队列并设置属性;如是否排它,是否持久化,是否自动删除;
3、将路由键(空字符串)与队列绑定起来;
4、发送消息至RabbitMQ Broker;
5、关闭信道;
6、关闭连接;
消费者接收消息
1、消费者创建连接(Connection),开启一个信道(Channel),连接到RabbitMQ Broker
2、向Broker 请求消费相应队列中的消息,设置相应的回调函数;
3、等待Broker回应闭关投递响应队列中的消息,消费者接收消息;
4、确认(ack,自动确认)接收到的消息;
5、RabbitMQ从队列中删除相应已经被确认的消息;
6、关闭信道;
7、关闭连接;
4.3. 生产者流转过程说明
1、客户端与代理服务器Broker建立连接。会调用newConnection() 方法,这个方法会进一步封装Protocol Header 0-9-1 的报文头发送给Broker ,以此通知Broker 本次交互采用的是AMQPO-9-1 协议,紧接着Broker 返回Connection.Start 来建立连接,在连接的过程中涉及Connection.Start/.Start-OK 、Connection.Tune/.Tune-Ok ,Connection.Open/ .Open-Ok 这6 个命令的交互。
2、客户端调用connection.createChannel方法。此方法开启信道,其包装的channel.open命令发送给Broker,等待channel.basicPublish方法,对应的AMQP命令为Basic.Publish,这个命令包含了content Header 和content Body()。content Header 包含了消息体的属性,例如:投递模式,优先级等,content Body 包含了消息体本身。
3、客户端发送完消息需要关闭资源时,涉及到Channel.Close和Channl.Close-Ok 与Connetion.Close和Connection.Close-Ok的命令交互。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QcAgBkKR-1614701356653)(F:\12-消息中间件-RabbitMQ\day01_RabbitMQ基础入门\讲义\assets/生产者流转过程图.bmp)]
4.4. 消费者流转过程说明
1、消费者客户端与代理服务器Broker建立连接。会调用newConnection() 方法,这个方法会进一步封装Protocol Header 0-9-1 的报文头发送给Broker ,以此通知Broker 本次交互采用的是AMQPO-9-1 协议,紧接着Broker 返回Connection.Start 来建立连接,在连接的过程中涉及Connection.Start/.Start-OK 、Connection.Tune/.Tune-Ok ,Connection.Open/ .Open-Ok 这6 个命令的交互。
2、消费者客户端调用connection.createChannel方法。和生产者客户端一样,协议涉及Channel . Open/Open-Ok命令。
在真正消费之前,消费者客户端需要向Broker 发送Basic.Consume 命令(即调用channel.basicConsume 方法〉将Channel 置为接收模式,之后Broker 回执Basic . Consume - Ok 以告诉消费者客户端准备好消费消息。
Broker 向消费者客户端推送(Push) 消息,即Basic.Deliver 命令,这个命令和Basic.Publish 命令一样会携带Content Header 和Content Body。
3、消费者接收到消息并正确消费之后,向Broker 发送确认,即Basic.Ack 命令。
4、客户端发送完消息需要关闭资源时,涉及到Channel.Close和Channl.Close-Ok 与Connetion.Close和Connection.Close-Ok的命令交互。
250、商城业务-消息队列-RabbitMQ安装
3.0 docker安装rabbitMq
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management
4369,25672 (Erlang发现&集群端口)
4369,25672(Erlang发现&集群端口)
5672,5671 (AMQP端口)
5672,5671(AMQP端口)
15672 (web管理后台端口)
15672(网络管理后台端口)
61613,61614 (STOMP协议端口)
1883,8883(MQTT协议端口)
61613,61614(STMP协议端口)
1883,8883(mqtt协议端口)
https://www.rabbitmq.com/networking.html
Https://www.rabbitmq.com/networking.html
docker update rabbitmq --restart=always
自动启动
docker update rabbitmq --restart=always
访问客户端地址:
192.168.10.11:15672 密码和账号都是 guest
251、商城业务-消息队列-Exchange类型
RabbitMQ运行机制
AMQP 中的消息路由
• AMQP 中消息的路由过程和 Java 开发者熟悉的 JMS 存在一些差别,AMQP 中增加了 Exchange 和 Binding 的角色。生产者把消息发布到 Exchange 上,消息最终到达队列 并被消费者接收,而 Binding 决定交换器的消息应该发送到那个队列。
Exchange 类型
Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、 fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由键, headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了,所以直接看另外三种类型:
252、商城业务-消息队列-Direct-Exchange
消息中的路由键(routing key)如果和Binding 中的 binding key 一致, 交换器 就将消息发到对应的队列中。路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为“dog”,则只转发 routing key 标记为“dog”的消息,不会转发 “dog.puppy”,也不会发“dog.guard” 等等。它是完全匹配、单播的模式。
Work Queues与入门程序的简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。
应用场景:对于 任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
代码
Work Queues与入门程序的简单模式的代码是几乎一样的;可以完全复制,并复制多一个消费者进行多个消费者同时消费消息的测试。
1)生产者
import com.itheima.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class Producer {
static final String QUEUE_NAME = "work_queue";
public static void main(String[] args) throws Exception {
//创建连接
Connection connection = ConnectionUtil.getConnection();
// 创建频道
Channel channel = connection.createChannel();
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
for (int i = 1; i <= 30; i++) {
// 发送信息
String message = "你好;小兔子!work模式--" + i;
/**
* 参数1:交换机名称,如果没有指定则使用默认Default Exchage
* 参数2:路由key,简单模式可以传递队列名称
* 参数3:消息其它属性
* 参数4:消息内容
*/
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("已发送消息:" + message);
}
// 关闭资源
channel.close();
connection.close();
}
}
2)消费者1
import com.itheima.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Consumer1 {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 创建频道
Channel channel = connection.createChannel();
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(Producer.QUEUE_NAME, true, false, false, null);
//一次只能接收并处理一个消息
channel.basicQos(1);
//创建消费者;并设置消息处理
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
/**
* consumerTag 消息者标签,在channel.basicConsume时候可以指定
* envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* properties 属性信息
* body 消息
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8"));
Thread.sleep(1000);
//确认消息
channel.basicAck(envelope.getDeliveryTag(), false);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//监听消息
/**
* 参数1:队列名称
* 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
* 参数3:消息接收到后回调
*/
channel.basicConsume(Producer.QUEUE_NAME, false, consumer);
}
}
和消费者1一模一样,复制一份改名Consumer2.java即可
3) 测试
启动两个消费者,然后再启动生产者发送消息;到IDEA的两个消费者对应的控制台查看是否竞争性的接收到消息。
消费者1:接收到消息 1 3 5
消费者2:接收到消息 2 4 6
4.1.4. 小结
在一个队列中如果有多个消费者,那么消费者之间对于同一个消息的关系是竞争的关系。
253、商城业务-消息队列-Fanout-Exchange
每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。fanout 交换器不处理路由键,只是简单的将队列 绑定到交换器上,每个发送到交换器的 消息都会被转发到与该交换器绑定的所 有队列上。很像子网广播,每台子网内 的主机都获得了一份复制的消息。 fanout 类型转发消息是最快的。
254、商城业务-消息队列-Topic-Exchange
topic 交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。 它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也 会识别两个通配符:符号“#”和符号 “”。#匹配0**个或多个单词,****匹配一个单词。
1、 订阅模式类型
订阅模式示例图:
前面2个案例中,只有3个角色:
P:生产者,也就是要发送消息的程序
C:消费者:消息的接受者,会一直等待消息到来。
queue:消息队列,图中红色部分
而在订阅模型中,多了一个exchange角色,而且过程略有变化:
P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
C:消费者,消息的接受者,会一直等待消息到来。
Queue:消息队列,接收消息、缓存消息。
Exchange:交换机,图中的X。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型:
Fanout:广播,将消息交给所有绑定到交换机的队列
Direct:定向,把消息交给符合指定routing key 的队列
Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
模式说明
Sending messages to many consumers at once
发布订阅模式:
1、每个消费者监听自己的队列。
2、生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息
代码
1)生产者
package com.itheima.rabbitmq.ps;
import com.itheima.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
/**
* 发布与订阅使用的交换机类型为:fanout
*/
public class Producer {
//交换机名称
static final String FANOUT_EXCHAGE = "fanout_exchange";
//队列名称
static final String FANOUT_QUEUE_1 = "fanout_queue_1";
//队列名称
static final String FANOUT_QUEUE_2 = "fanout_queue_2";
public static void main(String[] args) throws Exception {
//创建连接
Connection connection = ConnectionUtil.getConnection();
// 创建频道
Channel channel = connection.createChannel();
/**
* 声明交换机
* 参数1:交换机名称
* 参数2:交换机类型,fanout、topic、direct、headers
*/
channel.exchangeDeclare(FANOUT_EXCHAGE, BuiltinExchangeType.FANOUT);
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(FANOUT_QUEUE_1, true, false, false, null);
channel.queueDeclare(FANOUT_QUEUE_2, true, false, false, null);
//队列绑定交换机
channel.queueBind(FANOUT_QUEUE_1, FANOUT_EXCHAGE, "");
channel.queueBind(FANOUT_QUEUE_2, FANOUT_EXCHAGE, "");
for (int i = 1; i <= 10; i++) {
// 发送信息
String message = "你好;小兔子!发布订阅模式--" + i;
/**
* 参数1:交换机名称,如果没有指定则使用默认Default Exchage
* 参数2:路由key,简单模式可以传递队列名称
* 参数3:消息其它属性
* 参数4:消息内容
*/
channel.basicPublish(FANOUT_EXCHAGE, "", null, message.getBytes());
System.out.println("已发送消息:" + message);
}
// 关闭资源
channel.close();
connection.close();
}
}
2)消费者1
public class Consumer1 {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 创建频道
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(Producer.FANOUT_EXCHAGE, BuiltinExchangeType.FANOUT);
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(Producer.FANOUT_QUEUE_1, true, false, false, null);
//队列绑定交换机
channel.queueBind(Producer.FANOUT_QUEUE_1, Producer.FANOUT_EXCHAGE, "");
//创建消费者;并设置消息处理
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
/**
* consumerTag 消息者标签,在channel.basicConsume时候可以指定
* envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* properties 属性信息
* body 消息
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8"));
}
};
//监听消息
/**
* 参数1:队列名称
* 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
* 参数3:消息接收到后回调
*/
channel.basicConsume(Producer.FANOUT_QUEUE_1, true, consumer);
}
}
3)消费者2
指定不同的队列名称而已
import com.itheima.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
public class Consumer2 {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 创建频道
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(Producer.FANOUT_EXCHAGE, BuiltinExchangeType.FANOUT);
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(Producer.FANOUT_QUEUE_2, true, false, false, null);
//队列绑定交换机
channel.queueBind(Producer.FANOUT_QUEUE_2, Producer.FANOUT_EXCHAGE, "");
//创建消费者;并设置消息处理
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
/**
* consumerTag 消息者标签,在channel.basicConsume时候可以指定
* envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* properties 属性信息
* body 消息
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者2-接收到的消息为:" + new String(body, "utf-8"));
}
};
//监听消息
/**
* 参数1:队列名称
* 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
* 参数3:消息接收到后回调
*/
channel.basicConsume(Producer.FANOUT_QUEUE_2, true, consumer);
}
}
测试
启动所有消费者,然后使用生产者发送消息;在每个消费者对应的控制台可以查看到生产者发送的所有消息;到达广播的效果。
在执行完测试代码后,其实到RabbitMQ的管理后台找到Exchanges选项卡,点击 fanout_exchange 的交换机,可以查看到如下的绑定:
fanout-queue-1
fanout-queue-2
小结
交换机需要与队列进行绑定,绑定之后;一个消息可以被多个消费者都收到。
发布订阅模式与工作队列模式的区别
1、工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机。
2、发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用默认交换机)。
3、发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑 定到默认的交换机 。
2、 Routing路由模式
1、模式说明
路由模式特点:
队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
消息的发送方在 向 Exchange发送消息时,也必须指定消息的 RoutingKey。
Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routingkey与消息的 Routing key完全一致,才会接收到消息
Receiving messages selectively。下图代表不同级别的日志怎么处理,比如C1是文件,C2是控制台
图解:
P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列
C1:消费者,其所在队列指定了需要routing key 为 error 的消息
C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息
发送的路由key和绑定的路由key一致时才匹配
代码
在编码上与 Publish/Subscribe发布与订阅模式 的区别是交换机的类型为:Direct,还有队列绑定交换机的时候需要指定routing key。
1)生产者
/**
* 路由模式的交换机类型为:direct
*/
public class Producer {
//交换机名称
static final String DIRECT_EXCHAGE = "direct_exchange";
//队列名称
static final String DIRECT_QUEUE_INSERT = "direct_queue_insert";
//队列名称
static final String DIRECT_QUEUE_UPDATE = "direct_queue_update";
public static void main(String[] args) throws Exception {
//创建连接
Connection connection = ConnectionUtil.getConnection();
// 创建频道
Channel channel = connection.createChannel();
/**
* 声明交换机
* 参数1:交换机名称
* 参数2:交换机类型,fanout、topic、direct、headers
*/
channel.exchangeDeclare(DIRECT_EXCHAGE, BuiltinExchangeType.DIRECT);
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(DIRECT_QUEUE_INSERT, true, false, false, null);
channel.queueDeclare(DIRECT_QUEUE_UPDATE, true, false, false, null);
//队列绑定交换机
channel.queueBind(DIRECT_QUEUE_INSERT, DIRECT_EXCHAGE, "insert");
channel.queueBind(DIRECT_QUEUE_UPDATE, DIRECT_EXCHAGE, "update");
// 发送信息
String message = "新增了商品。路由模式;routing key 为 insert " ;
/**
* 参数1:交换机名称,如果没有指定则使用默认Default Exchage
* 参数2:路由key,简单模式可以传递队列名称
* 参数3:消息其它属性
* 参数4:消息内容
*/
channel.basicPublish(DIRECT_EXCHAGE, "insert", null, message.getBytes());
System.out.println("已发送消息:" + message);
// 发送信息
message = "修改了商品。路由模式;routing key 为 update" ;
/**
* 参数1:交换机名称,如果没有指定则使用默认Default Exchage
* 参数2:路由key,简单模式可以传递队列名称
* 参数3:消息其它属性
* 参数4:消息内容
*/
channel.basicPublish(DIRECT_EXCHAGE, "update", null, message.getBytes());
System.out.println("已发送消息:" + message);
// 关闭资源
channel.close();
connection.close();
}
}
2)消费者1
public class Consumer1 {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 创建频道
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(Producer.DIRECT_EXCHAGE, BuiltinExchangeType.DIRECT);
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(Producer.DIRECT_QUEUE_INSERT, true, false, false, null);
//队列绑定交换机
channel.queueBind(Producer.DIRECT_QUEUE_INSERT, Producer.DIRECT_EXCHAGE, "insert");
//创建消费者;并设置消息处理
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
/**
* consumerTag 消息者标签,在channel.basicConsume时候可以指定
* envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* properties 属性信息
* body 消息
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8"));
}
};
//监听消息
/**
* 参数1:队列名称
* 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
* 参数3:消息接收到后回调
*/
channel.basicConsume(Producer.DIRECT_QUEUE_INSERT, true, consumer);
}
}
3)消费者2
public class Consumer2 {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 创建频道
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(Producer.DIRECT_EXCHAGE, BuiltinExchangeType.DIRECT);
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(Producer.DIRECT_QUEUE_UPDATE, true, false, false, null);
//队列绑定交换机
channel.queueBind(Producer.DIRECT_QUEUE_UPDATE, Producer.DIRECT_EXCHAGE, "update");
//创建消费者;并设置消息处理
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
/**
* consumerTag 消息者标签,在channel.basicConsume时候可以指定
* envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* properties 属性信息
* body 消息
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者2-接收到的消息为:" + new String(body, "utf-8"));
}
};
//监听消息
/**
* 参数1:队列名称
* 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
* 参数3:消息接收到后回调
*/
channel.basicConsume(Producer.DIRECT_QUEUE_UPDATE, true, consumer);
}
}
测试
启动所有消费者,然后使用生产者发送消息;在消费者对应的控制台可以查看到生产者发送对应routing key对应队列的消息;到达按照需要接收的效果。
在执行完测试代码后,其实到RabbitMQ的管理后台找到Exchanges选项卡,点击 direct_exchange 的交换机,可以查看到如下的绑定:
direct-queue-insert:insert
direct-queue-update:update
小结
Routing模式要求队列在绑定交换机时要指定routing key,消息会转发到符合routing key的队列。
3、Topics通配符模式
模式说明
Topic类型与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符!
Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:
#:匹配一个或多个词
*:匹配不多不少恰好1个词
举例:
item.#:能够匹配item.insert.abc 或者 item.insert
item.*:只能匹配item.insert
Receiving messages based on a pattern (topics)
图解:
- 红色Queue:绑定的是
usa.#
,因此凡是以usa.
开头的routing key
都会被匹配到 - 黄色Queue:绑定的是
#.news
,因此凡是以.news
结尾的routing key
都会被匹配
代码
1)生产者
使用topic类型的Exchange,发送消息的routing key有3种: item.insert
、item.update
、item.delete
:
/**
* 通配符Topic的交换机类型为:topic
*/
public class Producer {
//交换机名称
static final String TOPIC_EXCHAGE = "topic_exchange";
//队列名称
static final String TOPIC_QUEUE_1 = "topic_queue_1";
//队列名称
static final String TOPIC_QUEUE_2 = "topic_queue_2";
public static void main(String[] args) throws Exception {
//创建连接
Connection connection = ConnectionUtil.getConnection();
// 创建频道
Channel channel = connection.createChannel();
/**
* 声明交换机
* 参数1:交换机名称
* 参数2:交换机类型,fanout、topic、topic、headers
*/
channel.exchangeDeclare(TOPIC_EXCHAGE, BuiltinExchangeType.TOPIC);
// 发送信息
String message = "新增了商品。Topic模式;routing key 为 item.insert " ;
channel.basicPublish(TOPIC_EXCHAGE, "item.insert", null, message.getBytes());
System.out.println("已发送消息:" + message);
// 发送信息
message = "修改了商品。Topic模式;routing key 为 item.update" ;
channel.basicPublish(TOPIC_EXCHAGE, "item.update", null, message.getBytes());
System.out.println("已发送消息:" + message);
// 发送信息
message = "删除了商品。Topic模式;routing key 为 item.delete" ;
channel.basicPublish(TOPIC_EXCHAGE, "item.delete", null, message.getBytes());
System.out.println("已发送消息:" + message);
// 关闭资源
channel.close();
connection.close();
}
}
2)消费者1
接收两种类型的消息:更新商品和删除商品
public class Consumer1 {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 创建频道
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(Producer.TOPIC_EXCHAGE, BuiltinExchangeType.TOPIC);
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(Producer.TOPIC_QUEUE_1, true, false, false, null);
//队列绑定交换机
channel.queueBind(Producer.TOPIC_QUEUE_1, Producer.TOPIC_EXCHAGE, "item.update");
channel.queueBind(Producer.TOPIC_QUEUE_1, Producer.TOPIC_EXCHAGE, "item.delete");
//创建消费者;并设置消息处理
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
/**
* consumerTag 消息者标签,在channel.basicConsume时候可以指定
* envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* properties 属性信息
* body 消息
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8"));
}
};
//监听消息
/**
* 参数1:队列名称
* 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
* 参数3:消息接收到后回调
*/
channel.basicConsume(Producer.TOPIC_QUEUE_1, true, consumer);
}
}
3)消费者2
接收所有类型的消息:新增商品,更新商品和删除商品。
public class Consumer2 {
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtil.getConnection();
// 创建频道
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(Producer.TOPIC_EXCHAGE, BuiltinExchangeType.TOPIC);
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(Producer.TOPIC_QUEUE_2, true, false, false, null);
//队列绑定交换机
channel.queueBind(Producer.TOPIC_QUEUE_2, Producer.TOPIC_EXCHAGE, "item.*");
//创建消费者;并设置消息处理
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
/**
* consumerTag 消息者标签,在channel.basicConsume时候可以指定
* envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* properties 属性信息
* body 消息
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者2-接收到的消息为:" + new String(body, "utf-8"));
}
};
//监听消息
/**
* 参数1:队列名称
* 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
* 参数3:消息接收到后回调
*/
channel.basicConsume(Producer.TOPIC_QUEUE_2, true, consumer);
}
}
测试
启动所有消费者,然后使用生产者发送消息;在消费者对应的控制台可以查看到生产者发送对应routing key对应队列的消息;到达按照需要接收的效果;并且这些routing key可以使用通配符。
在执行完测试代码后,其实到RabbitMQ的管理后台找到Exchanges选项卡,点击 topic_exchange 的交换机,可以查看到如下的绑定:
小结
Topic主题模式可以实现 Publish/Subscribe发布与订阅模式 和 Routing路由模式 的功能;只是Topic在配置routing key 的时候可以使用通配符,显得更加灵活。
模式总结
RabbitMQ工作模式:
1、简单模式 HelloWorld
一个生产者、一个消费者,不需要设置交换机(使用默认的交换机)
2、工作队列模式 Work Queue
一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)
3、发布订阅模式 Publish/subscribe
需要设置类型为fanout的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列
4、路由模式 Routing
需要设置类型为direct的交换机,交换机和队列进行绑定,并且指定routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列
5、通配符模式 Topic
需要设置类型为topic的交换机,交换机和队列进行绑定,并且指定通配符方式的routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列
255、商城业务-消息队列-SpringBoot整合RabbitMQ
# 使用RabbitMQ
1、引入amqp场景;RabbitAutoConfiguration 就会自动生效
2、给容器中自动配置了
RabbitTemplate、AmqpAdmin、CachingConnectionFactory、RabbitMessagingTemplate;
所有的属性都是 spring.rabbitmq
@ConfigurationProperties(prefix = "spring.rabbitmq")
public class RabbitProperties
3、给配置文件中配置 spring.rabbitmq 信息
4、@EnableRabbit: @EnableXxxxx;开启功能
5、监听消息:使用@RabbitListener(消费消息);必须有@EnableRabbit
@RabbitListener: 类+方法上(监听哪些队列即可)
@RabbitHandler:标在方法上(重载区分不同的消息)
1、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2、配置文件
application.properties
spring.rabbitmq.host=119.3.105.108
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
3、序列化配置
rabbitmq 默认是是字节流,自定义json序列化
package com.atguigu.gulimall.order.config;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @创建人: 放生
* @创建时间: 2022/5/4
* @描述:
*/
@Configuration
public class MyRabbitConfig {
/**
* 使用JSON序列化机制,进行消息转换
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
4、主启动
@EnableRabbit
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan(value="com.atguigu.gulimall.order.dao")
@SpringBootApplication
public class GulimallOrderApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallOrderApplication.class, args);
}
}
5、代码案列
创建交换机,案列中创建的是一个直连的交机,其他的自行摸索
@Autowired
AmqpAdmin amqpAdmin;
/**
* 1、如何创建Exchange[hello-java-exchange]、Queue、Binding
* 1)、使用 AmqpAdmin 进行创建
* 2、如何收发消息
*/
@Test
public void createExchange() {
//amqpAdmin
//Exchange
/**
* DirectExchange(String name, 交换机的名字
* boolean durable, 是否持久化(重启后数据是否还在)
* boolean autoDelete, 是否自动删除
* Map<String, Object> arguments) 【自定义参数】
*/
DirectExchange directExchange = new DirectExchange("hello-java-exchange", true, false);
amqpAdmin.declareExchange(directExchange);
log.info("Exchange[{}]创建成功", "hello-java-exchange");
}
创建队列
@Test
public void createQueue() {
/**
* public Queue(String name, 队列的名字
* boolean durable, 是否持久化(重启后数据是否还在)
* boolean exclusive, 是否排他
* boolean autoDelete, 是否自动删除
* Map<String, Object> arguments) 【自定义参数】
*/
Queue queue = new Queue("hello-java-queue", true, false, false);
amqpAdmin.declareQueue(queue);
log.info("Queue[{}]创建成功", "hello-java-queue");
}
将上面创建的交换机和队列进行绑定
@Test
public void createBinding() {
/**
* public Binding(String destination, 【目的地】就是指要绑定的哪一个队列,也可以是交换机
* Binding.DestinationType destinationType,【目的地类型】 绑定的类型(是交换机绑定交换机还是交换机绑定队列)
* String exchange, 【交换机】
* String routingKey, 【路由键】
* @Nullable
* Map<String, Object> arguments 【自定义参数】
* ) {
*/
//将exchange指定的交换机和destination目的地进行绑定,使用routingKey作为指定的路由键
Binding binding = new Binding("hello-java-queue",
Binding.DestinationType.QUEUE,
"hello-java-exchange",
"hello.java", null);
amqpAdmin.declareBinding(binding);
log.info("Binding[{}]创建成功", "hello-java-binding");
}
发送消息
@Test
public void sendMessageTest() {
//1、发送消息,如果发送的消息是个对象,我们会使用序列化机制,将对象写出去。对象必须实现Serializable
String msg = "Hello World!";
//2、发送的对象类型的消息,可以转成一个json
for (int i=0;i<10;i++){
if(i%2 == 0){
OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
reasonEntity.setId(1L);
reasonEntity.setCreateTime(new Date());
reasonEntity.setName("哈哈-"+i);
rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", reasonEntity,new CorrelationData(UUID.randomUUID().toString()));
}else {
OrderEntity entity = new OrderEntity();
entity.setOrderSn(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", entity,new CorrelationData(UUID.randomUUID().toString()));
}
log.info("消息发送完成{}");
}
}
6、测试
分别执行上面的代码
256、商城业务-消息队列-AmqpAdmin使用
AmqpAdmin 是所有创建交换机,队列,绑定关系,等一系列的操作
案列代码见上面的案列
257、商城业务-消息队列-RabbitTemplate使用
RabbitTemplate 是发送消息的,操作消息的封装,案列代码见上面的案列
258、商城业务-消息队列-RabbitListener&RabbitHandler接收消息
1、发送消息
编写一个RabbitController 来发送消息,注意会往同一个交换机中发送两种类型(OrderReturnReasonEntity和OrderEntity)的消息,而且路由key都是hello.java
package com.atguigu.gulimall.order.controller;
import com.atguigu.gulimall.order.entity.OrderEntity;
import com.atguigu.gulimall.order.entity.OrderReturnReasonEntity;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
import java.util.UUID;
/**
* @创建人: 放生
* @创建时间: 2022/5/4
* @描述:
*/
@RestController
public class RabbitController {
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("/sendMq")
public String sendMq(@RequestParam(value = "num",defaultValue = "10") Integer num){
for (int i=0;i<num;i++){
if(i%2 == 0){
OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
reasonEntity.setId(1L);
reasonEntity.setCreateTime(new Date());
reasonEntity.setName("哈哈-"+i);
// CorrelationData(UUID.randomUUID().toString()) 自定义消息的唯一id
rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", reasonEntity,new CorrelationData(UUID.randomUUID().toString()));
}else {
OrderEntity entity = new OrderEntity();
entity.setOrderSn(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", entity,new CorrelationData(UUID.randomUUID().toString()));
}
}
return "ok";
}
}
2、消息的消费- @RabbitListener
消息消费利用@RabbitListener(queues = {“hello-java-queue”}) 注解,我们在recieveMessage()的方法上用的是 Object content,来接收消息,因为我们在发送消息的时候是发送了两种类型的消息,如果只是一种消息就可以用具体的对象去接收。
package com.atguigu.gulimall.order.service.impl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;
import com.atguigu.gulimall.order.dao.OrderItemDao;
import com.atguigu.gulimall.order.entity.OrderItemEntity;
import com.atguigu.gulimall.order.entity.OrderReturnReasonEntity;
import com.atguigu.gulimall.order.service.OrderItemService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service("orderItemService")
public class OrderItemServiceImpl extends ServiceImpl<OrderItemDao, OrderItemEntity> implements OrderItemService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<OrderItemEntity> page = this.page(
new Query<OrderItemEntity>().getPage(params),
new QueryWrapper<OrderItemEntity>()
);
return new PageUtils(page);
}
/**
* queues:声明需要监听的所有队列
*
* org.springframework.amqp.core.Message
*
* 参数可以写一下类型
* 1、Message message:原生消息详细信息。头+体
* 2、T<发送的消息的类型> OrderReturnReasonEntity content;
* 3、Channel channel:当前传输数据的通道
*
* Queue:可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一个收到此消息
* 场景:
* 1)、订单服务启动多个;同一个消息,只能有一个客户端收到(我们如果把消费端开启多个实列,一条消息也只能被一个实列消费)
* 2)、 只有一个消息完全处理完,方法运行结束,我们就可以接收到下一个消息
*/
@RabbitListener(queues = {"hello-java-queue"})
public void recieveMessage(Message message,
Object content,
Channel channel) throws InterruptedException {
//{"id":1,"name":"哈哈","sort":null,"status":null,"createTime":1581144531744}
System.out.println("接收到消息..."+content);
byte[] body = message.getBody();
//消息头属性信息
MessageProperties properties = message.getMessageProperties();
System.out.println("消息处理完成=>"+content);
}
}
3、消息的消费-@RabbitHandler
消息的消费除了可以用@RabbitListener还可以用@RabbitHandler
区别:
使用@RabbitListener(消费消息);必须有@EnableRabbit
@RabbitListener: 类+方法上(监听哪些队列即可)
@RabbitHandler:标在方法上(重载区分不同的消息)前提也是要在类上使用@RabbitListener的
3.1 案列如下
消息的发送还是上面的controller来生产消息,消息的消费改造如下
package com.atguigu.gulimall.order.service.impl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;
import com.atguigu.gulimall.order.dao.OrderItemDao;
import com.atguigu.gulimall.order.entity.OrderEntity;
import com.atguigu.gulimall.order.entity.OrderItemEntity;
import com.atguigu.gulimall.order.entity.OrderReturnReasonEntity;
import com.atguigu.gulimall.order.service.OrderItemService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import java.util.Map;
@RabbitListener(queues = {"hello-java-queue"})
@Service("orderItemService")
public class OrderItemServiceImpl extends ServiceImpl<OrderItemDao, OrderItemEntity> implements OrderItemService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<OrderItemEntity> page = this.page(
new Query<OrderItemEntity>().getPage(params),
new QueryWrapper<OrderItemEntity>()
);
return new PageUtils(page);
}
/**
* queues:声明需要监听的所有队列
*
* org.springframework.amqp.core.Message
*
* 参数可以写一下类型
* 1、Message message:原生消息详细信息。头+体
* 2、T<发送的消息的类型> OrderReturnReasonEntity content;
* 3、Channel channel:当前传输数据的通道
*
* Queue:可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一个收到此消息
* 场景:
* 1)、订单服务启动多个;同一个消息,只能有一个客户端收到
* 2)、 只有一个消息完全处理完,方法运行结束,我们就可以接收到下一个消息
*/
// @RabbitListener(queues = {"hello-java-queue"})
@RabbitHandler
public void recieveMessage(Message message,
OrderReturnReasonEntity content,
Channel channel) throws InterruptedException {
//{"id":1,"name":"哈哈","sort":null,"status":null,"createTime":1581144531744}
byte[] body = message.getBody();
//消息头属性信息
MessageProperties properties = message.getMessageProperties();
// System.out.println("消息处理完成=>"+content.getName());
System.out.println("接收到消息..."+content);
}
@RabbitHandler
public void recieveMessage2(OrderEntity content) throws InterruptedException {
//{"id":1,"name":"哈哈","sort":null,"status":null,"createTime":1581144531744}
System.out.println("接收到消息..."+content);
}
}
3.2 、测试
发送消息后查看控制台
259、商城业务-消息队列-可靠投递-发送端确认
- 保证消息不丢失,可靠抵达,可以使用事务消息,性能下降250倍,为此引入确认机制
• publisher confirmCallback 确认模式
• publisher returnCallback 未投递到 queue 退回模式
• consumer ack机制
1、可靠抵达-ConfirmCallback
• spring.rabbitmq.publisher-confirms=true
• 在创建 connectionFactory 的时候设置 PublisherConfirms(true) 选项,开启confirmcallback 。
• CorrelationData:用来表示当前消息唯一性。
• 消息只要被 broker 接收到就会执行 confirmCallback,如果是 cluster 模式,需要所有broker 接收到才会调用 confirmCallback。
• 被 broker 接收到只能表示 message 已经到达服务器,并不能保证消息一定会被投递到目标 queue 里。所以需要用到接下来的 returnCallback 。
RabbitTemplate 循坏依赖问题
在经行编码实现ConfirmCallback之前,如果在原先的MyRabbitConfig配置类中既注入RabbitTemplate 和 注入 MessageConverter就会导致循坏依赖问题,我们将两个分开配置,新建一个配置类RabbitMqMessageConverterConfig,如下
package com.atguigu.gulimall.order.config;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @创建人: 放生
* @创建时间: 2022/5/4
* @描述:
*/
@Configuration
public class RabbitMqMessageConverterConfig {
/**
* 使用JSON序列化机制,进行消息转换
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
1.1 代码实现
1.1.1 添加配置文件配置
application.properties
spring.rabbitmq.host=119.3.105.108
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
# 开启发送端确认,消息达到消息服务器
#spring.rabbitmq.publisher-confirms=true 老版本
# 新版本
spring.rabbitmq.publisher-confirm-type=correlated
1.1.2 MyRabbitConfig
package com.atguigu.gulimall.order.config;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
/**
* @创建人: 放生
* @创建时间: 2022/5/4
* @描述:
*/
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
/**
* 定制RabbitTemplate
* 1、服务器收到消息就回调
* 1、老版本spring.rabbitmq.publisher-confirms=true
* 新版本spring.rabbitmq.publisher-confirm-type=correlated
* 2、设置确认回调ConfirmCallback
*/
@PostConstruct //MyRabbitConfig对象创建完成以后,执行这个方法
public void initRabbitTemplate(){
//设置确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
*
* 1、只要消息抵达Broker就ack=true
* @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id)
* @param ack 消息是否成功收到
* @param cause 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
/**
* 1、做好消息确认机制(pulisher,consumer【手动ack】)
* 2、每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一遍
*/
//服务器收到了;
//修改消息的状态
System.out.println("confirm...correlationData["+correlationData+"]==>ack["+ack+"]==>cause["+cause+"]");
}
});
}
}
1.1.3 注释掉相关代码并测试
OrderItemServiceImpl中消息消费输出的相关代码注释掉,方便看日志
结果:只要消息被发送到消息服务器就会触发ConfirmCallback的回调
2、可靠抵达-ReturnCallback
• spring.rabbitmq.publisher-returns=true
• spring.rabbitmq.template.mandatory=true
• confrim 模式只能保证消息到达 broker,不能保证消息准确投递到目标 queue 里。在有些业务场景下,我们需要保证消息一定要投递到目标 queue 里,此时就需要用到return 退回模式。
• 这样如果未能投递到目标 queue 里将调用 returnCallback ,可以记录下详细到投递数据,定期的巡检或者自动纠错都需要这些数据。
2.1 代码实现
2.1.1 修改添加配置文件配置
application.properties
spring.rabbitmq.host=119.3.105.108
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
# 开启发送端确认,消息达到消息服务器
#spring.rabbitmq.publisher-confirms=true 老版本
# 新版本
spring.rabbitmq.publisher-confirm-type=correlated
# 开启发送端消息抵达队列的确认
spring.rabbitmq.publisher-returns=true
# 只要抵达队列, 以异步发送优先回调我们这个returnconfirm
spring.rabbitmq.template.mandatory=true
2.1.2 MyRabbitConfig
package com.atguigu.gulimall.order.config;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
/**
* @创建人: 放生
* @创建时间: 2022/5/4
* @描述:
*/
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
/**
* 定制RabbitTemplate
* 1、服务器收到消息就回调
* 1、spring.rabbitmq.publisher-confirms=true
* 2、设置确认回调ConfirmCallback
*
* 2、消息正确抵达队列进行回调
* 1、 spring.rabbitmq.publisher-returns=true
* spring.rabbitmq.template.mandatory=true
* 2、设置确认回调ReturnCallback
*
*/
@PostConstruct //MyRabbitConfig对象创建完成以后,执行这个方法
@PostConstruct //MyRabbitConfig对象创建完成以后,执行这个方法
public void initRabbitTemplate(){
//设置确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
*
* 1、只要消息抵达Broker就ack=true
* @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id)
* @param ack 消息是否成功收到
* @param cause 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
/**
* 1、做好消息确认机制(pulisher,consumer【手动ack】)
* 2、每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一遍
*/
//服务器收到了;
//修改消息的状态
System.out.println("confirm...correlationData["+correlationData+"]==>ack["+ack+"]==>cause["+cause+"]");
}
});
//设置消息抵达队列的确认回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
* 只要消息没有投递给指定的队列,就触发这个失败回调
* @param message 投递失败的消息详细信息
* @param replyCode 回复的状态码
* @param replyText 回复的文本内容
* @param exchange 当时这个消息发给哪个交换机
* @param routingKey 当时这个消息用哪个路由键
*/
@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+"]");
}
});
}
}
2.1.3 模拟发送消息到队列的异常
将RabbitController中的发送路由key hello.java 改为 hello22.java(hello22.java之前是没有绑定任何交换机和队列的,从而抛出异常)
@GetMapping("/sendMq")
public String sendMq(@RequestParam(value = "num",defaultValue = "10") Integer num){
for (int i=0;i<num;i++){
if(i%2 == 0){
OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
reasonEntity.setId(1L);
reasonEntity.setCreateTime(new Date());
reasonEntity.setName("哈哈-"+i);
// new CorrelationData(UUID.randomUUID().toString()) 自定义消息唯一id
rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", reasonEntity,new CorrelationData(UUID.randomUUID().toString()));
}else {
OrderEntity entity = new OrderEntity();
entity.setOrderSn(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("hello-java-exchange", "hello22.java", entity,new CorrelationData(UUID.randomUUID().toString()));
}
}
return "ok";
}
2.1.4 测试
260、商城业务-消息队列-可靠投递-消费端确认
1、可靠抵达-Ack消息确认机制
•消费者获取到消息,成功处理,可以回复Ack给Broker
•basic.ack用于肯定确认;broker将移除此消息
•basic.nack用于否定确认;可以指定broker是否丢弃此消息,可以批量
•basic.reject用于否定确认;同上,但不能批量
•默认自动ack,消息被消费者收到,就会从broker的queue中移除
•queue无消费者,消息依然会被存储,直到消费者消费
•消费者收到消息,默认会自动ack。但是如果无法确定此消息是否被处理完成,或者成功处理。我们可以开启手动ack模式
•消息处理成功,ack(),接受下一个消息,此消息broker就会移除
•消息处理失败,nack()/reject(),重新发送给其他人进行处理,或者容错处理后ack
•消息一直没有调用ack/nack方法,broker认为此消息正在被处理,不会投递给别人,此时客户端断开,消息不会被broker移除,会投递给别人
2、编码实现
2.1 修配置文件
spring.rabbitmq.host=119.3.105.108
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
# 开启发送端确认,消息达到消息服务器
#spring.rabbitmq.publisher-confirms=true 老版本
# 新版本
spring.rabbitmq.publisher-confirm-type=correlated
# 开启发送端消息抵达队列的确认
spring.rabbitmq.publisher-returns=true
# 只要抵达队列, 以异步发送优先回调我们这个returnconfirm
spring.rabbitmq.template.mandatory=true
# 手动ack 消息
spring.rabbitmq.listener.simple.acknowledge-mode=manual
2.2 OrderItemServiceImpl
@RabbitListener(queues = {"hello-java-queue"})
@Service("orderItemService")
public class OrderItemServiceImpl extends ServiceImpl<OrderItemDao, OrderItemEntity> implements OrderItemService {
/**
* queues:声明需要监听的所有队列
*
* org.springframework.amqp.core.Message
*
* 参数可以写一下类型
* 1、Message message:原生消息详细信息。头+体
* 2、T<发送的消息的类型> OrderReturnReasonEntity content;
* 3、Channel channel:当前传输数据的通道
*
* Queue:可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一个收到此消息
* 场景:
* 1)、订单服务启动多个;同一个消息,只能有一个客户端收到
* 2)、 只有一个消息完全处理完,方法运行结束,我们就可以接收到下一个消息
*/
// @RabbitListener(queues = {"hello-java-queue"})
@RabbitHandler
public void recieveMessage(Message message,
OrderReturnReasonEntity content,
Channel channel) throws InterruptedException {
//{"id":1,"name":"哈哈","sort":null,"status":null,"createTime":1581144531744}
System.out.println("接收到消息..."+content);
byte[] body = message.getBody();
//消息头属性信息
MessageProperties properties = message.getMessageProperties();
/**
* deliveryTag 是 channel内按顺序自增的。我们的消费者端启动后跟消息服务器建立channel连接后
* 并不是生产者生产一条,消费端获取一条,而是会批量获取并放到channel中,deliveryTag就是channel
* 内按顺序自增的的一个编号,后面的消息签收和拒绝都是需要用到deliveryTag
*/
long deliveryTag = message.getMessageProperties().getDeliveryTag();
System.out.println("deliveryTag==>"+deliveryTag);
//签收货物,非批量模式
try {
if(deliveryTag%2 == 0){
/**
* 收货
* channel.basicAck(deliveryTag,false) 表示签收消息,第二个参数表示是否批量签收
*/
channel.basicAck(deliveryTag,false);
System.out.println("签收了货物..."+deliveryTag);
}else {
/**
* 拒绝签收消息
* channel.basicNack(deliveryTag,false,true)接受三个参数
* 第一个: deliveryTag
* 第二个: 表示是否批量拒绝
* 第三个:拒绝后消息是否发回服务器重新入队,false 丢弃,true 放回
*/
channel.basicNack(deliveryTag,false,true);
System.out.println("没有签收了货物..."+deliveryTag);
}
}catch (Exception e){
//网络中断
}
}
@RabbitHandler
public void recieveMessage2(OrderEntity content) throws InterruptedException {
//{"id":1,"name":"哈哈","sort":null,"status":null,"createTime":1581144531744}
System.out.println("接收到消息..."+content);
}
}
2…3 测试
261、商城业务-订单服务-页面环境搭建
页面换环境的搭建和之前的其他模块一样 总结步骤如下:
-
本地域名映射
-
复制html文件到templates下
-
上传静态文件到nginx
-
修改templates下的html文件中的静态资源引用为nginx中的资源,以及HTML中的按钮跳转等路径
-
配置网关gateway中的跳转
262、商城业务-订单服务-整合SpringSession
1、添加依赖
添加redis 和springSession的相关依赖
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
2、复制相关配置
将product模块中的 ThreadPoolConfigProperties,GulimallSessionConfig,MyThreadConfig复制到order模块
3、配置文件
spring.rabbitmq.host=119.3.105.108
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
# 开启发送端确认,消息达到消息服务器
#spring.rabbitmq.publisher-confirms=true 老版本
# 新版本
spring.rabbitmq.publisher-confirm-type=correlated
# 开启发送端消息抵达队列的确认
spring.rabbitmq.publisher-returns=true
# 只要抵达队列, 以异步发送优先回调我们这个returnconfirm
spring.rabbitmq.template.mandatory=true
# 手动ack 消息
spring.rabbitmq.listener.simple.acknowledge-mode=manual
spring.thymeleaf.cache=false
spring.session.store-type=redis
gulimall.thread.core-size=20
gulimall.thread.max-size=200
gulimall.thread.keep-alive-time=10
spring.redis.host=119.3.105.108
spring.redis.port=6379
spring.mvc.date-format=yyyy-MM-dd HH:mm:ss
263、商城业务-订单服务-订单基本概念
1 、订单中心
电商系统涉及到 3 流,分别时信息流,资金流,物流,而订单系统作为中枢将三者有机的集 合起来。 订单模块是电商系统的枢纽,在订单这个环节上需求获取多个模块的数据和信息,同时对这 些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息流通。
2 、订单构成
1 、用户信息
用户信息包括用户账号、用户等级、用户的收货地址、收货人、收货人电话等组成,用户账户需要绑定手机号码,但是用户绑定的手机号码不一定是收货信息上的电话。用户可以添加 多个收货信息,用户等级信息可以用来和促销系统进行匹配,获取商品折扣,同时用户等级 还可以获取积分的奖励等
2 、订单基础信息
订单基础信息是订单流转的核心,其包括订单类型、父/子订单、订单编号、订单状态、订单流转的时间等。
(1)订单类型包括实体商品订单和虚拟订单商品等,这个根据商城商品和服务类型进行区分。
(2)同时订单都需要做父子订单处理,之前在初创公司一直只有一个订单,没有做父子订单处理后期需要进行拆单的时候就比较麻烦,尤其是多商户商场,和不同仓库商品的时候,父子订单就是为后期做拆单准备的。
(3)订单编号不多说了,需要强调的一点是父子订单都需要有订单编号,需要完善的时候可以对订单编号的每个字段进行统一定义和诠释。
(4)订单状态记录订单每次流转过程,后面会对订单状态进行单独的说明。
(5)订单流转时间需要记录下单时间,支付时间,发货时间,结束时间/关闭时间等等
3 、商品信息
商品信息从商品库中获取商品的 SKU 信息、图片、名称、属性规格、商品单价、商户信息等,从用户下单行为记录的用户下单数量,商品合计价格等。
4. 优惠信息
优惠信息记录用户参与的优惠活动,包括优惠促销活动,比如满减、满赠、秒杀等,用户使用的优惠券信息,优惠券满足条件的优惠券需要默认展示出来,具体方式已在之前的优惠券 篇章做过详细介绍,另外还虚拟币抵扣信息等进行记录。为什么把优惠信息单独拿出来而不放在支付信息里面呢?因为优惠信息只是记录用户使用的条目,而支付信息需要加入数据进行计算,所以做为区分。
5. 支付信息
(1)支付流水单号,这个流水单号是在唤起网关支付后支付通道返回给电商业务平台的支付流水号,财务通过订单号和流水单号与支付通道进行对账使用。
(2)支付方式用户使用的支付方式,比如微信支付、支付宝支付、钱包支付、快捷支付等。支付方式有时候可能有两个——余额支付+第三方支付。
(3)商品总金额,每个商品加总后的金额;运费,物流产生的费用;优惠总金额,包括促销活动的优惠金额,优惠券优惠金额,虚拟积分或者虚拟币抵扣的金额,会员折扣的金额等之和;实付金额,用户实际需要付款的金额。 用户实付金额=商品总金额+运费-优惠总金额
6. 物流信息
物流信息包括配送方式,物流公司,物流单号,物流状态,物流状态可以通过第三方接口来 获取和向用户展示物流每个状态节点。
3 、订单状态
1. 待付款
用户提交订单后,订单进行预下单,目前主流电商网站都会唤起支付,便于用户快速完成支付,需要注意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超 时后将自动取消订单,订单变更关闭状态。
2. 已付款 / 待发货
用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到 WMS系统,仓库进行调拨,配货,分拣,出库等操作。
3. 待收货 / 已发货
仓储将商品出库后,订单进入物流环节,订单系统需要同步物流信息,便于用户实时知悉物品物流状态
4. 已完成
用户确认收货后,订单交易完成。后续支付侧进行结算,如果订单存在问题进入售后状态
5. 已取消
付款之前取消订单。包括超时未付款或用户商户取消订单都会产生这种订单状态。
6. 售后中
用户在付款后申请退款,或商家发货后用户申请退换货。 售后也同样存在各种状态,当发起售后申请后生成售后订单,售后订单状态为待审核,等待商家审核,商家审核通过后订单状态变更为待退货,等待用户将商品寄回,商家收货后订单 状态更新为待退款状态,退款到用户原账户后订单状态更新为售后成功。
4 、订单流程
订单流程是指从订单产生到完成整个流转的过程,从而行程了一套标准流程规则。而不同的产品类型或业务类型在系统中的流程会千差万别,比如上面提到的线上实物订单和虚拟订单的流程,线上实物订单与 O2O 订单等,所以需要根据不同的类型进行构建订单流程。 不管类型如何订单都包括正向流程和逆向流程,对应的场景就是购买商品和退换货流程,正向流程就是一个正常的网购步骤:订单生成–>支付订单–>卖家发货–>确认收货–>交易成功。而每个步骤的背后,订单是如何在多系统之间交互流转的,可概括如下图
1、订单创建与支付
(1) 、订单创建前需要预览订单,选择收货信息等
(2) 、订单创建需要锁定库存,库存有才可创建,否则不能创建
(3) 、订单创建后超时未支付需要解锁库存
(4) 、支付成功后,需要进行拆单,根据商品打包方式,所在仓库,物流等进行拆单
(5) 、支付的每笔流水都需要记录,以待查账
(6) 、订单创建,支付成功等状态都需要给 MQ 发送消息,方便其他系统感知订阅
2、逆向流程
(1) 、修改订单,用户没有提交订单,可以对订单一些信息进行修改,比如配送信息,优惠信息,及其他一些订单可修改范围的内容,此时只需对数据进行变更即可。
(2) 、订单取消,用户主动取消订单和用户超时未支付,两种情况下订单都会取消订单,而超时情况是系统自动关闭订单,所以在订单支付的响应机制上面要做支付的限时处理,尤其是在前面说的下单减库存的情形下面,可以保证快速的释放库存。另外需要需要处理的是促销优惠中使用的优惠券,权益等视平台规则,进行相应补回给用户。
(3) 、退款,在待发货订单状态下取消订单时,分为缺货退款和用户申请退款。如果是全部退款则订单更新为关闭状态,若只是做部分退款则订单仍需进行进行,同时生成一条退款的售后订单,走退款流程。退款金额需原路返回用户的账户。
(4) 、发货后的退款,发生在仓储货物配送,在配送过程中商品遗失,用户拒收,用户收货后对商品不满意,这样情况下用户发起退款的售后诉求后,需要商户进行退款的审核,双方达成一致后,系统更新退款状态,对订单进行退款操作,金额原路返 回用户的账户,同时关闭原订单数据。仅退款情况下暂不考虑仓库系统变化。如果发生双方协调不一致情况下,可以申请平台客服介入。在退款订单商户不处理的情况下,系统需要做限期判断,比如 5 天商户不处理,退款单自动变更同意退款。
264、商城业务-订单服务-订单登录拦截
1、在order创建一个web目录
用于存放我们所有的到HTML的控制器,不和之前的controller放在一起
2、创建拦截器
和之前一样创建一个拦截器拦截所有的请求,用于校验用户是否登入,是临时用户还是已经登入的用户,访问订单服务的肯定是要已经登入的用户,否则要让其登入在做其他的操作
创建拦截器LoginUserInterceptor
package com.atguigu.gulimall.order.interceptor;
import com.atguigu.common.constant.AuthServerConstant;
import com.atguigu.common.vo.MemberRespVo;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @创建人: 放生
* @创建时间: 2022/5/4
* @描述:
*/
@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 {
// /order/order/status/2948294820984028420
String uri = request.getRequestURI();
AntPathMatcher antPathMatcher = new AntPathMatcher();
boolean match = antPathMatcher.match("/order/order/status/**", uri);
boolean match1 = antPathMatcher.match("/payed/notify", uri);
if(match || match1){
return true;
}
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;
}
}
}
加入拦截器
package com.atguigu.gulimall.order.config;
import com.atguigu.gulimall.order.interceptor.LoginUserInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @创建人: 放生
* @创建时间: 2022/5/4
* @描述:
*/
@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {
@Autowired
LoginUserInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor).addPathPatterns("/**");
}
}
265、商城业务-订单服务-订单确认页模型抽取
package com.atguigu.gulimall.order.vo;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
//订单确认页需要用的数据
public class OrderConfirmVo {
收货地址,ums_member_receive_address表
@Setter @Getter
List<MemberAddressVo> address;
//所有选中的购物项
@Setter @Getter
List<OrderItemVo> items;
//发票记录....
//优惠券信息...
@Setter @Getter
Integer integration;
@Setter @Getter
Map<Long,Boolean> stocks;
//防重令牌
@Setter @Getter
String orderToken;
public Integer getCount(){
Integer i = 0 ;
if(items!=null){
for (OrderItemVo item : items) {
i+=item.getCount();
}
}
return i;
}
// 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;
}
// BigDecimal payPrice;
public BigDecimal getPayPrice() {
return getTotal();
}
//应付价格
}
266、商城业务-订单服务-订单确认页数据获取
1、OrderWebController
package com.atguigu.gulimall.order.web;
import com.atguigu.gulimall.order.service.OrderService;
import com.atguigu.gulimall.order.vo.OrderConfirmVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.ExecutionException;
/**
* @创建人: 放生
* @创建时间: 2022/5/4
* @描述:
*/
@Controller
public class OrderWebController {
@Autowired
OrderService orderService;
/**
* 去结算确认页
* @param model
* @param request
* @return
* @throws ExecutionException
* @throws InterruptedException
*/
@GetMapping("/toTrade")
public String toTrade(Model model, HttpServletRequest request) throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = orderService.confirmOrder();
model.addAttribute("orderConfirmData",confirmVo);
//展示订单确认的数据
return "confirm";
}
}
2、OrderServiceImpl
package com.atguigu.gulimall.order.service.impl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;
import com.atguigu.common.vo.MemberRespVo;
import com.atguigu.gulimall.order.dao.OrderDao;
import com.atguigu.gulimall.order.entity.OrderEntity;
import com.atguigu.gulimall.order.feign.CartFeignService;
import com.atguigu.gulimall.order.feign.MemberFeignService;
import com.atguigu.gulimall.order.interceptor.LoginUserInterceptor;
import com.atguigu.gulimall.order.service.OrderService;
import com.atguigu.gulimall.order.vo.MemberAddressVo;
import com.atguigu.gulimall.order.vo.OrderConfirmVo;
import com.atguigu.gulimall.order.vo.OrderItemVo;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<OrderEntity> page = this.page(
new Query<OrderEntity>().getPage(params),
new QueryWrapper<OrderEntity>()
);
return new PageUtils(page);
}
@Autowired
private CartFeignService cartFeignService;
@Autowired
private MemberFeignService memberFeignService;
@Override
public OrderConfirmVo confirmOrder() {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
//1、远程查询所有的收货地址列表
//每一个线程都来共享之前的请求数据
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddress(address);
//2、远程查询购物车所有选中的购物项
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//3、查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
//4、其他数据自动计算
//TODO 5、防重令牌
return confirmVo;
}
}
267、商城业务-订单服务-Feign远程调用丢失请求头问题
1、问题描述
根据上一章节的代码(266),我们浏览器发送请求调用我们订单模块的OrderWebController中的toTrade方法,获取订单结算页数据,在OrderServiceImpl中的confirmOrder方法中,我们会发起feign远程调用,分别去member模块获取用户的收获地址和cart模块获取购物车的信息,因为feign的底层是会重新构建一个新的请求(RequestTemplate)从而来调用远程服务的,就是在重新构建的请求中没有把原来的请求中的cookies等相关信息带上,导致了调用异常,因为我们在cart 和 member 是使用的spring session来共享session的,cart 和 member会对所有的请求拦截,获取相关cookies信息,就无法获取到,导致 MemberRespVo memberRespVo =LoginUserInterceptor.loginUser.get() 获取到的memberRespVo =null,
如果远程的被调用放不需要session的相关请求信息的话,也不会有这个问题
2、解决思路
我们自定义一个拦截器放入容器中,在feign构建完请求后,在拦截器中将之前的请求的cookies信息添加到feign新构建的请求中去
3、代码解决
package com.atguigu.gulimall.order.config;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* @创建人: 放生
* @创建时间: 2022/5/4
* @描述:
*/
@Configuration
public class GuliFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor(){
@Override
public void apply(RequestTemplate template) {
//1、RequestContextHolder拿到刚进来的这个请求
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if(attributes!=null){
System.out.println("RequestInterceptor线程...."+Thread.currentThread().getId());
HttpServletRequest request = attributes.getRequest(); //老请求
if(request != null){
//同步请求头数据,Cookie
String cookie = request.getHeader("Cookie");
//给新请求同步了老请求的cookie
template.header("Cookie",cookie);
}
}
}
};
}
}
268、商城业务-订单服务-Feign异步调用丢失请求头问题
1、多线程改造OrderServiceImpl中的confirmOrder方法
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//1、远程查询所有的收货地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddress(address);
}, executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//2、远程查询购物车所有选中的购物项
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
}, executor);
//3、查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
//4、其他数据自动计算
//TODO 5、防重令牌
CompletableFuture.allOf(getAddressFuture, cartFuture).get();
return confirmVo;
}
2、改造后出现了Feign异步调用丢失请求头问题
以上代码改造后测试 发现在程序执行到之前定义的feign拦截器配置类中报空指针异常, HttpServletRequest request = attributes.getRequest();时获取到的request = null,
3、问题分析
出现问题的原因是因为我们开启了多线程,调用feign的远程服务不是在主线程中发起的,而是在新的线程中发起的,新的线程中当然ServletRequestAttributes是为空的,所以。。。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IOgW2RjA-1652453882209)(https://gitee.com/jiushuli/images/raw/master/image-20220504225842875.png)]
解决的思路:在开启异步之前,我们获取到原来(主线程)的RequestAttributes,然后将RequestAttributes设置到新的线程中去,代码如下
4、代码解决Feign异步调用丢失请求头问题
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
System.out.println("主线程...." + Thread.currentThread().getId());
//获取之前的请求
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//1、远程查询所有的收货地址列表
System.out.println("member线程...." + Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddress(address);
}, executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//2、远程查询购物车所有选中的购物项
System.out.println("cart线程...." + Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//feign在远程调用之前要构造请求,调用很多的拦截器
//RequestInterceptor interceptor : requestInterceptors
}, executor);
//3、查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
//4、其他数据自动计算
//TODO 5、防重令牌
CompletableFuture.allOf(getAddressFuture, cartFuture).get();
return confirmVo;
}
269、商城业务-订单服务-bug修改
CartController
@GetMapping("/currentUserCartItems")
@ResponseBody
public List<CartItem> getCurrentUserCartItems(){
return cartService.getUserCartItems();
}
CartServiceImpl
@Override
public List<CartItem> getUserCartItems() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
if(userInfoTo.getUserId()==null){
return null;
}else{
String cartKey = CART_PREFIX + userInfoTo.getUserId();
List<CartItem> cartItems = getCartItems(cartKey);
//获取所有被选中的购物项
List<CartItem> collect = cartItems.stream()
.filter(item -> item.getCheck())
.map(item->{
R price = productFeignService.getPrice(item.getSkuId());
//TODO 1、更新为最新价格
String data = (String) price.get("data");
item.setPrice(new BigDecimal(data));
return item;
})
.collect(Collectors.toList());
return collect;
}
}
270、商城业务-订单服务-订单确认页渲染
根据上一章节的返回的数据渲染HTML页面,参考confirm.html的页面
271、商城业务-订单服务-订单确认页库存查询
1、添加库存查询
在上面几个章节的代码实现过程中查询了返回订单信息,现在需要在之前的逻辑中添加查询订单的库存情况
2、代码实现
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
System.out.println("主线程...." + Thread.currentThread().getId());
//获取之前的请求
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//1、远程查询所有的收货地址列表
System.out.println("member线程...." + Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddress(address);
}, executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//2、远程查询购物车所有选中的购物项
System.out.println("cart线程...." + Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//feign在远程调用之前要构造请求,调用很多的拦截器
//RequestInterceptor interceptor : requestInterceptors
}, executor).thenRunAsync(() -> {
List<OrderItemVo> items = confirmVo.getItems();
List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList());
//TODO 一定要启动库存服务,否则库存查不出。
R hasStock = wmsFeignService.getSkusHasStock(collect);
List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() {
});
if (data != null) {
Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
confirmVo.setStocks(map);
}
}, executor);
//3、查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
//4、其他数据自动计算
//TODO 5、防重令牌
CompletableFuture.allOf(getAddressFuture, cartFuture).get();
return confirmVo;
}
272、商城业务-订单服务-订单确认页模拟运费效果
1、默认地址高亮
function highlight() {
$(".addr-item p").css({"border": "2px solid gray"})
$(".addr-item p[def='1']").css({"border": "2px solid red"})
}
2、模拟运费
用户切换地址后,不仅选择的地址要高亮,并且我们的运费要重新计算并返回页面
$(".addr-item p").click(function () {
$(".addr-item p").attr("def", "0")
$(this).attr("def", "1");
highlight();
//获取到当前的地址id
var addrId = $(this).attr("addrId");
//发送ajax获取运费信息
getFare(addrId);
});
function getFare(addrId) {
//给表单回填选择的地址
$("#addrIdInput").val(addrId);
$.get("http://gulimall.com/api/ware/wareinfo/fare?addrId=" + addrId, function (resp) {
console.log(resp);
//fareEle
$("#fareEle").text(resp.data.fare);
var total = [[${orderConfirmData.total}]];
//设置运费等
var payPrice = total * 1 + resp.data.fare * 1;
$("#payPriceEle").text(payPrice);
$("#payPriceInput").val(payPrice);
//设置收货人信息
$("#recieveAddressEle").text(resp.data.address.province + " " + resp.data.address.detailAddress);
$("#recieverEle").text(resp.data.address.name);
})
}
WareInfoController
@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);
}
.....
WareInfoServiceImpl
@Override
public FareVo getFare(Long addrId) {
FareVo fareVo = new FareVo();
R r = memberFeignService.addrInfo(addrId);
MemberAddressVo data = r.getData("memberReceiveAddress",new TypeReference<MemberAddressVo>() {
});
if(data!=null){
String phone = data.getPhone();
//123
String substring = phone.substring(phone.length() - 1, phone.length());
BigDecimal bigDecimal = new BigDecimal(substring);
fareVo.setAddress(data);
fareVo.setFare(bigDecimal);
return fareVo;
}
return null;
}
273、商城业务-订单服务-订单确认页细节显示
添加上不同的收货地址可能有不同的收货人。。。。
274、商城业务-订单服务-接口幂等性讨论
1、什么是幂等性
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用;比如说支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结 果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...,这就没有保证接口 的幂等性。
讨论:多次点击 【提交订单】 按钮
幂等性:订单提交一次和提交多次结果是一致的
2、哪些情况需要防止
- 用户多次点击按钮
- 用户页面回退再次提交
- 服务相互调用,由于网络间题,导致请求失败。feign触发重试机制
- 其他业务情况
3、什么情况下需要幂等
以SQL为例,有些操作是天然原子的。
SELECT FROM table WHERE id=? -- 无论执行多少次都不会改变状态,是天然的幂等
UPDATE table SET col1=1 WHERE col2=2 -- 无论执行成功多少次状态都是一致的,也是幂等操作。
delete from user where userid=l -- 多次操作,结果一样,具备幂等性
insert into user (useridname) values(l,'a') -- 如userid为唯一主键,即重复操作上面的业务,只会插入一条用户数据,具备幂等性。同理也可以把订单号作为唯一的约束,这样也可以把防止订单的重复提交插入等。。。
UPDATE table1 SET col=col+1 where col2= 22 -- 每执行的結果都会发生变化,不是幂等的。
insert into user(userId,name)values(1,'a') -- 如userid不是主键,可以重复,那上面业务多次操作,都会新增多条,不具备幂等性
4、幂等解决方案
(1)、token机制
如12306选中座位后提交,带上验证码与后台该token对应的验证码一致才通过。如果通过了就删除,第二个即使带着验证码也匹配不到
前面我们返回订单页面时也在redis中设置了用户的uuid
-
服务端提供了发送token的接囗。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取token,服务器会把token保存到redis中。
-
然后调用业务接囗请求时,把token携带过去,一般放在请求头部。
-
服务器判断token是否存在redis中,存在表示第一次请求,然后先删除token,继续执行业务。
-
但要保证只能有一个去redis看,否则就可能都看到redis中有,删除两次
-
【对比+删除】得是原子性的,所以就想到了用redis-luna脚本分布式锁
-
if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end
-
-
如果判断token不存在redis中,就表示是重复操作,直接返回重复标记给client,这样就保证了业务代码,不重复执行。
(2)、各种锁
a、数据库悲观锁
select * from ×× where id=1 for update;
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。
另外要注意的是,id字段一定是主键或者唯一幸引,不然可能造成锁表的结果,处理起来会非常麻烦。
b、数据库乐观锁
这种方法适合在更新的场景中,
updatet _goods set count=count-1,version=version+1 where good_id=2 and version=1
根据version版本,也就是在操作库存前先获取当前商品的version版本号,然后操作的时候带上此version号。我们梳理下,我们第一次操作库存时,得到version为1,调用库存服务version变成了2;但返回给订单服务出现了间题,订单服务又一次发起调用库存,服务,当订单服务传如的version还是1,再执行上面的四!v语句时,就不会执行;因为version已经变为2了,where条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。
乐观锁主要使用于处理读多写少的问题
c、业务层分布式锁
(3)、各种唯一约束
a、数据库唯一约束
插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入。
我们在数据库层面防止重复。
这个机制是利用了数据库的主键唯一约束的特性,解决了在insert场景时幂等问题。但主键的要求不是自增的主,这样就需要业务生成全局唯一的主键。
如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。
b、redis set防重
很多数据需要处理,只能被处理一次,比如我们可以计算数据的MD5将其放入redis的set,每次处理数据,先看这个MD5是否已经存在,存在就不处理。
(4)、防重表
使用订单号orderNo做为去重表的唯一索引,把唯一幸引插入去重表,再进行业务操作,且他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。
之前说的redis防重也算
(5)、全局请求唯一id
调用接口时,生成一个唯一id,redis将数据保存到集合(去重),存在即处理过。
可以使用nginx设置每一个请求的唯一id
proxy_set_header X-Request-id $request_id;
275、商城业务-订单服务-订单确认页完成 (防重令牌)
1、防重令牌
根据前面的幂等性知识,我们这里用token令牌机制解决幂等性
-
准备好订单确认数据后,返回给用户看运费等信息,同时创建防重令牌
redis.set('order:token:(userId)',uuid)
- 也就是给服务器一个,给用户一个(比如我们点击购物车的时候,回去后端请求购物车的相关数据,这个时候就把token生成好,在redis服务器存放一份,要设置自动过期时间,然后连同购物车数据一起返给前端一份,这样前端在点击结算按钮的时候就把token一起带上) 如下实战:在OrderServiceImpl 获取购物车数据时设置token
@Override public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException { OrderConfirmVo confirmVo = new OrderConfirmVo(); MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get(); System.out.println("主线程...." + Thread.currentThread().getId()); //获取之前的请求 RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> { //1、远程查询所有的收货地址列表 System.out.println("member线程...." + Thread.currentThread().getId()); //每一个线程都来共享之前的请求数据 RequestContextHolder.setRequestAttributes(requestAttributes); List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId()); confirmVo.setAddress(address); }, executor); CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> { //2、远程查询购物车所有选中的购物项 System.out.println("cart线程...." + Thread.currentThread().getId()); //每一个线程都来共享之前的请求数据 RequestContextHolder.setRequestAttributes(requestAttributes); List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems(); confirmVo.setItems(items); //feign在远程调用之前要构造请求,调用很多的拦截器 //RequestInterceptor interceptor : requestInterceptors }, executor).thenRunAsync(() -> { List<OrderItemVo> items = confirmVo.getItems(); List<Long> collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList()); //TODO 一定要启动库存服务,否则库存查不出。 R hasStock = wmsFeignService.getSkusHasStock(collect); List<SkuStockVo> data = hasStock.getData(new TypeReference<List<SkuStockVo>>() { }); if (data != null) { Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock)); confirmVo.setStocks(map); } }, executor); //3、查询用户积分 Integer integration = memberRespVo.getIntegration(); confirmVo.setIntegration(integration); //4、其他数据自动计算 //TODO 5、防重令牌 String token = UUID.randomUUID().toString().replace("-", ""); redisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId(), token, 30, TimeUnit.MINUTES); confirmVo.setOrderToken(token); CompletableFuture.allOf(getAddressFuture, cartFuture).get(); return confirmVo; }
-
用户点击提交订单按钮,带着token。怎么带?设置个hidden元素带着就行
-
渲染订单确认页,后台处理的时候确认请求带过来token的uuid和redis库中是否一致
- 此处是重点,比对后立刻删除,比对和删除要求具有原子性,通过redis-lua脚本完成
-
提交订单时不要提交购买的商品,去购物车数据库重新获取即可,防止购物车变化和修改页面值
-
但可以提交总额,防止商品金额变了还提交订单,用户不满意
-
其他信息可以用token和session获取
(1)订单数据
从属性可以看出,订单提交时需要带的数据
@Data public class OrderSubmitVo { /** 收获地址的id **/ private Long addrId; /** 支付方式 **/ private Integer payType; //无需提交要购买的商品,去购物车再获取一遍 //优惠、发票 /** 防重令牌 **/ private String orderToken; /** 应付价格 **/ private BigDecimal payPrice; /** 订单备注 **/ private String remarks; //用户相关的信息,直接去session中取出即可 }
成功后转发至支付页面携带的数据
@Data public class SubmitOrderResponseVo { // 该实体为order表的映射 private OrderEntity order; /** 错误状态码 **/ private Integer code; }
(2)提交订单
- 提交订单成功,则携带返回数据转发至支付页面
- 提交订单失败,则携带错误信息重定向至确认页
在OrderWebController里接收到下单请求,然后去OrderServiceImpl里验证和下单,然后再返回到OrderWebController。相当于OrderWebController是封装了我们原来的OrderServiceImpl,用作web的
调用service,service返回了失败Code信息,可以看是什么原因引起的下单失败
以上章节就完成了所有的订单确认页的数据,接下来提交订单信息
2、订单提交接口
OrderWebController
/** * 下单功能 * @param vo * @return */ @PostMapping("/submitOrder") public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes redirectAttributes){ try { SubmitOrderResponseVo responseVo = orderService.submitOrder(vo); //下单失败回到订单确认页重新确认订单信息 System.out.println("订单提交的数据..."+vo); 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 message = ((NoStockException) e).getMessage(); redirectAttributes.addFlashAttribute("msg",message); } return "redirect:http://order.gulimall.com/toTrade"; } }
3、提交接口 OrderServiceImpl
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
//1、验证令牌【令牌的对比和删除必须保证原子性】
//TODO 1 下单:去创建订单,验令牌,验价格,锁库存...
//下单成功来到支付页
//下单失败回到订单确认页重新确定订单信息
return null;
}
什么的逻辑其实是交给orderService.submitOrder(submitVo);
去做的,那么我们就接着往下看他是如何与令牌结合保证幂等性的
276、商城业务-订单服务-原子验令牌
1、验证原子性令牌
- 为防止在【获取令牌、对比值和删除令牌】之间发生错误导入令牌校验出错,我们必须使用lua脚本保证原子性操作
- 改为先锁库存再生成订单
- 库存服务后面讲
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
//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 = vo.getOrderToken();
//原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
if (result == 0L) {
//令牌验证失败
return response;
}else {
//令牌验证成功
//TODO 1 下单:去创建订单,验令牌,验价格,锁库存...
//TODO 2、创建订单,订单项等信息
//TODO 3、保存订单
//4、库存锁定。只要有异常回滚订单数据。
//TODO 5、远程扣减积分 出异常
//订单创建成功发送消息给MQ
//TODO 6、清除购物车已经下单的商品
}
return response;
}
277、商城业务-订单服务-构造订单数据
订单创建To
最终订单后要返回的数据
@Data
public class OrderCreateTo {
private OrderEntity order;
private List<OrderItemEntity> orderItems;
/** 订单计算的应付价格 **/
private BigDecimal payPrice;
/** 运费 **/
private BigDecimal fare;
}
创建订单、订单项
- 用
IdWorker
生成订单号,是时间和本身对象的组合 - 构建订单。此时还没商品
- 用threadlocal保存一些当前线程的数据,就不用写形参了
- 构建订单项。填入具体的商品,设计锁库存的问题
- 计算价格
private ThreadLocal<OrderSubmitVo> confirmVoThreadLocal = new ThreadLocal<>();
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
//线程共享数据
confirmVoThreadLocal.set(vo);
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
//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 = vo.getOrderToken();
//原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
if (result == 0L) {
//令牌验证失败
return response;
}else {
//令牌验证成功
//1、创建订单,订单项等信息
OrderCreateTo order = createOrder();
}
return response;
}
private OrderCreateTo createOrder() {
OrderCreateTo createTo = new OrderCreateTo();
//1、生成订单号
String orderSn = IdWorker.getTimeId();
//创建订单号
OrderEntity orderEntity = buildOrder(orderSn);
//2、获取到所有的订单项
List<OrderItemEntity> itemEntities = buildOrderItems(orderSn);
return createTo;
}
private OrderEntity buildOrder(String orderSn) {
MemberRespVo respVo = LoginUserInterceptor.loginUser.get();
OrderEntity entity = new OrderEntity();
entity.setOrderSn(orderSn);
entity.setMemberId(respVo.getId());
OrderSubmitVo submitVo = confirmVoThreadLocal.get();
//获取收货地址信息
R fare = wmsFeignService.getFare(submitVo.getAddrId());
FareVo fareResp = fare.getData(new TypeReference<FareVo>() {
});
//设置运费信息
entity.setFreightAmount(fareResp.getFare());
//设置收货人信息
entity.setReceiverCity(fareResp.getAddress().getCity());
entity.setReceiverDetailAddress(fareResp.getAddress().getDetailAddress());
entity.setReceiverName(fareResp.getAddress().getName());
entity.setReceiverPhone(fareResp.getAddress().getPhone());
entity.setReceiverPostCode(fareResp.getAddress().getPostCode());
entity.setReceiverProvince(fareResp.getAddress().getProvince());
entity.setReceiverRegion(fareResp.getAddress().getRegion());
//设置订单的相关状态信息
entity.setStatus(OrderStatusEnum.CREATE_NEW.getCode());
entity.setAutoConfirmDay(7);
return entity;
}
/**
* 构建所有订单项数据
*
* @return
*/
private List<OrderItemEntity> buildOrderItems(String orderSn) {
//最后确定每个购物项的价格
List<OrderItemVo> currentUserCartItems = cartFeignService.getCurrentUserCartItems();
if (currentUserCartItems != null && currentUserCartItems.size() > 0) {
List<OrderItemEntity> itemEntities = currentUserCartItems.stream().map(cartItem -> {
OrderItemEntity itemEntity = buildOrderItem(cartItem);
itemEntity.setOrderSn(orderSn);
return itemEntity;
}).collect(Collectors.toList());
return itemEntities;
}
return null;
}
278、商城业务-订单服务-构造订单项数据
构建订单项
订单项指的是订单里具体的商品
StringUtils.collectionToDelimitedString(list, ";分隔符")
工具可以集合/数组转string- 订单项得算优惠后的价格
- 用BigDecimal精确计算
// OrderServiceImpl
private List<OrderItemEntity> buildOrderItems(String orderSn) {
// 这里是最后一次来确认购物项的价格 这个远程方法还会查询一次数据库
List<OrderItemVo> cartItems = cartFeignService.getCurrentUserCartItems();
List<OrderItemEntity> itemEntities = null;
if(cartItems != null && cartItems.size() > 0){
itemEntities = cartItems.stream().map(cartItem -> {
OrderItemEntity itemEntity = buildOrderItem(cartItem);
itemEntity.setOrderSn(orderSn);
return itemEntity;
}).collect(Collectors.toList());
}
return itemEntities;
}
/**
* 构建某一个订单项
*
* @param cartItem
*
* @return
*/
private OrderItemEntity buildOrderItem(OrderItemVo cartItem) {
OrderItemEntity itemEntity = new OrderItemEntity();
//1、订单信息:订单号 v
//2、商品的SPU信息 V
Long skuId = cartItem.getSkuId();
R r = productFeignService.getSpuInfoBySkuId(skuId);
SpuInfoVo data = r.getData(new TypeReference<SpuInfoVo>() {
});
itemEntity.setSpuId(data.getId());
itemEntity.setSpuBrand(data.getBrandId().toString());
itemEntity.setSpuName(data.getSpuName());
itemEntity.setCategoryId(data.getCatalogId());
//3、商品的sku信息 v
itemEntity.setSkuId(cartItem.getSkuId());
itemEntity.setSkuName(cartItem.getTitle());
itemEntity.setSkuPic(cartItem.getImage());
itemEntity.setSkuPrice(cartItem.getPrice());
String skuAttr = StringUtils.collectionToDelimitedString(cartItem.getSkuAttr(), ";");
itemEntity.setSkuAttrsVals(skuAttr);
itemEntity.setSkuQuantity(cartItem.getCount());
//4、优惠信息[不做]
//5、积分信息
itemEntity.setGiftGrowth(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
itemEntity.setGiftIntegration(cartItem.getPrice().multiply(new BigDecimal(cartItem.getCount().toString())).intValue());
//6、订单项的价格信息
itemEntity.setPromotionAmount(new BigDecimal("0"));
itemEntity.setCouponAmount(new BigDecimal("0"));
itemEntity.setIntegrationAmount(new BigDecimal("0"));
//当前订单项的实际金额。 总额-各种优惠
BigDecimal orign = itemEntity.getSkuPrice().multiply(new BigDecimal(itemEntity.getSkuQuantity().toString()));
BigDecimal subtract = orign.subtract(itemEntity.getCouponAmount())
.subtract(itemEntity.getPromotionAmount())
.subtract(itemEntity.getIntegrationAmount());
itemEntity.setRealAmount(subtract);
return itemEntity;
}
279、商城业务-订单服务-订单验价
计算总价
private void computerPrice(OrderEntity orderEntity, List<OrderItemEntity> items) {
// 叠加每一个订单项的金额
BigDecimal coupon = new BigDecimal("0.0");
BigDecimal integration = new BigDecimal("0.0");
BigDecimal promotion = new BigDecimal("0.0");
BigDecimal gift = new BigDecimal("0.0");
BigDecimal growth = new BigDecimal("0.0");
// 总价
BigDecimal totalPrice = new BigDecimal("0.0");
for (OrderItemEntity item : items) { // 这段逻辑不是特别合理,最重要的是累积总价,别的可以跳过
// 优惠券的金额
coupon = coupon.add(item.getCouponAmount());
// 积分优惠的金额
integration = integration.add(item.getIntegrationAmount());
// 打折的金额
promotion = promotion.add(item.getPromotionAmount());
BigDecimal realAmount = item.getRealAmount();
totalPrice = totalPrice.add(realAmount);
// 购物获取的积分、成长值
gift.add(new BigDecimal(item.getGiftIntegration().toString()));
growth.add(new BigDecimal(item.getGiftGrowth().toString()));
}
// 1.订单价格相关 总额、应付总额
orderEntity.setTotalAmount(totalPrice);
orderEntity.setPayAmount(totalPrice.add(orderEntity.getFreightAmount()));
orderEntity.setPromotionAmount(promotion);
orderEntity.setIntegrationAmount(integration);
orderEntity.setCouponAmount(coupon);
// 设置积分、成长值
orderEntity.setIntegration(gift.intValue());
orderEntity.setGrowth(growth.intValue());
// 设置订单的删除状态
orderEntity.setDeleteStatus(OrderStatusEnum.CREATE_NEW.getCode());
}
3) 验价
计算完总价后,返回主逻辑
// @GlobalTransactional
@Transactional
@Override // OrderServiceImpl
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
// 1. 验证令牌 [必须保证原子性] 返回 0 or 1
if (result == 0L) { // 令牌验证失败
} else { // 令牌验证成功
// 1 .创建订单等信息
OrderCreateTo order = createOrder();
// 2. 验价
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal voPayPrice = vo.getPayPrice();// 获取带过来的价格
if (Math.abs(payAmount.subtract(voPayPrice).doubleValue()) < 0.01) {
将"页面提交的价格"
和"后台计算的价格"
进行对比,若不同则提示用户商品价格发生变化
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = submitVo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
/****************/
}else {
//验价失败
responseVo.setCode(2);
return responseVo;
}
280、商城业务-订单服务-保存订单数据
/**
* 保存订单数据
*
* @param order
*/
private void saveOrder(OrderCreateTo order) {
OrderEntity orderEntity = order.getOrder();
orderEntity.setModifyTime(new Date());
this.save(orderEntity);
List<OrderItemEntity> orderItems = order.getOrderItems();
orderItemService.saveBatch(orderItems);
}
281、商城务-订单服务-锁定库存
锁定库存,发送延迟队列
- 锁定库存失败要取消订单
// 在订单里的逻辑:
// 前面是创建订单、订单项、验价等逻辑...
// .....
//
List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> {
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(item.getSkuId());
orderItemVo.setCount(item.getSkuQuantity());
return orderItemVo;
}).collect(Collectors.toList());
// 去锁库存 @RequestMapping("/lock/order")
R r = wareFeignService.orderLockStock(orderItemVos);
//5.1 锁定库存成功
if (r.getCode()==0){
responseVo.setOrder(order.getOrder());
responseVo.setCode(0);
return responseVo;
}else {
//5.2 锁定库存失败
String msg = (String) r.get("msg");
throw new NoStockException(msg);
}
远程服务
@RequestMapping("/lock/order")
public R orderLockStock(@RequestBody List<OrderItemVo> itemVos) {
try {
Boolean lock = wareSkuService.orderLockStock(itemVos);
return R.ok();
} catch (NoStockException e) {
return R.error(BizCodeEnum.NO_STOCK_EXCEPTION.getCode(), BizCodeEnum.NO_STOCK_EXCEPTION.getMsg());
}
}
- 找出所有库存大于商品数的仓库
- 遍历所有满足条件的仓库,逐个尝试锁库存,若锁库存成功则退出遍历
- 完整的锁库存的代码在gulimall-ware 的 WareSkuServiceImpl中
锁库存的语句sql
<update id="lockSkuStock">
UPDATE `wms_ware_sku` SET stock_locked = stock_locked + #{num}
WHERE sku_id = #{skuId}
AND ware_id = #{wareId}
AND stock-stock_locked >= #{num}
</update>
- for遍历sku
- for遍历仓库
- wareSkuDao.lockSkuStock(skuId, wareId, hasStock.getNum());
- cas所库存成功后发送延迟队列
- for遍历仓库
@Transactional // 事务
@Override
public Boolean orderLockStock(List<OrderItemVo> itemVos) {
List<SkuLockVo> lockVos = itemVos.stream().map((item) -> {
SkuLockVo skuLockVo = new SkuLockVo();
skuLockVo.setSkuId(item.getSkuId());
skuLockVo.setNum(item.getCount());
//找出所有库存大于商品数的仓库(因为有多个仓库)// 这个地方问题很大,后面得改
List<Long> wareIds = baseMapper.listWareIdsHasStock(item.getSkuId(), item.getCount());
skuLockVo.setWareIds(wareIds);
return skuLockVo;
}).collect(Collectors.toList());
for (SkuLockVo lockVo : lockVos) {
boolean lock = true;
Long skuId = lockVo.getSkuId();
List<Long> wareIds = lockVo.getWareIds();
//如果没有满足条件的仓库,抛出异常
if (wareIds == null || wareIds.size() == 0) {
throw new NoStockException(skuId);
}else {
// 遍历仓库
for (Long wareId : wareIds) {
// 锁库存,更新sql用到了cas,如果返回非0代表更新对了
Long count=baseMapper.lockWareSku(skuId, lockVo.getNum(), wareId);
if (count==0){
lock=false;
}else {
lock = true;
break;
}
}
}
if (!lock) throw new NoStockException(skuId);
}
return true;
}
这里通过异常机制控制事务回滚,如果在锁定库存失败则抛出NoStockException
s,订单服务和库存服务都会回滚。
后面有消息队列后,会进行优化
优化逻辑为:锁库存后,把内容发到消息队列里
消息队列并不立刻消费,而是让其过期,过期后重新入队别的消息队列,别的消息队列拿到后验证订单是否被支付,没被支付的话还原到库存里。
完整的提交订单接口
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
//线程共享数据
confirmVoThreadLocal.set(vo);
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
//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 = vo.getOrderToken();
//原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
if (result == 0L) {
//令牌验证失败
return response;
}else {
//令牌验证成功
//1、创建订单,订单项等信息
OrderCreateTo order = createOrder();
//2、验价
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//金额对比
//TODO 3、保存订单
saveOrder(order);
//4、库存锁定。只要有异常回滚订单数据。
// 订单号,所有订单项(skuId,skuName,num)
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
List<OrderItemVo> locks = order.getOrderItems().stream().map(item -> {
OrderItemVo itemVo = new OrderItemVo();
itemVo.setSkuId(item.getSkuId());
itemVo.setCount(item.getSkuQuantity());
itemVo.setTitle(item.getSkuName());
return itemVo;
}).collect(Collectors.toList());
lockVo.setLocks(locks);
//4、远程锁库存
//库存成功了,但是网络原因超时了,订单回滚,库存不滚。
//为了保证高并发。库存服务自己回滚。可以发消息给库存服务;
//库存服务本身也可以使用自动解锁模式 消息
R r = wmsFeignService.orderLockStock(lockVo);
if (r.getCode() == 0) {
//锁成功了
response.setOrder(order.getOrder());
//TODO 5、远程扣减积分 出异常
// int i = 10/0; //订单回滚,库存不滚
//订单创建成功发送消息给MQ
rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", order.getOrder());
//TODO 6、清除购物车已经下单的商品
return response;
} else {
//锁定失败
String msg = (String) r.get("msg");
throw new NoStockException(msg);
}
} else {
response.setCode(2);
return response;
}
}
}
282、商城业务-订单服务-提交订单的问题
以上代码已经解决
283、商城业务-分布式事务-本地事务在分布式下的问题
1、分布式事务的产生
订单服务下订单---------\
库存服务锁库存---------->分布式事务
用户服务扣减积分-------/
//本地事务,在分布式系统,只能控制住自己的回滚,控制不了其他服务的回滚
//分布式事务: 最大原因。网络问题+分布式机器。
//(isolation = Isolation.REPEATABLE_READ)
//REQUIRED、REQUIRES_NEW
// @GlobalTransactional //高并发
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
confirmVoThreadLocal.set(vo);
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
response.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 = vo.getOrderToken();
//原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
if (result == 0L) {
//令牌验证失败
response.setCode(1);
return response;
} else {
//令牌验证成功
//下单:去创建订单,验令牌,验价格,锁库存...
//1、创建订单,订单项等信息
OrderCreateTo order = createOrder();
//2、验价
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//金额对比
//....
//TODO 3、保存订单
saveOrder(order);
//4、库存锁定。只要有异常回滚订单数据。
// 订单号,所有订单项(skuId,skuName,num)
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
List<OrderItemVo> locks = order.getOrderItems().stream().map(item -> {
OrderItemVo itemVo = new OrderItemVo();
itemVo.setSkuId(item.getSkuId());
itemVo.setCount(item.getSkuQuantity());
itemVo.setTitle(item.getSkuName());
return itemVo;
}).collect(Collectors.toList());
lockVo.setLocks(locks);
//4、远程锁库存
//库存成功了,但是网络原因超时了,订单回滚,库存不滚。
//为了保证高并发。库存服务自己回滚。可以发消息给库存服务;
//库存服务本身也可以使用自动解锁模式 消息
R r = wmsFeignService.orderLockStock(lockVo);
if (r.getCode() == 0) {
//锁成功了
response.setOrder(order.getOrder());
//TODO 5、远程扣减积分 出异常
// int i = 10/0; //订单回滚,库存不滚
//订单创建成功发送消息给MQ
rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", order.getOrder());
//TODO 6、清除购物车已经下单的商品
return response;
} else {
//锁定失败
String msg = (String) r.get("msg");
throw new NoStockException(msg);
}
} else {
response.setCode(2);
return response;
}
}
}
分布式事务的场景总结:如本案列的代码所示,在本地事务中(@Transactional 就是本地事务),如果该标注的方法中抛出了异常全部执行回回滚,比如如上几个步骤,1、创建订单--》2、验价--》3、保存订单--》4、锁定库存(是远程调用仓库服务服务来锁定库存的)--》5、远程扣减积分。。。。, 如果在在当前方法中发生的异常如(int i = 10/0; //订单回滚,库存不滚)此处刚好是在之前调用了远程锁库存服务,所以远程的库存已经锁定成功,无法回滚,但是之前的保存订单的操作回回滚。就导致了,库存锁了,订单回滚了却没有生成,所以需要分布式事务来解决,这个问题。
# 分布式事务的场景总结:
如本案列的代码所示,在本地事务中(@Transactional 就是本地事务),如果该标注的方法中抛出了异常全部执行回回滚,比如如上几个步骤,1、创建订单--》2、验价--》3、保存订单--》4、锁定库存(是远程调用仓库服务服务来锁定库存的)--》5、远程扣减积分。。。。, 如果在在当前方法中发生的异常如(int i = 10/0; //订单回滚,库存不滚)此处刚好是在之前调用了远程锁库存服务,所以远程的库存已经锁定成功,无法回滚,但是之前的保存订单的操作回回滚。就导致了,库存锁了,订单回滚了却没有生成,所以需要分布式事务来解决,这个问题。
事务保证:
1、订单服务异常,库存锁定不运行,全部回滚, 撤销操作
2、库存服务事务自治,锁定失败全部回滚,订单感受到,继续回滚
3、库存服务锁定成功了,但是网络原因返回数据途中问题?
4、库存服务锁定成功了,库存服务下面的逻辑发生故障,订单回滚了,怎么处理?
利用消息队列实现最终一致 库存服务锁定成功后发给消息队列消息(当前库 存工作单),过段时间自动解锁,解锁时先查询 订单的支付状态。解锁成功修改库存工作单详情项状态为已解锁
1、远程服务假失败: 远程服务其实成功了,由于网络故障等没有返回导致:订单回滚,库存却扣减
2、远程服务执行完成,下面的其他方法出现问题导致:已执行的远程请求,肯定不能回滚
284、商城业务-分布式事务-本地事务隔离级别&传播行为等复习
一、本地事务
1、事务的基本性质
数据库事务的几个特性:原子性(Atomicity )、一致性( Consistency )、隔离性或独立性( Isolation) 和持久性(Durabilily),简称就是 ACID;
l 原子性:一系列的操作整体不可拆分,要么同时成功,要么同时失败
l 一致性:数据在事务的前后,业务整体一致。
n 转账。A:1000;B:1000; 转 200 事务成功; A:800 B:1200
l 隔离性:事务之间互相隔离。
l 持久性:一旦事务成功,数据一定会落盘在数据库。
在以往的单体应用中,我们多个业务操作使用同一条连接操作不同的数据表,一旦有异常,我们可以很容易的整体回滚;
Business:我们具体的业务代码
Storage:库存业务代码;扣库存 Order:订单业务代码;保存订单
Account:账号业务代码;减账户余额
比如买东西业务,扣库存,下订单,账户扣款,是一个整体;必须同时成功或者失败
一个事务开始,代表以下的所有操作都在同一个连接里面;
2、事务的隔离级别
l READ UNCOMMITTED(读未提交)
该隔离级别的事务会读到其它未提交事务的数据,此现象也称之为脏读。
l READ COMMITTED(读提交)
一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重复读问题,Oracle 和 SQL Server 的默认隔离级别。
l REPEATABLE READ(可重复读)
该隔离级别是 MySQL 默认的隔离级别,在同一个事务里,select 的结果是事务开始时时间点的状态,因此,同样的 select 操作读到的结果会是一致的,但是,会有幻读现象。MySQL 的 InnoDB 引擎可以通过 next-key locks 机制(参考下文"行锁的算法"一节)来避免幻读。
l SERIALIZABLE(序列化)
在该隔离级别下事务都是串行顺序执行的,MySQL 数据库的 InnoDB 引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。
3、事务的传播行为
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 类似的操作。
4、SpringBoot 事务关键点
1、事务的自动配置
TransactionAutoConfiguration
2、事务的坑在同一个类里面,编写两个方法,内部调用的时候,会导致事务设置失效。原因是没有用到代理对象的缘故。
解决:
0)、导入 spring-boot-starter-aop
1)、@EnableTransactionManagement(proxyTargetClass = true)
2)、@EnableAspectJAutoProxy(exposeProxy=true)
3)、AopContext.currentProxy() 调用方法
1、本地事务设置事务的隔离级别
@Transactional(isolation = Isolation.REPEATABLE_READ) //设置事务的隔离级别
2、本地事务设置事务的传播行为
@Transactional(propagation = Propagation.REQUIRED, timeout = 2) //设置事务的传播行为
public class AServiceImpl implement AService{
//同一个对象内事务方法互调默认失效,原因 绕过了代理对象
//事务使用代理对象来控制的
@Transactional(timeout = 30) //a事务的所有设置就传播到了和他公用一个事务的方法
//timeout超时时间,如果在该时间内没有完成 会回滚。
public void a() {
b()
c()
int i = 10 / 0; //发生异常时候,b()和c() 都回滚,哪怕下面的c方法是新建一个事务,因为同一个对象内事务方法互调默认失效,原因 绕过了代理对象
}
@Transactional(propagation = Propagation.REQUIRED, timeout = 2)//需要一个事务,被a方法调用,有事务就加入,
public void b() {
}
@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 20)//不管怎么样都会新建一个事务
public void c() {
}
}
解决同一个serviceImpl中也能调用其他方法实现事务控制
1、导入依赖 引入aop-starter;spring-boot-starter-aop;引入了aspectj
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、在启动类上加上 @EnableAspectJAutoProxy(exposeProxy = true);开启 aspectj 动态代理功能。以后所有的动态代理都是aspectj创建的(即使没有接口也可以创建动态代理)。因为默认是jdk代理的(jdk代理必须要有接口才能代理)。
exposeProxy = true表示对外暴露代理对象
本地事务失效问题
* 同一个对象内事务方法互调默认失效,原因 绕过了代理对象,事务使用代理对象来控制的
* 解决:使用代理对象来调用事务方法
* 1)、引入aop-starter;spring-boot-starter-aop;引入了aspectj
* 2)、@EnableAspectJAutoProxy(exposeProxy = true);开启 aspectj 动态代理功能。以后所有的动态代理都是aspectj创建的(即使没有接口也可以创建动态代理)。
* 对外暴露代理对象
* 3)、本类互调用调用对象
* OrderServiceImpl orderService = (OrderServiceImpl) AopContext.currentProxy();
* orderService.b();
* orderService.c();
3、案列调用,这样就能保证,在同一个serviceImpl中调用本类的多个方法,并且用到事务(a和b同一个事务,c是自己的事务),
@Service("orderService")
public class OrderServiceImpl extends ServiceImpl<OrderDao, OrderEntity> implements OrderService {
@Autowired
OrderService orderService;
//同一个对象内事务方法互调默认失效,原因 绕过了代理对象
//事务使用代理对象来控制的
@Transactional(timeout = 30) //a事务的所有设置就传播到了和他公用一个事务的方法
public void a() {
OrderServiceImpl orderService = (OrderServiceImpl) AopContext.currentProxy();
orderService.b();
orderService.c();
int i = 10 / 0;
}
@Transactional(propagation = Propagation.REQUIRED, timeout = 2)
public void b() {
}
@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 20)
public void c() {
}
第二中情况,不是同一个service的方法调用。
public class BServiceImpl implement BService{
@Transactional(propagation = Propagation.REQUIRED, timeout = 2)//需要一个事务,被a方法调用,有事务就加入,
public void b() {
}
}
public class CServiceImpl implement cService{
@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 20)//不管怎么样都会新建一个事务
public void c() {
}
}
public class AServiceImpl implement AService{
@Resouce
private BService bService;
@Resouce
private CService cService;
//同一个对象内事务方法互调默认失效,原因 绕过了代理对象
//事务使用代理对象来控制的
@Transactional(timeout = 30) //a事务的所有设置就传播到了和他公用一个事务的方法
public void a() {
bService.b(); //和a方法共一个事务
cService.c(); //新事务(不回滚)
int i = 10 / 0;//发生异常时,a()方法回滚,b()方法也会滚,因为b和a是同一个事务,c()方法不会回滚,因为c是新建的另一个事务,不是同一个。
}
@Transactional(propagation = Propagation.REQUIRED, timeout = 2)
public void b() {
//7s
}
@Transactional(propagation = Propagation.REQUIRES_NEW, timeout = 20)
public void c() {
}
}
二、事务保证:
1、订单服务异常,库存锁定不运行,全部回滚,撤销操作
2、库存服务事务自治,锁定失败全部回滚,订单感受到,继续回滚
3、库存服务锁定成功了,但是网络原因返回数据途中问题?
4、库存服务锁定成功了,库存服务下面的逻辑发生故障,订单回滚了,怎么处理?
利用消息队列实现最终一致
库存服务锁定成功后发给消息队列消息(当前库存工作单),过段时间自动解锁,解锁时先查询订单的支付状态。解锁成功修改库存工作单详情项状态为已解锁
- 1、远程服务假失败:远程服务其实成功了,由于网络故障等没有返回导致:订单回滚,库存却扣减
- 2、远程服务执行完成,下面的其他方法出现问题导致:已执行的远程请求,肯定不能回滚
事务传播问题中,传播后事务设置还是原来的,如果不想用原来设置,必须new事务
注意同类中调用的话,被调用事务会失效,原因在于aop。事务基于代理,同对象的方法动态代理都是同一个。解决方案是使用代理对象调用。引用aop-starter后,使用aspectJ,开启AspectJ动态代理
,原来默认使用的是jdk动态代理。
使用@EnableAspectJAutoProxy(exposeProxy=true)
后,就取代了jdk动态代理
。它没有接口也可以创建动态代理。设置true是为了对外暴露代理对象。
AopContext.currentProxy()
然后强转,就是当前代理对象。
AopContext.currentProxy()
然后强转,就是当前代理对象。
public interface AService {
public void a();
public void b();
}
@Service()
public class AServiceImpl1 implements AService{
@Transactional(propagation = Propagation.REQUIRED)
public void a() {
this.b();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void b() {
}
}
此处的this指向目标对象,因此调用this.b()将不会执行b事务切面,即不会执行事务增强,
因此b方法的事务定义“@Transactional(propagation = Propagation.REQUIRES_NEW)”将不会实施,
即结果是b和a方法的事务定义是一样的(我们可以看到事务切面只对a方法进行了事务增强,没有对b方法进行增强)
Q1:b中的事务会不会生效?
A1:不会,a的事务会生效,b中不会有事务,因为a中调用b属于内部调用,没有通过代理,所以不会有事务产生。
Q2:如果想要b中有事务存在,要如何做?
A2:<aop:aspectj-autoproxy expose-proxy=“true”> ,设置expose-proxy属性为true,将代理暴露出来,使用AopContext.currentProxy()获取当前代理,将this.b()改为((UserService)AopContext.currentProxy()).b()
解决方案:
public void a() {
((AService) AopContext.currentProxy()).b();//即调用AOP代理对象的b方法即可执行事务切面进行事务增强
}
285、商城业务-分布式事务-分布式CAP&Raft原理
1 、为什么有分布式事务
分布式系统经常出现的异常
机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的 TCP、存储数据丢失…
分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎可以说是无法避免。
2 、 CAP 定理与 BASE 理论
1 、 CAP 定理
CAP 原则又称 CAP 定理,指的是在一个分布式系统中
-
一致性(Consistency):
-
在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
-
可用性(Availability)
-
在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
-
分区容错性(Partition tolerance)
-
大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。 分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。
CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。
- CP要求一致性(有一个没同步好就不可用)
- AP要求高可用
选举与同步理论
分布式一致性动画演示:http://thesecretlivesofdata.com/raft/
raft是一个实现分布式一致性的协议
结点的状态:
- follower
- candidate
- leader
选举leader:
- 默认都以follower状态启动,follower监听不到leader,就称为一个candidate
- 投票给自己,然后告诉其他人,同时也收到别人的投票信息。根据投票信息和投票信息里带的信息(如那个节点里的数据)
- 收到投票后,改选一个自己觉得最靠谱的。某一节点收到票数超过一半就变成leader
raft有两个超时时间控制领导选举:
- 选举超时:从follower到candidate的时间,150ms-300ms(自旋时间),这个时间段内没收到leader的心跳就变为候选者。
- 自旋时间结束后变成candidate,开始一轮新的选举(老师上课举的例子是)
- 投出去票后重新计时自旋
- leader就发送追加日志给follower,follower就正常
- 消息发送的,心跳时间:如10ms,leader收到投票后,下一次心跳时就带上消息,follower收到消息后重置选举时间
- leader宕机,follower收不到心跳,开始新的选举
写数据:
- 接下来所有的数据都要先给leader,leader派发给follower
- 比如领导收到信息5后,领导先在leader的log中写入变化set 5。(上面的动态红颜色代表没提交),此时5还没提交,而是改了leader的log后,
- leader下一次心跳时,顺便带着信息让follower也去改变follower的log,follower写入日志成功后,发送确认ack 5给leader,
- leader收到大多数的ack后,leader就自己正式写入数据,然后告诉follower提交写入硬盘/内存吧(这个过程和响应客户端是同时的)。这个过程叫做日志复制(也有过半机制)
- 然后leader响应说集群写入好了
如果有的结点消息滞后了:
5台机器因为局域网隔离又分为3、2生成两个leader怎么办:
对于1,2结点那个leader,更新log后收不到大多数的ack(得超过1个ack),所以改log不成功,一直保存不成功
对于345结点的leader:收到消息后更新log并且收到ack过半且超过1个,成功保存。
此时网络又通了,以更高轮选举的leader为主,退位一个leader。那1,2结点日志都回滚,同步新leader的log。这样就都一致性了
另外注意:集群一般都是单数,因为有过半机制。比如原来集群6个机器,分为2半后,各3个,选leader时谁都拿不到6/2+1=4个投票,所以都没有leader
更多动画(可以自己选择宕机情况)raft.github.io
但一般都是保证AP,舍弃C
2 、面临的问题
对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到 99.99999%(N 个 9),即保证 P 和 A,舍弃 C。
286、商城业务-分布式事务-BASE
后续发现扣减不一致后,再恢复
BASE理论是对CAP理论的延伸,思想是即使无法做到强一致性(CAP的一致性就是强一致性),但可以采用弱一致
性,即最终一致性
BASE是指:
- 基本可用(BasicallyAvailable)
基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等价于系统不可用。- 响应时间上的损失:正常情况下搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒。
- 功能上的损失:购物网站在购物高峰(如双十一)时,为了保护系统的稳定性,部分消费者可能会被引导到一个降级页面。
- 软状态(soft state)
- 软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据会有多个副本,允许不同副本同步的延时就是软状态的体现。mysql replication的异步复制也是一种体现。
- 最终一致性(Eventual Consistency)
- 最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。(这也是分布式事务的想法)
从客户端角度,多进程并发访同时,更新过的数据在不同程如何获的不同策珞,决定了不同的一致性。
- 对于关系型要求更新过据能后续的访同都能看到,这是强一致性。
- 如果能容忍后经部分过者全部访问不到,则是弱一致性
- 如果经过一段时间后要求能访问到更新后的数据,则是最终一致性
287、商城业务-分布式事务-分布式事务常见解决方案
分布式事务几种方案
1) 2PC模式(XA事务)
数据库支持的2pc
【2二阶段提交】,又叫做XA Transactions
支持情况:mysql从5.5版本开始支持,SQLserver2005开始支持,Oracle7开始支持。
其中,XA是一个两阶段提交协议,该协议分为以下两个阶段:
- 第一阶段:事务协调器要求每个涉及到事务的数据库预提交(P090此操作,并反映是否可以提交,
- 第二阶段:事务协调器要求每个数据库提交数据。
- 其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息
如图所示,如果有订单服务和库存服务要求分布式事务,要求有一个总的事务管理器
总的事务管理让事务分为两个阶段,
- 第一个阶段是预备(log)。
- 第二个阶段是正式提交(commit)
总事务管理器接收到两个服务都预备好了log(收到ack),就告诉他们commit
如果有一个没准备好,就回滚所有人。
总结2PC:
- XA协议比较简单,而且一旦商业数据库实现了XA协议,使用分布式事务的成本也比较低。
- 性能不理想,特别是在交易下单链路,往往并发量很高,XA无法满足高并发场景
- XA目前在商业数据库支持的比较理想,在mysql数据库中支持的不太理想,mysql的XA实现,没有记录阶段日志,主备切换回导致主库与备库数据不一致。
- 许多nosql没有支持XA,这让XA的应用场景变得非常狭隘。
- 也有3PC,引入了超时机制(无论协调者还是参与者,在向对方发送请求后,若长时间未收到回应则做出相应处理)
2) 柔性事务-TCC事务补偿型方案
- 刚性事务:遵循ACID原则,强一致性。
- 柔性事务:遵循BASE理论,最终一致性;
与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。
- 一阶段
prepare
行为:调用自定义的prepare逻辑。 - 二阶段
commit
行为:调用自定义的commit逻憬。 - 二阶段
rollback
行为:调用自定义的rollback逻辑。
所TCC模式,是指支持 自定义的 分支事务纳入到全局事务的管理中。
![img](https://i-blog.csdnimg.cn/blog_migrate/1aa4bba359ff28852e6f61fc9a2f11f8.webp?x-image-process=image/format,png)
3)柔性事务-最大努力通知型方案
按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接囗进行核对。这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合MQ进行实现,例如:通过MQ发送就请求,设置最大通知次数。达到通知次数后即不再通知。
案例:银行涌知、商户通知等(各大交易业务平台间的商户涌知:多次通知、查询校对、对账文件),支付宝的支付成功异步回调
大业务调用订单,库存,积分。最后积分失败,则一遍遍通知订单和库存扣减他们回滚会发送mq 消息,
让子业务监听消息队列
如果收不到就重新发
4)柔性事务=可靠消息+最终一致性方案(异步确保型)
实现:业务处理服务在业务事务提交之前,向实时消息服务请求发送捎息,实时捎息服务只记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。
实现方案就是: 大业务处理,保存订单—》扣减库存----》用户积分,只要有一个节点处理异常都向mq发送失败的消息,后期通过mq的消息消费来回滚其他的节点处理的数据,这就是最终一致性方案。
288、商城务-分布式事务-Seata&环境准备
seata解决分布式事务问题
体验2pc两阶段提交。继续正例前面订单出错的逻辑
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
快速开始:http://seata.io/zh-cn/docs/user/quickstart.html
TC (Transaction Coordinator) -
事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) -
事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) -
资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
![img](https://i-blog.csdnimg.cn/blog_migrate/7b83b011de7809f4569dd238fb2342ed.png)
要执行下单,
- TM告诉TC开启一个全局事务。
- storage注册分支事务,实时向TC汇报分支状态
- account失败,告诉TC失败了,TC回滚全部全局事务。
我们只需要使用一个 @GlobalTransactional
注解在业务方法上: 就是哪一个是主要业务方法(这个方法会远程调用其他的服务),
@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {
......
}
我们有业务步骤,但是SEATA AT
模式需要 UNDO_LOG
表,记录之前执行的操作。每个涉及的子系统对应的数据库都要新建表
就是每一个吧远程服务的自己的数据库中都要新加以下的回滚日志表
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
带上版本号
开始应用
-
从 https://github.com/seata/seata/archive/v0.7.1.zip 下载服务器软件包senta-server-0.7.1,将其解压缩。作为TC
就是下载一个senta 的服务器 TC.让他来协调。
-
为了节省git资源,我们下载源码的项目自己编译。
-
编译项目:
-
下载后复制到guli项目下,然后在project structure–module中点击+号import module,选择项目里的seata
-
会有报错,protobuf这个包找不到。在idea中安装
proto buffer editor
插件,重启idea(还找不到就重新编译一下,在mvn中找到seata-serializer子项目,点击protobuf里的compile选项。有个grpc的test报错,先全注释掉) -
有一个server项目,找到注册中心配置
resource/registry.conf
,修改启动的nacos信息。可以修改注册中心和配置中心(先不用管file.conf
)registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa # 修改这个 type = "nacos" nacos { # 修改这个 serverAddr = "localhost:8848" namespace = "public" cluster = "default" }
-
启动server下的主类
-
在nacos中看到一个serverAddr服务
-
@GlobalTransactional
在大事务的入口标记注解@GlobalTransactional
开启全局事务,并且每个小事务标记注解@Transactional
@GlobalTransactional
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo submitVo) {
}
怎么用:https://github.com/seata/seata-samples/tree/master/springcloud-jpa-seata
注意
- 注入 DataSourceProxy
因为 Seata 通过代理数据源实现分支事务,如果没有注入,事务无法成功回滚
@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DruidDataSource druidDataSource() { return new DruidDataSource(); } /** * 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚 * * @param druidDataSource The DruidDataSource */ @Primary @Bean("dataSource") public DataSource dataSource(DruidDataSource druidDataSource) { return new DataSourceProxy(druidDataSource); } }
- file.conf 的 service.vgroup_mapping 配置必须和
spring.application.name
一致在
GlobalTransactionAutoConfiguration
类中,默认会使用${spring.application.name}-fescar-service-group
作为服务名注册到 Seata Server上(每个小事务也要注册到tc上),如果和file.conf
中的配置不一致,会提示no available server to connect
错误也可以通过配置yaml的
spring.cloud.alibaba.seata.tx-service-group
修改后缀,但是必须和file.conf
中的配置保持一致
与上面配置数据源的方式等价,这么配置
@Configuration
public class MySeataConfig {
@Autowired
DataSourceProperties dataSourceProperties;
@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);
}
}
在order、ware中都配置好上面的配置
然后它还要求每个微服务要有register.conf
和file.conf
将register.conf
和file.conf
复制到需要开启分布式事务的根目录,并修改file.conf
vgroup_mapping.${application.name}-fescar-service-group = "default"
service {
#vgroup->rgroup
vgroup_mapping.gulimall-ware-fescar-service-group = "default"
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
}
在大事务上@GlobalTransactional
,小事务上@Transactional
即可
289、商城业务-分布式事务-Seata分布式事务体验
谷粒商城运用senta案例
1、使用流程概述
Seata控制分布式事务
* 1)、每一个微服务先必须创建 undo_log;
* 2)、安装事务协调器;seata-server: https://github.com/seata/seata/releases
* 3)、整合
* 1、导入依赖 spring-cloud-starter-alibaba-seata seata-all-0.7.1
* 2、解压并启动seata-server;
* registry.conf: 注册中心配置; 修改registry type=nacos
* file.conf:
* 3、所有想要用到分布式事务的微服务使用seata DataSourceProxy代理自己的数据源
* 4、每个微服务,都必须导入
* registry.conf
* file.conf vgroup_mapping.{application.name}-fescar-service-group = "default"
* 5、启动测试分布式事务
* 6、给分布式大事务的入口标注@GlobalTransactional
* 7、每一个远程的小事务用 @Transactional
2、每一个微服务先必须创建 undo_log;
就是在每一个微服务的所在数据库中创建undo_log,用于记录操作日志,方便回滚。
3、安装事务协调器;
seata-server: https://github.com/seata/seata/releases 这个网址下载,资料中也有,就是一个seata-server服务器
就是一个TC
4、整合导入依赖
整合导入依赖 spring-cloud-starter-alibaba-seata seata-all-0.7.1 在gulimall-common模块导入
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
5、解压并启动seata-server
解压并启动seata-server,registry.conf: 注册中心配置; 修改registry type=nacos, file.conf:
6、 DataSourceProxy代理
所有想要用到分布式事务的微服务使用seata DataSourceProxy代理自己的数据源,每个微服务加入以下配置
package com.atguigu.gulimall.order.config;
import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
@Configuration
public class MySeataConfig {
@Autowired
DataSourceProperties dataSourceProperties;
@Bean
public DataSource dataSource(DataSourceProperties dataSourceProperties){
//properties.initializeDataSourceBuilder().type(type).build();
HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
if (StringUtils.hasText(dataSourceProperties.getName())) {
dataSource.setPoolName(dataSourceProperties.getName());
}
return new DataSourceProxy(dataSource);
}
}
7、每个微服务,都必须导入
-
registry.conf
-
file.conf vgroup_mapping.{application.name}-fescar-service-group = "default"
8、启动测试分布式事务
9、给分布式大事务的入口标注@GlobalTransactional
@GlobalTransactional
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
confirmVoThreadLocal.set(vo);
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
response.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 = vo.getOrderToken();
//原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
if (result == 0L) {
//令牌验证失败
response.setCode(1);
return response;
} else {
//令牌验证成功
//下单:去创建订单,验令牌,验价格,锁库存...
//1、创建订单,订单项等信息
OrderCreateTo order = createOrder();
//2、验价
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//金额对比
//....
//TODO 3、保存订单
saveOrder(order);
//4、库存锁定。只要有异常回滚订单数据。
// 订单号,所有订单项(skuId,skuName,num)
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
List<OrderItemVo> locks = order.getOrderItems().stream().map(item -> {
OrderItemVo itemVo = new OrderItemVo();
itemVo.setSkuId(item.getSkuId());
itemVo.setCount(item.getSkuQuantity());
itemVo.setTitle(item.getSkuName());
return itemVo;
}).collect(Collectors.toList());
lockVo.setLocks(locks);
//4、远程锁库存
//库存成功了,但是网络原因超时了,订单回滚,库存不滚。
//为了保证高并发。库存服务自己回滚。可以发消息给库存服务;
//库存服务本身也可以使用自动解锁模式 消息
R r = wmsFeignService.orderLockStock(lockVo);
if (r.getCode() == 0) {
//锁成功了
response.setOrder(order.getOrder());
//TODO 5、远程扣减积分 出异常
// int i = 10/0; //订单回滚,库存不滚
//TODO 6、清除购物车已经下单的商品
return response;
} else {
//锁定失败
String msg = (String) r.get("msg");
throw new NoStockException(msg);
}
} else {
response.setCode(2);
return response;
}
}
}
10、每一个远程的小事务用 @Transactional
以上的代码中调用了远程的服务R r = wmsFeignService.orderLockStock(lockVo);
是在gulimall-ware 服务的 WareSkuServiceImpl中。
@Transactional
@Override
public Boolean orderLockStock(WareSkuLockVo vo) {
/**
* 保存库存工作单的详情。
* 追溯。
*/
WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
taskEntity.setOrderSn(vo.getOrderSn());
orderTaskService.save(taskEntity);
//1、按照下单的收货地址,找到一个就近仓库,锁定库存。
//1、找到每个商品在哪个仓库都有库存
List<OrderItemVo> locks = vo.getLocks();
List<SkuWareHasStock> collect = locks.stream().map(item -> {
SkuWareHasStock stock = new SkuWareHasStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
stock.setNum(item.getCount());
//查询这个商品在哪里有库存
List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
stock.setWareId(wareIds);
return stock;
}).collect(Collectors.toList());
//2、锁定库存
for (SkuWareHasStock hasStock : collect) {
Boolean skuStocked = false;
Long skuId = hasStock.getSkuId();
List<Long> wareIds = hasStock.getWareId();
if (wareIds == null || wareIds.size() == 0) {
//没有任何仓库有这个商品的库存
throw new NoStockException(skuId);
}
//1、如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
//2、锁定失败。前面保存的工作单信息就回滚了。发送出去的消息,即使要解锁记录,由于去数据库查不到id,所以就不用解锁
// 1: 1 - 2 - 1 2:2-1-2 3:3-1-1(x)
for (Long wareId : wareIds) {
//成功就返回1,否则就是0
Long count = wareSkuDao.lockSkuStock(skuId, wareId, hasStock.getNum());
if (count == 1) {
skuStocked = true;
//TODO 告诉MQ库存锁定成功
WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity(null, skuId, "", hasStock.getNum(), taskEntity.getId(), wareId, 1);
orderTaskDetailService.save(entity);
StockLockedTo lockedTo = new StockLockedTo();
lockedTo.setId(taskEntity.getId());
StockDetailTo stockDetailTo = new StockDetailTo();
BeanUtils.copyProperties(entity, stockDetailTo);
//只发id不行,防止回滚以后找不到数据
lockedTo.setDetail(stockDetailTo);
// rabbitTemplate
rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", lockedTo);
break;
} else {
//当前仓库锁失败,重试下一个仓库
}
}
if (skuStocked == false) {
//当前商品所有仓库都没有锁住
throw new NoStockException(skuId);
}
}
tcc也可以看samples。
但是上面使用的是AT模式,AT模式也是基于数据库的反向补偿,2pc不适用高并发。
发生了几次远程调用。去保存spu,适合使用at模式。
290、商城业务-分布式事务-最终一致性库存解锁逻辑
高并发,如下单,at模式有很多锁,影响效率。所以不使用at tcc。使用消息方式
失败了之后发消息。库存服务本身也可以使用自动解锁模式。消息队列。
自动解锁:库存服务订阅消息队列,库存解锁发给消息队列
保存库存工作单和库存工作单详情,
锁定库存后数据库记录。后面的事务失败后看前面的库存,有没解锁的就解锁。
定期全部检索很麻烦,索引引入延迟队列。
锁库存后害怕订单失败,锁库存后发送给消息队列,只不过要暂存一会先别被消费。半小时以后再消费就可以知道大事务成功没有。
1、订单业务代码
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
//线程共享数据
confirmVoThreadLocal.set(vo);
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
//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 = vo.getOrderToken();
//原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
if (result == 0L) {
//令牌验证失败
return response;
}else {
//令牌验证成功
//1、创建订单,订单项等信息
OrderCreateTo order = createOrder();
//2、验价
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//金额对比
//TODO 3、保存订单
saveOrder(order);
//4、库存锁定。只要有异常回滚订单数据。
// 订单号,所有订单项(skuId,skuName,num)
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
List<OrderItemVo> locks = order.getOrderItems().stream().map(item -> {
OrderItemVo itemVo = new OrderItemVo();
itemVo.setSkuId(item.getSkuId());
itemVo.setCount(item.getSkuQuantity());
itemVo.setTitle(item.getSkuName());
return itemVo;
}).collect(Collectors.toList());
lockVo.setLocks(locks);
//4、远程锁库存
//库存成功了,但是网络原因超时了,订单回滚,库存不滚。
//为了保证高并发。库存服务自己回滚。可以发消息给库存服务;
//库存服务本身也可以使用自动解锁模式 消息
R r = wmsFeignService.orderLockStock(lockVo);
if (r.getCode() == 0) {
//锁成功了
response.setOrder(order.getOrder());
//TODO 5、远程扣减积分 出异常
// int i = 10/0; //订单回滚,库存不滚
//订单创建成功发送消息给MQ
rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", order.getOrder());
//TODO 6、清除购物车已经下单的商品
return response;
} else {
//锁定失败
String msg = (String) r.get("msg");
throw new NoStockException(msg);
}
} else {
response.setCode(2);
return response;
}
}
}
2、锁库存代码
/**
* 为某个订单锁定库存
* <p>
* (rollbackFor = NoStockException.class)
* 默认只要是运行时异常都会回滚
*
* @param vo 库存解锁的场景
* 1)、下订单成功,订单过期没有支付被系统自动取消、被用户手动取消。都要解锁库存
* <p>
* <p>
* 2)、下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。
* 之前锁定的库存就要自动解锁。
*
* @return
*/
@Transactional
@Override
public Boolean orderLockStock(WareSkuLockVo vo) {
/**
* 保存库存工作单的详情。
* 追溯。
*/
WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
taskEntity.setOrderSn(vo.getOrderSn());
orderTaskService.save(taskEntity);
//1、按照下单的收货地址,找到一个就近仓库,锁定库存。
//1、找到每个商品在哪个仓库都有库存
List<OrderItemVo> locks = vo.getLocks();
List<SkuWareHasStock> collect = locks.stream().map(item -> {
SkuWareHasStock stock = new SkuWareHasStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
stock.setNum(item.getCount());
//查询这个商品在哪里有库存
List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
stock.setWareId(wareIds);
return stock;
}).collect(Collectors.toList());
//2、锁定库存
for (SkuWareHasStock hasStock : collect) {
Boolean skuStocked = false;
Long skuId = hasStock.getSkuId();
List<Long> wareIds = hasStock.getWareId();
if (wareIds == null || wareIds.size() == 0) {
//没有任何仓库有这个商品的库存
throw new NoStockException(skuId);
}
//1、如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
//2、锁定失败。前面保存的工作单信息就回滚了。发送出去的消息,即使要解锁记录,由于去数据库查不到id,所以就不用解锁
// 1: 1 - 2 - 1 2:2-1-2 3:3-1-1(x)
for (Long wareId : wareIds) {
//成功就返回1,否则就是0
Long count = wareSkuDao.lockSkuStock(skuId, wareId, hasStock.getNum());
if (count == 1) {
skuStocked = true;
//TODO 告诉MQ库存锁定成功
WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity(null, skuId, "", hasStock.getNum(), taskEntity.getId(), wareId, 1);
orderTaskDetailService.save(entity);
StockLockedTo lockedTo = new StockLockedTo();
lockedTo.setId(taskEntity.getId());
StockDetailTo stockDetailTo = new StockDetailTo();
BeanUtils.copyProperties(entity, stockDetailTo);
//只发id不行,防止回滚以后找不到数据
lockedTo.setDetail(stockDetailTo);
// rabbitTemplate
rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", lockedTo);
break;
} else {
//当前仓库锁失败,重试下一个仓库
}
}
if (skuStocked == false) {
//当前商品所有仓库都没有锁住
throw new NoStockException(skuId);
}
}
//3、肯定全部都是锁定成功过的
return true;
}
}
291、商城业务-订单服务-RabbitMQ延时队列
延迟队列
场景:比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品
常用解决方案:
- spring的schedule走时任务轮询数据库
- 消耗系统内存、增加了数据库的压力、存在较大时间误差
- 解决:rabbitmq的消息TTL和死信Exchange结合
为什么不能用定时任务完成?
如果恰好在一次扫描后完成业务逻辑,那么就会等待两个扫描周期才能扫到过期的订单,不能保证时效性
订单关了之后40分钟后库存检查订单存在还是取消。
定时任务此外还有超时和检测时间段错开的情况(时效性问题)。最高等2倍的定时任务时间。
下订单延迟队列,
不要设置消息过期,要设置为队列过期方式。
节省一个交换机
使用bean方式创建交换机。
注意a
-
定义:延迟队列存储的对象肯定是对应的延时消息,所谓"延时消息"是指当消息被发送以后,并不想让消费者立即拿到消息,而是等待指定时间后,消费者才拿到这个消息进行消费。
-
实现:rabbitmq可以通过设置队列的
TTL
+死信路由实现延迟队列-
TTL:RabbitMQ可以针对Queue设置x-expires 或者 针对Message设置 x-message-ttl,来控制消息的生存时间,如果超时(两者同时设置以最先到期的时间为准),则消息变为dead letter(死信)
-
死信路由DLX:RabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可选)两个参数,如果队列内出现了dead letter,则按照这两个参数重新路由转发到指定的队列。
- x-dead-letter-exchange:出现dead letter之后将dead letter重新发送到指定exchange
- x-dead-letter-routing-key:出现dead letter之后将dead letter重新按照指定的routing-key发送
-
消息的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)确认消息消费失败
延时队列实现-1
延时队列实现-2
(1生产消息,2创建交换机,3创建队列,4用routing-key绑定交换机和队列,5所以当消息发送给交换价后,交换机会根据绑定的routing-key找到对应的队列)
订单分布式主体逻辑(这也我们采取的模式)
- 订单超时未支付触发订单过期状态修改与库存解锁
创建订单时消息会被发送至队列
order.delay.queue
,经过TTL
的时间后消息会变成死信以order.release.order
的路由键经交换机转发至队列order.release.order.queue
,再通过监听该队列的消息来实现过期订单的处理
- 如果该订单已支付,则无需处理
- 否则说明该订单已过期,修改该订单的状态并通过路由键
order.release.other
发送消息至队列stock.release.stock.queue
进行库存解锁
(图中虽然两次经过了order-evevt-exchange交换机,但是第一次的路由key是 order.creat.order发送到私信队列order.delay.queue中去的,第二次的路由key是order.release.order到消费者队列order.release.order.queue)
- 库存锁定后延迟检查是否需要解锁库存
在库存锁定后通过
路由键stock.locked
发送至延迟队列stock.delay.queue
,延迟时间到,死信通过路由键stock.release
转发至stock.release.stock.queue
,通过监听该队列进行判断当前订单状态,来确定库存是否需要解锁
- 由于
关闭订单
和库存解锁
都有可能被执行多次,因此要保证业务逻辑的幂等性,在执行业务是重新查询当前的状态进行判断 - 订单关闭和库存解锁都会进行库存解锁的操作,来确保业务异常或者订单过期时库存会被可靠解锁
292、商城业务-订单服务-延时队列定时关单模拟
1、流程图
(图中虽然两次经过了order-evevt-exchange交换机,但是第一次的路由key是 order.creat.order发送到私信队列order.delay.queue中去的,第二次的路由key是order.release.order到消费者队列order.release.order.queue)
2、代码实现
SpringBoot中使用延时队列
1、Queue、Exchange、Binding可以@Bean进去 (注意只有队列有监听者,这样第一次启动springboot的时候才会自动创建队列,并绑定交换机)
package com.atguigu.gulimall.order.config;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @创建人: 放生
* @创建时间: 2022/5/6
* @描述:
*/
@Configuration
public class MyMQConfig {
//@Bean Binding,Queue,Exchange
/**
* 容器中的 Binding,Queue,Exchange 都会自动创建(RabbitMQ没有的情况)
* RabbitMQ 只要有。@Bean声明属性发生变化也不会覆盖
* @return
*/
@Bean
public Queue orderDelayQueue() {
Map<String,Object> arguments = new HashMap<>();
/**
* x-dead-letter-exchange: order-event-exchange
* x-dead-letter-routing-key: order.release.order
* x-message-ttl: 60000
*/
arguments.put("x-dead-letter-exchange","order-event-exchange");
arguments.put("x-dead-letter-routing-key","order.release.order");
arguments.put("x-message-ttl",60000);
//String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
Queue queue = new Queue("order.delay.queue", true, false, false,arguments);
return queue;
}
@Bean
public Queue orderReleaseOrderQueue() {
Queue queue = new Queue("order.release.order.queue", true, false, false);
return queue;
}
@Bean
public Exchange orderEventExchange() {
//String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
return new TopicExchange("order-event-exchange",true,false);
}
@Bean
public Binding orderCreateOrderBingding() {
//String destination, DestinationType destinationType, String exchange, String routingKey,
// Map<String, Object> arguments
return new Binding("order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null);
}
@Bean
public Binding orderReleaseOrderBingding() {
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
}
/**
* 订单释放直接和库存释放进行绑定
* @return
*/
@Bean
public Binding orderReleaseOtherBingding() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.other.#",
null);
}
@Bean
public Queue orderSeckillOrderQueue(){
//String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
return new Queue("order.seckill.order.queue",true,false,false);
}
@Bean
public Binding orderSeckillOrderQueueBinding(){
/**
* String destination, DestinationType destinationType, String exchange, String routingKey,
* Map<String, Object> arguments
*/
return new Binding("order.seckill.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.seckill.order",
null);
}
}
3、消费者
@Configuration
public class MyMQConfig {
@RabbitListener(queues = "order.release.order.queue")
public void listener(OrderEntity orderEntity, Channel channel,Message message) throws IOException {
System.out.println("收到了过期的订单"+orderEntity);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
.......
4、生产者
@Controller
public class HelloController {
@Autowired
RabbitTemplate rabbitTemplate;
@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";
}
5、启动测试
293、商城业务-订单服务-创建业务交换机&队列
1、流程图
2、流程概述
编码之前先做一个简单的流程概述,我用消息中间件来实现分布式事务的最终一致性,代码的案列只涉及到订单服务和库存服务之间的案列,我们的流程是:
-
客户下单,防重token校验,通过先删除(对比和删除是一个原子性的)redis中的token,然后在走订单的逻辑
-
根据订单的数据,远程调用库存服务,锁定库存(哪些商品sku,在哪个仓库,锁定数量。。),
- 锁定失败(库存不足),抛出异常,回滚本服务(库存)的操作,订单服务感知异常,订单服务抛出异常,回滚之前创建的订单信息。告知页面库存不足。
- 锁定成功,将锁定的详情发送MQ的延时队列(40分钟)中并且把具体的锁定信息以流水表的形式存入mysql,MQ到时消费(到时后根据订单的状态来解锁库存,因为可能在ware服务锁定库存成功,但是在订单服务中后续操作出了异常,或者30分钟没有支付)总之根据订单的状态来消费数据,是解锁库存,还是反向补偿。
-
订单创建成功,成功后也需要将订单发送MQ延迟队列(30分钟),用于后面的自动关单,订单创建成功了,但是30分钟后没有支付我们就需要自动关单,解锁库存。
-
以上在ware的mq是为了解锁库存的,order的mq是为了自动关单的,ware中的延迟队列时间要大于order中的延迟队列的时间,考虑到特殊的情况下,比如我解锁的队列时间是40分钟,订单的是30分钟,由于订单的服务机器卡顿,延时间等原因,订单服务延迟了11分钟后在发送数据到mq,那么自动关单就是在41分钟之后了,这个时候解锁的消费者已经消费完成了,所以后面的操作还需要在关单的时候再次发送数据到解锁队列。
3、代码实现
以下的编码在ware实现,整个代码的跨度从293节-298节
231引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3.2properties
spring.rabbitmq.host=119.3.105.108
spring.rabbitmq.virtual-host=/
spring.rabbitmq.listener.simple.acknowledge-mode=manual
3.3MyRabbitConfig
package com.atguigu.gulimall.ware.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class MyRabbitConfig {
/**
* 使用JSON序列化机制,进行消息转换
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
//注意只有队列有监听者,这样第一次启动springboot的时候才会自动创建队列,并绑定交换机
// @RabbitListener(queues = "stock.release.stock.queue")
// public void handle(Message message){
//
// }
@Bean
public Exchange stockEventExchange(){
//String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
return new TopicExchange("stock-event-exchange",true,false);
}
@Bean
public Queue stockReleaseStockQueue(){
//String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
return new Queue("stock.release.stock.queue",true,false,false);
}
@Bean
public Queue stockDelayQueue(){
/**
* x-dead-letter-exchange: stock-event-exchange
* x-dead-letter-routing-key: order.release.order
* x-message-ttl: 60000
*/
Map<String,Object> args = new HashMap<>();
args.put("x-dead-letter-exchange","stock-event-exchange");
args.put("x-dead-letter-routing-key","stock.release");
args.put("x-message-ttl",120000);
return new Queue("stock.delay.queue",true,false,false,args);
}
@Bean
public Binding stockReleaseBinding(){
/**
* String destination, DestinationType destinationType, String exchange, String routingKey,
* Map<String, Object> arguments
*/
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.release.#",
null);
}
@Bean
public Binding stockLockedBinding(){
/**
* String destination, DestinationType destinationType, String exchange, String routingKey,
* Map<String, Object> arguments
*/
return new Binding("stock.delay.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.locked",
null);
}
}
294、商城业务-订单服务-监听库存解锁
4、订单服务调用库存服务锁定库存
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
//线程共享数据
confirmVoThreadLocal.set(vo);
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
//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 = vo.getOrderToken();
//原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
if (result == 0L) {
//令牌验证失败
return response;
}else {
//令牌验证成功
//1、创建订单,订单项等信息
OrderCreateTo order = createOrder();
//2、验价
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//金额对比
//TODO 3、保存订单
saveOrder(order);
//4、库存锁定。只要有异常回滚订单数据。
// 订单号,所有订单项(skuId,skuName,num)
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
List<OrderItemVo> locks = order.getOrderItems().stream().map(item -> {
OrderItemVo itemVo = new OrderItemVo();
itemVo.setSkuId(item.getSkuId());
itemVo.setCount(item.getSkuQuantity());
itemVo.setTitle(item.getSkuName());
return itemVo;
}).collect(Collectors.toList());
lockVo.setLocks(locks);
//4、远程锁库存
//库存成功了,但是网络原因超时了,订单回滚,库存不滚。
//为了保证高并发。库存服务自己回滚。可以发消息给库存服务;
//库存服务本身也可以使用自动解锁模式 消息
R r = wmsFeignService.orderLockStock(lockVo); // 订单服务调用库存服务锁定库存
........
5、库存服务锁定库存WareSkuServiceImpl
并发送数据到延迟队列
/**
* 为某个订单锁定库存
* <p>
* (rollbackFor = NoStockException.class)
* 默认只要是运行时异常都会回滚
*
* @param vo 库存解锁的场景
* 1)、下订单成功,订单过期没有支付被系统自动取消、被用户手动取消。都要解锁库存
* <p>
* <p>
* 2)、下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。
* 之前锁定的库存就要自动解锁。
*
* @return
*/
@Transactional
@Override
public Boolean orderLockStock(WareSkuLockVo vo) {
/**
* 保存库存工作单的详情。
* 追溯。
*/
WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
taskEntity.setOrderSn(vo.getOrderSn());
orderTaskService.save(taskEntity);
//1、按照下单的收货地址,找到一个就近仓库,锁定库存。
//1、找到每个商品在哪个仓库都有库存
List<OrderItemVo> locks = vo.getLocks();
List<SkuWareHasStock> collect = locks.stream().map(item -> {
SkuWareHasStock stock = new SkuWareHasStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
stock.setNum(item.getCount());
//查询这个商品在哪里有库存
List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
stock.setWareId(wareIds);
return stock;
}).collect(Collectors.toList());
//2、锁定库存
for (SkuWareHasStock hasStock : collect) {
Boolean skuStocked = false;
Long skuId = hasStock.getSkuId();
List<Long> wareIds = hasStock.getWareId();
if (wareIds == null || wareIds.size() == 0) {
//没有任何仓库有这个商品的库存
throw new NoStockException(skuId);
}
//1、如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给MQ
//2、锁定失败。前面保存的工作单信息就回滚了。发送出去的消息,即使要解锁记录,由于去数据库查不到id,所以就不用解锁
// 1: 1 - 2 - 1 2:2-1-2 3:3-1-1(x)
for (Long wareId : wareIds) {
//成功就返回1,否则就是0
Long count = wareSkuDao.lockSkuStock(skuId, wareId, hasStock.getNum());
if (count == 1) {
skuStocked = true;
//TODO 告诉MQ库存锁定成功
WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity(null, skuId, "", hasStock.getNum(), taskEntity.getId(), wareId, 1);
orderTaskDetailService.save(entity);
StockLockedTo lockedTo = new StockLockedTo();
lockedTo.setId(taskEntity.getId());
StockDetailTo stockDetailTo = new StockDetailTo();
BeanUtils.copyProperties(entity, stockDetailTo);
//只发id不行,防止回滚以后找不到数据
lockedTo.setDetail(stockDetailTo);
// rabbitTemplate
rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", lockedTo);
break;
} else {
//当前仓库锁失败,重试下一个仓库
}
}
if (skuStocked == false) {
//当前商品所有仓库都没有锁住
throw new NoStockException(skuId);
}
}
//3、肯定全部都是锁定成功过的
return true;
}
6、订单服务创建成功发送数据到MQ
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
//线程共享数据
confirmVoThreadLocal.set(vo);
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
//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 = vo.getOrderToken();
//原子验证令牌和删除令牌
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
if (result == 0L) {
//令牌验证失败
return response;
}else {
//令牌验证成功
//1、创建订单,订单项等信息
OrderCreateTo order = createOrder();
//2、验价
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//金额对比
//TODO 3、保存订单
saveOrder(order);
//4、库存锁定。只要有异常回滚订单数据。
// 订单号,所有订单项(skuId,skuName,num)
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
List<OrderItemVo> locks = order.getOrderItems().stream().map(item -> {
OrderItemVo itemVo = new OrderItemVo();
itemVo.setSkuId(item.getSkuId());
itemVo.setCount(item.getSkuQuantity());
itemVo.setTitle(item.getSkuName());
return itemVo;
}).collect(Collectors.toList());
lockVo.setLocks(locks);
//4、远程锁库存
//库存成功了,但是网络原因超时了,订单回滚,库存不滚。
//为了保证高并发。库存服务自己回滚。可以发消息给库存服务;
//库存服务本身也可以使用自动解锁模式 消息
R r = wmsFeignService.orderLockStock(lockVo);
if (r.getCode() == 0) {
//锁成功了
response.setOrder(order.getOrder());
//TODO 5、远程扣减积分 出异常
// int i = 10/0; //订单回滚,库存不滚
//订单创建成功发送消息给MQ
rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", order.getOrder());
//TODO 6、清除购物车已经下单的商品
return response;
} else {
//锁定失败
String msg = (String) r.get("msg");
throw new NoStockException(msg);
}
} else {
response.setCode(2);
return response;
}
}
}
295、商城务-订单服务-库存解锁逻辑
7、库存解锁
- 延迟队列会将过期的消息路由至
"stock.release.stock.queue"
,通过监听该队列实现库存的解锁 - 为保证消息的可靠到达,我们使用手动确认消息的模式,在解锁成功后确认消息,若出现异常则重新归队
package com.atguigu.gulimall.ware.listener;
import com.atguigu.common.to.mq.OrderTo;
import com.atguigu.common.to.mq.StockLockedTo;
import com.atguigu.gulimall.ware.service.WareSkuService;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
/**
* @创建人: 放生
* @创建时间: 2022/5/6
* @描述:
*/
@Service
@RabbitListener(queues = "stock.release.stock.queue")
public class StockReleaseListener {
@Autowired
WareSkuService wareSkuService;
/**
*当前的消费方法是,在库存服务发送到延迟队列的消费
* 锁定成功,将锁定的详情发送MQ的延时队列(40分钟)中并且把具体的锁定信息以流水表的形式存入mysql,
* MQ到时消费(到时后根据订单的状态来解锁库存,因为可能在ware服务锁定库存成功,
* 但是在订单服务中后续操作出了异常,或者30分钟没有支付)总之根据订单的状态来消费数据,
* 是解锁库存,还是反向补偿。
*/
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
System.out.println("收到解锁库存的消息...");
try{
//当前消息是否被第二次及以后(重新)派发过来了。
// Boolean redelivered = message.getMessageProperties().getRedelivered();
wareSkuService.unlockStock(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
/**
*当前的消费方法是,订单服务在关单后发送过来的消费
* 考虑到特殊的情况下,比如我解锁的队列时间是40分钟,订单的是30分钟,由于订单的服务机器卡顿,延时间等原因,
* 订单服务延迟了11分钟后在发送数据到mq,那么自动关单就是在41分钟之后了,这个时候解锁的消费者已经消费完成了,
* 所以后面的操作还需要在关单的时候再次发送数据到解锁队列
*/
@RabbitHandler
public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
System.out.println("订单关闭准备解锁库存...");
try{
wareSkuService.unlockStock(orderTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
7.1 以上两个方法的具体实现
@Override
public void unlockStock(StockLockedTo to) {
StockDetailTo detail = to.getDetail();
Long detailId = detail.getId();
//解锁
//1、查询数据库关于这个订单的锁定库存信息。
// 有:证明库存锁定成功了
// 解锁:订单情况。
// 1、没有这个订单。必须解锁
// 2、有这个订单。不是解锁库存。
// 订单状态: 已取消:解锁库存
// 没取消:不能解锁
// 没有:库存锁定失败了,库存回滚了。这种情况无需解锁
WareOrderTaskDetailEntity byId = orderTaskDetailService.getById(detailId);
if (byId != null) {
//解锁
Long id = to.getId();
WareOrderTaskEntity taskEntity = orderTaskService.getById(id);
String orderSn = taskEntity.getOrderSn();//根据订单号查询订单的状态
R r = orderFeignService.getOrderStatus(orderSn);
if (r.getCode() == 0) {
//订单数据返回成功
OrderVo data = r.getData(new TypeReference<OrderVo>() {
});
if (data == null || data.getStatus() == 4) {
//订单不存在
//订单已经被取消了。才能解锁库存
//detailId
if (byId.getLockStatus() == 1) {
//当前库存工作单详情,状态1 已锁定但是未解锁才可以解锁
unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);
}
}
} else {
//消息拒绝以后重新放到队列里面,让别人继续消费解锁。
throw new RuntimeException("远程服务失败");
}
} else {
//无需解锁
}
}
/**
* 1、库存自动解锁。
* 下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁。
* 2、订单失败。
* 锁库存失败
* <p>
* <p>
* 只要解锁库存的消息失败。一定要告诉服务解锁失败。
*/
private void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId) {
//库存解锁
wareSkuDao.unlockStock(skuId, wareId, num);
//更新库存工作单的状态
WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity();
entity.setId(taskDetailId);
entity.setLockStatus(2);//变为已解锁
orderTaskDetailService.updateById(entity);
}
//防止订单服务卡顿,导致订单状态消息一直改不了,库存消息优先到期。查订单状态新建状态,什么都不做就走了。
//导致卡顿的订单,永远不能解锁库存
@Transactional
@Override
public void unlockStock(OrderTo orderTo) {
String orderSn = orderTo.getOrderSn();
//查一下最新库存的状态,防止重复解锁库存
WareOrderTaskEntity task = orderTaskService.getOrderTaskByOrderSn(orderSn);
Long id = task.getId();
//按照工作单找到所有 没有解锁的库存,进行解锁
List<WareOrderTaskDetailEntity> entities = orderTaskDetailService.list(
new QueryWrapper<WareOrderTaskDetailEntity>()
.eq("task_id", id)
.eq("lock_status", 1));
//Long skuId, Long wareId, Integer num, Long taskDetailId
for (WareOrderTaskDetailEntity entity : entities) {
unLockStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum(),entity.getId());
}
}
7.2 拦截器放行
@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 {
// /order/order/status/2948294820984028420
String uri = request.getRequestURI();
AntPathMatcher antPathMatcher = new AntPathMatcher();
boolean match = antPathMatcher.match("/order/order/status/**", uri);
boolean match1 = antPathMatcher.match("/payed/notify", uri);
if(match || match1){
return true;
}
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;
}
}
}
296、商城业务-订单服务-库存自动解锁完成
以上代码已经实现
297、商城业务-订单服务-测试库存自动解锁
以上代码已经实现
298、商城业务-订单服务-定时关单完成
定时自动关单是指我们订单创建成功后,在指定的时间内没有支付,我们需要自动关单,释放锁定的库存
创建订单的消息会进入延迟队列,最终发送至队列order.release.order.queue
,因此我们对该队列进行监听,进行订单的关闭
- 由于要保证幂等性,因此要查询最新的订单状态判断是否需要关单
- 关闭订单后也需要解锁库存,因此发送消息进行库存、会员服务对应的解锁
package com.atguigu.gulimall.order.listener;
import com.atguigu.gulimall.order.entity.OrderEntity;
import com.atguigu.gulimall.order.service.OrderService;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
/**
* @创建人: 放生
* @创建时间: 2022/5/6
* @描述:
*/
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {
@Autowired
OrderService orderService;
/**
* 订单服务的自动关单
*/
@RabbitHandler
public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
System.out.println("收到过期的订单信息:准备关闭订单"+entity.getOrderSn()+"==>"+entity.getId());
try{
orderService.closeOrder(entity);
//手动调用支付宝收单;
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
具体的关单实现,并且关单后在向mq发送解锁消息
@Override
public void closeOrder(OrderEntity entity) {
//查询当前这个订单的最新状态
OrderEntity orderEntity = this.getById(entity.getId());
if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()) {
//关单
OrderEntity update = new OrderEntity();
update.setId(entity.getId());
update.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(update);
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderEntity, orderTo);
//发给MQ一个
try {
//TODO 保证消息一定会发送出去,每一个消息都可以做好日志记录(给数据库保存每一个消息的详细信息)。
//TODO 定期扫描数据库将失败的消息再发送一遍;
rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo);
} catch (Exception e) {
//TODO 将没法送成功的消息进行重试发送。
// while)
}
}
}
8、流程回顾
总体流程简述: 首先在gulimall-order模块,下订单通过OrderServiceImpl的SubmitOrderResponseVo方法,会创建订单数据–》验价—》保存订单数据——》锁库存——》会员积分扣减。一系列的操作,其中远程调用库存服务gulimall-ware模块的锁库存服务wmsFeignService.orderLockStock(lockVo),在远程的锁库存的方法中,会将每一个商品的锁定成功的数据记录持久化到对应的数据库中,有专门的表来维护锁了 几号库,什么单,几件库存等记录(方便解库存),同时发送一条消息(也可以时持久化到数据库的数据)给MQ的延迟队列,,如果传过来的是3个产品,到第三个锁定失败,就让wmsFeignService.orderLockStock(lockVo)的方法抛出异常,就会回滚之前的持久化到数据库的数据,然后返回给OrderServiceImpl的SubmitOrderResponseVo也是一个失败的标识,从而在这个方法也抛出异常回滚之前的保存订单的操作,如果三件的锁定成功,返回OrderServiceImpl的SubmitOrderResponseVo也是一个成功的标识,但是在下面的扣除积分的时候报错,但是这个时候抛出异常只能回滚之前的保存订单的数据,锁定库存时远程调用的,无法回滚,这个时候因为之前锁库存的每条数据都在延迟队列(比如时延迟10分钟)有,可以在延迟队列的消费端去根据单号是否在下单表中有这个记录,如果积分回滚会撤销订单保存的记录,如果没找到说明订单回滚了,这个时候解锁库存,如果订单有,说明没有回滚,在查看支付状态,如果10分钟还未支付也锁库存,并修改下单号的状态,为未支付,需要重新去下单。如果时正常的单子,就不用做任何操作。
299、商城业务-订单服务-消息丢失、积压、重复等解决方案
1、消息丢失
• 消息发送出去,由于网络问题没有抵达服务器
• 做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式
• 做好日志记录,每个消息状态是否都被服务器收到都应该记录
• 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发
• 消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机。
• publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。
• 自动ACK的状态下。消费者收到消息,但没来得及消息然后宕机
• 一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队如何保证消息可靠性-消息重复
2、消息重复
• 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者
• 消息消费失败,由于重试机制,自动又将消息发送出去
• 成功消费,ack时宕机,消息由unack变为ready,Broker又重新发送
• 消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志
• 使用防重表(redis/mysql),发送消息每一个都有业务的唯一标识,处理过就不用处理
• rabbitMQ的每一个消息都有redelivered字段,可以获取是否 是被重新投递过来的,而不是第一次投递过来的如何保证消息可靠性-消息积压
3、消息积压
• 消费者宕机积压
• 消费者消费能力不足积压
• 发送者发送流量太大
• 上线更多的消费者,进行正常消费
• 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理
rabbitmq 除了使用死信队列来实现延迟队列外 还可以通过rabbitmq的插件也能实现延迟队列
askService.getOrderTaskByOrderSn(orderSn);
Long id = task.getId();
//按照工作单找到所有 没有解锁的库存,进行解锁
List entities = orderTaskDetailService.list(
new QueryWrapper()
.eq(“task_id”, id)
.eq(“lock_status”, 1));
//Long skuId, Long wareId, Integer num, Long taskDetailId
for (WareOrderTaskDetailEntity entity : entities) {
unLockStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum(),entity.getId());
}
}
### 7.2 拦截器放行
```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 {
// /order/order/status/2948294820984028420
String uri = request.getRequestURI();
AntPathMatcher antPathMatcher = new AntPathMatcher();
boolean match = antPathMatcher.match("/order/order/status/**", uri);
boolean match1 = antPathMatcher.match("/payed/notify", uri);
if(match || match1){
return true;
}
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;
}
}
}
296、商城业务-订单服务-库存自动解锁完成
以上代码已经实现
297、商城业务-订单服务-测试库存自动解锁
以上代码已经实现
298、商城业务-订单服务-定时关单完成
定时自动关单是指我们订单创建成功后,在指定的时间内没有支付,我们需要自动关单,释放锁定的库存
创建订单的消息会进入延迟队列,最终发送至队列order.release.order.queue
,因此我们对该队列进行监听,进行订单的关闭
- 由于要保证幂等性,因此要查询最新的订单状态判断是否需要关单
- 关闭订单后也需要解锁库存,因此发送消息进行库存、会员服务对应的解锁
package com.atguigu.gulimall.order.listener;
import com.atguigu.gulimall.order.entity.OrderEntity;
import com.atguigu.gulimall.order.service.OrderService;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
/**
* @创建人: 放生
* @创建时间: 2022/5/6
* @描述:
*/
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {
@Autowired
OrderService orderService;
/**
* 订单服务的自动关单
*/
@RabbitHandler
public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
System.out.println("收到过期的订单信息:准备关闭订单"+entity.getOrderSn()+"==>"+entity.getId());
try{
orderService.closeOrder(entity);
//手动调用支付宝收单;
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}catch (Exception e){
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
具体的关单实现,并且关单后在向mq发送解锁消息
@Override
public void closeOrder(OrderEntity entity) {
//查询当前这个订单的最新状态
OrderEntity orderEntity = this.getById(entity.getId());
if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()) {
//关单
OrderEntity update = new OrderEntity();
update.setId(entity.getId());
update.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(update);
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderEntity, orderTo);
//发给MQ一个
try {
//TODO 保证消息一定会发送出去,每一个消息都可以做好日志记录(给数据库保存每一个消息的详细信息)。
//TODO 定期扫描数据库将失败的消息再发送一遍;
rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo);
} catch (Exception e) {
//TODO 将没法送成功的消息进行重试发送。
// while)
}
}
}
8、流程回顾
总体流程简述: 首先在gulimall-order模块,下订单通过OrderServiceImpl的SubmitOrderResponseVo方法,会创建订单数据–》验价—》保存订单数据——》锁库存——》会员积分扣减。一系列的操作,其中远程调用库存服务gulimall-ware模块的锁库存服务wmsFeignService.orderLockStock(lockVo),在远程的锁库存的方法中,会将每一个商品的锁定成功的数据记录持久化到对应的数据库中,有专门的表来维护锁了 几号库,什么单,几件库存等记录(方便解库存),同时发送一条消息(也可以时持久化到数据库的数据)给MQ的延迟队列,,如果传过来的是3个产品,到第三个锁定失败,就让wmsFeignService.orderLockStock(lockVo)的方法抛出异常,就会回滚之前的持久化到数据库的数据,然后返回给OrderServiceImpl的SubmitOrderResponseVo也是一个失败的标识,从而在这个方法也抛出异常回滚之前的保存订单的操作,如果三件的锁定成功,返回OrderServiceImpl的SubmitOrderResponseVo也是一个成功的标识,但是在下面的扣除积分的时候报错,但是这个时候抛出异常只能回滚之前的保存订单的数据,锁定库存时远程调用的,无法回滚,这个时候因为之前锁库存的每条数据都在延迟队列(比如时延迟10分钟)有,可以在延迟队列的消费端去根据单号是否在下单表中有这个记录,如果积分回滚会撤销订单保存的记录,如果没找到说明订单回滚了,这个时候解锁库存,如果订单有,说明没有回滚,在查看支付状态,如果10分钟还未支付也锁库存,并修改下单号的状态,为未支付,需要重新去下单。如果时正常的单子,就不用做任何操作。
299、商城业务-订单服务-消息丢失、积压、重复等解决方案
1、消息丢失
• 消息发送出去,由于网络问题没有抵达服务器
• 做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式
• 做好日志记录,每个消息状态是否都被服务器收到都应该记录
• 做好定期重发,如果消息没有发送成功,定期去数据库扫描未成功的消息进行重发
• 消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,宕机。
• publisher也必须加入确认回调机制,确认成功的消息,修改数据库消息状态。
• 自动ACK的状态下。消费者收到消息,但没来得及消息然后宕机
• 一定开启手动ACK,消费成功才移除,失败或者没来得及处理就noAck并重新入队如何保证消息可靠性-消息重复
2、消息重复
• 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费者
• 消息消费失败,由于重试机制,自动又将消息发送出去
• 成功消费,ack时宕机,消息由unack变为ready,Broker又重新发送
• 消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态标志
• 使用防重表(redis/mysql),发送消息每一个都有业务的唯一标识,处理过就不用处理
• rabbitMQ的每一个消息都有redelivered字段,可以获取是否 是被重新投递过来的,而不是第一次投递过来的如何保证消息可靠性-消息积压
3、消息积压
• 消费者宕机积压
• 消费者消费能力不足积压
• 发送者发送流量太大
• 上线更多的消费者,进行正常消费
• 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理
rabbitmq 除了使用死信队列来实现延迟队列外 还可以通过rabbitmq的插件也能实现延迟队列