六、订单
5.8.RabbitMQ
5.8.1、RabbitMQ 基本使用
1、基本概念
主流的消息队列对比
| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka |
|---|---|---|---|---|
| 单机吞吐量 | 万级,比 RocketMQ、Kafka 低一个数量级 | 同 ActiveMQ | 10 万级,支撑高吞吐 | 10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景 |
| topic 数量对吞吐量的影响 | topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源 | ||
| 时效性 | ms 级 | 微秒级,这是 RabbitMQ 的一大特点,延迟最低 | ms 级 | 延迟在 ms 级以内 |
| 可用性 | 高,基于主从架构实现高可用 | 同 ActiveMQ | 非常高,分布式架构 | 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 |
| 消息可靠性 | 有较低的概率丢失数据 | 基本不丢 | 经过参数优化配置,可以做到 0 丢失 | 同 RocketMQ |
| 功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,并发能力很强,性能极好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用 |

1、异步处理



2、应用解耦


3、流量控制

大多应用中,可通过消息服务中间件来提升系统异步通信、扩展解耦能力
消息服务中两个重要概念:
消息代理(message broker)消息发送后,消息代理进行管理,消息代理保证消息传递到指定目的地;
目的地(destination)消息发送的目的地
当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地。
消息队列主要有两种形式的目的地
队列(queue):点对点消息通信(point-to-point)
主题(topic):发布(publish)/订阅(subscribe)消息通信
点对点式: 消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获 取消息内容,消息读取后被移出队列 消息只有唯一的发送者和接受者,但并不是说只能有一个接收者
.发布订阅式: 发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个 主题,那么就会在消息到达时同时收到消息
JMS(Java Message Service)JAVA消息服务: 基于JVM消息代理的规范。ActiveMQ、HornetMQ是JMS实现
AMQP(Advanced Message Queuing Protocol) 高级消息队列协议,也是一个消息代理的规范,兼容JMS,RabbitMQ是AMQP的实现
JMS和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 BytesMessage StreamMessage ObjectMessage Message (只有消息头和属性) | byte[] 当实际应用时,有复杂的消息,可以将消息序列化后发 送。 |
| 综合评价 | JMS 定义了JAVA API层面的标准; 在java体系中, 多个client均可以通过JMS进行交互,不需要应用修 改代码,但是其对跨平台的支持较差; | AMQP定义了wire-level层的协议标准;天然具有跨平 台、跨语言特性。 |
Spring支持 spring-jms提供了对JMS的支持 spring-rabbit提供了对AMQP的支持 需要ConnectionFactory的实现来连接消息代理 提供JmsTemplate、RabbitTemplate来发送消息 @JmsListener(JMS)、@RabbitListener(AMQP)注解在方法上监听消息 代理发布的消息 @EnableJms、@EnableRabbit开启支持
Spring Boot自动配置 JmsAutoConfiguration
RabbitAutoConfiguration 10、市面的MQ产品 ActiveMQ、RabbitMQ、RocketMQ、Kafka
RabbitMQ概念
- 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 表示消息队列服务器实体

2、简单测试
1、下载 rabbitmq
在 https://hub.docker.com/ 页面里搜索rabbitmq,选择官方镜像,下载带web管理后台的
docker pull rabbitmq:management

在linux虚拟机里输入如下命令,下载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发现&集群端口)
5672, 5671 (AMQP端口)
15672 (web管理后台端口)
61613, 61614 (STOMP协议端口)
1883, 8883 (MQTT协议端口)
完整命令
# 查看镜像
docker images
# 下载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
# 设置rabbitmq开机自启动
docker update rabbitmq --restart=always
# 查看正在运行的容器
docker ps
可以看到rabbitmq已经启动了

浏览器访问http://192.168.56.10:15672,用户名和密码默认都为guest

2、rabbitmq管理后台信息
登录后,在 http://192.168.56.10:15672/#/ 页面里可以看到RabbitMQ、Erlang的版本信息,5s钟刷新一次
Connections (连接数)、Channels (信道)、Exchanges(交换机)、Queues (消息队列) 、Consumers(消费者数量)等信息

在 http://192.168.56.10:15672/#/ 页面里往下滑, Ports and contexts里可以看到端口的占用信息,Export definitions可以导出配置、Import definitions可以导入配置

点击 Exchanges(交换机),默认就会有7个交换机

3、添加虚拟机
在Admin ->Virtual Hosts->Add a new virtual host里输入Name:,然后点击Add virtual host可以添加一个虚拟主机

添加好虚拟主机后,会自动分配7个默认的交换机

点击刚刚新建的虚拟主机,可以在Permissions里给用户设置各种权限

在Delete this vhost里可以点击Delete this virtual host删除一个虚拟主机

3、添加交换机
1、交换机类型
Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct(点对点)、 fanout(扇出)、topic(发布/订阅)、headers 。headers 匹配 AMQP 消息的 header 而不是路由键, headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了,所以直接 看另外三种类型:
- direct(直连(点对点),类似于
单播)
消息中的路由键(routing key)如果和Binding 中的 binding key 一致, 交换器就将消息发到对应的队列中。路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为“dog”,则只转发 routingkey 标记为“dog”的消息,不会转发“dog.puppy”,、“dog.guard” 等等。它是完全匹配、单播的模式。(将消息发送给指定路由键的队列)

- fanout(扇形(扇出)、类似于
广播)
每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。fanout 交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的 消息都会被转发到与该交换器绑定的所 有队列上。很像子网广播,每台子网内 的主机都获得了一份复制的消息。fanout 类型转发消息是最快的。(不分路由键,将所有消息交给所有绑定的队列)

- topic(主题(发布/订阅),类似于
组播)
topic 交换器通过模式匹配分配消息的 路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。 它将路由键和绑定键的字符串切分成单 词,这些单词之间用点隔开。它同样也 会识别两个通配符:符号#和符号*。#匹配0个或多个单词,*匹配一 个单词。(将消息发送给匹配路由键的绑定的队列)

2、添加交换机
添加交换机(交换机可以绑定交换机和队列)
Type: 交换机类型Durability: 是否持久化Auto delete: If yes, the exchange will delete itself after at least one queue or exchange has been bound to this one, and then all queues or exchanges have been unbound.如果选择yes,则在至少有一个队列或交换机绑定到该交换机。所有队列或交换机都已解除绑定,该交换机将自行删除
Internal: If yes, clients cannot publish to this exchange directly. It can only be used with exchange to exchange bindings.如果选择yes,客户端不能直接发布到这个交换机,它仅能被用于交换机与交换机之间绑定
点击Exchanges->Add a new exchange,输入Name为my.exchange.direct,其他默认即可,然后点击Add exchange

点击刚刚新建的my.exchange.direct交换机,可以在Bindings里查看绑定的Queues,可以在Publish message里发消息

3、新建一个队列
点击Queues->Add a new queue,输入name,其他默认即可,然后点击Add queue
Auto delete:If yes, the queue will delete itself after at least one consumer has connected, and then all consumers have disconnected.
如果选择yes,则至少需要有一个消费者连接到该队列,当所有消费者都断开连接后该队列将自动删除

在Exchanges里点击刚刚创建的my.exchange.direct,在Bindings里的Add binding from this exchange里,选择To queue,然后输入刚刚新建的Queues

4、搭建RabbitMQ测试结构
1、结构

2、添加队列
点击Queues->Add a new queue,输入name为atguigu.news,其他默认即可,然后点击Add queue

点击Queues->Add a new queue,输入name为atguigu.emps,其他默认即可,然后点击Add queue

点击Queues->Add a new queue,输入name为gulixueyuan.news,其他默认即可,然后点击Add queue

查看创建的队列是否正确

在Exchanges里点击刚刚创建的my.exchange.direct交换机,点击Delete this exchange里的Delete,删除该交换机

5、直连交换机(类似单播)
1、搭建直连交换机
点击Exchanges->Add a new exchange,输入Name为exchange.direct,类型选择direct,其他默认即可,然后点击Add exchange

然后在Exchanges里点击刚刚创建的exchange.direct交换机,在Bindings里的Add binding from this exchange里,绑定atguigu队列、atguigu.emps队列、atguigu.news队列、gulixueyuan.news队列,绑定直连交换机的To queue队列名和Routing Key路由键要一致。
绑定atguigu队列

绑定atguigu.emps队列

绑定atguigu.news队列

绑定gulixueyuan.news队列

最终exchange.direct交换机绑定的队列和路由键如下图所示

2、测试
在Exchanges里点击刚刚创建的exchange.direct交换机,在Publish message里Routing key输入路由键,Payload里输入要发送的消息内容。比如在Routing key里输入atguigu.news,Payload里输入atguigu.news atguigu.news atguigu.news

点击Queues,可以看到atguigu.news收到了一条消息

点击Queues里的atguigu.news,在Get messages里直接Get messages按钮,可以看到在Payload里已经显示消息了

此时atguigu.news队列里的消息还在,这是因为默认选择了Nack message requeue true ( 获取消息,但是不做ack应答确认,消息重新入队) 模式

Get messages里Ack Mode有四种选择
Nack message requeue true 获取消息,但是不做ack应答确认,消息重新入队
Automatic ack 获取消息,应答确认,消息不重新入队,将会从队列中删除
Reject requeue true 拒绝获取消息,消息重新入队
Reject requeue false 拒绝获取消息,消息不重新入队,将会被删除

点击Queues里的atguigu.news,在Get messages里Ack Mode选择Automatic ack,再点击Get messages按钮

此时atguigu.news队列里就没有消息了

6、扇形交换机(类似多播)
1、搭建扇形交换机
点击Exchanges->Add a new exchange,输入Name为exchange.fanout, 类型选择fanout,其他默认即可,然后点击Add exchange

然后在Exchanges里点击刚刚创建的exchange.direct交换机,在Bindings里的Add binding from this exchange里,绑定atguigu队列、atguigu.emps队列、atguigu.news队列、gulixueyuan.news队列,绑定扇形交换机队列名可以不指定路由键(因为指不指定都会将消息发送给绑定的全部队列)。
绑定atguigu队列

绑定atguigu.emps队列

绑定atguigu.news队列

绑定gulixueyuan.news队列

最终exchange.fanout交换机绑定的队列和路由键如下图所示

2、测试
在Exchanges里点击刚刚创建的exchange.fanout交换机,在Publish message里Routing key输入路由键,Payload里输入要发送的消息内容。比如在Routing key里输入atguigu.emps,Payload里输入atguigu.emps atguigu.emps atguigu.emps

可以看到即使指定了路由键,所有绑定的队列都有一条消息(因此写不写路由键都一样)

点击Queues里的atguigu,在Get messages里Ack Mode选择Automatic ack,再点击Get messages按钮即可获得消息并确认收到

此时atguigu队列里就没有消息了

使用相同的方法,清空完所有消息

7、主题交换机(类似组播)
#匹配0个或多个单词,*匹配一 个单词
1、搭建主题交换机
点击Exchanges->Add a new exchange,输入Name为exchange.topic, 类型选择topic,其他默认即可,然后点击Add exchange

主题交换机绑定的To queue队列名和Routing Key路由键可以一致也可以不一致可以灵活的配置(类似于正则表达式),可以完成直连、扇形交换机的功能和它们不具有的功能。
在Exchanges里点击刚刚创建的exchange.topic交换机,在Bindings里的Add binding from this exchange里,选择To queue并输入atguigu,Routing key输入atguigu.#

在Exchanges里点击刚刚创建的exchange.topic交换机,在Bindings里的Add binding from this exchange里,选择To queue并输入atguigu.emps,Routing key输入atguigu.#

在Exchanges里点击刚刚创建的exchange.topic交换机,在Bindings里的Add binding from this exchange里,选择To queue并输入atguigu.news,Routing key输入atguigu.#

在Exchanges里点击刚刚创建的exchange.topic交换机,在Bindings里的Add binding from this exchange里,选择To queue并输入gulixueyuan.news,Routing key输入*.news

最终exchange.topic交换机绑定的队列和路由键如下图所示

2、测试
1、测试一
在Exchanges里点击刚刚创建的exchange.topic交换机,在Publish message里Routing key输入路由键,Payload里输入要发送的消息内容。比如在Routing key里输入atguigu.news,Payload里输入atguigu.news atguigu.news atguigu.news

由于4个队列都匹配到了输入的路由键,因此4个队列全收到了消息

把4个队列的消息全部清空,再做测试

2、测试二
在Exchanges里点击刚刚创建的exchange.topic交换机,在Publish message里Routing key输入hello.news,Payload里输入hello.news hello.news hello.news。

只有gulixueyuan.news队列匹配到了输入的路由键,因此也只有该队列收到了消息

点击Queues里的gulixueyuan.news,在Get messages里Ack Mode选择Automatic ack,再点击Get messages按钮即可获得消息并确认收到

5.8.2、整合RabbitMQ
1、整合RabbitMQ
1、引入RabbitMQ
在gulimall-order模块的pom.xml文件里添加RabbitMQ依赖
<!--引入amqp场景,使用RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

在gulimall-order模块的com.atguigu.gulimall.order.GulimallOrderApplication类上添加@EnableRabbit注解。(引入spring-boot-starter-amqp后RabbitAutoConfiguration自动生效)
@EnableRabbit

在gulimall-order模块的src/main/resources/application.properties文件里添加RabbitMQ相关的配置
spring.rabbitmq.host=192.168.56.10
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#虚拟主机
spring.rabbitmq.virtual-host=/

2、自动配置
在org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration类里配置了连接工厂
@Bean
public CachingConnectionFactory rabbitConnectionFactory(RabbitProperties properties,
ObjectProvider<ConnectionNameStrategy> connectionNameStrategy) throws Exception {
PropertyMapper map = PropertyMapper.get();
CachingConnectionFactory factory = new CachingConnectionFactory(
getRabbitConnectionFactoryBean(properties).getObject());
map.from(properties::determineAddresses).to(factory::setAddresses);
map.from(properties::isPublisherConfirms).to(factory::setPublisherConfirms);
map.from(properties::isPublisherReturns).to(factory::setPublisherReturns);
RabbitProperties.Cache.Channel channel = properties.getCache().getChannel();
map.from(channel::getSize).whenNonNull().to(factory::setChannelCacheSize);
map.from(channel::getCheckoutTimeout).whenNonNull().as(Duration::toMillis)
.to(factory::setChannelCheckoutTimeout);
RabbitProperties.Cache.Connection connection = properties.getCache().getConnection();
map.from(connection::getMode).whenNonNull().to(factory::setCacheMode);
map.from(connection::getSize).whenNonNull().to(factory::setConnectionCacheSize);
map.from(connectionNameStrategy::getIfUnique).whenNonNull().to(factory::setConnectionNameStrategy);
return factory;
}

RabbitTemplate 常用于发送消息
@Bean
@ConditionalOnSingleCandidate(ConnectionFactory.class)
@ConditionalOnMissingBean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
PropertyMapper map = PropertyMapper.get();
RabbitTemplate template = new RabbitTemplate(connectionFactory);
MessageConverter messageConverter = this.messageConverter.getIfUnique();
if (messageConverter != null) {
template.setMessageConverter(messageConverter);
}
template.setMandatory(determineMandatoryFlag());
RabbitProperties.Template properties = this.properties.getTemplate();
if (properties.getRetry().isEnabled()) {
template.setRetryTemplate(new RetryTemplateFactory(
this.retryTemplateCustomizers.orderedStream().collect(Collectors.toList())).createRetryTemplate(
properties.getRetry(), RabbitRetryTemplateCustomizer.Target.SENDER));
}
map.from(properties::getReceiveTimeout).whenNonNull().as(Duration::toMillis)
.to(template::setReceiveTimeout);
map.from(properties::getReplyTimeout).whenNonNull().as(Duration::toMillis).to(template::setReplyTimeout);
map.from(properties::getExchange).to(template::setExchange);
map.from(properties::getRoutingKey).to(template::setRoutingKey);
map.from(properties::getDefaultReceiveQueue).whenNonNull().to(template::setDefaultReceiveQueue);
return template;
}

AmqpAdmin 常用于管理交换机、队列
@Bean
@ConditionalOnSingleCandidate(ConnectionFactory.class)
@ConditionalOnProperty(prefix = "spring.rabbitmq", name = "dynamic", matchIfMissing = true)
@ConditionalOnMissingBean
public AmqpAdmin amqpAdmin(ConnectionFactory connectionFactory) {
return new RabbitAdmin(connectionFactory);
}

RabbitMessagingTemplate
@Bean
@ConditionalOnSingleCandidate(RabbitTemplate.class)
public RabbitMessagingTemplate rabbitMessagingTemplate(RabbitTemplate rabbitTemplate) {
return new RabbitMessagingTemplate(rabbitTemplate);
}

点击org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration类里的AmqpAdmin就来到了org.springframework.amqp.core.AmqpAdmin,再点击Exchange就来到了org.springframework.amqp.core.Exchange,使用ctrl + H快捷键可以看到该类与其他类的继承关系
org.springframework.amqp.core.AbstractExchange抽象类实现了org.springframework.amqp.core.Exchange接口,该抽象类有如下几个实现类:
org.springframework.amqp.core.DirectExchange
org.springframework.amqp.core.FanoutExchange
org.springframework.amqp.core.CustomExchange 自定义
org.springframework.amqp.core.TopicExchange
org.springframework.amqp.core.HeadersExchange
这里面包含了前面测试RabbitMQ的几种交换机,而且还可以自定义交换机

2、测试
1、创建交换机
在gulimall-order模块com.atguigu.gulimall.order.GulimallOrderApplicationTests测试类里修改为如下代码
@Slf4j
@Autowired
AmqpAdmin amqpAdmin;
@Test
public void contextLoads() {
//声明交换机(durable:持久化)
//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[{}]创建成功",directExchange.getName());
}
控制台输出:
Exchange[hello-java-exchange]创建成功

浏览器打开 http://192.168.56.10:15672/#/exchanges 页面,可以看到hello-java-exchange交换机创建成功了

2、创建队列
在gulimall-order模块com.atguigu.gulimall.order.GulimallOrderApplicationTests测试类里添加createQueue方法
注意: Queue要选org.springframework.amqp.core包下的
/**
* 创建队列
*/
@Test
public void createQueue(){
//exclusive:排他(只能被声明的连接使用,只要一个连接连上该队列,其他连接就连不上该队列)
//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("Exchange[{}]创建成功",queue.getName());
}
控制台输出:
Exchange[hello-java-queue]创建成功

浏览器打开 http://192.168.56.10:15672/#/queues 页面,可以看到hello-java-queue队列创建成功了

org.springframework.amqp.core.Queue类的构造函数详细说明如下
/**
* Construct a new queue, given a name, durability, exclusive and auto-delete flags.
* @param name the name of the queue.
* @param durable true if we are declaring a durable queue (the queue will survive a server restart)
* @param exclusive true if we are declaring an exclusive queue (the queue will only be used by the declarer's connection) 排他队列:只能被声明的连接使用
* @param autoDelete true if the server should delete the queue when it is no longer in use
*/
public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete) {
this(name, durable, exclusive, autoDelete, null);
}

3、创建绑定关系
在gulimall-order模块com.atguigu.gulimall.order.GulimallOrderApplicationTests测试类里添加createBinding方法
注意: Binding要选org.springframework.amqp.core包下的
/**
* 创建绑定
*/
@Test
public void createBinding() {
/*
* String destination :目的地
* DestinationType destinationType :目的地类型(交换机/队列)
* String exchange :交换机
* String routingKey :路由键
* 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");
}
控制台输出:
Binding[hello-java-Binding]创建成功

在 http://192.168.56.10:15672/#/exchanges 页面里点击刚刚创建的hello-java-exchange交换机,可以看到该交换机已经绑定hello-java-queue队列了,且绑定的路由键为hello.java

4、发送字符串消息
在gulimall-order模块com.atguigu.gulimall.order.GulimallOrderApplicationTests测试类里添加sendMessage方法,用于hello-java-exchange交换机使用hello.java路由键发送字符串消息
@Autowired
RabbitTemplate rabbitTemplate;
@Test
public void sendMessage(){
//rabbitTemplate.send();
//convertAndSend(String exchange, String routingKey, final Object object)
String msg = "hello world";
rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",msg);
log.info("消息发送完成{}",msg);
}
控制台输出:
消息发送完成hello world

浏览器访问 http://192.168.56.10:15672/#/queues 页面,可以看到hello-java-queue已经有一条消息了

点击Queues里的hello-java-queue,在Get messages里Ack Mode选择Reject requeue false,再点击Get messages按钮,此时已经正确获取到消息了

5、发送Java对象消息
在gulimall-order模块com.atguigu.gulimall.order.GulimallOrderApplicationTests测试类里添加sendMessage2方法,用于hello-java-exchange交换机使用hello.java路由键发送Java对象消息
@Test
public void sendMessage2(){
OrderReturnReasonEntity entity = new OrderReturnReasonEntity();
entity.setId(1L);
entity.setCreateTime(new Date());
entity.setName("啊啊啊");
//rabbitTemplate.send();
//convertAndSend(String exchange, String routingKey, final Object object)
rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",entity);
log.info("消息发送完成{}",entity);
}
控制台输出:
消息发送完成OrderReturnReasonEntity(id=1, name=啊啊啊, sort=null, status=null, createTime=Thu Aug 11 16:40:19 CST 2022)

点击Queues里的hello-java-queue,在Get messages里Ack Mode选择Reject requeue false,再点击Get messages按钮,此时Properties里面的content_type的值为application/x-java-serialized-object,内容也是乱的

3、使用JSON进行序列化
1、实现Serializable接口
让gulimall-order模块的com.atguigu.gulimall.order.entity.OrderReturnReasonEntity类实现Serializable接口

2、源码分析
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration类的RabbitTemplateConfiguration静态内部类的RabbitTemplateConfiguration方法有一个ObjectProvider<MessageConverter> messageConverter参数,该方法会从容器种获取所有MessageConverter(消息转换器),然后设给本类的messageConverter字段(如果这个消息转换器唯一,则使用该消息转换器)
public RabbitTemplateConfiguration(RabbitProperties properties,
//ObjectProvider<MessageConverter>:从容器种获取所有MessageConverter(消息转换器)
ObjectProvider<MessageConverter> messageConverter,
ObjectProvider<RabbitRetryTemplateCustomizer> retryTemplateCustomizers) {
this.properties = properties;
this.messageConverter = messageConverter;
this.retryTemplateCustomizers = retryTemplateCustomizers;
}
@Bean
@ConditionalOnSingleCandidate(ConnectionFactory.class)
@ConditionalOnMissingBean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
PropertyMapper map = PropertyMapper.get();
RabbitTemplate template = new RabbitTemplate(connectionFactory);
//获取消息转换器
MessageConverter messageConverter = this.messageConverter.getIfUnique();
if (messageConverter != null) {
template.setMessageConverter(messageConverter);
}
template.setMandatory(determineMandatoryFlag());
RabbitProperties.Template properties = this.properties.getTemplate();
if (properties.getRetry().isEnabled()) {
template.setRetryTemplate(new RetryTemplateFactory(
this.retryTemplateCustomizers.orderedStream().collect(Collectors.toList())).createRetryTemplate(
properties.getRetry(), RabbitRetryTemplateCustomizer.Target.SENDER));
}
map.from(properties::getReceiveTimeout).whenNonNull().as(Duration::toMillis)
.to(template::setReceiveTimeout);
map.from(properties::getReplyTimeout).whenNonNull().as(Duration::toMillis).to(template::setReplyTimeout);
map.from(properties::getExchange).to(template::setExchange);
map.from(properties::getRoutingKey).to(template::setRoutingKey);
map.from(properties::getDefaultReceiveQueue).whenNonNull().to(template::setDefaultReceiveQueue);
return template;
}

在org.springframework.amqp.rabbit.core.RabbitTemplate里如果容器中没有messageConverter(消息转换器)或不唯一,则会使用默认的private MessageConverter messageConverter = new SimpleMessageConverter();

在org.springframework.amqp.support.converter.SimpleMessageConverter#createMessage方法里,如果是String就转换为byte[],如果实现类Serializable就用SerializationUtils进行序列化,因此如果不加以设置,默认将使用 SerializationUtils.serialize(object);进行序列化
/**
* Creates an AMQP Message from the provided Object.
*/
@Override
protected Message createMessage(Object object, MessageProperties messageProperties) throws MessageConversionException {
byte[] bytes = null;
if (object instanceof byte[]) {
bytes = (byte[]) object;
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_BYTES);
}
else if (object instanceof String) {
try {
bytes = ((String) object).getBytes(this.defaultCharset);
}
catch (UnsupportedEncodingException e) {
throw new MessageConversionException(
"failed to convert to Message content", e);
}
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN);
messageProperties.setContentEncoding(this.defaultCharset);
}
else if (object instanceof Serializable) {
try {
bytes = SerializationUtils.serialize(object);
}
catch (IllegalArgumentException e) {
throw new MessageConversionException(
"failed to convert to serialized Message content", e);
}
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_SERIALIZED_OBJECT);
}
if (bytes != null) {
messageProperties.setContentLength(bytes.length);
return new Message(bytes, messageProperties);
}
throw new IllegalArgumentException(getClass().getSimpleName()
+ " only supports String, byte[] and Serializable payloads, received: " + object.getClass().getName());
}

如何使用JSON进行序列化呢?
搜先我们点进org.springframework.amqp.support.converter.MessageConverter接口,按住ctrl + H查看该类和其他类的依赖关系,即可看到org.springframework.amqp.support.converter.Jackson2JsonMessageConverter类。(Jackson是Spring默认的JSON序列化器)
因此我们只需向容器中注入Jackson2JsonMessageConverter类即可

3、注入JSON消息转换器
在gulimall-order模块的com.atguigu.gulimall.order包下新建config文件夹,在config文件夹下新建MyRabbitConfig配置类
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;
/**
* @author 无名氏
* @date 2022/8/11
* @Description:
*/
@Configuration
public class MyRabbitConfig {
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}

4、再次发送Java对象消息
在gulimall-order模块com.atguigu.gulimall.order.GulimallOrderApplicationTests测试类里再次执行sendMessage2方法,用于hello-java-exchange交换机使用hello.java路由键发送Java对象消息
控制台输出:
消息发送完成OrderReturnReasonEntity(id=1, name=啊啊啊, sort=null, status=null, createTime=Thu Aug 11 17:10:30 CST 2022)

点击Queues里的hello-java-queue,在Get messages里Ack Mode选择Reject requeue false,再点击Get messages按钮,这时消息就是JSON格式的对象了
headers:
__TypeId__: com.atguigu.gulimall.order.entity.OrderReturnReasonEntity
content_type: application/json
{"id":1,"name":"啊啊啊","sort":null,"status":null,"createTime":1660209030108}

4、接收消息
1、参数指定为Object类型
在需要监听队列的业务方法上标注@RabbitListener注解(该方法所在的类需要在容器中)
在gulimall-order模块的com.atguigu.gulimall.order.config.MyRabbitConfig类里添加如下方法(这里只是测试用,平时开发不要放在配置类里),然后重启gulimall-order模块
/**
* 在需要监听队列的业务方法上标注@RabbitListener注解(该方法所在的类需要在容器中)
*/
@RabbitListener(queues = {"hello-java-queue"})
public void receiveMessage(Object message){
System.out.println("接收到消息...内容:"+message+"==>类型:"+ message.getClass());
}

在gulimall-order模块com.atguigu.gulimall.order.GulimallOrderApplicationTests测试类里再次执行sendMessage2方法,用于hello-java-exchange交换机使用hello.java路由键发送Java对象消息
2022-08-11 17:27:15.913 INFO 14580 --- [ main] c.a.g.o.GulimallOrderApplicationTests : 消息发送完成OrderReturnReasonEntity(id=1, name=啊啊啊, sort=null, status=null, createTime=Thu Aug 11 17:27:15 CST 2022)

打开gulimall-order模块的控制台,可以看到接收到的消息的类型为org.springframework.amqp.core.Message
接收到消息...内容:(Body:'{"id":1,"name":"啊啊啊","sort":null,"status":null,"createTime":1660210035834}' MessageProperties [headers={__TypeId__=com.atguigu.gulimall.order.entity.OrderReturnReasonEntity}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=hello-java-exchange, receivedRoutingKey=hello.java, deliveryTag=1, consumerTag=amq.ctag-5sX1WFjvna_vui2BTkXVdg, consumerQueue=hello-java-queue])==>类型:class org.springframework.amqp.core.Message

2、参数指定为Message类型
由于接收到的消息的类型为org.springframework.amqp.core.Message,因此业务方法可以参数直接传Message类型,这样可以方便获取信息
/**
* 在需要监听队列的业务方法上标注@RabbitListener注解(该方法所在的类需要在容器中)
*/
@RabbitListener(queues = {"hello-java-queue"})
public void receiveMessage(Message message){
//{"id":1,"name":"啊啊啊","sort":null,"status":null,"createTime":1660210035834}
byte[] body = message.getBody();
//消息的属性信息
//[headers={__TypeId__=com.atguigu.gulimall.order.entity.OrderReturnReasonEntity}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=hello-java-exchange, receivedRoutingKey=hello.java, deliveryTag=1, consumerTag=amq.ctag-5sX1WFjvna_vui2BTkXVdg, consumerQueue=hello-java-queue]
MessageProperties messageProperties = message.getMessageProperties();
String contentType = messageProperties.getContentType();
System.out.println("接收到消息...内容:"+message+"==>类型:"+ message.getClass());
}

3、添加消息内容实体参数
也可以在Message message后面添加第二个参数用于接收发送的消息内容对象,Spring会自动封装,然后重启gulimall-order模块
/**
* 在需要监听队列的业务方法上标注@RabbitListener注解(该方法所在的类需要在容器中)
* 参数可以写一下类型
* 1、Message message:原生消息详细信息。头+体
* 2、T<发送的消息的类型> OrderReturnReasonEntity content;
*/
@RabbitListener(queues = {"hello-java-queue"})
public void receiveMessage(Message message, OrderReturnReasonEntity entity){
//{"id":1,"name":"啊啊啊","sort":null,"status":null,"createTime":1660210035834}
byte[] body = message.getBody();
//消息的属性信息
//[headers={__TypeId__=com.atguigu.gulimall.order.entity.OrderReturnReasonEntity}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=hello-java-exchange, receivedRoutingKey=hello.java, deliveryTag=1, consumerTag=amq.ctag-5sX1WFjvna_vui2BTkXVdg, consumerQueue=hello-java-queue]
MessageProperties messageProperties = message.getMessageProperties();
String contentType = messageProperties.getContentType();
System.out.println("接收到消息:"+message+"==>内容:"+ entity+"==>内容类型:"+contentType);
}

在gulimall-order模块com.atguigu.gulimall.order.GulimallOrderApplicationTests测试类里再次执行sendMessage2方法,用于hello-java-exchange交换机使用hello.java路由键发送Java对象消息
控制台输出:
2022-08-11 19:11:53.932 INFO 13548 --- [ main] c.a.g.o.GulimallOrderApplicationTests : 消息发送完成OrderReturnReasonEntity(id=1, name=啊啊啊, sort=null, status=null, createTime=Thu Aug 11 19:11:53 CST 2022)

打开gulimall-order模块的控制台,可以看到接收到的消息和序列化好的消息内容对象
接收到消息:(Body:'{"id":1,"name":"啊啊啊","sort":null,"status":null,"createTime":1660216409211}' MessageProperties [headers={__TypeId__=com.atguigu.gulimall.order.entity.OrderReturnReasonEntity}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=hello-java-exchange, receivedRoutingKey=hello.java, deliveryTag=1, consumerTag=amq.ctag-7ScvVL_ZKCfOBosHgXUfMQ, consumerQueue=hello-java-queue])==>内容:OrderReturnReasonEntity(id=1, name=啊啊啊, sort=null, status=null, createTime=Thu Aug 11 19:13:29 CST 2022)==>内容类型application/json

4、添加Channel类型参数
参数还可以传com.rabbitmq.client.Channel类型,获取当前传输数据的通道
/**
* 在需要监听队列的业务方法上标注@RabbitListener注解(该方法所在的类需要在容器中)
* 参数可以写一下类型
* 1、Message message:原生消息详细信息。头+体
* 2、T<发送的消息的类型> OrderReturnReasonEntity content;
* Channel channel: 当前传输数据的通道
*/
@RabbitListener(queues = {"hello-java-queue"})
public void receiveMessage(Message message, OrderReturnReasonEntity entity, Channel channel){
//{"id":1,"name":"啊啊啊","sort":null,"status":null,"createTime":1660210035834}
byte[] body = message.getBody();
//消息的属性信息
//[headers={__TypeId__=com.atguigu.gulimall.order.entity.OrderReturnReasonEntity}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=hello-java-exchange, receivedRoutingKey=hello.java, deliveryTag=1, consumerTag=amq.ctag-5sX1WFjvna_vui2BTkXVdg, consumerQueue=hello-java-queue]
MessageProperties messageProperties = message.getMessageProperties();
String contentType = messageProperties.getContentType();
System.out.println("接收到消息:"+message+"==>内容:"+ entity+"==>内容类型:"+contentType);
}

5、集群接收消息
1、批量发送消息
选中GulimallorderApplication :9000/这个服务,右键选择Copy Configuration...,在弹出的对话框的Program arguments:里输入--server.port=9001,然后点击OK就复制了一份配置,启动刚刚复制的GulimallorderApplication(1)这个服务,模拟集群接收消息

在gulimall-order模块com.atguigu.gulimall.order.GulimallOrderApplicationTests测试类里修改sendMessage2方法,用于hello-java-exchange交换机使用hello.java路由键发送10条Java对象消息,然后执行该测试方法
/**
* 发送其他对象类型消息
*/
@Test
public void sendMessage2(){
for (int i = 0; i < 10; i++) {
//发送消息,如果发送的消息是个对象,我们会使用序列化机制,将对象写出去。对象必须实现Serializable
OrderReturnReasonEntity entity = new OrderReturnReasonEntity();
entity.setId(1L);
entity.setCreateTime(new Date());
entity.setName("啊啊啊-->"+ i);
//rabbitTemplate.send();
//convertAndSend(String exchange, String routingKey, final Object object)
rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",entity);
log.info("消息发送完成{}",entity);
}
}

GulimallorderApplication :9000/ 服务收到了0、3、6、9

GulimallorderApplication(1) :9001/ 服务收到了1、4、7

还有3个消息被刚刚执行的sendMessage2测试方法收到了

2、模拟业务处理时间长
修改gulimall-order模块的com.atguigu.gulimall.order.config.MyRabbitConfig类里的receiveMessage方法,在方法里面让程序睡3s ,模拟业务处理时间长
/**
* 在需要监听队列的业务方法上标注@RabbitListener注解(该方法所在的类需要在容器中)
* 参数可以写一下类型
* 1、Message message:原生消息详细信息。头+体
* 2、T<发送的消息的类型> OrderReturnReasonEntity content;
* Channel channel: 当前传输数据的通道
*
* Queue:可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一一个收到此消息
* 场景:
* 1)、订单服务启动多个;同一个消息,只能有一个客户端收到
* 2)、只有一个消息完全处理完,方法运行结束,我们就可以接收到下一个消息
*/
@RabbitListener(queues = {"hello-java-queue"})
public void receiveMessage(Message message, OrderReturnReasonEntity entity, Channel channel) throws InterruptedException {
//{"id":1,"name":"啊啊啊","sort":null,"status":null,"createTime":1660210035834}
byte[] body = message.getBody();
//消息的属性信息
//[headers={__TypeId__=com.atguigu.gulimall.order.entity.OrderReturnReasonEntity}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=hello-java-exchange, receivedRoutingKey=hello.java, deliveryTag=1, consumerTag=amq.ctag-5sX1WFjvna_vui2BTkXVdg, consumerQueue=hello-java-queue]
MessageProperties messageProperties = message.getMessageProperties();
String contentType = messageProperties.getContentType();
System.out.println("接收到消息:"+entity);
Thread.sleep(3000);
System.out.println("消息处理完成:" + entity.getName());
}

关闭GulimallOrderApplication (1)服务,重启GulimallOrderApplication服务(gulimall-order模块只启动一个服务)
可以看到即使业务执行时间很长,也是当前业务执行完后再处理的下一个请求,不会出现这个请求还没处理完下一个请求又开始处理的问题。

在gulimall-order模块的com.atguigu.gulimall.order包下新建test文件夹,在test文件夹里新建ReceiveMessage类,将com.atguigu.gulimall.order.config.MyRabbitConfig类里的receiveMessage方法移动到该类(方便回来看代码)
package com.atguigu.gulimall.order.test;
import com.atguigu.gulimall.order.entity.OrderReturnReasonEntity;
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.Component;
/**
* @author 无名氏
* @date 2022/8/11
* @Description: com.atguigu.gulimall.order.GulimallOrderApplicationTests.sendMessage2发送消息
* 测试发放的控制台也有可能收到消息,测试方法的控制台也要看
* 测试该类时,注释掉ReceiveMessage2类的方法
*/
@Component
public class ReceiveMessage {
/**
* 在需要监听队列的业务方法上标注@RabbitListener注解(该方法所在的类需要在容器中)
* 参数可以写一下类型
* 1、Message message:原生消息详细信息。头+体
* 2、T<发送的消息的类型> OrderReturnReasonEntity content;
* Channel channel: 当前传输数据的通道
*
* Queue:可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一一个收到此消息
* 场景:
* 1)、订单服务启动多个;同一个消息,只能有一个客户端收到
* 2)、只有一 -个消息完全处理完,方法运行结束,我们就可以接收到下一个消息
*/
@RabbitListener(queues = {"hello-java-queue"})
public void receiveMessage(Message message, OrderReturnReasonEntity entity, Channel channel) throws InterruptedException {
//{"id":1,"name":"啊啊啊","sort":null,"status":null,"createTime":1660210035834}
byte[] body = message.getBody();
//消息的属性信息
//[headers={__TypeId__=com.atguigu.gulimall.order.entity.OrderReturnReasonEntity}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=hello-java-exchange, receivedRoutingKey=hello.java, deliveryTag=1, consumerTag=amq.ctag-5sX1WFjvna_vui2BTkXVdg, consumerQueue=hello-java-queue]
MessageProperties messageProperties = message.getMessageProperties();
String contentType = messageProperties.getContentType();
System.out.println("接收到消息:"+entity);
Thread.sleep(3000);
System.out.println("消息处理完成:" + entity.getName());
}
}

3、接收多种实体对应的消息
在gulimall-order模块com.atguigu.gulimall.order.GulimallOrderApplicationTests测试类里添加sendMessage3方法,用于发送不同实体类型的消息内容
/**
* 发送不同对象类型消息(模拟同一个队列发送不同对象 或 不同队列发送的对象不同)
* 发送OrderReturnReasonEntity对象 ReceiveMessage2类的receiveMessage1方法接收该消息
* 发送OrderEntity对象 ReceiveMessage2类的receiveMessage2方法接收该消息
*
*/
@Test
public void sendMessage3(){
for (int i = 0; i < 10; i++) {
if (i%2==0) {
//发送消息,如果发送的消息是个对象,我们会使用序列化机制,将对象写出去。对象必须实现Serializable
OrderReturnReasonEntity entity = new OrderReturnReasonEntity();
entity.setId(1L);
entity.setCreateTime(new Date());
entity.setName("啊啊啊-->" + i);
//rabbitTemplate.send();
//convertAndSend(String exchange, String routingKey, final Object object)
rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", entity);
log.info("消息发送完成{}", entity);
}else {
OrderEntity entity = new OrderEntity();
entity.setOrderSn(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", entity);
log.info("消息发送完成{}", entity);
}
}
}

重启GulimallOrderApplication服务,执行sendMessage3测试方法,可以看到如果消息内容与前面参数的接收类型不一致,则获取不到别的类型的信息

上节介绍的那种方式只能处理发送的消息内容是对应参数实体的消息,如何处理消息内容是多个不同实体的消息呢?
可以在类上添加@RabbitListener(queues = {"hello-java-queue"})注解,不同方法的参数传不同类型的实体,然后在方法上添加@RabbitHandler注解,用于处理不同对象
package com.atguigu.gulimall.order.test;
import com.atguigu.gulimall.order.entity.OrderEntity;
import com.atguigu.gulimall.order.entity.OrderReturnReasonEntity;
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.stereotype.Component;
/**
* @author 无名氏
* @date 2022/8/11
* @Description: com.atguigu.gulimall.order.GulimallOrderApplicationTests.sendMessage3发送消息
* 测试发放的控制台也有可能收到消息,测试方法的控制台也要看
* 测试该类时,注释掉ReceiveMessage1类的方法
*/
@Component
@RabbitListener(queues = {"hello-java-queue"})
public class ReceiveMessage2 {
@RabbitHandler
public void receiveMessage1(Message message, OrderReturnReasonEntity entity, Channel channel){
System.out.println("OrderReturnReasonEntity类消息处理完成:" + entity.getName());
}
@RabbitHandler
public void receiveMessage2(OrderEntity entity){
System.out.println("OrderEntity类消息处理完成:" + entity.getOrderSn());
}
}

注释掉com.atguigu.gulimall.order.test.ReceiveMessage类的receiveMessage方法

重启gulimall-order模块,再次执行sendMessage3测试方法,查看gulimall-order模块控制台,可以看到,消息被不同的方法正确处理了

6、消息可靠投递与消息可靠抵达
Reliability Guide:可靠投递:https://www.rabbitmq.com/reliability.html

消息可靠抵达: https://www.rabbitmq.com/confirms.html#publisher-confirms
Using standard AMQP 0-9-1, the only way to guarantee that a message isn't lost is by using transactions -- make the channel transactional then for each message or set of messages publish, commit. In this case, transactions are unnecessarily heavyweight and decrease throughput by a factor of 250. To remedy this, a confirmation mechanism was introduced. It mimics the consumer acknowledgements mechanism already present in the protocol.
使用标准 AMQP 0-9-1,保证消息不丢失的唯一方法是使用事务——使通道具有事务性,然后为每条消息或一组消息发布、提交。 在这种情况下,事务不确定是不是重量级的,并且会将吞吐量降低了 250 倍。为了解决这个问题,引入了确认机制。 它模仿了协议中已经存在的消费者确认机制。

RabbitMQ消息确认机制简介
保证消息不丢失,可靠抵达,可以使用事务消息,但性能会下降250倍,为此引入确认机制
publisher confirmCallback 确认模式(发布者 -> 消息代理,这里的消息代理指的是RabbitMQ)
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 。
在org.springframework.amqp.rabbit.core.RabbitTemplate类里定义了ConfirmCallback函数式接口
/**
* A callback for publisher confirmations.
*
*/
@FunctionalInterface
public interface ConfirmCallback {
/**
* Confirmation callback.
* @param correlationData correlation data for the callback.(消息的关联标识,唯一id)
* @param ack true for ack, false for nack (消息有没有正确的收到)
* @param cause An optional cause, for nack, when available, otherwise null. (消息如果没有被正确收到的原因)
*/
void confirm(@Nullable CorrelationData correlationData, boolean ack, @Nullable String cause);
}

想要开启发布者发送消息到消息代理的确认,首先需要将spring.rabbitmq.publisher-confirms设为true
在gulimall-order模块的src/main/resources/application.properties配置文件里添加如下配置
#开启 发布者发送消息到消息代理的确认 publisher->broker(默认false)
spring.rabbitmq.publisher-confirms=true

然后调用rabbitTemplate.setConfirmCallback()方法,参数传ConfirmCallback函数式接口的实现类
在gulimall-order模块的com.atguigu.gulimall.order.config.MyRabbitConfig类里添加initRabbitTemplate方法,用于接收发布者发送消息到消息代理的确认
/**
* 在构造器执行之后执行
* 定制RabbitTemplate
* 1、spring.rabbitmq.publisher-confirms=true
* 2、设置确认回调
*/
@PostConstruct
public void initRabbitTemplate(){
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
*只要消息抵达Broker就ack=true
* @param correlationData 当前消息的唯一关联数据(这个是消息的唯一-id)
* @param ack 消息是否成功收到
* @param cause 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("confirm...CorrelationData==>["+correlationData+"]ack==>["+ack+"]cause==>["+cause+"]");
}
});
}

注释掉gulimall-order模块的com.atguigu.gulimall.order.test.ReceiveMessage类上的方法

注释掉gulimall-order模块的com.atguigu.gulimall.order.test.ReceiveMessage2类上的方法

执行gulimall-order模块的com.atguigu.gulimall.order.GulimallOrderApplicationTests测试类的sendMessage3测试方法,可以看到没有消费者也能收到回调,但是获取的内容是null,这是因为发送消息时没有设置id
confirm...CorrelationData==>[null]ack==>[true]cause==>[null]

修改gulimall-order模块的com.atguigu.gulimall.order.GulimallOrderApplicationTests测试类的sendMessage3测试方法,在发送消息时指定CorrelationData,用于设置唯一id
@Test
public void sendMessage3(){
for (int i = 0; i < 10; i++) {
String uuid = UUID.randomUUID().toString();
if (i%2==0) {
//发送消息,如果发送的消息是个对象,我们会使用序列化机制,将对象写出去。对象必须实现Serializable
OrderReturnReasonEntity entity = new OrderReturnReasonEntity();
entity.setId(1L);
entity.setCreateTime(new Date());
entity.setName("啊啊啊-->" + i);
//rabbitTemplate.send();
//convertAndSend(String exchange, String routingKey, final Object object)
rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", entity,new CorrelationData(uuid));
log.info("消息发送完成{}", entity);
}else {
OrderEntity entity = new OrderEntity();
entity.setOrderSn(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", entity,new CorrelationData(uuid));
log.info("消息发送完成{}", entity);
}
}
}

再次执行sendMessage3测试方法,在gulimall-order控制台就可以看到id信息了,这样就可以知道是哪个消息投递失败了
confirm...CorrelationData==>[CorrelationData [id=a89916d3-4d48-443a-a32e-3a902c773b6d]]ack==>[true]cause==>[null]
confirm...CorrelationData==>[CorrelationData [id=e5d6db9a-a929-4345-b08f-cd2ef6fd24be]]ack==>[true]cause==>[null]
confirm...CorrelationData==>[CorrelationData [id=2b5a0e61-477c-4c5d-9043-df127c917c88]]ack==>[true]cause==>[null]

2、ReturnCallback
spring.rabbitmq.publisher-returns=truespring.rabbitmq.template.mandatory=true
confrim 模式只能保证消息到达 broker,不能保证消息准确投递到目标 queue 里。在有 些业务场景下,我们需要保证消息一定要投递到目标 queue 里,此时就需要用到return 退回模式。
这样如果未能投递到目标 queue 里将调用 returnCallback ,可以记录下详细到投递数 据,定期的巡检或者自动纠错都需要这些数据。
在gulimall-order模块的src/main/resources/application.properties配置文件里添加如下配置,用于开启发送端消息从交换机抵达队列失败的回调
#开启发送端消息从交换机抵达队列失败的回调(默认false)
spring.rabbitmq.publisher-returns=true
#只要抵达队列,以异步发送优先回调我们这个returnConfirm(当然也可以不设置,默认false)
spring.rabbitmq.template.mandatory=true

在gulimall-order模块的com.atguigu.gulimall.order.config.MyRabbitConfig类的initRabbitTemplate方法添加如下代码,用于接收消息从交换机到队列的确认
//消息从交换机抵达队列失败的回调(比如:让路由键匹配不到绑定的交换机或队列)
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("message = " + message + ", replyCode = " + replyCode +
", replyText = " + replyText + ", exchange = " + exchange + ", routingKey = " + routingKey);
}
});

可以看到,只有消息投递到Broker的回调,没有交换机投递到队列的回调,这是因为只有消息从交换机投递到队列失败才会执行ReturnCallback的回调
confirm...CorrelationData==>[null]ack==>[true]cause==>[null]

那么如何让消息从交换机投递到队列失败呢?只需让路由键匹配不到绑定的交换机或队列即可
复制gulimall-order模块的com.atguigu.gulimall.order.GulimallOrderApplicationTests测试类的sendMessage3方法,将方法名修改为sendMessage4并让指定的路由键不对
@Test
public void sendMessage4(){
for (int i = 0; i < 10; i++) {
if (i%2==0) {
//发送消息,如果发送的消息是个对象,我们会使用序列化机制,将对象写出去。对象必须实现Serializable
OrderReturnReasonEntity entity = new OrderReturnReasonEntity();
entity.setId(1L);
entity.setCreateTime(new Date());
entity.setName("啊啊啊-->" + i);
//rabbitTemplate.send();
//convertAndSend(String exchange, String routingKey, final Object object)
rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", entity);
log.info("消息发送完成{}", entity);
}else {
OrderEntity entity = new OrderEntity();
entity.setOrderSn(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("hello-java-exchange", "hello.javxxxxa", entity);
log.info("消息发送完成{}", entity);
}
}
}

执行sendMessage4测试方法,查看gulimall-order模块的控制台即可看到,此时就执行了消息从交换机抵达队列失败的回调
message = (Body:'{"id":null,"memberId":null,"orderSn":"9e4b4e95-4ed3-4ce5-a384-85ce2ee6bdc5","couponId":null,"createTime":null,"memberUsername":null,"totalAmount":null,"payAmount":null,"freightAmount":null,"promotionAmount":null,"integrationAmount":null,"couponAmount":null,"discountAmount":null,"payType":null,"sourceType":null,"status":null,"deliveryCompany":null,"deliverySn":null,"autoConfirmDay":null,"integration":null,"growth":null,"billType":null,"billHeader":null,"billContent":null,"billReceiverPhone":null,"billReceiverEmail":null,"receiverName":null,"receiverPhone":null,"receiverPostCode":null,"receiverProvince":null,"receiverCity":null,"receiverRegion":null,"receiverDetailAddress":null,"note":null,"confirmStatus":null,"deleteStatus":null,"useIntegration":null,"paymentTime":null,"deliveryTime":null,"receiveTime":null,"commentTime":null,"modifyTime":null}' MessageProperties [headers={__TypeId__=com.atguigu.gulimall.order.entity.OrderEntity}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0]), replyCode = 312, replyText = NO_ROUTE, exchange = hello-java-exchange, routingKey = hello.javxxxxa

3、默认的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移除,会投递给别人
先取消注释gulimall-order模块com.atguigu.gulimall.order.test.ReceiveMessage2类的方法

先启动GulimallOrderApplication服务,让其把hello-java-queue里的消息都处理完,再停止GulimallOrderApplication服务,再注释掉com.atguigu.gulimall.order.test.ReceiveMessage2类的方法,再运行测试类的sendMessage4方法,保证hello-java-queue里有5条消息

再打开com.atguigu.gulimall.order.test.ReceiveMessage2类的方法,并在receiveMessage1方法的System.out.println("OrderReturnReasonEntity类消息处理完成:" + entity.getName());这一行打上断点

以debug方式启动GulimallOrderApplication服务,可以看到如果服务停机,队列里的消息直接清零了,这是IDEA的问题,点击IDEA的Stop不会强制停止服务,而是等程序处理完才停止(类似于调用tomcat的shutdown.bat程序,而不是直接关闭startup.bat窗口。即告诉程序要将其关闭了,这个程序接送到关闭指令后可以选择不理会或者把剩余的事情做完后再关闭,而不是直接杀死进程。类似于电脑关机,点击关机后如果文件没保存这不会关机成功,如果文件都保存则自己会执行关闭指令,而并非直接拔掉电源)

使用上面的方法,再次让hello-java-queue里有5条消息
然后再以debug方式启动GulimallOrderApplication服务,可以看到放行一个后队列里减少了一个,然后直接强行杀死java.exe进程,此时队列里的消息还是4个,这样就不会变到0了
taskkill /f /t /im java.exe
可以看到即使服务宕机,消息也不会丢失

4、手动确认消息
在gulimall-order模块的src/main/resources/application.properties配置文件里添加如下配置,将自动回复模式调为手动模式
#将自动回复模式调为手动模式 (默认auto:自动回复)
spring.rabbitmq.listener.simple.acknowledge-mode=manual

注释掉com.atguigu.gulimall.order.test.ReceiveMessage2类的方法,再运行测试类的sendMessage4方法,保证hello-java-queue里有5条消息,再打开com.atguigu.gulimall.order.test.ReceiveMessage2类的方法,并在receiveMessage1方法的System.out.println("OrderReturnReasonEntity类消息处理完成:" + entity.getName());这一行打上断点,以debug方式启动GulimallOrderApplication服务,
可以看到,只要不手动签收,消息就不会减少,服务停掉后,消息状态由Unacked变为了Ready

修改gulimall-order模块的com.atguigu.gulimall.order.test.ReceiveMessage2类的receiveMessage1方法,调用channel.basicAck(deliveryTag,false);手动确认消息
@RabbitHandler
public void receiveMessage1(Message message, OrderReturnReasonEntity entity, Channel channel){
System.out.println("OrderReturnReasonEntity类消息处理完成:" + entity.getName());
//channel信道内按顺序自增
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
//签收货物
//basicAck(long deliveryTag, boolean multiple是否批量确认收货,如果为false只签收当前消息)
channel.basicAck(deliveryTag,false);
System.out.println("签收了第"+deliveryTag+"个货物");
}catch (Exception e){
//网络中断,签收失败
e.printStackTrace();
}
}

再次以debug方式启动GulimallOrderApplication服务,此时手动确认后消息就会减少了

修改gulimall-order模块的com.atguigu.gulimall.order.test.ReceiveMessage2类的receiveMessage1方法,这次让deliveryTag为偶数的签收,deliveryTag为奇数的拒收
@RabbitHandler
public void receiveMessage1(Message message, OrderReturnReasonEntity entity, Channel channel) throws IOException {
System.out.println("OrderReturnReasonEntity类消息处理完成:" + entity.getName());
//channel信道内按顺序自增
long deliveryTag = message.getMessageProperties().getDeliveryTag();
if (deliveryTag % 2 == 0) {
//签收消息
//basicAck(long deliveryTag, boolean multiple是否批量确认收货,如果为false只签收当前消息)
channel.basicAck(deliveryTag, false);
System.out.println("签收了第" + deliveryTag + "个货物");
} else {
//拒收消息
//requeue=false丢弃消息 requeue=true 消息发回服务器,服务器重新入队。
//basicReject(long deliveryTag, boolean requeue)
//channel.basicReject();
//basicNack(long deliveryTag, boolean multiple, boolean requeue)
channel.basicNack(deliveryTag, false, true);
System.out.println("拒签了第"+deliveryTag+"个货物");
}
}

修改gulimall-order模块的com.atguigu.gulimall.order.GulimallOrderApplicationTests测试类,添加sendMessage5方法,重新让hello-java-queue里有5条消息
@Test
public void sendMessage5() {
for (int i = 1; i <= 5; i++) {
String uuid = UUID.randomUUID().toString();
//发送消息,如果发送的消息是个对象,我们会使用序列化机制,将对象写出去。对象必须实现Serializable
OrderReturnReasonEntity entity = new OrderReturnReasonEntity();
entity.setId(1L);
entity.setCreateTime(new Date());
entity.setName("发送第【" + i + "】个货物");
//rabbitTemplate.send();
//convertAndSend(String exchange, String routingKey, final Object object)
rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", entity, new CorrelationData(uuid));
log.info("消息发送完成{}", entity);
}
}

查看gulimall-order模块的控制台,可以看到拒绝签收的货物会重新入队
OrderReturnReasonEntity类消息处理完成:发送第【1】个货物
拒签了第1个货物
OrderReturnReasonEntity类消息处理完成:发送第【2】个货物
签收了第2个货物
OrderReturnReasonEntity类消息处理完成:发送第【3】个货物
拒签了第3个货物
OrderReturnReasonEntity类消息处理完成:发送第【4】个货物
签收了第4个货物
OrderReturnReasonEntity类消息处理完成:发送第【5】个货物
拒签了第5个货物
OrderReturnReasonEntity类消息处理完成:发送第【1】个货物
签收了第6个货物
OrderReturnReasonEntity类消息处理完成:发送第【3】个货物
拒签了第7个货物
OrderReturnReasonEntity类消息处理完成:发送第【5】个货物
签收了第8个货物
OrderReturnReasonEntity类消息处理完成:发送第【3】个货物
拒签了第9个货物
OrderReturnReasonEntity类消息处理完成:发送第【3】个货物
签收了第10个货物

5.8.3、基本页面准备
1、添加文件
1、添加购物车详情页
在linux虚拟机里的/mydata/nginx/html/static目录下新建order目录,在/mydata/nginx/html/static/order目录下新建detail目录,将2.分布式高级篇(微服务架构篇)\资料源码\代码\html\等待付款里的文件夹全部复制到linux虚拟机里的/mydata/nginx/html/static/order/detail目录下(不包括index.html)

在gulimall-order模块的src/main/resources新建templates文件夹,将2.分布式高级篇(微服务架构篇)\资料源码\代码\html\等待付款里的index.html复制到src/main/resources/templates里面,并将刚刚粘贴的index.html重命名为detail.html(我图片上的名字写错了,文件名应该是detail.html)

2、添加全部订单页
在linux虚拟机里的/mydata/nginx/html/static/order目录下新建list目录,将\2.分布式高级篇(微服务架构篇)\资料源码\代码\html\订单页里的文件夹全部复制到linux虚拟机里的/mydata/nginx/html/static/order/list目录下(不包括index.html)

将2.分布式高级篇(微服务架构篇)\资料源码\代码\html\订单页里的index.html复制到src/main/resources/templates里面,并将刚刚粘贴的index.html重命名为list.html

3、添加确认支付页
在linux虚拟机里的/mydata/nginx/html/static/order目录下新建confirm目录,将\2.分布式高级篇(微服务架构篇)\资料源码\代码\html\结算页里的文件夹全部复制到linux虚拟机里的/mydata/nginx/html/static/order/confirm目录下(不包括index.html)

将2.分布式高级篇(微服务架构篇)\资料源码\代码\html\结算页里的index.html复制到src/main/resources/templates里面,并将刚刚粘贴的index.html重命名为confirm.html

4、添加支付页
在linux虚拟机里的/mydata/nginx/html/static/order目录下新建pay目录,将\2.分布式高级篇(微服务架构篇)\资料源码\代码\html\收银页里的文件夹全部复制到linux虚拟机里的/mydata/nginx/html/static/order/pay目录下(不包括index.html)

将2.分布式高级篇(微服务架构篇)\资料源码\代码\html\收银页里的index.html复制到src/main/resources/templates里面,并将刚刚粘贴的index.html重命名为pay.html

2、修改配置
1、添加域名
在hosts文件里添加order.gulimall.com域名

2、负载均衡到订单模块
在gulimall-gateway网关模块的src/main/resources/application.yml配置文件里添加如下配置,负载均衡到订单模块
spring:
cloud:
gateway:
routes:
- id: gulimall_order_route
uri: lb://gulimall-order
predicates:
- Host=order.gulimall.com

3、修改页面
修改gulimall-order模块的src/main/resources/templates/confirm.html文件,将src="全部替换为src="/static/order/confirm/,将href="全部替换为href="/static/order/confirm/
src="
src="/static/order/confirm/
href="
href="/static/order/confirm/

修改gulimall-order模块的src/main/resources/templates/detail.html文件(我图片上的名字写错了,文件名应该是detail.html),将href="全部替换为href="/static/order/detail/,将src="全部替换为src="/static/order/detail/
href="
href="/static/order/detail/
src="
src="/static/order/detail/

修改gulimall-order模块的src/main/resources/templates/list.html文件,将href="全部替换为href="/static/order/list/,将src="全部替换为src="/static/order/list/
href="
href="/static/order/list/
src="
src="/static/order/list/

修改gulimall-order模块的src/main/resources/templates/pay.html文件,将href="全部替换为href="/static/order/pay/,将src="全部替换为src="/static/order/pay/
href="
href="/static/order/pay/
src="
src="/static/order/pay/

后面发现detail.html名字写错了,重新将gulimall-order模块的src/main/resources/templates文件夹里面的deatil.html改名为detail.html

3、添加配置
1、引入thymeleaf
在gulimall-order模块的com.atguigu.gulimall.order包下新建web文件夹,在web文件夹下新建HelloController类,用于跳转到对应的页面
package com.atguigu.gulimall.order.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* @author 无名氏
* @date 2022/8/12
* @Description:
*/
@Controller
public class HelloController {
@GetMapping("/{page}.html")
public String listPage(@PathVariable("page") String page){
return page;
}
}

在gulimall-order模块的pom.xml文件中引入thymeleaf依赖
<!--模板引擎:thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

在gulimall-order模块的src/main/resources/application.properties配置文件里添加如下配置,关闭thymeleaf缓存
spring.thymeleaf.cache=false

2、开启服务发现
在gulimall-order模块的com.atguigu.gulimall.order.GulimallOrderApplication启动类里添加如下注解,开启服务发现功能
@EnableDiscoveryClient

在gulimall-order模块的src/main/resources/application.properties配置文件里添加如下配置,配置服务发现的服务端地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.application.name=gulimall-order

启动GulimallOrderApplication服务和GulimallGatewayApplication服务
浏览器访问 http://order.gulimall.com/list.html 页面,可以看到已经成功显示了

浏览器访问 http://order.gulimall.com/confirm.html 页面,只有顶部的显示了,很明显:下面的渲染失败了

查看gulimall-order模块控制台,报了未完成的块结构的异常(其实就是只有/*没有*/的意思)
java.io.IOException: Unfinished block structure <!--/*...*/-->
在gulimall-order模块的src/main/resources/templates/confirm.html页面里搜索/*,把这个/*删掉

重启gulimall-order模块,浏览器再次访问 http://order.gulimall.com/confirm.html 页面就可以发现访问成功了

浏览器访问 http://order.gulimall.com/detail.html 页面,也成功显示了

浏览器访问 http://order.gulimall.com/pay.html 页面,也成功显示了

3、引入Spring Session
在gulimall-order模块的pom.xml文件里引入Spring Session
<!--引入redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--引入SpringSession-->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

在gulimall-order模块的src/main/resources/application.properties配置文件里添加如下配置,指定session存储用redis
#使用redis做session
spring.session.store-type=redis

在gulimall-order模块的src/main/resources/application.yml配置文件里添加如下配置,配置redis的host
spring:
redis:
host: 192.168.56.10
port: 6379

复制gulimall-product模块的com.atguigu.gulimall.product.config.GulimallSessionConfig,粘贴到gulimall-order模块的com.atguigu.gulimall.order.config包下

4、添加线程池
再将gulimall-product模块的com.atguigu.gulimall.product.config包下的MyThreadConfig.java文件和ThreadPollConfigProperties.java文件复制一份,粘贴到gulimall-order模块的com.atguigu.gulimall.order.config包下

复制一份gulimall-product模块的src/main/resources/application.properties配置文件的线程池配置,粘贴到gulimall-order模块的src/main/resources/application.properties配置文件里
gulimall.thread.core-pool-size=20
gulimall.thread.maximum-pool-size=200
gulimall.thread.keep-alive-time=10

5、完善页面
在 http://gulimall.com/ 页面里,打开控制台,定位到我的订单位置,复制我的订单

在gulimall-product模块的src/main/resources/templates/index.html文件夹搜索我的订单,将该文本对应的<a>标签的href的值修改为http://order.gulimall.com/list.html
<a href="http://order.gulimall.com/list.html">我的订单</a>

在 http://order.gulimall.com/detail.html 页面里,打开控制台,定位到你好,请登录位置,复制你好

在gulimall-order模块的src/main/resources/templates/detail.html文件里,将<html>修改为<html xmlns:th="http://www.thymeleaf.org">,以引入thymeleaf
<html xmlns:th="http://www.thymeleaf.org">

然后在detail.html文件里搜索你好,修改周围的代码
<li style="border: 0;">
<a th:if="${session.loginUser!=null}">欢迎:[[${session.loginUser?.nickname}]]</a>
<a href="http://auth.gulimall.com/login.html" th:if="${session.loginUser==null}">你好,请登录</a>
</li>
<li>
<a href="http://auth.gulimall.com/reg.html" th:if="${session.loginUser==null}" style="color: red;">免费注册</a>
|</li>

在 http://order.gulimall.com/confirm.html 页面里,打开控制台,定位到尚硅谷位置,复制尚硅谷

在gulimall-order模块的src/main/resources/templates/confirm.html文件里,将<html>修改为<html xmlns:th="http://www.thymeleaf.org">,以引入thymeleaf
<html xmlns:th="http://www.thymeleaf.org">

然后在confirm.html文件里搜索尚硅谷,将尚硅谷替换为用户的昵称
<li><!--尚硅谷-->[[${session.loginUser?.nickname}]]<img src="/static/order/confirm/img/03.png" style="margin-bottom: 0px;margin-left3: 3px;" /><img src="/static/order/confirm/img/06.png" /></li>

在 http://order.gulimall.com/pay.html 页面里,打开控制台,定位到尚硅谷位置,复制尚硅谷

在gulimall-order模块的src/main/resources/templates/pay.html文件里,将<html>修改为<html xmlns:th="http://www.thymeleaf.org">,以引入thymeleaf
<html xmlns:th="http://www.thymeleaf.org">

然后在pay.html文件里搜索尚硅谷,将尚硅谷替换为用户的昵称
<li><span><!--尚硅谷-->[[${session.loginUser?.nickname}]]</span><span>退出</span></li>

重启gulimall-order模块和gulimall-product模块,浏览器打开以下页面,可以发现都可以正常访问
http://order.gulimall.com/list.html
http://order.gulimall.com/pay.html
http://order.gulimall.com/detail.html
http://order.gulimall.com/confirm.html

六、订单
6.1、订单模块
6.1.1、编写基本功能
1、基本功能
1、订单中心
电商系统涉及到 3 流,分别时信息流,资金流,物流,而订单系统作为中枢将三者有机的集合起来。 用户信息包括用户账号、用户等级、用户的收货地址、收货人、收货人电话等组成,用户账户需要绑定手机号码,但是用户绑定的手机号码不一定是收货信息上的电话。用户可以添加多个收货信息,用户等级信息可以用来和促销系统进行匹配,获取商品折扣,同时用户等级还可以获取积分的奖励等 2、订单基础信息订单基础信息是订单流转的核心,其包括订单类型、父/子订单、订单编号、订单状态、订单流转的时间等。 (1)订单类型包括实体商品订单和虚拟订单商品等,这个根据商城商品和服务类型进行区分。(2)同时订单都需要做父子订单处理,之前在初创公司一直只有一个订单,没有做父子订单处理后期需要进行拆单的时候就比较麻烦,尤其是多商户商场,和不同仓库商品的时候,父子订单就是为后期做拆单准备的。 (3)订单编号不多说了,需要强调的一点是父子订单都需要有订单编号,需要完善的时候可以对订单编号的每个字段进行统一定义和诠释。 (4)订单状态记录订单每次流转过程,后面会对订单状态进行单独的说明。 (5)订单流转时间需要记录下单时间,支付时间,发货时间,结束时间/关闭时间等等 3、商品信息商品信息从商品库中获取商品的 SKU 信息、图片、名称、属性规格、商品单价、商户信息等,从用户下单行为记录的用户下单数量,商品合计价格等。4.优惠信息优惠信息记录用户参与的优惠活动,包括优惠促销活动,比如满减、满赠、秒杀等,用户使用的优惠券信息,优惠券满足条件的优惠券需要默认展示出来,具体方式已在之前的优惠券篇章做过详细介绍,另外还虚拟币抵扣信息等进行记录。 为什么把优惠信息单独拿出来而不放在支付信息里面呢?因为优惠信息只是记录用户使用的条目,而支付信息需要加入数据进行计算,所以做为区分。 5.支付信息 (1)支付流水单号,这个流水单号是在唤起网关支付后支付通道返回给电商业务平台的支付流水号,财务通过订单号和流水单号与支付通道进行对账使用。 (2)支付方式用户使用的支付方式,比如微信支付、支付宝支付、钱包支付、快捷支付等。 支付方式有时候可能有两个——余额支付+第三方支付。 (3)商品总金额,每个商品加总后的金额;运费,物流产生的费用;优惠总金额,包括促销活动的优惠金额,优惠券优惠金额,虚拟积分或者虚拟币抵扣的金额,会员折扣的金额等之和;实付金额,用户实际需要付款的金额。 用户实付金额=商品总金额+运费-优惠总金额
6.物流信息物流信息包括配送方式,物流公司,物流单号,物流状态,物流状态可以通过第三方接口来获取和向用户展示物流每个状态节点。
2、订单状态
- 待付款 用户提交订单后,订单进行预下单,目前主流电商网站都会唤起支付,便于用户快速完成支付,需要注意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超时后将自动取消订单,订单变更关闭状态。
- 已付款/待发货 用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到 WMS系统,仓库进行调拨,配货,分拣,出库等操作。
- 待收货/已发货 仓储将商品出库后,订单进入物流环节,订单系统需要同步物流信息,便于用户实时知悉物品物流状态
- 已完成 用户确认收货后,订单交易完成。后续支付侧进行结算,如果订单存在问题进入售后状态
- 已取消 付款之前取消订单。包括超时未付款或用户商户取消订单都会产生这种订单状态。
- 售后中 用户在付款后申请退款,或商家发货后用户申请退换货。售后也同样存在各种状态,当发起售后申请后生成售后订单,售后订单状态为待审核,等待商家审核,商家审核通过后订单状态变更为待退货,等待用户将商品寄回,商家收货后订单状态更新为待退款状态,退款到用户原账户后订单状态更新为售后成功。
售后也同样存在各种状态,当发起售后申请后生成售后订单,售后订单状态为待审核,等待 商家审核,商家审核通过后订单状态变更为待退货,等待用户将商品寄回,商家收货后订单 状态更新为待退款状态,退款到用户原账户后订单状态更新为售后成功。

3、订单流程
订单流程是指从订单产生到完成整个流转的过程,从而行程了一套标准流程规则。而不同的 产品类型或业务类型在系统中的流程会千差万别,比如上面提到的线上实物订单和虚拟订单 的流程,线上实物订单与 O2O 订单等,所以需要根据不同的类型进行构建订单流程。 不管类型如何订单都包括正向流程和逆向流程,对应的场景就是购买商品和退换货流程,正 向流程就是一个正常的网购步骤:
订单生成–>支付订单–>卖家发货–>确认收货–>交易成功。 而每个步骤的背后,订单是如何在多系统之间交互流转的,可概括如下图

2、去结算
1、修改页面
在 http://cart.gulimall.com/cart.html 页面里, 打开控制台,定位到去结算位置,复制去结算

在gulimall-cart模块的src/main/resources/templates/cartList.html配置文件里搜去结算,会发现去结算 会调用 toTrade()

在gulimall-cart模块的src/main/resources/templates/cartList.html的<script>标签里添加toTrade方法,让其跳转到http://order.gulimall.com/toTrade页面
function toTrade() {
window.location.href = "http://order.gulimall.com/toTrade";
}

2、添加拦截器等配置
在gulimall-order模块的com.atguigu.gulimall.order.web包下,新建OrderWebController类,添加toTrade方法,将/toTrade请求返回confirm.html页面
package com.atguigu.gulimall.order.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @author 无名氏
* @date 2022/8/12
* @Description:
*/
@Controller
public class OrderWebController {
@GetMapping("/toTrade")
public String toTrade(){
return "confirm";
}
}

修改gulimall-auth-server模块的com.atguigu.gulimall.auth.controller.LoginController类的login方法
如果登录成功会在redis里放一个MemberEntityTo
@PostMapping("/login")
public String login(UserLoginVo vo, RedirectAttributes redirectAttributes, HttpSession session){
R r = memberFeignService.login(vo);
if (r.getCode()==0){
Object data = r.get("data");
String json = JSON.toJSONString(data);
MemberEntityTo memberEntityTo = JSON.parseObject(json, MemberEntityTo.class);
session.setAttribute(AuthServerConstant.LOGIN_USER,memberEntityTo);
return "redirect:http://gulimall.com";
}else {
Map<String, String> errors = new HashMap<>();
errors.put("msg",r.getMsg());
redirectAttributes.addFlashAttribute("errors",errors);
return "redirect:http://auth.gulimall.com/login.html";
}
}

在gulimall-order模块的com.atguigu.gulimall.order包下新建interceptor文件夹,在interceptor文件夹里添加LoginUserInterceptor类。如果登录了,就把MemberEntityTo放到ThreadLocal里
package com.atguigu.gulimall.order.interceptor;
import com.atguigu.common.constant.auth.AuthServerConstant;
import com.atguigu.common.to.MemberEntityTo;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author 无名氏
* @date 2022/8/12
* @Description: 添加拦截器
*/
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberEntityTo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Object attribute = request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute!=null){
MemberEntityTo memberEntityTo= (MemberEntityTo) attribute;
loginUser.set(memberEntityTo);
return true;
}else {
request.getSession().setAttribute("msg","请先进行登录");
//没登陆就重定向到登录页面
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}

在gulimall-order模块的com.atguigu.gulimall.order.config包下新建OrderWebConfig类,指定拦截器的拦截路径
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;
/**
* @author 无名氏
* @date 2022/8/12
* @Description:
*/
@Configuration
public class OrderWebConfig implements WebMvcConfigurer {
@Autowired
LoginUserInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor).addPathPatterns("/**");
}
}

在gulimall-auth-server模块的src/main/resources/templates/login.html文件里的<span>谷粒商城不会以任何理由要求您转账汇款,谨防诈骗。</span>下面添加如下代码,如果用户未登录或登录失败了给与一些提示
<br><span style="color: red"> [[${session?.msg}]]</span>

重启gulimall-auth-server模块,在购物车页面 http://cart.gulimall.com/cart.html 里点击去结算,如果没有登录就会来到了登录页 http://auth.gulimall.com/login.html ,并会提示请先进行登录

登录成功后,就可以正常购买了

3、确认订单
在gulimall-order模块的com.atguigu.gulimall.order包下新建vo文件夹,在vo文件夹下新建OrderConfirmVo类

在gulimall-order模块的com.atguigu.gulimall.order.web.OrderWebController类里修改toTrade方法
@Autowired
OrderService orderService;
@GetMapping("/toTrade")
public String toTrade(Model model){
OrderConfirmVo orderConfirmVo = orderService.confirmOrder();
model.addAttribute("orderConfirmData",orderConfirmVo);
//订单确认页
return "confirm";
}

在gulimall-order模块的com.atguigu.gulimall.order.service.OrderService接口里添加confirmOrder方法
/**
* 给订单确认页返回需要的数据
* @return
*/
OrderConfirmVo confirmOrder();

在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类里实现confirmOrder方法
@Override
public OrderConfirmVo confirmOrder() {
OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
return orderConfirmVo;
}

4、获取用户地址
在gulimall-member模块的com.atguigu.gulimall.member.controller.MemberReceiveAddressController类里添加getAddress方法
@GetMapping("/{memberId}/address")
public List<MemberReceiveAddressEntity> getAddress(@PathVariable("memberId") Long memberId) {
return memberReceiveAddressService.getAddress(memberId);
}

在gulimall-member模块的com.atguigu.gulimall.member.service.MemberReceiveAddressService接口里添加getAddress方法
/**
* 获取会员的收货地址列表
* @param memberId
* @return
*/
List<MemberReceiveAddressEntity> getAddress(Long memberId);

在gulimall-member模块的com.atguigu.gulimall.member.service.impl.MemberReceiveAddressServiceImpl类里实现getAddress方法
@Override
public List<MemberReceiveAddressEntity> getAddress(Long memberId) {
LambdaQueryWrapper<MemberReceiveAddressEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(MemberReceiveAddressEntity::getMemberId,memberId);
return this.baseMapper.selectList(lambdaQueryWrapper);
}

5、开启远程调用
在gulimall-order模块的com.atguigu.gulimall.order.GulimallOrderApplication类上添加如下注解,用于开启远程调用
@EnableFeignClients

在gulimall-order模块的com.atguigu.gulimall.order.feign.MemberFeignService接口里添加getAddress方法
@GetMapping("/member/memberreceiveaddress/{memberId}/address")
List<OrderConfirmVo.MemberAddressVo> getAddress(@PathVariable("memberId") Long memberId);

在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类里,修改confirmOrder方法
@Override
public OrderConfirmVo confirmOrder() {
OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
MemberEntityTo memberEntityTo = LoginUserInterceptor.loginUser.get();
//1、远程查询所有的收货地址列表
List<OrderConfirmVo.MemberAddressVo> address = memberFeignService.getAddress(memberEntityTo.getId());
orderConfirmVo.setAddress(address);
//2、远程查询购物车所有选中的购物项
return orderConfirmVo;
}

6、获取用户购物项
在gulimall-cart类的com.atguigu.gulimall.cart.controller.CartController类里,添加getCurrentUserCartItems方法
@GetMapping("/currentUserCartItems")
public List<CartItemVo> getCurrentUserCartItems(){
return cartService.getUserCartItems();
}

在gulimall-cart模块的com.atguigu.gulimall.cart.service.CartService接口里添加getUserCartItems方法
List<CartItemVo> getUserCartItems();

在gulimall-cart模块的com.atguigu.gulimall.cart.service.impl.CartServiceImpl类里实现getUserCartItems方法
@Override
public List<CartItemVo> getUserCartItems() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
if (userInfoTo == null) {
return null;
}else {
String cartKey = CART_PREFIX + userInfoTo.getUserId();
List<CartItemVo> cartItems = getCartItems(cartKey);
if (cartItems!=null) {
//获取所有被选中的购物项
List<CartItemVo> collect = cartItems.stream().filter(CartItemVo::getCheck)
.map(item->{
//获取商品最新价格
//item.setPrice();
return item;
})
.collect(Collectors.toList());
return null;
}else {
return null;
}
}
}

7、查询最新价格
在gulimall-product模块的com.atguigu.gulimall.product.controller.SkuInfoController类里添加getPrice方法,用于查询商品最新价格
/**
* 实时查询商品价格
* @param skuId
* @return
*/
@GetMapping("/{skuId}/price")
public BigDecimal getPrice(@PathVariable("skuId") Long skuId){
SkuInfoEntity skuInfoEntity = skuInfoService.getById(skuId);
return skuInfoEntity.getPrice();
}

在gulimall-cart模块的com.atguigu.gulimall.cart.feign.ProductFeignService接口里添加getPrice方法
@GetMapping("/product/skuinfo/{skuId}/price")
public BigDecimal getPrice(@PathVariable("skuId") Long skuId);

在gulimall-cart模块的com.atguigu.gulimall.cart.service.impl.CartServiceImpl类里修改getUserCartItems方法
@Override
public List<CartItemVo> getUserCartItems() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
if (userInfoTo == null) {
return null;
}else {
String cartKey = CART_PREFIX + userInfoTo.getUserId();
List<CartItemVo> cartItems = getCartItems(cartKey);
if (cartItems!=null) {
//获取所有被选中的购物项
List<CartItemVo> collect = cartItems.stream().filter(CartItemVo::getCheck)
.map(item->{
//获取商品最新价格
BigDecimal price = productFeignService.getPrice(item.getSkuId());
item.setPrice(price);
return item;
})
.collect(Collectors.toList());
return null;
}else {
return null;
}
}
}

在gulimall-order模块的com.atguigu.gulimall.order.feign包下新建CartFeignService接口,用于调用购物车模块
package com.atguigu.gulimall.order.feign;
import com.atguigu.gulimall.order.vo.OrderConfirmVo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
/**
* @author 无名氏
* @date 2022/8/15
* @Description:
*/
@FeignClient("gulimall-cart")
public interface CartFeignService {
@GetMapping("/currentUserCartItems")
List<OrderConfirmVo.OrderItemVo> getCurrentUserCartItems();
}

在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类里修改confirmOrder方法
@Override
public OrderConfirmVo confirmOrder() {
OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
MemberEntityTo memberEntityTo = LoginUserInterceptor.loginUser.get();
//1、远程查询所有的收货地址列表
List<OrderConfirmVo.MemberAddressVo> address = memberFeignService.getAddress(memberEntityTo.getId());
orderConfirmVo.setAddress(address);
//2、远程查询购物车所有选中的购物项
List<OrderConfirmVo.OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
orderConfirmVo.setItems(items);
//3、查询用户积分
Integer integration = memberEntityTo.getIntegration();
orderConfirmVo.setIntegration(integration);
//orderConfirmVo.setIntegration(orderConfirmVo.getIntegration());
orderConfirmVo.setPayPrice(orderConfirmVo.getPayPrice());
orderConfirmVo.setTotal(orderConfirmVo.getTotal());
//TODO 防重令牌
return orderConfirmVo;
}

在gulimall-order模块的com.atguigu.gulimall.order.vo.OrderConfirmVo类里修改getPayPrice方法和getTotal方法,添加orderToken字段
public class OrderConfirmVo {
...
/**
* 令牌,防止重复提交
*/
@Getter @Setter
String orderToken;
public BigDecimal getTotal() {
BigDecimal sum = new BigDecimal("0");
if (!CollectionUtils.isEmpty(items)){
for (OrderItemVo item : items) {
BigDecimal bigDecimal = item.getPrice().multiply(new BigDecimal(item.getCount()));
sum = sum.add(bigDecimal);
}
}
return sum;
}
public BigDecimal getPayPrice() {
return getTotal();
}
...
}

3、测试
1、测试一
在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类的confirmOrder方法的List<OrderConfirmVo.MemberAddressVo> address = memberFeignService.getAddress(memberEntityTo.getId());和List<OrderConfirmVo.OrderItemVo> items = cartFeignService.getCurrentUserCartItems();上打断点
启动GulimallThirdPartyApplication、GulimallSearchApplication、GulimallGatewayApplication、GulimallProductApplication、GulimallAuthServerApplication服务,以debug方式启动GulimallOrderApplication、GulimallMemberApplication、GulimallCartApplication服务

登录后,在 http://gulimall.com/ 页面点击 我的购物车 -> 去结算,程序就会停到断点

切换到IDEA,此时断点停在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类的confirmOrder方法的List<OrderConfirmVo.MemberAddressVo> address = memberFeignService.getAddress(memberEntityTo.getId());上,此时已经获取到7号用户的基本信息了

查看gulimall_ums数据库的ums_member_receive_address表,可以看到7号用户此时还没有收货地址

在gulimall_ums数据库的ums_member_receive_address表里,随便给member_id为7的用户增加点信息

再次切换到IDEA,点击GulimallOrderApplication服务的Step Over(步过)按钮,执行当前方法的下一个语句,报了no-argument constructor没有无参构造的错误
2022-08-15 10:04:23.391 ERROR 5340 --- [nio-9000-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is feign.codec.DecodeException: Error while extracting response for type [java.util.List<com.atguigu.gulimall.order.vo.OrderConfirmVo$MemberAddressVo>] and content type [application/json;charset=UTF-8]; nested exception is org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `com.atguigu.gulimall.order.vo.OrderConfirmVo$MemberAddressVo` (although at least one Creator exists): can only instantiate non-static inner class by using default, no-argument constructor; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.atguigu.gulimall.order.vo.OrderConfirmVo$MemberAddressVo` (although at least one Creator exists): can only instantiate non-static inner class by using default, no-argument constructor
at [Source: (PushbackInputStream); line: 1, column: 3] (through reference chain: java.util.ArrayList[0])] with root cause
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.atguigu.gulimall.order.vo.OrderConfirmVo$MemberAddressVo` (although at least one Creator exists): can only instantiate non-static inner class by using default, no-argument constructor

给MemberAddressVo和OrderItemVo加上static即可,点击查看完整代码
给gulimall-order模块的com.atguigu.gulimall.order.vo.OrderConfirmVo类的内部类MemberAddressVo加static修饰符

给gulimall-order模块的com.atguigu.gulimall.order.vo.OrderConfirmVo类的内部类OrderItemVo加static修饰符

2、测试二
重启gulimall-order模块,刷新http://order.gulimall.com/toTrade页面,再次来到了List<OrderConfirmVo.MemberAddressVo> address = memberFeignService.getAddress(memberEntityTo.getId());这个断点

再在gulimall-cart模块的com.atguigu.gulimall.cart.service.impl.CartServiceImpl类的getUserCartItems方法的第一行上打断点

点击GulimallCartApplication服务8: Services里的Resume Program F9按钮,跳转到下一处断点
点击两次该按钮,来到gulimall-cart模块的com.atguigu.gulimall.cart.service.impl.CartServiceImpl类的getUserCartItems方法的第一行

点击GulimallCartApplication服务的Step Over(步过)按钮,执行完UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();获取用户登录状态的代码,此时userId却为空

但是浏览器访问http://cart.gulimall.com/cart.html页面,可以看到明明是登录状态确获取不到

再次点击GulimallCartApplication服务8: Services里的Resume Program F9按钮,准备跳转到下一处断点时就抛异常了
2022-08-15 10:20:40.489 ERROR 2368 --- [o-30000-exec-10] org.thymeleaf.TemplateEngine : [THYMELEAF][http-nio-30000-exec-10] Exception processing template "currentUserCartItems": Error resolving template [currentUserCartItems], template might not exist or might not be accessible by any of the configured Template Resolvers

在gulimall-cart模块的com.atguigu.gulimall.cart.interceptor.CartInterceptor类的preHandle方法的HttpSession session = request.getSession();这一行上打断点

然后浏览器刷新http://order.gulimall.com/toTrade页面,切换到IDEA,点击2次GulimallCartApplication服务的Step Over(步过)按钮,跳转到preHandle方法的if (userInfoTo == null) {这一行,可以看到此时的member是空的,点击8: Services里的Resume Program F9按钮,让这个线程执行完

切换到浏览器,刷新http://cart.gulimall.com/cart.html页面,可以看到请求头的Cookie里有GULIMALL_JSESSIONID

可以看到页面访问时一切是正常的,这些信息都能获取到,然后一直点击Resume Program F9按钮放行完这个请求

页面也是能正常访问的

6.1.2、Feign丢失请求头
1、Feign远程调用丢失请求头
1、源码调试
打开浏览器,刷新http://order.gulimall.com/toTrade页面
一直放行到gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类的confirmOrder方法的List<OrderConfirmVo.OrderItemVo> items = cartFeignService.getCurrentUserCartItems();这一行,可以看到这个cartFeignService是一个代理对象,step into进来

首先判断是不是equals、hashCode、toString方法,如果不是则执行dispatch.get(method).invoke(args);
点击Step Over直到最后一行
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("equals".equals(method.getName())) {
try {
Object otherHandler =
args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
} catch (IllegalArgumentException e) {
return false;
}
} else if ("hashCode".equals(method.getName())) {
return hashCode();
} else if ("toString".equals(method.getName())) {
return toString();
}
return dispatch.get(method).invoke(args);
}

再点击Step Into,选择invoke,一般调用invoke方法就开始准备执行核心方法了

没有传参,所以argv为null,先拿到一个克隆的重试器Retryer
@Override
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
return executeAndDecode(template);
} catch (RetryableException e) {
try {
retryer.continueOrPropagate(e);
} catch (RetryableException th) {
Throwable cause = th.getCause();
if (propagationPolicy == UNWRAP && cause != null) {
throw cause;
} else {
throw th;
}
}
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}

运行到这里,executeAndDecode(template);这里才是真正的执行,点击Step Into

先准备一个请求的模板,指定了请求的path
Object executeAndDecode(RequestTemplate template) throws Throwable {
Request request = targetRequest(template);
if (logLevel != Logger.Level.NONE) {
logger.logRequest(metadata.configKey(), logLevel, request);
}
Response response;
long start = System.nanoTime();
try {
response = client.execute(request, options);
} catch (IOException e) {
if (logLevel != Logger.Level.NONE) {
logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
}
throw errorExecuting(request, e);
}
......

先得到当前请求Request request = targetRequest(template);,然后利用客户端去执行client.execute(request, options);
在Request request = targetRequest(template);这一行,点击Step Into看怎么得到的请求

feign在远程调用之前要构造请求,拿到所有的request拦截器for(RequestInterceptor interceptor : requestInterceptors),然后调用各拦截器的apply方法。但是我们这里没有拦截器,所以feign没有什么功能要增强的,所以将原生的RequestTemplate给传递过来,但此时的template的queries(请求参数),headers(请求头)都为0个,问题就出现在这了,参数的确为0。但是请求头里应该有cookie啊,请求头至少也需要有一个cookie啊
Request targetRequest(RequestTemplate template) {
for (RequestInterceptor interceptor : requestInterceptors) {
interceptor.apply(template);
}
return target.apply(template);
}

Feign远程调用丢失请求头问题可以用下图描述

解决办法如下图描述

2、查看该拦截器
由于使用feign进行远程调用时会重新创建一个新的request,所以请求头丢失了,但feign在进行远程调用之前会遍历所有的RequestInterceptor,调用其apply方法,因此我们可以实现feign.RequestInterceptor接口,将cookie添加到该request的请求头中即可
public interface RequestInterceptor {
/**
* Called for every request. Add data using methods on the supplied {@link RequestTemplate}.
*/
void apply(RequestTemplate template);
}

回到feign.SynchronousMethodHandler类,可以看到SynchronousMethodHandler的构造器里会得到所有的request拦截器,按ctrl点击这个构造器,看看哪个类使用了该构造器

可以看到这个feign.SynchronousMethodHandler类的内部类Factory的create方法返回MethodHandler类型,SynchronousMethodHandler类实现了MethodHandler接口(这个MethodHandler也需要requestInterceptors)
点击查看SynchronousMethodHandler类完整代码

点击最开始的feign.ReflectiveFeign类继承的Feign类

在Feign抽象类里调用的SynchronousMethodHandler对象的Factory方法的参数中也需要requestInterceptors
public Feign build() {
SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
logLevel, decode404, closeAfterDecode, propagationPolicy);
ParseHandlersByName handlersByName =
new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
errorDecoder, synchronousMethodHandlerFactory);
return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}
}

而这个拦截器默认是空的,相当于什么都没有
private final List<RequestInterceptor> requestInterceptors =
new ArrayList<RequestInterceptor>();

但是容器中只有一个RequestInterceptor,就会将这个requestInterceptor给我们添进来。
如果容器中有多个,就会清空this.requestInterceptors,然后将这些都添加进this.requestInterceptors里
/**
* Adds a single request interceptor to the builder.
*/
public Builder requestInterceptor(RequestInterceptor requestInterceptor) {
this.requestInterceptors.add(requestInterceptor);
return this;
}
/**
* Sets the full set of request interceptors for the builder, overwriting any previous
* interceptors.
*/
public Builder requestInterceptors(Iterable<RequestInterceptor> requestInterceptors) {
this.requestInterceptors.clear();
for (RequestInterceptor requestInterceptor : requestInterceptors) {
this.requestInterceptors.add(requestInterceptor);
}
return this;
}

3、添加拦截器
在gulimall-order模块的com.atguigu.gulimall.order.config包里新建GuliFeignConfig类
package com.atguigu.gulimall.order.config;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author 无名氏
* @date 2022/8/15
* @Description:
*/
@Configuration
public class GuliFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
System.out.println("feign远程之前先进行RequestInterceptor.apply");
}
};
}
}

要想在拦截器里获取之前的request请求,我们可以在Controller里添加HttpServletRequest httpServletRequest参数,然后使用ThreadLocal共享数据,不过Spring团队已经考虑到我们可能需要经常获取这些数据了,已经封装了一个工具类叫RequestContextHolder(可以看到Spring团队也是用的ThreadLocal)
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<>("Request context");

ServletRequestAttributes继承了AbstractRequestAttributes

AbstractRequestAttributes实现了RequestAttributes

4、测试
访问http://order.gulimall.com/toTrade请求时请求头会带上Cookie,不过http://order.gulimall.com/toTrade请求的Cookie好像少了user-key,这里应该有user-key的

在gulimall-order模块的com.atguigu.gulimall.order.config.GulimallSessionConfig类上添加@EnableRedisHttpSession方法,开启Spring Session
@EnableRedisHttpSession

在gulimall-cart模块的com.atguigu.gulimall.cart.interceptor.CartInterceptor类的postHandle方法的cookie.setPath("gulimall.com");打上断点,在 http://cart.gulimall.com/cart.html 页面里清空cookie,然后刷新 http://cart.gulimall.com/cart.html 页面。可以发现设置错了,应该设置的是domain,而不是path

将gulimall-cart模块的com.atguigu.gulimall.cart.interceptor.CartInterceptor类的postHandle方法里的cookie.setPath("gulimall.com");修改为cookie.setDomain("gulimall.com");
/**
* 业务执行完后,如果当前用户的cookies里没有user-key为键的cookie,就存放该cookie
*
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
if (!(handler instanceof HandlerMethod)){
return;
}
UserInfoTo userInfoTo = threadLocal.get();
if (!userInfoTo.isHasTempUserCookie()) {
Cookie cookie = new Cookie(CartConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
cookie.setDomain("gulimall.com");
cookie.setMaxAge(CartConstant.TEMP_USER_COOKIE_TIMEOUT);
response.addCookie(cookie);
}
//删除ThreadLocal,防止线程复用,获取到别的用户信息
threadLocal.remove();
}

在 http://cart.gulimall.com/cart.html 页面里清空cookie,然后刷新 http://cart.gulimall.com/cart.html 页面。这次就可以发现domain已经设置上去了。放行该请求后,切换到 http://cart.gulimall.com/cart.html 页面,打开控制台,此时名为user-key的cookie的作用范围已经变为.gulimall.com(本域名及其子域名)了

修改gulimall-order模块的com.atguigu.gulimall.order.config.GuliFeignConfig类的requestInterceptor方法,将http://order.gulimall.com/toTrade请求的请求头里的Cookie复制给为远程调用而构造的新请求,在ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;上打断点
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//拿到刚进来的这个请求(/toTrade)
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//ServletRequestAttributes extends AbstractRequestAttributes
//AbstractRequestAttributes implements RequestAttributes
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
//原本的 /toTrade 请求
HttpServletRequest request = attributes.getRequest();
//同步请求头数据,主要是Cookie
String cookie = request.getHeader("Cookie");
//为远程调用而构造的新请求
template.header("Cookie",cookie);
//template.
}
};
}

重新以debug方式启动GulimallOrderApplication服务和GulimallCartApplication服务
重新发送http://order.gulimall.com/toTrade请求,就来到这List<OrderConfirmVo.MemberAddressVo> address = memberFeignService.getAddress(memberEntityTo.getId());,点击Step Over F8

可以看到在gulimall-order模块配置的com.atguigu.gulimall.order.config.GuliFeignConfig拦截器就起作用了

切换到浏览器的 http://cart.gulimall.com/cart.html 页面,可以看到此时带的cookie有user-key和GULIMALL_JSESSIONID

点击GulimallOrderApplication服务的Step Over(步过)按钮,直到执行到template.header("Cookie",cookie);这一行,查看cookie可以看到user-key和GULIMALL_JSESSIONID都获取到了
user-key=1ae1d57c-cdde-4748-9ed8-384132ae47a9; GULIMALL_JSESSIONID=YjA2MGY4YjYtMTcwNi00MzdmLTg1MzItMzM0ZjRlMjBmNmRk

再次点三次Step Over F8步过)按钮,此时就有请求头了,然后点击8: Services里的Resume Program F9按钮,跳转到下一处断点

此时就来到了gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类的confirmOrder方法的List<OrderConfirmVo.OrderItemVo> items = cartFeignService.getCurrentUserCartItems();这,点击8: Services里的Resume Program F9按钮,跳转到下一处断点

此时就来到了gulimall-cart模块的com.atguigu.gulimall.cart.interceptor.CartInterceptor类的preHandle方法的MemberEntityTo member = (MemberEntityTo) session.getAttribute(AuthServerConstant.LOGIN_USER);这,点击Step Over F8 (步过)按钮,此时member就不为null了

2、修改confirmOrder方法
1、修改代码
修改gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类的confirmOrder方法
@Override
public OrderConfirmVo confirmOrder() {
OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
MemberEntityTo memberEntityTo = LoginUserInterceptor.loginUser.get();
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//1、远程查询所有的收货地址列表
List<OrderConfirmVo.MemberAddressVo> address = memberFeignService.getAddress(memberEntityTo.getId());
orderConfirmVo.setAddress(address);
},executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//2、远程查询购物车所有选中的购物项
List<OrderConfirmVo.OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
orderConfirmVo.setItems(items);
}, executor);
//3、查询用户积分
Integer integration = memberEntityTo.getIntegration();
orderConfirmVo.setIntegration(integration);
//orderConfirmVo.setIntegration(orderConfirmVo.getIntegration());
orderConfirmVo.setPayPrice(orderConfirmVo.getPayPrice());
orderConfirmVo.setTotal(orderConfirmVo.getTotal());
//TODO 防重令牌
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return orderConfirmVo;
}

gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类的confirmOrder方法上声明抛出异常,gulimall-order模块的com.atguigu.gulimall.order.web.OrderWebController类的toTrade方法上声明抛出异常

2、测试
在GulimallOrderApplication服务里,点击View Breakpoints... Ctrl+ Shift+F8取消所有断点(当然默认的Java Exception Breakpoints和JavaScript Exception Breakpoints不用管)
然后在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类的confirmOrder方法的CompletableFuture.allOf(getAddressFuture,cartFuture).get();这一行打个断点

在GulimallCartApplication服务里,点击View Breakpoints... Ctrl+ Shift+F8取消所有断点(当然默认的Java Exception Breakpoints和JavaScript Exception Breakpoints不用管)
然后在gulimall-cart模块的com.atguigu.gulimall.cart.interceptor.CartInterceptor类的preHandle方法的HttpSession session = request.getSession();这一行打个断点

重新以debug方式启动GulimallOrderApplication服务和GulimallCartApplication服务
重新发送http://order.gulimall.com/toTrade请求,就来到了这,点击Resume Program F9跳到下一处断点,此时GulimallOrderApplication服务执行完了

然后点击GulimallCartApplication服务,来到了空指针异常类,再点击Resume Program F9跳到下一处断点,此时GulimallCartApplication服务执行完了

再点击GulimallOrderApplication服务,此时就报了空指针异常
java.lang.NullPointerException: null
at com.atguigu.gulimall.order.config.GuliFeignConfig$1.apply(GuliFeignConfig.java:32) ~[classes/:na]
at feign.SynchronousMethodHandler.targetRequest(SynchronousMethodHandler.java:169) ~[feign-core-10.2.3.jar:na]
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:99) ~[feign-core-10.2.3.jar:na]
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:78) ~[feign-core-10.2.3.jar:na]
at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:103) ~[feign-core-10.2.3.jar:na]
at com.sun.proxy.$Proxy100.getAddress(Unknown Source) ~[na:na]
at com.atguigu.gulimall.order.service.impl.OrderServiceImpl.lambda$confirmOrder$0(OrderServiceImpl.java:53) ~[classes/:na]
at java.util.concurrent.CompletableFuture$AsyncRun.run$$$capture(CompletableFuture.java:1640) ~[na:1.8.0_301]
at java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java) ~[na:1.8.0_301]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ~[na:1.8.0_301]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ~[na:1.8.0_301]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_301]
然后在HttpServletRequest request = attributes.getRequest();这一行打个断点,看看怎么报的空指针异常

重新发送http://order.gulimall.com/toTrade请求(不用重启服务),点击Resume Program F9跳到下一处断点

此时就来到了gulimall-order模块的com.atguigu.gulimall.order.config.GuliFeignConfig类的apply方法的HttpServletRequest request = attributes.getRequest();这一行,可以看到attributes的值为null,所以就报空指针了

点击Debugger的Frames里的当前GuliFeignConfig类的下面那个类,可以看到是gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类的confirmOrder方法的List<OrderConfirmVo.OrderItemVo> items = cartFeignService.getCurrentUserCartItems();这一行调用的

3、Feign异步情况丢失上下文问题
1、原因
没使用异步之前,所有执行都使用的是同一个thread

而开启异步后,查address和cart又开了不同的线程,新开的线程的ThreadLocal里肯定没有cookie
2、测试
修改gulimall-order模块的com.atguigu.gulimall.order.config.GuliFeignConfig类的requestInterceptor方法,在HttpServletRequest request = attributes.getRequest();方法之前输出当前线程,并注释掉输出当前线程之后的代码,避免报错
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//拿到刚进来的这个请求(/toTrade)
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//ServletRequestAttributes extends AbstractRequestAttributes
//AbstractRequestAttributes implements RequestAttributes
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
System.out.println("RequestInterceptor线程:"+Thread.currentThread().getId());
////原本的 /toTrade 请求
//HttpServletRequest request = attributes.getRequest();
////同步请求头数据,主要是Cookie
//String cookie = request.getHeader("Cookie");
////为远程调用而构造的新请求
//template.header("Cookie",cookie);
////template.
}
};
}

在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类的confirmOrder方法里,在开启异步之前,开启异步之后,都输出当前线程的id
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
MemberEntityTo memberEntityTo = LoginUserInterceptor.loginUser.get();
System.out.println("主线程:"+Thread.currentThread().getId());
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//1、远程查询所有的收货地址列表
System.out.println("getAddressFuture线程:"+Thread.currentThread().getId());
List<OrderConfirmVo.MemberAddressVo> address = memberFeignService.getAddress(memberEntityTo.getId());
orderConfirmVo.setAddress(address);
},executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//2、远程查询购物车所有选中的购物项
System.out.println("cartFuture线程:"+Thread.currentThread().getId());
List<OrderConfirmVo.OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
orderConfirmVo.setItems(items);
}, executor);
//3、查询用户积分
Integer integration = memberEntityTo.getIntegration();
orderConfirmVo.setIntegration(integration);
//orderConfirmVo.setIntegration(orderConfirmVo.getIntegration());
orderConfirmVo.setPayPrice(orderConfirmVo.getPayPrice());
orderConfirmVo.setTotal(orderConfirmVo.getTotal());
//TODO 防重令牌
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return orderConfirmVo;
}

取消所有GulimallOrderApplication服务的断点,以debug方式启动GulimallOrderApplication服务
重新发送http://order.gulimall.com/toTrade请求,可以看到开启异步后线程id变了,故线程变了,所以获取不到ThreadLocal本地线程数据了
主线程:69
getAddressFuture线程:110
cartFuture线程:111
RequestInterceptor线程:111
RequestInterceptor线程:110

图解大概是这个样子:订单服务开了两个异步任务来获取收货地址和购物车数据,由于用户的数据保存在ThreadLocal本地线程中,当线程改变后,就获取不到原来线程的ThreadLocal数据了

3、修改代码
在gulimall-order模块的com.atguigu.gulimall.order.config.GuliFeignConfig类的requestInterceptor方法的attributes.getRequest()之前加一个判断,并在ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;上打个断点
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//拿到刚进来的这个请求(/toTrade)
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//ServletRequestAttributes extends AbstractRequestAttributes
//AbstractRequestAttributes implements RequestAttributes
ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
System.out.println("RequestInterceptor线程:"+Thread.currentThread().getId());
if (attributes != null) {
//原本的 /toTrade 请求
HttpServletRequest request = attributes.getRequest();
//同步请求头数据,主要是Cookie
String cookie = request.getHeader("Cookie");
//为远程调用而构造的新请求
template.header("Cookie",cookie);
//template.
}
}
};
}

在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类的confirmOrder方法里,先获取主线程RequestAttributes数据,然后将开启异步后的线程也设上RequestAttributes数据,这样新开的线程就有原来线程的数据了

虽然用的RequestContextHolder从头到尾都一样,但封装数据用的是ThreadLocal,只要线程不一样,ThreadLocal里的数据就不一样(但是新开的线程是复用的,设置完数据后没有清除,有可能给别的用户用了)

4、重新测试
重启GulimallOrderApplication服务,发送http://order.gulimall.com/toTrade请求
此时attributes就不为null了,也能正确获得cookie的值了

4、编解码异常
1、查看异常
老师gulimall-cart模块的com.atguigu.gulimall.cart.feign.ProductFeignService接口的getPrice方法这里出现了编解码异常

而我并没有(还是改一下吧,不过下面的176行的那个return null;要改为return collect;,后面会改的)
老师出现的异常可能是直接返回的BigDecimal类型的数据出现了问题,而我的正确编码了

2、修改代码
修改gulimall-product模块的com.atguigu.gulimall.product.controller.SkuInfoController类的getPrice方法,让其返回R对象
/**
* 实时查询商品价格
* @param skuId
* @return
*/
@GetMapping("/{skuId}/price")
public R getPrice(@PathVariable("skuId") Long skuId){
SkuInfoEntity skuInfoEntity = skuInfoService.getById(skuId);
return R.ok().put("data",skuInfoEntity.getPrice().toString());
}

修改gulimall-cart模块的com.atguigu.gulimall.cart.feign.ProductFeignService接口的getPrice方法的返回类型
@GetMapping("/product/skuinfo/{skuId}/price")
public R getPrice(@PathVariable("skuId") Long skuId);

修改gulimall-cart模块的com.atguigu.gulimall.cart.service.impl.CartServiceImpl类的getUserCartItems方法,让其接收R对象
@Override
public List<CartItemVo> getUserCartItems() {
UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
if (userInfoTo == null) {
return null;
}else {
String cartKey = CART_PREFIX + userInfoTo.getUserId();
List<CartItemVo> cartItems = getCartItems(cartKey);
if (cartItems!=null) {
//获取所有被选中的购物项
List<CartItemVo> collect = cartItems.stream().filter(CartItemVo::getCheck)
.map(item->{
//获取商品最新价格
R r = productFeignService.getPrice(item.getSkuId());
String price = (String) r.get("data");
item.setPrice(new BigDecimal(price));
return item;
})
.collect(Collectors.toList());
return collect;
}else {
return null;
}
}
}

3、测试
重启GulimallCartApplication服务和GulimallOrderApplication服务,访问 http://order.gulimall.com/toTrade 页面报了如下错误,告诉我们执行CartFeignService#getCurrentUserCartItems()方法报错了
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Mon Aug 15 19:28:25 CST 2022
There was an unexpected error (type=Internal Server Error, status=500).
feign.FeignException$InternalServerError: status 500 reading CartFeignService#getCurrentUserCartItems()

查看GulimallCartApplication服务的控制台,出现了thymeleaf的问题,但getCurrentUserCartItems方法根本就没返回页面
2022-08-15 19:27:50.266 ERROR 6972 --- [o-30000-exec-10] org.thymeleaf.TemplateEngine : [THYMELEAF][http-nio-30000-exec-10] Exception processing template "currentUserCartItems": Error resolving template [currentUserCartItems], template might not exist or might not be accessible by any of the configured Template Resolvers
org.thymeleaf.exceptions.TemplateInputException: Error resolving template [currentUserCartItems], template might not exist or might not be accessible by any of the configured Template Resolvers

在gulimall-cart模块的com.atguigu.gulimall.cart.controller.CartController类里修改getCurrentUserCartItems方法,在该方法上加上@ResponseBody注解
@GetMapping("/currentUserCartItems")
@ResponseBody
public List<CartItemVo> getCurrentUserCartItems(){
return cartService.getUserCartItems();
}

重启GulimallCartApplication服务,刷新http://order.gulimall.com/toTrade页面,此时就可以看到页面了

6.1.3、完善结算页
1、修改结算页面
1、修改收货人信息
在 http://order.gulimall.com/toTrade 页面里,打开控制台,定位到收货人信息位置,复制收货人信息

在gulimall-order模块的src/main/resources/templates/confirm.html文件里搜索收货人信息,将收货人信息修改为动态获取的信息,然后点击Build -> Recompile 'confirm.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件
<div class="section">
<!--收货人信息-->
<div class="top-2">
<span>收货人信息</span>
<span>新增收货地址</span>
</div>
<!--地址-->
<div class="top-3" th:each="addr: ${orderConfirmData.address}">
<!--<p>家里</p><span>齐天大圣 北京市 昌平区城区晨曦小区-16号楼 吉利大学 150****2245</span>-->
<p>[[${addr.name}]]</p><span>[[${addr.name}]] [[${addr.province}]] [[${addr.detailAddress}]] 吉利大学 [[${addr.phone}]]</span>
</div>
<p class="p2">更多地址︾</p>
<div class="hh1"/></div>

修改gulimall_ums数据库的ums_member_receive_address表的member_id为7的那个元组的属性,修改其province属性为上海市、detail_address属性为上海市松江区大厦6层、default_status属性为1

刷新 http://order.gulimall.com/toTrade 页面,这样就显示用户的收货地址信息了

2、删除自提点信息
在 http://order.gulimall.com/toTrade 页面里,打开控制台,定位到北京市昌平区位置,复制北京市昌平区

在gulimall-order模块的src/main/resources/templates/confirm.html文件里搜索北京市昌平区,注释掉这部分代码,然后点击Build -> Recompile 'confirm.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件

3、修改商品项信息
刷新 http://order.gulimall.com/toTrade 页面就没有刚才那一行信息了,再打开控制台,定位到小米手环2的那个购物项,复制商家:谷粒学院自营

在gulimall-order模块的src/main/resources/templates/confirm.html文件里搜索商家:谷粒学院自营,将里面的商品项修改为动态的数据
<div class="to_right">
<h5>商家:谷粒学院自营</h5>
<div><button>换购</button><span>已购满20.00元,再加49.90元,可返回购物车领取赠品</span></div>
<!--图片-->
<div class="yun1" th:each="item : ${orderConfirmData.items}">
<img th:src="${item.image}" class="yun"/>
<div class="mi">
<p>[[${item.title}]] <span style="color: red;"> ¥ [[${#numbers.formatDecimal(item.price,1,2)}]] </span> <span> x[[${item.count}]] </span> <span>[[${item.hasStock?'有货':'无货'}]]</span></p>
<p><span>0.095kg</span></p>
<p class="tui-1"><img src="/static/order/confirm/img/i_07.png" />支持7天无理由退货</p>
</div>
</div>
<div class="hh1"></div>
<p>退换无忧 <span class="money">¥ 0.00</span></p>
</div>

修改gulimall-order模块的com.atguigu.gulimall.order.vo.OrderConfirmVo类的OrderItemVo内部类,添加是否有货字段和货物重量字段
/**
* //TODO 查询库存状态
* 是否有货
*/
private boolean hasStock;
/**
* 货物重量
*/
private BigDecimal weight;

重启GulimallOrderApplication服务,刷新 http://order.gulimall.com/toTrade 页面,这样就动态显示购物项数据了

4、修改结算信息
在 http://order.gulimall.com/toTrade 页面里,打开控制台,定位到1 件商品,总商品金额:位置,复制件商品,总商品金额:

在gulimall-order模块的src/main/resources/templates/confirm.html文件里搜索件商品,总商品金额:,将这些结算信息修改为动态的数据
<div class="xia">
<div class="qian">
<p class="qian_y">
<span>[[${orderConfirmData.count}]]</span>
<span>件商品,总商品金额:</span>
<span class="rmb">¥[[${#numbers.formatDecimal(orderConfirmData.total,1,2)}]]</span>
</p>
<p class="qian_y">
<span>返现:</span>
<span class="rmb"> -¥0.00</span>
</p>
<p class="qian_y">
<span>运费: </span>
<span class="rmb">   ¥0.00</span>
</p>
<p class="qian_y">
<span>服务费: </span>
<span class="rmb">   ¥0.00</span>
</p>
<p class="qian_y">
<span>退换无忧: </span>
<span class="rmb">   ¥0.00</span>
</p>
</div>
<div class="yfze">
<p class="yfze_a"><span class="z">应付总额:</span><span class="hq">¥[[${#numbers.formatDecimal(orderConfirmData.payPrice,1,2)}]]</span></p>
<!--<p class="yfze_b">寄送至: 北京 朝阳区 三环到四环之间 朝阳北路复兴国际大厦23层麦田房产 IT-中心研发二部 收货人:赵存权 188****5052</p>-->
<p class="yfze_b">寄送至: xxx 收货人:xxx 188****5052</p>
</div>
<button class="tijiao">提交订单</button>
</div>

在gulimall-order模块的com.atguigu.gulimall.order.vo.OrderConfirmVo类里添加getCount方法,页面直接写${orderConfirmData.count}就会调用orderConfirmData对象的getCount方法
public Integer getCount(){
Integer count = 0;
for (OrderItemVo item : items) {
count+=item.count;
}
return count;
}

重启GulimallOrderApplication服务,刷新 http://order.gulimall.com/toTrade 页面,这样就动态显示结算信息了

5、完善收货人信息
在gulimall_ums数据库的ums_member_receive_address表里,再添加一条member_id为7的数据

刷新 http://order.gulimall.com/toTrade 页面,可以看到这两个收货地址的name都有红框,并且都有吉林大学

在gulimall-order模块的src/main/resources/templates/confirm.html文件里搜索吉利大学,删掉这里的吉利大学

刷新 http://order.gulimall.com/toTrade 页面,可以看到这两个收货地址的name都有红框,但是没有吉林大学了,红框后面会解决的

2、批量查有货无货状态
1、修改字段
去掉gulimall-order模块的com.atguigu.gulimall.order.vo.OrderConfirmVo类的OrderItemVo内部类的private boolean hasStock;字段
private boolean hasStock;

在gulimall-order模块的com.atguigu.gulimall.order.vo.OrderConfirmVo类里,添加stocks字段,用于判断是否有库存
/**
* 是否有库存
* Long:skuId
* Boolean:是否有库存
*/
@Getter @Setter
Map<Long,Boolean> stocks;

2、添加方法
gulimall-ware模块的com.atguigu.gulimall.ware.controller.WareSkuController类里已经有了一个getSkuHasStock批量查库存方法了,直接调用就好了

在gulimall-order模块的com.atguigu.gulimall.order.feign包里添加WmsFeignService接口,再里面远程调用gulimall-ware模块,用于查库存
package com.atguigu.gulimall.order.feign;
import com.atguigu.common.to.SkuHasStockTo;
import com.atguigu.common.utils.RS;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
/**
* @author 无名氏
* @date 2022/8/15
* @Description:
*/
@FeignClient("gulimall-ware")
public interface WmsFeignService {
@PostMapping("/ware/waresku/hasStock")
public RS<List<SkuHasStockTo>> getSkuHasStock(@RequestBody List<Long> skuIds);
}

在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类里修改confirmOrder方法,用于获取库存信息
@Autowired
WmsFeignService wmsFeignService;
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
MemberEntityTo memberEntityTo = LoginUserInterceptor.loginUser.get();
System.out.println("主线程:"+Thread.currentThread().getId());
//获取之前的请求
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//1、远程查询所有的收货地址列表
System.out.println("getAddressFuture线程:"+Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderConfirmVo.MemberAddressVo> address = memberFeignService.getAddress(memberEntityTo.getId());
orderConfirmVo.setAddress(address);
},executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//2、远程查询购物车所有选中的购物项
System.out.println("cartFuture线程:" + Thread.currentThread().getId());
//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderConfirmVo.OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
orderConfirmVo.setItems(items);
}, executor).thenRunAsync(() -> {
List<OrderConfirmVo.OrderItemVo> items = orderConfirmVo.getItems();
List<Long> collect = items.stream().map(OrderConfirmVo.OrderItemVo::getSkuId).collect(Collectors.toList());
RS<List<SkuHasStockTo>> skuHasStock = wmsFeignService.getSkuHasStock(collect);
List<SkuHasStockTo> data = skuHasStock.getData();
if (!CollectionUtils.isEmpty(data)) {
Map<Long, Boolean> stocks = data.stream().collect(Collectors.toMap(SkuHasStockTo::getSkuId, SkuHasStockTo::getHasStock));
orderConfirmVo.setStocks(stocks);
}
}, executor);
//3、查询用户积分
Integer integration = memberEntityTo.getIntegration();
orderConfirmVo.setIntegration(integration);
//orderConfirmVo.setIntegration(orderConfirmVo.getIntegration());
orderConfirmVo.setPayPrice(orderConfirmVo.getPayPrice());
orderConfirmVo.setTotal(orderConfirmVo.getTotal());
//TODO 防重令牌
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return orderConfirmVo;
}

在gulimall-order模块的src/main/resources/templates/confirm.html文件里,将<span>[[${item.hasStock?'有货':'无货'}]]</span>修改为<span>[[${orderConfirmData.stocks[item.skuId]?'有货':'无货'}]]</span>

3、测试
重启GulimallOrderApplication服务,启动GulimallWareApplication服务,此时购物项就显示有货、无货状态了

3、默认地址显示红色边框
1、显示默认地址边框
在 http://order.gulimall.com/toTrade 页面里,打开控制台,定位到收货人信息里的某个收货人的位置,复制top-3

在gulimall-order模块的src/main/resources/templates/confirm.html文件里搜索top-3,给地址所在的<div>再加一个class为addr-item,给收货人的<p>标签加一个自定义属性th:attr="def=${addr.defaultStatus}"
<!--地址-->
<div class="top-3 addr-item" th:each="addr: ${orderConfirmData.address}">
<!--<p>家里</p><span>齐天大圣 北京市 昌平区城区晨曦小区-16号楼 吉利大学 150****2245</span>-->
<p th:attr="def=${addr.defaultStatus}">[[${addr.name}]]</p><span>[[${addr.name}]] [[${addr.province}]] [[${addr.detailAddress}]] [[${addr.phone}]]</span>
</div>

在gulimall_ums数据库的ums_member_receive_address表里,给member_id为7的第二个元组(这里指的是id为2元组)的default_status设置为0,表示不是默认地址

在 http://order.gulimall.com/toTrade 页面里,打开控制台,可以看到默认地址的姓名所在的<p>标签的自定义def属性值为1,不是默认地址的姓名所在的<p>标签的自定义def属性值为0

在gulimall-order模块的src/main/resources/templates/confirm.html文件里的<script>标签里添加highlight方法,并在$(document).ready()方法(页面初始化方法)里调用该方法
function highlight() {
//让收货地址的姓名所在的边框置灰
$(".addr-item p").css({"border":"2px solid gray"})
$(".addr-item p[def='1']").css({"border":"2px solid red"})
}

在 http://order.gulimall.com/toTrade 页面里,打开控制台,可以看到def="1"的是红色边框,def="0"的是灰色边框

2、修改配送的地址
在gulimall-order模块的src/main/resources/templates/confirm.html文件里的<script>标签里添加.addr-item p对应元素点击事件,修改配送的地址,然后点击Build -> Recompile 'confirm.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件
$(".addr-item p").click(function () {
$(".addr-item p").attr("def","0")
$(this).attr("def","1")
highlight()
})

3、测试
打开 http://order.gulimall.com/toTrade 页面,可以看到当点击其他收货人时,红色边框也跟着变了

4、获取运费
1、页面添加标识
在gulimall-order模块的src/main/resources/templates/confirm.html文件里,在遍历收货人信息的<p>标签上添加自定义addrId=${addr.id}属性,方便获取addrId
<!--地址-->
<div class="top-3 addr-item" th:each="addr: ${orderConfirmData.address}">
<!--<p>家里</p><span>齐天大圣 北京市 昌平区城区晨曦小区-16号楼 吉利大学 150****2245</span>-->
<p th:attr="def=${addr.defaultStatus},addrId=${addr.id}">[[${addr.name}]]</p><span>[[${addr.name}]] [[${addr.province}]] [[${addr.detailAddress}]] [[${addr.phone}]]</span>
</div>

2、编写获取运费接口
在gulimall-ware模块的com.atguigu.gulimall.ware.controller.WareInfoController类里添加getFare(Long addrId)方法,用于获取运费
@GetMapping("/fare")
public R getFare(Long addrId) {
BigDecimal fare= wareInfoService.getFare(addrId);
return R.ok().put("data",fare);
}

在gulimall-ware模块的com.atguigu.gulimall.ware.service.WareInfoService接口里添加getFare抽象方法(下面先不急着实现该抽象方法)
BigDecimal getFare(Long addrId);

gulimall-member模块的com.atguigu.gulimall.member.controller.MemberReceiveAddressController类的info方法,可以根据addrId获取收货地址

在gulimall-ware模块的com.atguigu.gulimall.ware.feign包下新建MemberFeignService接口,在里面添加addrInfo(@PathVariable("id") Long id)方法,用于获取地址信息
package com.atguigu.gulimall.ware.feign;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author 无名氏
* @date 2022/8/16
* @Description:
*/
@FeignClient("gulimall-member")
public interface MemberFeignService {
@RequestMapping("/member/memberreceiveaddress/info/{id}")
public R addrInfo(@PathVariable("id") Long id);
}

复制gulimall-member模块的com.atguigu.gulimall.member.entity.MemberReceiveAddressEntity类,粘贴到gulimall-ware模块的com.atguigu.gulimall.ware.vo包下
package com.atguigu.gulimall.ware.vo;
import lombok.Data;
/**
* @author 无名氏
* @date 2022/8/16
* @Description:
*/
@Data
public class MemberAddressVo {
/**
* id
*/
private Long id;
/**
* member_id
*/
private Long memberId;
/**
* 收货人姓名
*/
private String name;
/**
* 电话
*/
private String phone;
/**
* 邮政编码
*/
private String postCode;
/**
* 省份/直辖市
*/
private String province;
/**
* 城市
*/
private String city;
/**
* 区
*/
private String region;
/**
* 详细地址(街道)
*/
private String detailAddress;
/**
* 省市区代码
*/
private String areacode;
/**
* 是否默认
*/
private Integer defaultStatus;
}

在gulimall-ware模块的com.atguigu.gulimall.ware包下新建constant文件夹,在constant文件夹下新建FreightConstant类,用于指定本地运费和外地运费的价格
package com.atguigu.gulimall.ware.constant;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
/**
* @author 无名氏
* @date 2022/8/16
* @Description: 用户需要支付的运费
*/
@Data
@Component
@ConfigurationProperties(prefix = "gulimall.freight")
public class FreightConstant {
/**
* 本地运费
*/
private BigDecimal localFreight = new BigDecimal("8");
/**
* 外地运费
*/
private BigDecimal outlandFreight = new BigDecimal("12");
}

在gulimall-ware模块的src/main/resources/application.properties配置文件里添加如下配置,用于设置本地运费和外地运费
# 设置本地运费
gulimall.freight.localFreight=9
# 设置外地运费
gulimall.freight.outlandFreight=14

在gulimall-ware模块的com.atguigu.gulimall.ware.service.impl.WareInfoServiceImpl类里实现getFare方法
/**
* 根据收货地址计算运费
* 如果收货地址所在的省份/直辖市 有仓库就按本地运费计算
* 如果收货地址所在的省份/直辖市 没有有仓库就按外地运费计算
* @param addrId
* @return
*/
@Override
public BigDecimal getFare(Long addrId) {
R r = memberFeignService.addrInfo(addrId);
Object data = r.get("memberReceiveAddress");
if (data==null){
return null;
}
String s = JSON.toJSONString(data);
MemberAddressVo memberAddressVo = JSON.parseObject(s, MemberAddressVo.class);
//获取用户该收货地址 省份/直辖市
String city = memberAddressVo.getProvince();
LambdaQueryWrapper<WareInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
LambdaQueryWrapper<WareInfoEntity> eq = lambdaQueryWrapper.eq(WareInfoEntity::getAddress, city);
WareInfoEntity wareInfoEntity = this.baseMapper.selectOne(eq);
if (wareInfoEntity!=null){
//用户收货地址有仓库
return freightConstant.getLocalFreight();
}
return freightConstant.getOutlandFreight();
}

3、修改点击事件
在gulimall-order模块的src/main/resources/templates/confirm.html文件里的<script>标签里修改.addr-item p对应元素点击事件,发送请求,在控制台打印获取的数据
$(".addr-item p").click(function () {
$(".addr-item p").attr("def","0")
$(this).attr("def","1")
highlight()
//获取到当前的地址id
var addrId = $(this).attr("addrId");
$.get("http://gulimall.com/api/ware/wareinfo/fare?addrId=" +addrId,function (data) {
console.log(data)
})
})

4、测试
重启GulimallWareApplication服务,刷新 http://order.gulimall.com/toTrade 页面,点击北京市的收货地址,查看请求信息,显示的响应里的data的值为14,也就是运费为14(上面配置的本地仓库的运费为9,外地仓库的运费为14)

查看控制台,可以看到输出的data也为14

查看gulimall_wms数据库的wms_ware_info表的address字段,确实没有是北京市的仓库,只有北京xx和上海市

再点击上海市的收货地址,响应的data的值为9,有北京市的仓库,所以是本地仓库,运费为9元;但是点击北京市的收货人后红色边框没有换过来

查看控制台,可以看到输出的data也为9

5、前端显示运费
在 http://order.gulimall.com/toTrade 页面里,打开控制台,定位到提交订单上面的运费:位置,复制运费:

在gulimall-order模块的src/main/resources/templates/confirm.html文件里搜索运费: ,将<span class="rmb">   ¥0.00</span>修改为<span class="rmb">   ¥<b id="fareEle"></b></span>
<p class="qian_y">
<span>运费: </span>
<span class="rmb">   ¥<b id="fareEle"></b></span>
</p>

在gulimall-order模块的src/main/resources/templates/confirm.html文件里的<script>标签里,给收货地址的<p>标签绑定的click事件方法的最后添加 $("#fareEle").text(data.data),将页面初始化方法调用的highlight()删掉,并在$(document).ready()页面初始化方法调用$(".addr-item p[def='1']").click(),自动点击默认收货地址
$(".addr-item p").click(function () {
$(".addr-item p").attr("def","0")
$(this).attr("def","1")
highlight()
//获取到当前的地址id
var addrId = $(this).attr("addrId");
$.get("http://gulimall.com/api/ware/wareinfo/fare?addrId=" +addrId,function (data) {
console.log(data)
$("#fareEle").text(data.data)
})
})

6、测试
重启GulimallOrderApplication服务,刷新 http://order.gulimall.com/toTrade 页面,修改收货人信息,可以看到红色边框改变了,下滑找到运费,可以看到当修改收货人信息后运费也变了

5、应付总额
在 http://order.gulimall.com/toTrade 页面里,打开控制台,定位到应付总额:位置,复制应付总额:

在gulimall-order模块的src/main/resources/templates/confirm.html文件里搜索应付总额:,将¥[[${#numbers.formatDecimal(orderConfirmData.payPrice,1,2)}]]修改为¥<b id="payPriceEle">[[${#numbers.formatDecimal(orderConfirmData.payPrice,1,2)}]]</b>
<div class="yfze">
<p class="yfze_a"><span class="z">应付总额:</span><span class="hq">¥<b id="payPriceEle">[[${#numbers.formatDecimal(orderConfirmData.payPrice,1,2)}]]</b></span></p>
<!--<p class="yfze_b">寄送至: 北京 朝阳区 三环到四环之间 朝阳北路复兴国际大厦23层麦田房产 IT-中心研发二部 收货人:赵存权 188****5052</p>-->
<p class="yfze_b">寄送至: xxx 收货人:xxx 188****5052</p>
</div>

在gulimall-order模块的src/main/resources/templates/confirm.html文件里的<script>标签里修改.addr-item p对应元素点击事件,让其调用getFare(addrId)方法
$(".addr-item p").click(function () {
$(".addr-item p").attr("def","0")
$(this).attr("def","1")
highlight()
//获取到当前的地址id
var addrId = $(this).attr("addrId");
getFare(addrId)
})
function getFare(addrId) {
$.get("http://gulimall.com/api/ware/wareinfo/fare?addrId=" +addrId,function (data) {
console.log(data)
$("#fareEle").text(data.data)
var total = [[${#numbers.formatDecimal(orderConfirmData.total,1,2)}]]
// total*1 将其转为数字类型
$("#payPriceEle").text(total*1+data.data*1)
})
}

点击Build -> Recompile 'confirm.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件,刷新 http://order.gulimall.com/toTrade 页面,可以看到将收获地址从没有本地仓库的地址修改为有本地仓库的地址后,应付总额也跟着变了

6、寄送人
在gulimall-ware模块的com.atguigu.gulimall.ware.vo包里新建FareVo类
package com.atguigu.gulimall.ware.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* @author 无名氏
* @date 2022/8/16
* @Description:
*/
@Data
public class FareVo {
private MemberAddressVo memberAddressVo;
private BigDecimal fare;
}

修改gulimall-ware模块的com.atguigu.gulimall.ware.service.impl.WareInfoServiceImpl类的getFare方法
/**
* 根据收货地址计算运费
* 如果收货地址所在的省份/直辖市 有仓库就按本地运费计算
* 如果收货地址所在的省份/直辖市 没有有仓库就按外地运费计算
* @param addrId
* @return
*/
@Override
public FareVo getFare(Long addrId) {
R r = memberFeignService.addrInfo(addrId);
Object data = r.get("memberReceiveAddress");
if (data==null){
return null;
}
FareVo fareVo = new FareVo();
String s = JSON.toJSONString(data);
MemberAddressVo memberAddressVo = JSON.parseObject(s, MemberAddressVo.class);
fareVo.setMemberAddressVo(memberAddressVo);
//获取用户该收货地址 省份/直辖市
String city = memberAddressVo.getProvince();
LambdaQueryWrapper<WareInfoEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
LambdaQueryWrapper<WareInfoEntity> eq = lambdaQueryWrapper.eq(WareInfoEntity::getAddress, city);
WareInfoEntity wareInfoEntity = this.baseMapper.selectOne(eq);
BigDecimal fare = null;
if (wareInfoEntity!=null){
//用户收货地址有仓库
fareVo.setFare(freightConstant.getLocalFreight());
}else {
fareVo.setFare(freightConstant.getOutlandFreight());
}
return fareVo;
}

修改gulimall-ware模块的com.atguigu.gulimall.ware.service.WareInfoService接口的getFare方法返回值
FareVo getFare(Long addrId);

修改gulimall-warev模块的com.atguigu.gulimall.ware.controller.WareInfoController类的getFare方法
@GetMapping("/fare")
public R getFare(Long addrId) {
FareVo fare= wareInfoService.getFare(addrId);
return R.ok().put("data",fare);
}

在gulimall-order模块的src/main/resources/templates/confirm.html文件里的<script>标签里,修改getFare(addrId)方法
function getFare(addrId) {
$.get("http://gulimall.com/api/ware/wareinfo/fare?addrId=" +addrId,function (data) {
console.log(data)
$("#fareEle").text(data.data.fare)
var total = [[${#numbers.formatDecimal(orderConfirmData.total,1,2)}]]
// total*1 将其转为数字类型
$("#payPriceEle").text(total*1+data.data.fare*1)
})
}

在 http://order.gulimall.com/toTrade 页面里,打开控制台,定位到寄送至位置,复制寄送至

在gulimall-order模块的src/main/resources/templates/confirm.html文件里搜索寄送至,
把<p class="yfze_b">寄送至: xxx 收货人:xxx 188****5052</p>改为<p class="yfze_b">寄送至: <span id="receiveAddressEle"></span> 收货人:<span id="receiveEle"></span></p>
<div class="yfze">
<p class="yfze_a"><span class="z">应付总额:</span><span class="hq">¥<b id="payPriceEle">[[${#numbers.formatDecimal(orderConfirmData.payPrice,1,2)}]]</b></span></p>
<!--<p class="yfze_b">寄送至: 北京 朝阳区 三环到四环之间 朝阳北路复兴国际大厦23层麦田房产 IT-中心研发二部 收货人:赵存权 188****5052</p>-->
<p class="yfze_b">寄送至: <span id="receiveAddressEle"></span> 收货人:<span id="receiveEle"></span></p>
</div>

在gulimall-order模块的src/main/resources/templates/confirm.html文件里的<script>标签里,再次修改getFare(addrId)方法
function getFare(addrId) {
$.get("http://gulimall.com/api/ware/wareinfo/fare?addrId=" +addrId,function (resp) {
//设置运费
$("#fareEle").text(resp.data.fare)
//设置应付金额
var total = [[${#numbers.formatDecimal(orderConfirmData.total,1,2)}]]
// total*1 将其转为数字类型
$("#payPriceEle").text(total*1+resp.data.fare*1)
//设置收货人信息
$("#receiveAddressEle").text(resp.data.memberAddressVo.province+" "+resp.data.memberAddressVo.detailAddress)
$("#receiveEle").text(resp.data.memberAddressVo.name)
})
}

重启GulimallOrderApplication服务,可以看到当点击别的寄送地址后,下面的寄送至的信息也会跟着改变

6.1.4、接口幂等性
1、接口幂等性概述
一、什么是幂等性
接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用;比如说支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结 果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...,这就没有保证接口的幂等性。
二、哪些情况需要防止
- 用户多次点击按钮
- 用户页面回退再次提交
- 微服务互相调用,由于网络问题,导致请求失败。feign 触发重试机制
- 其他业务情况
三、什么情况下需要幂等
以 SQL 为例,有些操作是天然幂等的。
SELECT * FROM table WHER id=?
无论执行多少次都不会改变状态,是天然的幂等。
UPDATE tab1 SET col1=1 WHERE col2=2
无论执行成功多少次状态都是一致的,也是幂等操作。
delete from user where userid=1
多次操作,结果一样,具备幂等性
insert into user(userid,name) values(1,'a')
如果userid 为唯一主键,即重复操作上面的业务,只会插入一条用户数据,具备幂等性。
UPDATE tab1 SET col1=col1+1 WHERE col2=2
每次执行的结果都会发生变化,不是幂等的。
insert into user(userid,name) values(1,'a')
如 userid 不是主键,可以重复,那上面业务多次操作,数据都会新增多条,不具备幂等性。
可以给gulimall_oms数据库的oms_order表的order_sn订单号字段设置唯一索引(数据库设置级别,保证同一个订单只有一条数据)

四、幂等解决方案
1. token 机制
操作
服务端提供了发送 token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取
然后调用业务接口请求时,把 token 携带过去,一般放在请求头部。
服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token,继续执行业务。
如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码,不被重复执行。
危险性:
先删除 token 还是后删除 token; (1) 先删除可能导致,业务确实没有执行,重试还带上之前 token,由于防重设计导致,请求还是不能执行。 (2) 后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除 token,别人继续重试,导致业务被执行两边 (3) 我们最好设计为先删除 token,如果业务调用失败,就重新获取 token 再次请求。
Token 获取、比较和删除必须是原子性 (1) redis.get(token) 、token.equals、redis.del(token)如果这两个操作不是原子,可能导致,高并发下,都 get 到同样的数据,判断都成功,继续业务并发执行 (2) 可以在 redis 使用 lua 脚本完成这个操作
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
2、各种锁机制
1、数据库悲观锁
select * from xxxx where id = 1 for update;
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。另外要注意的是,id 字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会非常麻烦。
2、数据库乐观锁
这种方法适合在更新的场景中,
update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1
根据 version 版本,也就是在操作库存前先获取当前商品的 version 版本号,然后操作的时候 带上此 version 号。我们梳理下,我们第一次操作库存时,得到 version 为 1,调用库存服务 version 变成了 2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订 单服务传如的 version 还是 1,再执行上面的 sql 语句时,就不会执行;因为 version 已经变 为 2 了,where 条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。 乐观锁主要使用于处理读多写少的问题
3、业务层分布式锁
如果多个机器可能在同一时间同时处理相同的数据,比如多台机器定时任务都拿到了相同数据处理,我们就可以加分布式锁,锁定此数据,处理完成后释放锁。获取到锁的必须先判断 这个数据是否被处理过。
3、各种唯一约束
1、数据库唯一约束 插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入。我们在数据库层面防止重复。这个机制是利用了数据库的主键唯一约束的特性,解决了在 insert 场景时幂等问题。但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键。如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。 2、redis set 防重 很多数据需要处理,只能被处理一次,比如我们可以计算数据的 MD5 将其放入 redis 的 set,每次处理数据,先看这个 MD5 是否已经存在,存在就不处理。 4、防重表 使用订单号 orderNo 做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。(之前说的 redis防重也算)
5、全局请求唯一 id
调用接口时,生成一个唯一 id,redis 将数据保存到集合中(去重),存在即处理过。 可以使用 nginx 设置每一个请求的唯一 id;
proxy_set_header X-Request-Id $request_id;
2、订单确认
1、订单确认流程

2、修改代码
在gulimall-order模块的com.atguigu.gulimall.order包下新建constant文件夹,在constant文件夹里新建OrderConstant类
package com.atguigu.gulimall.order.constant;
/**
* @author 无名氏
* @date 2022/8/16
* @Description:
*/
public class OrderConstant {
/**
* 用户生成订单的令牌前缀
*/
public static final String USER_ORDER_TOKEN_PREFIX = "order:token:";
}

在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类里的confirmOrder方法里添加防重令牌,点击查看完整代码
@Autowired
StringRedisTemplate redisTemplate;
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
......
orderConfirmVo.setPayPrice(orderConfirmVo.getPayPrice());
orderConfirmVo.setTotal(orderConfirmVo.getTotal());
//TODO 防重令牌
String key = OrderConstant.USER_ORDER_TOKEN_PREFIX + memberEntityTo.getId();
String token = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set(key,token,30, TimeUnit.MINUTES);
orderConfirmVo.setOrderToken(token);
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return orderConfirmVo;
}

3、提交订单
1、前端添加提交订单按钮
在gulimall-order模块的src/main/resources/templates/confirm.html文件里搜索提交订单,在上面添加如下代码
<input name="orderToken" type="hidden" th:value="${orderConfirmData.orderToken}">

在gulimall-order模块的com.atguigu.gulimall.order.vo包里新建OrderSubmitVo类,用于封装提交订单的信息
package com.atguigu.gulimall.order.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* @author 无名氏
* @date 2022/8/16
* @Description: 封装订单提交的数据
*/
@Data
public class OrderSubmitVo {
/**
* 收货地址的id
*/
private Long addrId;
/**
* 支付方式(在线支付/货到付款)
*/
private Integer payType;
//再去购物车中查询商品,不用页面提交商品信息
//积分、优惠、发票
/**
* 防重令牌
*/
private String orderToken;
/**
* 页面提交的应付价格(如果提交订单后判断的应付价格和页面提交过来的价格不一样,给予用户提示)
*/
private BigDecimal payPrice;
/**
* 订单备注
*/
private String note;
///用户相关信息,直接去session取出登录的用户
}

在gulimall-order模块的src/main/resources/templates/confirm.html文件里,给提交订单的<button>标签和name为orderToken的<input>标签 添加一个父<form>标签,并在里面<form>标签里面(与提交订单的<button>标签、orderToken的<input>标签同级)添加隐藏的addrIdInput、payPriceInput、note
<form action="http://order.gulimall.com/submitOrder" method="post">
<input id="addrIdInput" name="addrId" type="hidden">
<input id="payPriceInput" name="payPrice" type="hidden">
<input name="note" type="hidden">
<input name="orderToken" type="hidden" th:value="${orderConfirmData.orderToken}">
<button class="tijiao" type="submit">提交订单</button>
</form>

在gulimall-order模块的src/main/resources/templates/confirm.html文件里的<script>标签里,修改getFare(addrId)方法
function getFare(addrId) {
$.get("http://gulimall.com/api/ware/wareinfo/fare?addrId=" +addrId,function (resp) {
//设置运费
$("#fareEle").text(resp.data.fare)
//设置应付金额
var total = [[${#numbers.formatDecimal(orderConfirmData.total,1,2)}]]
// total*1 将其转为数字类型
var payPrice = total*1+resp.data.fare*1;
$("#payPriceEle").text(payPrice)
$("#payPriceInput").val(payPrice)
//设置收货人信息
$("#receiveAddressEle").text(resp.data.memberAddressVo.province+" "+resp.data.memberAddressVo.detailAddress)
$("#receiveEle").text(resp.data.memberAddressVo.name)
//给表单回填选中的地址
$("#addrIdInput").val(addrId);
})
}

2、提交订单
在gulimall-order模块的com.atguigu.gulimall.order.vo包里新建SubmitOrderResponseVo类
package com.atguigu.gulimall.order.vo;
import com.atguigu.gulimall.order.entity.OrderEntity;
import lombok.Data;
/**
* @author 无名氏
* @date 2022/8/16
* @Description:
*/
@Data
public class SubmitOrderResponseVo {
/**
* 订单信息
*/
private OrderEntity order;
/**
* 下单状态码(成功为0)
*/
private Integer code;
}

在gulimall-order模块的com.atguigu.gulimall.order.web.OrderWebController类里添加submitOrder方法
@GetMapping("/submitOrder")
public String submitOrder(OrderSubmitVo vo) {
SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
//下单:去创建订单,验令牌,验价格,锁库存...
if (responseVo.getCode()==0){
//下单成功来到支付选择页
return "pay";
}else {
//下单失败回到订单确认页重新确认订单信息
return "redirect:http://order.gulimall.com/toTrade";
}
}

在gulimall-order模块的com.atguigu.gulimall.order.service.OrderService接口里添加submitOrder抽象方法
SubmitOrderResponseVo submitOrder(OrderSubmitVo vo);

在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类里实现submitOrder方法
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
MemberEntityTo memberEntityTo = LoginUserInterceptor.loginUser.get();
String key = OrderConstant.USER_ORDER_TOKEN_PREFIX + memberEntityTo.getId();
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
//下单:去创建订单,验令牌,验价格,锁库存...
String orderToken = vo.getOrderToken();
//验证并删除令牌[令牌的对比和删除必须保证原子性]
//0:令牌失败 - 1:删除成功
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Collections.singletonList(key), orderToken);
if (result==null || result == 0L) {
//令牌验证失败
response.setCode(1);
return response;
}
//令牌验证成功
return response;
}

在gulimall-order模块的com.atguigu.gulimall.order包里新建to文件夹,在to文件夹里新建OrderCreateTo类
package com.atguigu.gulimall.order.to;
import com.atguigu.gulimall.order.entity.OrderEntity;
import com.atguigu.gulimall.order.entity.OrderItemEntity;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* @author 无名氏
* @date 2022/8/16
* @Description:
*/
@Data
public class OrderCreateTo {
/**
* 订单实体类
*/
private OrderEntity order;
/**
* 订单项
*/
private List<OrderItemEntity> orderItems;
/**
* 运费
*/
private BigDecimal fare;
/**
* 订单计算的应付价格
*/
private BigDecimal payPrice;
}

3、创建订单
在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类里新建createOrder方法,然后submitOrder方法调用该createOrder方法
private OrderCreateTo createOrder(){
OrderCreateTo orderCreateTo = new OrderCreateTo();
OrderEntity orderEntity = new OrderEntity();
//订单号
String orderSn = IdWorker.getTimeId();
orderEntity.setOrderSn(orderSn);
return orderCreateTo;
}

将2.分布式高级篇(微服务架构篇)\资料源码\代码 里的 enume文件夹移动到gulimall-order模块里的com.atguigu.gulimall.order包下

4、获取运费
在gulimall-ware模块的com.atguigu.gulimall.ware.controller.WareInfoController类里已经有了一个计算运费的getFare方法了,因此直接调用即可

在gulimall-order模块的com.atguigu.gulimall.order.feign.WmsFeignService接口里添加如下方法,用于获取运费
/**
* 根据addrId获取运费 和 MemberAddressVo
* @param addrId
* @return
*/
@GetMapping("/ware/wareinfo/fare")
public R getFare(Long addrId);

复制gulimall-ware模块的com.atguigu.gulimall.ware.vo.FareVo类,粘贴到gulimall-order模块的com.atguigu.gulimall.order.vo包里。复制gulimall-ware模块的com.atguigu.gulimall.ware.vo.MemberAddressVo类里的代码,粘贴到gulimall-order模块的com.atguigu.gulimall.order.vo.FareVo类里,作为FareVo类的静态内部类
package com.atguigu.gulimall.order.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* @author 无名氏
* @date 2022/8/16
* @Description:
*/
@Data
public class FareVo {
private MemberAddressVo memberAddressVo;
private BigDecimal fare;
@Data
public static class MemberAddressVo {
/**
* id
*/
private Long id;
/**
* member_id
*/
private Long memberId;
/**
* 收货人姓名
*/
private String name;
/**
* 电话
*/
private String phone;
/**
* 邮政编码
*/
private String postCode;
/**
* 省份/直辖市
*/
private String province;
/**
* 城市
*/
private String city;
/**
* 区
*/
private String region;
/**
* 详细地址(街道)
*/
private String detailAddress;
/**
* 省市区代码
*/
private String areacode;
/**
* 是否默认
*/
private Integer defaultStatus;
}
}

5、获取spu信息
在gulimall-product模块的com.atguigu.gulimall.product.controller.SpuInfoController类里添加getSpuInfoBySkuId方法
@GetMapping("/skuId/{id}")
public R getSpuInfoBySkuId(@PathVariable("id") Long skuId) {
SpuInfoEntity spuInfoEntity = spuInfoService.getSpuInfoBySkuId(skuId);
return R.ok().put("data",spuInfoEntity);
}

在gulimall-product模块的com.atguigu.gulimall.product.service.SpuInfoService接口里添加getSpuInfoBySkuId抽象方法
SpuInfoEntity getSpuInfoBySkuId(Long skuId);

在gulimall-product模块的com.atguigu.gulimall.product.service.impl.SpuInfoServiceImpl类里实现getSpuInfoBySkuId方法
@Override
public SpuInfoEntity getSpuInfoBySkuId(Long skuId) {
SkuInfoEntity skuInfoEntity = skuInfoService.getById(skuId);
return this.getById(skuInfoEntity.getSpuId());
}

在gulimall-order模块的com.atguigu.gulimall.order.feign包里新建ProductFeignService类
package com.atguigu.gulimall.order.feign;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* @author 无名氏
* @date 2022/8/16
* @Description:
*/
@FeignClient("gulimall-product")
public interface ProductFeignService {
@GetMapping("/product/spuinfo/skuId/{id}")
public R getSpuInfoBySkuId(@PathVariable("id") Long skuId);
}

在gulimall-order模块的com.atguigu.gulimall.order.vo包里新建SpuInfoVo类
package com.atguigu.gulimall.order.vo;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* spu信息
*/
@Data
public class SpuInfoVo implements Serializable {
/**
* 商品id
*/
private Long id;
/**
* 商品名称
*/
private String spuName;
/**
* 商品描述
*/
private String spuDescription;
/**
* 所属分类id
*/
private Long catalogId;
/**
* 品牌id
*/
private Long brandId;
/**
*
*/
private BigDecimal weight;
/**
* 上架状态[0 - 下架,1 - 上架]
*/
private Integer publishStatus;
private Date createTime;
private Date updateTime;
}


4、锁定库存
1、添加To
在gulimall-common模块的com.atguigu.common.to类里新建ware文件夹,在ware文件夹里新建WareSkuLockTo类
package com.atguigu.common.to.ware;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* @author 无名氏
* @date 2022/8/17
* @Description: 下订单后锁库存
*/
@Data
public class WareSkuLockTo {
/**
* 订单号
*/
private String orderSn;
/**
* 需要锁的库存
*/
private List<OrderItemVo> locks;
/**
* 订单项(某一个具体商品)
*/
@Data
public static class OrderItemVo{
/**
* sku的id
*/
private Long skuId;
/**
* 商品的标题
*/
private String title;
/**
* 商品的图片
*/
private String image;
/**
* sku的属性(选中的 颜色、内存容量 等)
*/
private List<String> skuAttr;
/**
* 商品的价格
*/
private BigDecimal price;
/**
* 商品的数量
*/
private Integer count;
/**
* 总价(商品价格*商品数量)
*/0-
private BigDecimal totalPrice;
/**
* 货物重量
*/
private BigDecimal weight;
}
}

在gulimall-common模块的com.atguigu.common.to.ware包里新建WareLockStockResult类
package com.atguigu.common.to.ware;
import lombok.Data;
/**
* @author 无名氏
* @date 2022/8/17
* @Description:
*/
@Data
public class WareLockStockResult {
/**
* 要锁定的sku的id
*/
private Long skuId;
/**
* 锁定了的件数
*/
private Integer num;
/**
* 是否锁定成功
*/
private Boolean locked;
}

在gulimall-common模块的com.atguigu.common.exception.BizCodeException枚举类里添加枚举
/**
* 下订单锁库存,没有库存的异常
*/
NO_STOCK_EXCEPTION(21000,"商品库存不足");

2、锁订单
在gulimall-ware模块的com.atguigu.gulimall.ware.controller.WareSkuController类里添加orderLockStock方法
/**
* 下订单后。锁库存
*/
@PostMapping("/lock/order")
public R orderLockStock(@RequestBody WareSkuLockTo wareSkuLockTo){
try {
Boolean stock = wareSkuService.orderLockStock(wareSkuLockTo);
return R.ok();
} catch (Exception e) {
e.printStackTrace();
return R.error(BizCodeException.NO_STOCK_EXCEPTION);
}
}

在gulimall-ware模块的com.atguigu.gulimall.ware.service.WareSkuService接口里添加orderLockStock抽象方法
Boolean orderLockStock(WareSkuLockTo wareSkuLockTo);

在gulimall-ware模块的com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl类里实现orderLockStock方法
/**
* 为订单锁定库存
* @param wareSkuLockTo
* @return
*/
@Transactional(rollbackFor = Exception.class)
@Override
public Boolean orderLockStock(WareSkuLockTo wareSkuLockTo) {
//按照下单的收货地址,找到一-个就近仓库,锁定库存。
//找到每个商品在哪个仓库都有库存
List<WareSkuLockTo.OrderItemVo> locks = wareSkuLockTo.getLocks();
List<SkuWareHasStock> collect = locks.stream().map(orderItemVo -> {
SkuWareHasStock skuWareHasStock = new SkuWareHasStock();
Long skuId = orderItemVo.getSkuId();
skuWareHasStock.setSkuId(skuId);
//select ware_id from wms_ware_sku where sku_id = 1 and stock - stock_locked > 0
List<Long> wareId = wareSkuDao.listWareIdHasSkuStock(skuId);
skuWareHasStock.setWareId(wareId);
skuWareHasStock.setNum(orderItemVo.getCount());
return skuWareHasStock;
}).collect(Collectors.toList());
//锁定库存
for (SkuWareHasStock hasStock : collect) {
boolean skuStocked = false;
Long skuId = hasStock.getSkuId();
List<Long> wareIds = hasStock.getWareId();
//没有库存
if (CollectionUtils.isEmpty(wareIds)) {
throw new NoStockException(skuId);
}
//锁定库存
for (Long wareId : wareIds) {
//成功返回1,失败返回0
//update wms_ware_sku set stock_locked = stock_locked+2 where sku_id=1 and ware_id = 1 and stock - stock_locked>=2
Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
if(count==1){
//锁库存成功
skuStocked = true;
break;
}else {
//锁库存成功
}
}
if (!skuStocked){
//当前商品没有库存了
throw new NoStockException(skuId);
}
}
return null;
}
/**
* 判断哪些商品有库存
*/
@Data
class SkuWareHasStock{
private Long skuId;
private Integer num;
private List<Long> wareId;
}

在gulimall-ware模块的com.atguigu.gulimall.ware.dao.WareSkuDao接口里添加listWareIdHasSkuStock方法
List<Long> listWareIdHasSkuStock(@Param("skuId") Long skuId);

在gulimall_wms数据库的wms_ware_sku表里,修改stock_locked字段,设置默认值为0

把gulimall_wms数据库的wms_ware_sku表里stock_locked字段为null的数据都修改为0

在gulimall-ware模块的src/main/resources/mapper/ware/WareSkuDao.xml文件里添加sql语句
<!--根据skuId查询有库存的仓库列表-->
<select id="listWareIdHasSkuStock" resultType="java.lang.Long">
select ware_id from gulimall_wms.wms_ware_sku where sku_id = #{skuId} and stock - stock_locked > 0
</select>

在gulimall-ware模块的com.atguigu.gulimall.ware包里新建exception文件夹,在exception文件夹里新建NoStockException异常类,用于抛出没有库存异常
package com.atguigu.gulimall.ware.exception;
/**
* @author 无名氏
* @date 2022/8/17
* @Description:
*/
public class NoStockException extends RuntimeException {
private Long skuId;
public NoStockException(Long skuId) {
super("商品id:"+ skuId +";没有足够的库存了");
}
public Long getSkuId() {
return skuId;
}
public void setSkuId(Long skuId) {
this.skuId = skuId;
}
}

3、锁订单
在gulimall-ware模块的com.atguigu.gulimall.ware.dao.WareSkuDao接口里添加lockSkuStock方法
//锁库存
Long lockSkuStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("num") Integer num);

在gulimall-ware模块的src/main/resources/mapper/ware/WareSkuDao.xml文件里添加sql
<update id="lockSkuStock">
update gulimall_wms.wms_ware_sku set stock_locked = stock_locked+#{num}
where sku_id=#{skuId} and ware_id = #{wareId} and stock - stock_locked>=#{num}
</update>

在gulimall-order模块的com.atguigu.gulimall.order.feign.WmsFeignService接口里添加orderLockStock方法
/**
* 下订单后。锁库存
*/
@PostMapping("/ware/waresku/lock/order")
public R orderLockStock(@RequestBody WareSkuLockTo wareSkuLockTo);

在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类里修改submitOrder方法
@Transactional(rollbackFor = Exception.class)
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
orderSubmitVoThreadLocal.set(vo);
MemberEntityTo memberEntityTo = LoginUserInterceptor.loginUser.get();
String key = OrderConstant.USER_ORDER_TOKEN_PREFIX + memberEntityTo.getId();
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
//下单:去创建订单,验令牌,验价格,锁库存...
String orderToken = vo.getOrderToken();
//验证并删除令牌[令牌的对比和删除必须保证原子性]
//0:令牌失败 - 1:删除成功
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Collections.singletonList(key), orderToken);
if (result == null || result == 0L) {
//令牌验证失败
response.setCode(1);
return response;
}
//令牌验证成功
//创建订单
OrderCreateTo orderCreateTo = createOrder();
//验价
if (Math.abs(orderCreateTo.getPayPrice().subtract(vo.getPayPrice()).doubleValue()) < 0.01) {
//保存订单
this.saveOrder(orderCreateTo);
//锁定库存
WareSkuLockTo wareSkuLockTo = new WareSkuLockTo();
wareSkuLockTo.setOrderSn(orderCreateTo.getOrder().getOrderSn());
List<WareSkuLockTo.OrderItemVo> orderItemVos = orderCreateTo.getOrderItems().stream().map(orderItemEntity -> {
WareSkuLockTo.OrderItemVo orderItemVo = new WareSkuLockTo.OrderItemVo();
orderItemVo.setSkuId(orderItemEntity.getSkuId());
orderItemVo.setCount(orderItemEntity.getSkuQuantity());
orderItemVo.setTitle(orderItemEntity.getSkuName());
return orderItemVo;
}).collect(Collectors.toList());
wareSkuLockTo.setLocks(orderItemVos);
R r = wmsFeignService.orderLockStock(wareSkuLockTo);
if (r.getCode() == 0) {
//锁定库存成功
response.setCode(0);
response.setOrder(orderCreateTo.getOrder());
return response;
} else {
//锁定库存失败
response.setCode(3);
return response;
}
} else {
//金额对比失败
response.setCode(2);
return response;
}
}

5、准备支付
1、修改页面
修改gulimall-order模块的com.atguigu.gulimall.order.web.OrderWebController类的submitOrder方法
@GetMapping("/submitOrder")
public String submitOrder(OrderSubmitVo vo,Model model) {
SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
//下单:去创建订单,验令牌,验价格,锁库存...
if (responseVo.getCode()==0){
//下单成功来到支付选择页
model.addAttribute("submitOrderResp",responseVo);
return "pay";
}else {
//下单失败回到订单确认页重新确认订单信息
return "redirect:http://order.gulimall.com/toTrade";
}
}

在 http://order.gulimall.com/toTrade 页面里,打开控制台,定位到订单提交成功,请尽快付款!订单号:位置,复制订单号

在gulimall-order模块的src/main/resources/templates/pay.html文件里搜索订单号,修改成动态的订单号和金额数据
<dd>
<span>订单提交成功,请尽快付款!订单号:[[${submitOrderResp.order.orderSn}]]</span>
<span>应付金额<font> [[${#numbers.formatDecimal(submitOrderResp.order.payAmount,1,2)}]]</font>元</span>
</dd>

2、POST 请求方式不支持
重启GulimallProductApplication服务,GulimallOrderApplication服务、GulimallWareApplication服务、GulimallCartApplication服务
浏览器访问 http://order.gulimall.com/submitOrder 页面,报了POST 请求方式不支持
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Wed Aug 17 18:04:07 CST 2022
There was an unexpected error (type=Method Not Allowed, status=405).
Request method 'POST' not supported

在gulimall-order模块的com.atguigu.gulimall.order.web.OrderWebController类的submitOrder方法上,将@GetMapping("/submitOrder")修改为@PostMapping("/submitOrder")

3、方法不被允许
重启GulimallOrderApplication服务,登陆后,在 http://cart.gulimall.com/cart.html 购物页面里点击去结算,然后再 http://order.gulimall.com/toTrade 页面里点击提交订单,此时跳转到 http://order.gulimall.com/submitOrder 页面,并报了个错

在 http://order.gulimall.com/submitOrder 页面里报了如下错误,读取WmsFeignService#getFare(Long)失败
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Wed Aug 17 18:08:07 CST 2022
There was an unexpected error (type=Internal Server Error, status=500).
status 405 reading WmsFeignService#getFare(Long)

查看GulimallOrderApplication服务的控制台,提示WmsFeignService#getFare(Long)方法不被允许
2022-08-17 18:52:37.101 ERROR 12336 --- [nio-9000-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is feign.FeignException$MethodNotAllowed: status 405 reading WmsFeignService#getFare(Long)] with root cause
feign.FeignException$MethodNotAllowed: status 405 reading WmsFeignService#getFare(Long)
at feign.FeignException.errorStatus(FeignException.java:100) ~[feign-core-10.2.3.jar:na]
at feign.FeignException.errorStatus(FeignException.java:86) ~[feign-core-10.2.3.jar:na]
at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:93) ~[feign-core-10.2.3.jar:na]
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:149) ~[feign-core-10.2.3.jar:na]
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:78) ~[feign-core-10.2.3.jar:na]
at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:103) ~[feign-core-10.2.3.jar:na]
at com.sun.proxy.$Proxy105.getFare(Unknown Source) ~[na:na]
at com.atguigu.gulimall.order.service.impl.OrderServiceImpl.buildOrder(OrderServiceImpl.java:282) ~[classes/:na]
at com.atguigu.gulimall.order.service.impl.OrderServiceImpl.createOrder(OrderServiceImpl.java:208) ~[classes/:na]
at com.atguigu.gulimall.order.service.impl.OrderServiceImpl.submitOrder(OrderServiceImpl.java:150) ~[classes/:na]
at com.atguigu.gulimall.order.service.impl.OrderServiceImpl$$FastClassBySpringCGLIB$$99092a92.invoke(<generated>) ~[classes/:na]

在gulimall-order模块的com.atguigu.gulimall.order.feign.WmsFeignService类的getFare方法上,明明使用的是@GetMapping("/ware/wareinfo/fare")调用的远程服务,GulimallWareApplication服务的控制台却显示 Request method 'POST' not supported

可以看到调用远程的gulimall-ware模块的com.atguigu.gulimall.ware.controller.WareInfoController类的getFare方法也使用的是@GetMapping("/fare"),都使用的是@GetMapping应该是可以成功的啊
2022-08-17 18:52:37.086 WARN 15556 --- [io-11000-exec-9] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' not supported]

4、源码调试
调试发现,R r = wmsFeignService.getFare(orderSubmitVo.getAddrId());方法调用的远程服务的addrId=1,
此时的feign.ReflectiveFeign.FeignInvocationHandler#invoke方法的proxy参数里,h->target->type的name即为gulimall-order模块的com.atguigu.gulimall.order.feign.WmsFeignService远程调用gulimall-ware模块的类
h->target的name即为要调用的模块名,h->target的url即为要调用的GulimallWareApplication服务的url(负载均衡到gulimall-ware模块)
h->dispatch为LinkedHashMap类型的WmsFeignService类的所有方法集合,key为Method类型,该Method类的name即为方法名,returnType即为返回类型

h->dispatch里随便点击一个类,这个类的value为feign.SynchronousMethodHandler类型,里面的target和proxy参数的h->target差不多

由此可知h->target->type->name即为远程调用接口的全类名,@FeignClient("gulimall-ware")与feign.ReflectiveFeign.FeignInvocationHandler#invoke方法的proxy参数的h->target->name对应
h->dispatch为该类的方法的信息

method即为该方法的信息

argv即为调用该方法传递的参数

此时的feign.SynchronousMethodHandler#invoke方法的argv里面已经有attrId的1了,而template的queries里竟然没有数据

修改gulimall-order模块的com.atguigu.gulimall.order.feign.WmsFeignService类的getFare方法的参数,给Long addrId参数加上@RequestParam("addrId")注解,向feign指明把addrId放在请求参数里
/**
* 根据addrId获取运费 和 MemberAddressVo
* @param addrId
* @return
*/
@GetMapping("/ware/wareinfo/fare")
public R getFare(@RequestParam("addrId") Long addrId);

修改gulimall-ware模块的com.atguigu.gulimall.ware.controller.WareInfoController类的getFare方法的参数,给Long addrId参数加上@RequestParam("addrId")注解(其实这个指不指定都行,Spring MVC会从请求参数里获取数据,亲测)
@GetMapping("/fare")
public R getFare(@RequestParam("addrId") Long addrId) {
FareVo fare = wareInfoService.getFare(addrId);
return R.ok().put("data", fare);
}

重启GulimallOrderApplication服务和GulimallWareApplication服务,重新调试,可以看到queries里已经封装请求参数addrId=1了

5、空指针
在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类的submitOrder方法里报了空指针,orderEntity的growth字段是后面才赋值的,这里用错类了,应该使用orderItemEntity对象的giftGrowth字段
java.lang.NullPointerException: null
at com.atguigu.gulimall.order.service.impl.OrderServiceImpl.computePrice(OrderServiceImpl.java:248) ~[classes/:na]
at com.atguigu.gulimall.order.service.impl.OrderServiceImpl.createOrder(OrderServiceImpl.java:212) ~[classes/:na]
at com.atguigu.gulimall.order.service.impl.OrderServiceImpl.submitOrder(OrderServiceImpl.java:150) ~[classes/:na]
at com.atguigu.gulimall.order.service.impl.OrderServiceImpl$$FastClassBySpringCGLIB$$99092a92.invoke(<generated>) ~[classes/:na]

将gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类的submitOrder方法里的giftGrowth = giftGrowth.add(new BigDecimal(orderEntity.getGrowth().toString()));改为giftGrowth = giftGrowth.add(new BigDecimal(orderItemEntity.getGiftGrowth().toString()));

6、order_sn字段太长
重启GulimallOrderApplication服务,再次测试, 在 http://order.gulimall.com/submitOrder 页面里又报错了
Data too long for column 'order_sn' at row 1 ;插入的order_sn字段太长
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Wed Aug 17 20:01:45 CST 2022
There was an unexpected error (type=Internal Server Error, status=500).
### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'order_sn' at row 1 ### The error may exist in com/atguigu/gulimall/order/dao/OrderDao.java (best guess) ### The error may involve com.atguigu.gulimall.order.dao.OrderDao.insert-Inline ### The error occurred while setting parameters ### SQL: INSERT INTO oms_order ( integration_amount, order_sn, receiver_province, auto_confirm_day, coupon_amount, modify_time, receiver_phone, pay_amount, delete_status, member_username, member_id, freight_amount, receiver_detail_address, total_amount, integration, growth, promotion_amount, status ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) ### Cause: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'order_sn' at row 1 ; Data truncation: Data too long for column 'order_sn' at row 1; nested exception is com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'order_sn' at row 1

查看GulimallOrderApplication服务的控制台,可以发现是·1order_sn字段太长导致的
2022-08-17 20:01:45.700 ERROR 8480 --- [nio-9000-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException:
### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'order_sn' at row 1
### The error may exist in com/atguigu/gulimall/order/dao/OrderDao.java (best guess)
### The error may involve com.atguigu.gulimall.order.dao.OrderDao.insert-Inline
### The error occurred while setting parameters
### SQL: INSERT INTO oms_order ( integration_amount, order_sn, receiver_province, auto_confirm_day, coupon_amount, modify_time, receiver_phone, pay_amount, delete_status, member_username, member_id, freight_amount, receiver_detail_address, total_amount, integration, growth, promotion_amount, status ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
### Cause: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'order_sn' at row 1
; Data truncation: Data too long for column 'order_sn' at row 1; nested exception is com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'order_sn' at row 1] with root cause
com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'order_sn' at row 1
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:104) ~[mysql-connector-java-8.0.17.jar:8.0.17]
......
at com.sun.proxy.$Proxy90.insert(Unknown Source) ~[na:na]
at com.baomidou.mybatisplus.extension.service.impl.ServiceImpl.save(ServiceImpl.java:104) ~[mybatis-plus-extension-3.2.0.jar:3.2.0]
at com.atguigu.gulimall.order.service.impl.OrderServiceImpl.saveOrder(OrderServiceImpl.java:192) ~[classes/:na]
at com.atguigu.gulimall.order.service.impl.OrderServiceImpl.submitOrder(OrderServiceImpl.java:154) ~[classes/:na]
at com.atguigu.gulimall.order.service.impl.OrderServiceImpl$$FastClassBySpringCGLIB$$99092a92.invoke(<generated>) ~[classes/:na]

在gulimall_oms数据库的oms_order表里,将order_sn字段的长度从32改为64

在gulimall_oms数据库的oms_order_item表里,将order_sn字段的长度从32改为64

刷新http://order.gulimall.com/toTrade(不用重启任何服务),选择第二个收货地址,点击提交订单,可以看到已经生成了订单号和应付价格,gulimall_oms数据库的oms_order表已经生成了一条订单数据,gulimall_oms数据库的oms_order_item表生成了两条订单项数据

此时gulimall_wms数据库的wms_ware_sku表的华为 HUAWEI Mate30Pro 罗兰紫 8GB+128GB已经锁住了3件库存,苹果手机已经锁了5件库存

与 http://order.gulimall.com/toTrade 页面里显示的一样

6、完善细节
1、添加下单提示信息
在gulimall-order模块的com.atguigu.gulimall.order.web.OrderWebController类里,修改submitOrder方法
@PostMapping("/submitOrder")
public String submitOrder(OrderSubmitVo vo, Model model, RedirectAttributes redirectAttributes) {
SubmitOrderResponseVo responseVo = orderService.submitOrder(vo);
//下单:去创建订单,验令牌,验价格,锁库存...
if (responseVo.getCode()==0){
//下单成功来到支付选择页
model.addAttribute("submitOrderResp",responseVo);
return "pay";
}else {
//下单失败回到订单确认页重新确认订单信息
String msg = "下单失败,";
//成功为0,令牌验证失败为1,金额对比失败为2,锁定库存失败为3
switch (responseVo.getCode()){
case 1: msg+="订单信息过期,请刷新页面再提交"; break;
case 2: msg+="订单商品发送变化,请刷新页面重新获取订单信息";break;
case 3: msg+="库存锁定失败,商品库存不足";break;
default: msg+="未知异常,请刷新重试";
}
redirectAttributes.addFlashAttribute("msg",msg);
return "redirect:http://order.gulimall.com/toTrade";
}
}

重启GulimallOrderApplication服务,在 http://order.gulimall.com/toTrade 页面里,就显示填写并核对订单信息了

因为(亲测不行),默认给请求域也放了数据直接使用redirectAttributes.addFlashAttribute("msg",msg);模拟了session可以在session里获取msgmsg也能获取数据
在gulimall-order模块的src/main/resources/templates/confirm.html文件里搜索填写并核对订单信息,修改为如下代码
<p class="p1">填写并核对订单信息 <span style="color: red" th:if="${msg!=null}" th:text="${msg}"></span></p>

在gulimall_wms数据库的wms_ware_sku表里,修改锁定的库存数,让库存数和锁定的库存数相等

重启GulimallOrderApplication服务,打开 http://order.gulimall.com/toTrade 页面,点击提交订单,提示下单失败,库存锁定失败,商品库存不足

在gulimall_oms数据库的oms_order表里,此时还在创建一个订单

在gulimall_oms数据库的oms_order_item表里,也创建了2个订单项

在gulimall_wms数据库的wms_ware_sku表里,此时的锁定库存的数量不变

2、完善信息
在gulimall-order模块的src/main/resources/templates/confirm.html文件里搜索填写并核对订单信息,可以在session中获取msg,在<p>标签里的填写并核对订单信息后面添加<span style="color: red" th:if="${session.msg!=null}" th:text="${session.msg}"></span>
<p class="p1">填写并核对订单信息 <span style="color: red" th:if="${session.msg!=null}" th:text="${session.msg}"></span></p>

点击Build -> Recompile 'confirm.html' 或按快捷键Ctrl+ Shift+F9,重新编译当前静态文件,显示了错误的消息:请先进行登录

而调试是显示"下单失败,库存锁定失败,商品库存不足"

所以此方法不适用
6.2、分布式事务
6.2.1、分布式事务理论

事务保证:
1、订单服务异常,库存锁定不运行,全部回滚, 撤销操作
2、库存服务事务自治,锁定失败全部回滚,订单感受到异常,继续回滚
3、库存服务锁定成功了,但是网络原因返回数据途中问题?
订单服务检测到报了READ TIMEOUT读取超时异常,订单服务回滚了,而库存服务没回滚
4、库存服务锁定成功了,库存服务下面的逻辑发生故障,订单回滚了,怎么处理?
此时订单服务回滚了,而库存服务没有回滚
利用消息队列实 现最终一致
库存服务锁定成功后发给消息队列消息(当前库存工作单),过段时间自动解锁,解锁时先查询 订单的支付状态。解锁成功修改库存工作单详情 项状态为已解锁
1、远程服务假失败:
远程服务其实成功了,由于网络故障等没有返回 导致:订单回滚,库存却扣减
2、远程服务执行完成,下面的其他方法出现问题
导致:已执行的远程请求,肯定不能回滚
本地事务,在分布式系统,只能控制住自己的回滚,控制不了其他服务的回滚 分布式事务: 最大原因。网络问题+分布式机器。
同一个Service也可以,只不过调用不能直接调,需要把当前Service通过@AutoWired注入进来调用,不过会循环依赖
本地事务失效问题 同一个对象内事务方法互调默认失效,原因绕过了代理对象,事务使用代理对象来控制的 解决:使用代理对象来调用事务方法 1)、引入aop-starter;spring-boot-starter-aop 引入了aspectj 2)、@EnableAspectJAutoProxy(exposeProxy = true)开启aspectj动态代理功能。以后所有的动态代理都是aspectj创建的。(即使没有接口也可以创建动态代理) 对外暴露代理对象,然后本类互调用代理对象
1、本地事务
1、事务的基本性质
数据库事务的几个特性:原子性(Atomicity )、一致性( Consistency )、隔离性或独立性( Isolation) 和持久性(Durabilily),简称就是 ACID;
原子性:一系列的操作整体不可拆分,要么同时成功,要么同时失败
一致性:数据在事务的前后,业务整体一致。
- 转账。A:1000;B:1000; 转 200 事务成功; A:800 B:1200
隔离性:事务之间互相隔离。
持久性:一旦事务成功,数据一定会落盘在数据库。
在以往的单体应用中,我们多个业务操作使用同一条连接操作不同的数据表,一旦有异常, 我们可以很容易的整体回滚; Business:我们具体的业务代码Storage:库存业务代码;扣库存Order:订单业务代码;保存订单Account:账号业务代码;减账户余额 比如买东西业务,扣库存,下订单,账户扣款,是一个整体;必须同时成功或者失败 一个事务开始,代表以下的所有操作都在同一个连接里面;

@Transactional(rollbackFor = Exception.class,isolation = Isolation.REPEATABLE_READ,propagation = Propagation.REQUIRED,timeout = 30)
2、事务的隔离级别
READ UNCOMMITTED(读未提交) 该隔离级别的事务会读到其它未提交事务的数据,此现象也称之为脏读。 READ COMMITTED(读已提交) 一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重 复读问题,Oracle 和 SQL Server 的默认隔离级别。 REPEATABLE READ(可重复读) 该隔离级别是 MySQL 默认的隔离级别,在同一个事务里,select 的结果是事务开始时时间点的状态,因此,同样的 select 操作读到的结果会是一致的,但是,会有幻读现象。MySQL 的 InnoDB 引擎可以通过 next-key locks 机制(参考下文"行锁的算法"一节)来避免幻读。 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、事务的坑 在同一个类里面,编写两个方法,内部调用的时候,会导致事务设置失效。原因是没有用到代理对象的缘故。解决: 1)、导入 spring-boot-starter-aop 2)、@EnableTransactionManagement(proxyTargetClass = true) 3)、@EnableAspectJAutoProxy(exposeProxy=true) 4)、AopContext.currentProxy() 调用方法
2、分布式事务
1、为什么有分布式事务
分布式系统经常出现的异常 机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的 TCP、存储数据丢失...

分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在微服务架构中,几乎可以说是无法避免。
2、CAP 定理与 BASE 理论
1、CAP 定理 CAP 原则又称 CAP 定理,指的是在一个分布式系统中
一致性(Consistency):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访 问同一份最新的数据副本)
可用性(Availability):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据 更新具备高可用性)
分区容错性(Partition tolerance):大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务 器放在美国,这就是两个区,它们之间可能无法通信。
CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

一般来说,分区容错无法避免,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们, 剩下的 C 和 A 无法同时做到。 分布式系统中实现一致性的 raft 算法、paxos
raft算法动画演示: http://thesecretlivesofdata.com/raft/
2、面临的问题
对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所 以节点故障、网络故障是常态,而且要保证服务可用性达到 99.99999%(N 个 9),即保证P 和 A,舍弃 C。
3、BASE 理论
是对 CAP 理论的延伸,思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但可以采用适当的采取弱一致性,即最终一致性。BASE 是指
基本可用(Basically Available) 基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、 功能上的可用性),允许损失部分可用性。需要注意的是,基本可用绝不等价于系统不可用。 响应时间上的损失:正常情况下搜索引擎需要在 0.5 秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询 结果的响应时间增加到了 1~2 秒。 功能上的损失:购物网站在购物高峰(如双十一)时,为了保护系统的稳定性, 部分消费者可能会被引导到一个降级页面。
软状态( Soft State) 软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布 式存储中一般一份数据会有多个副本,允许不同副本同步的延时就是软状态的体 现。mysql replication 的异步复制也是一种体现。
最终一致性( Eventual Consistency) 最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状 态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
4、强一致性、弱一致性、最终一致性
从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。如果能容忍后续的部分或者全部访问不到,则是弱一致性。如果经过一段时间后要求 能访问到更新后的数据,则是最终一致性
3、分布式事务几种方案
1、2PC 模式
数据库支持的 2PC【2 phase commit 二阶提交】,又叫做 XA Transactions。MySQL 从 5.5 版本开始支持,SQL Server 2005 开始支持,Oracle 7 开始支持。其中,XA 是一个两阶段提交协议,该协议分为以下两个阶段: 第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交. 第二阶段:事务协调器要求每个数据库提交数据。 其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息。

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

一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
二阶段 commit 行为:调用 自定义 的 commit 逻辑。
二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。 所谓 TCC 模式,是指支持把 自定义的分支事务纳入到全局事务的管理中。

3、柔性事务-最大努力通知型方案
按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。这种 方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合 MQ 进行实现,例如:通过 MQ 发送 http 请求,设置最大通知次数。达到通知次数后即不再通知。 案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对账文件),支付宝的支付成功异步回调
4、柔性事务-可靠消息+最终一致性方案(异步确保型)
实现:业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只 记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确 认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。 防止消息丢失:
/**
*1、做好消息确认机制(pulisher,consumer【手动ack】)
*2、每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一遍
*/
CREATE TABLE `mq_message` (
`message_id` char(32) NOT NULL,
`content` text,
`to_exchane` varchar(255) DEFAULT NULL,
`routing_key` varchar(255) DEFAULT NULL,
`class_type` varchar(255) DEFAULT NULL,
`message_status` int(1) DEFAULT '0' COMMENT '0-新建 1-已发送 2-错误抵达 3-已抵达',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL, PRIMARY KEY (`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
6.2.2、Seata做分布式事务
1、Seata 简介
1、简介
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

2、Spring Cloud 中使用 Seata
Spring Cloud 中使用 Seata,使用 Feign 实现远程调用,使用 Spring Jpa 访问 MySQL 数据库
准备工作
- 执行
sql/all_in_one.sql - 下载最新版本的 Seata Sever
- 解压并启动 Seata server
unzip seata-server-xxx.zip
cd distribution
sh ./bin/seata-server.sh 8091 file
- 启动 Account, Order, Stock, Business 服务
数据库配置的用户名和密码是
root和123456,因为没有使用注册中心,所有的 Feign 的配置都是127.0.0.1+端口,如果不同请手动修改
测试
- 无错误成功提交
curl http://127.0.0.1:8084/purchase/commit
完成后可以看到数据库中 account_tbl的id为1的money会减少 5,order_tbl中会新增一条记录,stock_tbl的id为1的count字段减少 1
- 发生异常事务回滚
curl http://127.0.0.1:8084/purchase/rollback
此时 account-service 会抛出异常,发生回滚,待完成后数据库中的数据没有发生变化,回滚成功
注意
- 注入 DataSourceProxy
因为 Seata 通过代理数据源实现分支事务,如果没有注入,事务无法成功回滚
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
/**
* 需要将 DataSourceProxy 设置为主数据源,否则事务无法回滚
*
* @param druidDataSource The DruidDataSource
* @return The default datasource
*/
@Primary
@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}
- file.conf 的 service.vgroup_mapping 配置必须和
spring.application.name一致
在 org.springframework.cloud:spring-cloud-starter-alibaba-seata 的org.springframework.cloud.alibaba.seata.GlobalTransactionAutoConfiguration 类中,默认会使用 ${spring.application.name}-fescar-service-group作为服务名注册到 Seata Server上,如果和file.conf 中的配置不一致,会提示 no available server to connect错误
也可以通过配置 spring.cloud.alibaba.seata.tx-service-group修改后缀,但是必须和file.conf中的配置保持一致
2、使用
1、执行sql
在gulimall_wms数据库中执行如下sql(这个gulimall_wms数据库已经有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;
gulimall_wms数据库中已经有这个undo_log表了

在gulimall_ums数据库中执行刚刚的sql

在gulimall_sms数据库中执行刚刚的sql

在gulimall_pms数据库中执行刚刚的sql

在gulimall_oms数据库中执行刚刚的sql

在gulimall_admin数据库中执行刚刚的sql

2、添加seata依赖
在gulimall-common模块的pom.xml文件里添加seata依赖
<!--导入seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

导入这个依赖后,会自动导入spring-cloud-alibaba-seata-2.1.0.RELEASE.jar

和seata-all-0.7.1.jar,需要注意seata-all-0.7.1.jar的版本和seata-server的版本需要一致

3、启动seata-server
在seata-server-0.7.1\conf\registry.conf文件里,把registry里的type = "file"修改为type = "nacos",把registry->nacos里的serverAddr = "localhost"修改为serverAddr = "localhost:8848"

还可以指定配置中心,如果指定配置中心,需要把file.conf文件复制到配置中心里

双击seata-server-0.7.1\bin\seata-server.bat即可启动seata-server

打开nacos里的服务管理->服务列表即可看到serverAddr已经启动了

3、查看源码
在DataSourceAutoConfiguration类里,SpringBoot默认会导入DataSourceConfiguration.Hikari.class数据源
@Configuration
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class,
DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {
}

如果容器中没有DataSource.class会把HikariDataSource类放入到容器
/**
* Hikari DataSource configuration.
*/
@Configuration
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
matchIfMissing = true)
static class Hikari {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public HikariDataSource dataSource(DataSourceProperties properties) {
HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
}

创建HikariDataSource类时需要传递一个DataSourceProperties

org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration类使用了@EnableConfigurationProperties(DataSourceProperties.class)注解,把DataSourceProperties.class放入了容器

初始化HikariDataSource数据源时,调用的是createDataSource(properties, HikariDataSource.class);方法
/**
* Hikari DataSource configuration.
*/
@Configuration
@ConditionalOnClass(HikariDataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
matchIfMissing = true)
static class Hikari {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public HikariDataSource dataSource(DataSourceProperties properties) {
HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
}

调的其实就是DataSourceProperties类的initializeDataSourceBuilder().type(type).build();
@SuppressWarnings("unchecked")
protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) {
return (T) properties.initializeDataSourceBuilder().type(type).build();
}

4、修改配置
1、指定数据源
在gulimall-order模块的com.atguigu.gulimall.order.config包里新建MySeataConfig类,用于配置Seata相关的配置
package com.atguigu.gulimall.order.config;
import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
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;
/**
* @author 无名氏
* @date 2022/8/18
* @Description:
*/
@Configuration
public class MySeataConfig {
@Bean
public DataSource dataSource(DataSourceProperties properties){
HikariDataSource dataSource = properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return new DataSourceProxy(dataSource);
}
}

2、复制配置
把seata-server-0.7.1\conf里的file.conf和registry.conf复制到gulimall-order模块的src/main/resources里

在gulimall-order模块的src/main/resources/file.conf文件里,把vgroup_mapping.my_test_tx_group = "default"修改为vgroup_mapping.gulimall-order-fescar-service-group = "default"

复制gulimall-order模块的com.atguigu.gulimall.order.config.MySeataConfig类到gulimall-ware模块的com.atguigu.gulimall.ware.config包里

把seata-server-0.7.1\conf里的file.conf和registry.conf也复制一份到gulimall-ware模块的src/main/resources里,并把vgroup_mapping.my_test_tx_group = "default"修改为vgroup_mapping.gulimall-ware-fescar-service-group = "default"

5、使用分布式事务
1、简单使用
在需要使用分布式事务的入口业务方法上添加@GlobalTransactional注解和@Transactional注解。
在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类的submitOrder方法上@Transactional注解已经添加了,因此只需再添加@GlobalTransactional注解即可

在调用远程服务的业务方法只需使用@Transactional注解即可(在gulimall-ware模块的com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl类的orderLockStock方法这里已经使用@Transactional注解了,不需要再进行额外的配置)

2、服务启动失败
在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类的submitOrder方法里,在response.setOrder(orderCreateTo.getOrder());的下面添加int i = 10/0;,用于抛出除0异常,在else里的response.setCode(3);下面注释掉return response;,并在response.setCode(3);下面添加throw new RuntimeException("锁定库存失败");

重新启动所有服务后,GulimallThirdPartyApplication、GulimallGatewayApplication、GulimallMemberApplication三个服务都启动失败了,这是因为这些服务都引入了gulimall-common的依赖,所以也引入了seata依赖,但这些服务并没有配置seata

在gulimall-common模块的pom.xml文件里,注释掉对seata的依赖

在gulimall-order模块的pom.xml文件的<dependencies>里添加seata依赖
<!--导入seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
在gulimall-order模块的pom.xml文件的<dependencyManagement>的<dependencies>里对阿里巴巴的依赖进行版本管理
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

在gulimall-ware模块的pom.xml文件的<dependencies>里添加seata依赖
<!--导入seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
在gulimall-ware模块的pom.xml文件的<dependencyManagement>的<dependencies>里对阿里巴巴的依赖进行版本管理
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

3、端口被占用
启动GulimallProductApplication服务时在控制台报了10000端口被占用的异常
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2022-08-18 19:40:09.633 ERROR 7888 --- [ restartedMain] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
The Tomcat connector configured to listen on port 10000 failed to start. The port may already be in use or the connector may be misconfigured.
Action:
Verify the connector's configuration, identify and stop any process that's listening on port 10000, or configure this application to listen on another port.

在gulimall-product模块的src/main/resources/application.yml文件里修改端口为10001
server:
port: 10001

4、seata不支持异常
重启完各个服务后,在 http://order.gulimall.com/toTrade 页面里提交订单,来到 http://order.gulimall.com/submitOrder 页面,又报错了
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Thu Aug 18 19:47:42 CST 2022
There was an unexpected error (type=Internal Server Error, status=500).
### Error updating database. Cause: io.seata.common.exception.NotSupportYetException ### The error may exist in com/atguigu/gulimall/order/dao/OrderItemDao.java (best guess) ### The error may involve com.atguigu.gulimall.order.dao.OrderItemDao.insert-Inline ### The error occurred while setting parameters ### SQL: INSERT INTO oms_order_item ( sku_attrs_vals, spu_name, integration_amount, order_sn, sku_price, gift_integration, real_amount, sku_quantity, sku_name, spu_brand, coupon_amount, sku_pic, spu_id, gift_growth, promotion_amount, sku_id, category_id ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) ### Cause: io.seata.common.exception.NotSupportYetException

查看GulimallOrderApplication服务的控制台,可以看到OrderServiceImpl.java文件的203行报错了
2022-08-18 19:47:42.779 ERROR 16064 --- [nio-9000-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.apache.ibatis.exceptions.PersistenceException:
### Error updating database. Cause: io.seata.common.exception.NotSupportYetException
### The error may exist in com/atguigu/gulimall/order/dao/OrderItemDao.java (best guess)
### The error may involve com.atguigu.gulimall.order.dao.OrderItemDao.insert-Inline
### The error occurred while setting parameters
### SQL: INSERT INTO oms_order_item ( sku_attrs_vals, spu_name, integration_amount, order_sn, sku_price, gift_integration, real_amount, sku_quantity, sku_name, spu_brand, coupon_amount, sku_pic, spu_id, gift_growth, promotion_amount, sku_id, category_id ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )
### Cause: io.seata.common.exception.NotSupportYetException] with root cause
io.seata.common.exception.NotSupportYetException: null
at io.seata.rm.datasource.AbstractPreparedStatementProxy.addBatch(AbstractPreparedStatementProxy.java:252) ~[seata-all-0.7.1.jar:0.7.1]
......
org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688) ~[spring-aop-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at com.atguigu.gulimall.order.service.impl.OrderItemServiceImpl$$EnhancerBySpringCGLIB$$e1e4ea49.saveBatch(<generated>) ~[classes/:na]
at com.atguigu.gulimall.order.service.impl.OrderServiceImpl.saveOrder(OrderServiceImpl.java:203) ~[classes/:na]
at com.atguigu.gulimall.order.service.impl.OrderServiceImpl.submitOrder(OrderServiceImpl.java:163) ~[classes/:na]
at com.atguigu.gulimall.order.service.impl.OrderServiceImpl$$FastClassBySpringCGLIB$$99092a92.invoke(<generated>)

貌似是seata不支持批量保存,在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类里修改saveOrder方法,将批量保存修改为for循环单个保存
/**
* 保存订单数据
*
* @param orderCreateTo
*/
private void saveOrder(OrderCreateTo orderCreateTo) {
OrderEntity orderEntity = orderCreateTo.getOrder();
orderEntity.setModifyTime(new Date());
this.save(orderEntity);
for (OrderItemEntity orderItem : orderCreateTo.getOrderItems()) {
orderItemService.save(orderItem);
}
}

5、获取schema失败
在GulimallWareApplication服务的控制台报了如下两个主要错误
java.sql.SQLException: Failed to fetch schema of gulimall_wms.wms_ware_sku
at io.seata.rm.datasource.sql.struct.TableMetaCache.fetchSchemeInDefaultWay(TableMetaCache.java:115) ~[seata-all-0.7.1.jar:0.7.1]
at io.seata.rm.datasource.sql.struct.TableMetaCache.fetchSchema(TableMetaCache.java:94) ~[seata-all-0.7.1.jar:0.7.1]
at io.seata.rm.datasource.sql.struct.TableMetaCache.lambda$getTableMeta$0(TableMetaCache.java:73) ~[seata-all-0.7.1.jar:0.7.1]
at com.github.benmanes.caffeine.cache.BoundedLocalCache.lambda$doComputeIfAbsent$14(BoundedLocalCache.java:2039) ~[caffeine-2.6.2.jar:na]
at java.util.concurrent.ConcurrentHashMap.compute(ConcurrentHashMap.java:1853) ~[na:1.8.0_301]
......
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212) ~[spring-aop-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at com.sun.proxy.$Proxy94.lockSkuStock(Unknown Source) ~[na:na]
at com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl.orderLockStock(WareSkuServiceImpl.java:162) ~[classes/:na]
at com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl$$FastClassBySpringCGLIB$$422f6383.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.1.9.RELEASE.jar:5.1.9.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:749) ~[spring-aop-5.1.9.RELEASE.jar:5.1.9.RELEASE]

org.springframework.jdbc.UncategorizedSQLException:
### Error updating database. Cause: java.sql.SQLException: io.seata.common.exception.ShouldNeverHappenException: [xid:192.168.56.1:8091:2114539555]get tablemeta failed
### The error may exist in file [B:\gulimall\gulimall-ware\target\classes\mapper\ware\WareSkuDao.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: update gulimall_wms.wms_ware_sku set stock_locked = stock_locked+? where sku_id=? and ware_id = ? and stock - stock_locked>=?
### Cause: java.sql.SQLException: io.seata.common.exception.ShouldNeverHappenException: [xid:192.168.56.1:8091:2114539555]get tablemeta failed
; uncategorized SQLException; SQL state [null]; error code [0]; io.seata.common.exception.ShouldNeverHappenException: [xid:192.168.56.1:8091:2114539555]get tablemeta failed; nested exception is java.sql.SQLException: io.seata.common.exception.ShouldNeverHappenException: [xid:192.168.56.1:8091:2114539555]get tablemeta failed
at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:89)

尝试一:
百度一直在说Failed to fetch schema of XXXX表 java.sql.SQLException: Failed to fetch schema of XXXXX表是因为SPringCloud项目中使用了seata的分布式事务,其中xxx表中没有主键,给xxx表加一个主键ID就可以了。但是我的gulimall_wms数据库的wms_ware_sku表有主键啊

尝试二:
删除gulimall_wms数据库中已经存在的undo_log表

在gulimall_wms数据库里重新执行下面这条sql语句,重新创建表
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;

尝试三:(解决)
删掉gulimall-ware模块的src/main/resources/mapper/ware/WareSkuDao.xml文件里的id为lockSkuStock里的gulimall_wms.

gulimall-ware模块的src/main/resources/mapper/ware/WareSkuDao.xml文件报红了不用管,这IDEA不知道使用的是哪个数据库,检测到没有这些表才报红的,不影响程序运行,至此才解决这个问题

查看的链接:https://en.chowdera.com/2022/119/202204292239307418.html
原文链接:https://blog.csdn.net/weixin_44647371/article/details/124446175
io.seata.common.exception.ShouldNeverHappenException: Could not found any index in the table 报错关键语句:
Caused by: io.seata.common.exception.ShouldNeverHappenException: Could not found any index in the table
或者
io.seata.common.exception.ShouldNeverHappenException: [xid:192.168.25.1:8091:2104750907]get tablemeta failed
解决,我这边找了半天发现是mybatis的sql中要去掉数据库名导致的,去掉sql语句中的库名即可 
6、测试
重启GulimallWareApplication服务,截断gulimall_oms数据库的oms_order订单表和oms_order_item订单项表,将gulimall_wms数据库的wms_ware_sku库存表的stock_locked被锁库存属性全部修改为0,表示没有库存被锁住。在 http://order.gulimall.com/toTrade 页面里提交订单,来到 http://order.gulimall.com/submitOrder 页面,这时正确的报了/ by zero错误,查看数据库,可以看到gulimall_oms数据库的oms_order订单表和oms_order_item订单项表都没有新增数据,gulimall_wms数据库的wms_ware_sku库存表里也没有库存被锁住,因此可以判断出分布式事务已经生效了。

6、AT模式
Seata默认使用AT模式,类似于2PC二阶提交协议,不过2PC二阶提交协议的第一个阶段是准备阶段,不提交事务,而Seata直接把本地事务提交了。Seata在第二阶段如果失败了,就通过回滚日志进行反向补偿。
不过这并不适用于分布式系统中高并发的场景,倒是适合后台的saveSpuInfo方法

7、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=nacosfile.conf: 3、所有想要用到分布式事务的微服务使用seata DataSourceProxy代理 自己的数据源 4、每个微服务,都必须导入 registry. conffile.conf里需要配置vgroup mapping. {appl ication. name}-fescar-service-group = "default" 5、启动测试分布式事务 6、给分布式大事务的入口标注@GLobalTransactional 7、每一个远程的小事务用@Transactional
8、SpringBoot事务失效
如一个类里面定义了方法A()和方法B(),然后方法A和B都各自开启了事务,如果不做处理,直接在方法A中调用方法B,这时就会导致方法B里面的事务失效,也就是如果程序出错时,B的事务是失效的,数据回滚不了,我们可以使用aspect来解决这个问题
参考: Core Technologies (spring.io)
首先引入spring-boot-starter-aop依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

其实主要想使用spring-boot-starter-aop依赖的aspect

在主类上添加@EnableAspectJAutoProxy注解,并设置exposeProxy = true,对外暴露代理对象
@EnableAspectJAutoProxy(exposeProxy = true)

根据EnableAspectJAutoProxy注解接口的描述,我们可以使用AopContext获得当前代理对象
/**
* Indicate that the proxy should be exposed by the AOP framework as a {@code ThreadLocal}
* for retrieval via the {@link org.springframework.aop.framework.AopContext} class.
* Off by default, i.e. no guarantees that {@code AopContext} access will work.
* @since 4.3.1
*表明代理应该由 AOP 框架作为一个 {@code ThreadLocal} 被暴露
*通过*{@link org.springframework.aop.framework.AopContext} 类来获得。
*默认关闭,即不保证 {@code AopContext} 访问将起作用。
*/
boolean exposeProxy() default false;

然后调用代理对象的方法即可避免事务失效
package com.atguigu.gulimall.order.test;
import com.atguigu.gulimall.order.service.impl.OrderServiceImpl;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* @author 无名氏
* @date 2022/8/19
* @Description:
* 1、引入spring-boot-starter-aop依赖
* 2、@EnableAspectJAutoProxy(exposeProxy = true) exposeProxy = true对外暴露代理对象
* 3、AspectJTest orderService = (AspectJTest) AopContext.currentProxy();
*/
@Component
public class AspectJTest {
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED,timeout = 30)
public void a(){
System.out.println("执行a方法");
AspectJTest orderService = (AspectJTest) AopContext.currentProxy();
orderService.b();
orderService.c();
}
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED,timeout = 2)
public void b(){
System.out.println("执行b方法");
}
@Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRES_NEW,timeout = 20)
public void c(){
System.out.println("执行c方法");
}
}

6.2.3、延时队列
1、延时队列场景
1、场景
延时队列可以用于关闭订单和解锁库存等场景,比如当用户点击下订单,30分钟后仍未支付订单,我们可以关闭这个订单(将这个订单的状态设置为已关闭),并主动通知库存服务解锁库存。
当用户点击下订单后,我们会将库存锁住,表示用户已经准备购买这个商品了(锁库存是为了防止同一个商品被多个卖家购买到),过了40分钟后检查锁库存的对应订单的状态,如果是订单关闭(订单关闭后会通知解锁库存,保险起见库存服务还是要检查一下是否需要解锁库存)、订单不存在(下订单的过程中失败了,比如某个商品锁库存失败了,此时会通知已锁库存的服务让其解锁对应的库存,保险起见库存服务还是要检查一下是否需要解锁库存)等下单失败的状态时,解锁库存。

2、定时任务的时效性问题
如果我们使用传统的定时任务来做这件事情,就会有定时任务的时效性问题,比如用户在第1秒时下订单,此时用户一直未支付,直到第31秒时订单过期时需要及时解锁库存,但是由于定时任务是第0秒开始,每30秒执行一次,因此错过第30秒的检查后,只能在第60秒后才能检查到需要在第31秒时就应该解锁的库存,这样其他用户就不能及时看到其实应该已经解锁的库存。

3、RabbitMQ实现延时队列基础
使用定时任务不能解决问题的原因主要就是时效性问题,如果我们能给每个订单都设置设置一个过期时间就好了,因此我们可以使用RabbitMQ来实现延时队列
RabbitMQ延时队列(实现定时任务)
场景:
比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品。
常用解决方案
spring的 schedule 定时任务轮询数据库
缺点
消耗系统内存、增加了数据库的压力、存在较大的时间误差
解决:rabbitmq的消息TTL和死信Exchange结合
消息的TTL(Time To Live)
消息的TTL就是消息的存活时间。
RabbitMQ可以对队列和消息分别设置TTL。
对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。
如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。可以通过设置消息的expiration字段或者x- message-ttl属性来设置时间,两者是一样的效果。
Dead Letter Exchanges(DLX)
一个消息在满足如下条件下,会进死信路由,记住这里是路由而不是队列, 一个路由可以对应很多队列。(什么是死信)
一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不 会被再次放在队列里,被其他消费者使用。(basic.reject/ basic.nack)requeue=false
上面的消息的TTL到了,消息过期了。
队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上
Dead Letter Exchange其实就是一种普通的exchange,和创建其他exchange没有两样。只是在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去。
我们既可以控制消息在一段时间后变成死信,又可以控制变成死信的消息 被路由到某一个指定的交换机,结合二者,其实就可以实现一个延时队列
4、延时队列实现
1. 给队列设置过期时间
给一个队列设置一个过期时间,当时间到了仍然没有消费者处理时,这些消息就会交给死信交换机,死信交换机使用指定的死信路由键交给指定的队列,消费者可以获取指定队列的消息,这样获取到的都是延迟后的消息,进而间接实现了延时队列。

2. 给消息设置过期时间(不推荐)
不仅可以给队列设置过期时间,还可以给消息设置过期时间,不过不推荐该做法。
(RabbitMQ采用惰性检查机制,在队列中只有前面的消息被取走后才会检查下一个消息有没有过期)

5、简单设计
订单业务的简单设计如下图所示:下订单后,首先发布者将消息交给user.order.delay.exchange交换机,使用order_delay路由键交给user.order.delay.queue队列,到了过期时间后用户还没有支付则订单过期,过期后订单交给user.order.exchange交换机,使用order路由键交给user.order.queue队列,消费者监听user.order.queue队列就可以获取过期的订单消息了。

6、最终设计
最终设计的订单业务如下:(其实就是使用了同一个交换机,其他的也没怎么变动)
下订单后,首先发布者将消息交给order-event-exchange交换机,使用order.create.order路由键交给order.delay.queue队列,到了过期时间后用户还没有支付则订单过期,过期后订单还是交给order-event-exchange交换机不过路由键不同了,使用order.release.order路由键交给order.release.order.queue队列,消费者监听order.release.order.queue队列就可以获取过期的订单消息了。

本系统全部的消息列的路由过程如下

2、简单使用
1、创建队列、交换机、绑定关系
在gulimall-order模块的com.atguigu.gulimall.order.config包下新建MyMQConfig类,用于创建释放订单的队列、交换机、绑定关系,然后重启GulimallOrderApplication服务
package com.atguigu.gulimall.order.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @author 无名氏
* @date 2022/8/19
* @Description: 消息队列配置类
* 若RabbitMQ里没有,容器中的Binding, Queue, Exchange 都会自动创建
*/
@Configuration
public class MyMQConfig {
/**
* 给订单加上过期时间
* x-dead-letter-exchange: order-event-exchange
* x-dead-letter-routing-key: order.release.order
* x-message-ttl: 60000
* @return
*/
@Bean
public Queue orderDelayQueue(){
Map<String, Object> arguments = new HashMap<>();
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);
//Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
return new Queue("order.delay.queue",true,false,false,arguments);
}
/**
* 释放订单
* @return
*/
@Bean
public Queue orderReleaseOrderQueue(){
return new Queue("order.release.order.queue",true,false,false);
}
@Bean
public Exchange orderEventExchange(){
//TopicExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments)
return new TopicExchange("order-event-exchange",true,false);
}
@Bean
public Binding orderCreateOrderBinding(){
//Binding(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 orderReleaseOrderBinding(){
return new Binding("order.release.order.queue", Binding.DestinationType.QUEUE,
"order-event-exchange","order.release.order",null);
}
}

2、查看
(如果没有的话可以先不用管,spring默认使用的是懒加载,当消费者监听队列后才创建相应的交换机和队列。)
访问 http://192.168.56.10:15672/#/exchanges 页面即可看到自动创建了order-event-exchange交换机

访问 http://192.168.56.10:15672/#/queues 页面即可看到自动创建了order.delay.queue和order.release.order.queue队列

3、测试
先在gulimall-order模块的com.atguigu.gulimall.order.config.MyMQConfig类里添加如下方法,用于监听order.release.order.queue队列
/**
* com.atguigu.gulimall.order.web.HelloController类的createOrderTest方法发送消息
* @param orderEntity
* @param channel
* @param message
* @throws IOException
*/
@RabbitListener(queues = "order.release.order.queue")
public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException, InterruptedException {
Thread.sleep(5*1000);
System.out.println("收到过期的订单信息,准备关闭订单=>"+orderEntity.getOrderSn()+"时间=>"+orderEntity.getModifyTime());
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}

在gulimall-order模块的com.atguigu.gulimall.order.web.HelloController类里添加createOrderTest方法,用于测试给mq发消息
@Autowired
RabbitTemplate rabbitTemplate;
/**
*
* com.atguigu.gulimall.order.config.MyMQConfig类的listener方法监听消息
* @return
*/
@GetMapping("/test/createOrder")
@ResponseBody
public String createOrderTest(){
//订单下单成功
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(UUID.randomUUID().toString());
orderEntity.setModifyTime(new Date());
//给MQ发消息
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",orderEntity);
return "收到过期的订单信息,准备关闭订单=>"+orderEntity.getOrderSn()+"时间=>"+orderEntity.getModifyTime();
}

先登录,再刷新http://order.gulimall.com/test/createOrder页面5次,可以看到GulimallOrderApplication服务的控制台立即打出了5个消息抵达消息代理的回调。此时通过order.create.order路由键发送给order-event-exchange交换机,然后交换机根据路由键发送给了order.delay.queue队列,此时RabbltMQ的order.delay.queue也已近有5条准备的消息了。过了1分钟后,order.delay.queue队列已经到了过期时间,消息又使用order.release.order路由键发送给了order-event-exchange交换机,然后交换机根据路由键发送给了order.release.order.queue队列,gulimall-order模块的com.atguigu.gulimall.order.config.MyMQConfig类的listener方法监听该队列,因此控制台打印了关闭订单的信息,并手动确认了接收消息。

6.2.4、其他服务整合RabbitMQ
1、整合RabbitMQ
1、引入RabbitMQ
在gulimall-ware模块的pom.xml文件里添加引入amqp场景
<!--引入amqp场景,使用RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

在gulimall-ware模块的src/main/resources/application.properties配置文件里添加RabbitMQ配置
spring.rabbitmq.host=192.168.56.10
spring.rabbitmq.virtual-host=/

在gulimall-ware模块的com.atguigu.gulimall.ware.GulimallWareApplication启动类上添加@EnableRabbit注解,开启RabbitMQ功能
@EnableRabbit

2、业务图
接下来我们实现锁定库存的功能,其业务功能为下面画的红色方框的部分

在gulimall-ware模块的com.atguigu.gulimall.ware.config包里新建MyRabbitConfig类
package com.atguigu.gulimall.ware.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
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;
import java.util.concurrent.TimeUnit;
/**
* @author 无名氏
* @date 2022/8/19
* @Description:
*/
@Configuration
public class MyRabbitConfig {
/**
* 消息转换器(转换为JSON数据)
* @return
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
@Bean
public Exchange stockEventExchange(){
//TopicExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments)
return new TopicExchange("stock-event-exchange",true,false);
}
@Bean
public Queue stockReleaseStockQueue(){
//Queue(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(){
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange","stock-event-exchange");
arguments.put("x-dead-letter-routing-key", "stock.release");
arguments.put("x-message-ttl", TimeUnit.MINUTES.toMillis(2));
return new Queue("stock.delay.queue",true,false,false,arguments);
}
@Bean
public Binding stockReleaseBinging(){
//Binding(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(){
return new Binding("stock.delay.queue", Binding.DestinationType.QUEUE,
"stock-event-exchange","stock.locked",null);
}
}

启动GulimallWareApplication服务,访问 http://192.168.56.10:15672/#/exchanges 页面可以看到没有新增stock-event-exchange交换机

访问 http://192.168.56.10:15672/#/queues 页面可以看到没有新增stock.release.stock.queue和stock.delay.queue队列

这是因为Spring使用的是懒加载,当消费者监听队列后才创建相应的交换机和队列。使用@RabbitListener注解随便监听一个队列即可。
@RabbitListener(queues = "stock.release.stock.queue")
public void listener(Message message){
}

重启GulimallWareApplication服务,访问 http://192.168.56.10:15672/#/exchanges 页面可以看到已经创建stock-event-exchange交换机了

访问 http://192.168.56.10:15672/#/queues 页面可以看到已经创建stock.release.stock.queue和stock.delay.queue队列了

3、库存解锁的场景
库存解锁的场景
1)、下订单成功,订单过期没有支付被系统自动取消、被用户手动取消。都要解锁库存 2)、下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。 之前锁定的库存就要自动解锁。
在gulimall_wms数据库的wms_ware_order_task_detail表里,添加ware_id和lock_status(我这里已近添加过了)

在gulimall-ware模块的com.atguigu.gulimall.ware.entity.WareOrderTaskDetailEntity类里,添加private Long wareId;字段和private Integer lockStatus;字段。(这里已经添加过了)

在gulimall-ware模块的src/main/resources/mapper/ware/WareOrderTaskDetailDao.xml文件的id="wareOrderTaskDetailMap"的<resultMap>里,在后面添加`
<!-- 可根据自己的需求,是否要使用 -->
<resultMap type="com.atguigu.gulimall.ware.entity.WareOrderTaskDetailEntity" id="wareOrderTaskDetailMap">
<result property="id" column="id"/>
<result property="skuId" column="sku_id"/>
<result property="skuName" column="sku_name"/>
<result property="skuNum" column="sku_num"/>
<result property="taskId" column="task_id"/>
<result property="wareId" column="ware_id"/>
<result property="lockStatus" column="lock_status"/>
</resultMap>

在gulimall-ware模块的com.atguigu.gulimall.ware.entity.WareOrderTaskDetailEntity类上添加如下注解
@NoArgsConstructor
@AllArgsConstructor

在gulimall-common模块的com.atguigu.common包里新增mq文件夹,在mq文件夹里新建StockLockedTo类
package com.atguigu.common.mq;
import lombok.Data;
/**
* @author 无名氏
* @date 2022/8/19
* @Description: 每锁一件商品库存就向RabbitMQ发送一条消息
*/
@Data
public class StockLockedTo {
/**
* wms_ware_order_task
* 库存工作单的id
*/
private Long id;
/**
* wms_ware_order_task_detail
* 工作单详情的id
*/
private Long detailId;
}

在gulimall-ware模块的com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl类的orderLockStock方法里,保存库存工作单和工作单详情,并向RabbitMQ发送一条消息
@Autowired
RabbitTemplate rabbitTemplate;
@Autowired
WareOrderTaskService wareOrderTaskService;
@Autowired
WareOrderTaskDetailService wareOrderTaskDetailService;
/**
* 为订单锁定库存
* @param wareSkuLockTo
* @return
* 库存解锁的场景
* 1)、下订单成功,订单过期没有支付被系统自动取消、被用户手动取消。都要解锁库存
* 2)、下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。
* 之前锁定的库存就要自动解锁。
*/
@Transactional(rollbackFor = Exception.class)
@Override
public Boolean orderLockStock(WareSkuLockTo wareSkuLockTo) {
//保存库存工作单
WareOrderTaskEntity wareOrderTaskEntity = new WareOrderTaskEntity();
wareOrderTaskEntity.setOrderSn(wareSkuLockTo.getOrderSn());
wareOrderTaskService.save(wareOrderTaskEntity);
//按照下单的收货地址,找到一个就近仓库,锁定库存。
//找到每个商品在哪个仓库都有库存
List<WareSkuLockTo.OrderItemVo> locks = wareSkuLockTo.getLocks();
List<SkuWareHasStock> collect = locks.stream().map(orderItemVo -> {
SkuWareHasStock skuWareHasStock = new SkuWareHasStock();
Long skuId = orderItemVo.getSkuId();
skuWareHasStock.setSkuId(skuId);
//select ware_id from wms_ware_sku where sku_id = 1 and stock - stock_locked > 0
List<Long> wareId = wareSkuDao.listWareIdHasSkuStock(skuId);
skuWareHasStock.setWareId(wareId);
skuWareHasStock.setNum(orderItemVo.getCount());
return skuWareHasStock;
}).collect(Collectors.toList());
//锁定库存
for (SkuWareHasStock hasStock : collect) {
boolean skuStocked = false;
Long skuId = hasStock.getSkuId();
List<Long> wareIds = hasStock.getWareId();
//没有库存
if (CollectionUtils.isEmpty(wareIds)) {
throw new NoStockException(skuId);
}
//锁定库存
for (Long wareId : wareIds) {
//成功返回1,失败返回0
//update wms_ware_sku set stock_locked = stock_locked+2 where sku_id=1 and ware_id = 1 and stock - stock_locked>=2
Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());
if(count==1){
//锁库存成功
skuStocked = true;
//保存工作单详情
WareOrderTaskDetailEntity wareOrderTaskDetailEntity = new WareOrderTaskDetailEntity();
wareOrderTaskDetailEntity.setSkuId(skuId);
wareOrderTaskDetailEntity.setTaskId(wareOrderTaskEntity.getId());
wareOrderTaskDetailEntity.setWareId(wareId);
wareOrderTaskDetailEntity.setSkuNum(hasStock.getNum());
wareOrderTaskDetailEntity.setLockStatus(1);
wareOrderTaskDetailService.save(wareOrderTaskDetailEntity);
//向RabbitMQ发送一条消息
StockLockedTo stockLockedTo = new StockLockedTo();
stockLockedTo.setId(wareOrderTaskEntity.getId());
stockLockedTo.setDetailId(wareOrderTaskDetailEntity.getId());
rabbitTemplate.convertAndSend("order-event-exchange","stock.locked",stockLockedTo);
break;
}else {
//锁库存成功
}
}
if (!skuStocked){
//当前商品没有库存了
throw new NoStockException(skuId);
}
}
return null;
}

1.如果每一个商品都锁定成功,将当前商品锁定了几件的工作单记录发给RabbitMQ 2.如果锁定失败,前面保存的工作单信息就回滚了,数据库就没有工作单信息了。给RabbitMQ发送出去的消息过期后即使要解锁记录,但由于去数据库查不到id,所以也就不用解锁了。
老师说在wms_ware_sku表里已经锁库存了,但是wms_ware_order_task_detail详细工作单回滚了,由于根据id查不出数据,相当于就不知道当时这个人是锁了多少个了。但是我觉得在同一个事务里wms_ware_order_task_detail详细工作单回滚了,wms_ware_sku表里已经锁定的库存也应该回滚啊,所以我觉得只发id没问题啊。
4、修改代码
修改gulimall-common模块的com.atguigu.common.mq.StockLockedTo类
package com.atguigu.common.mq;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
/**
* @author 无名氏
* @date 2022/8/19
* @Description: 每锁一件商品库存就向RabbitMQ发送一条消息
*/
@Data
public class StockLockedTo {
/**
* wms_ware_order_task
* 库存工作单的id
*/
private Long id;
/**
* wms_ware_order_task_detail
* 工作单详情的id
*/
private StockDetailTo detail;
@Data
public static class StockDetailTo{
/**
* id
*/
@TableId
private Long id;
/**
* sku_id
*/
private Long skuId;
/**
* sku_name
*/
private String skuName;
/**
* 购买个数
*/
private Integer skuNum;
/**
* 工作单id
*/
private Long taskId;
/**
* 仓库id
*/
private Long wareId;
/**
* 1-已锁定 2-已解锁 3-扣减
*/
private Integer lockStatus;
}
}

在gulimall-ware模块的com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl类的orderLockStock方法里,将stockLockedTo.setDetailId(wareOrderTaskDetailEntity.getId());修改为如下代码
//只发id不行,防止回滚以后找不到数据(我觉得只发id没问题)
StockLockedTo.StockDetailTo stockDetailTo = new StockLockedTo.StockDetailTo();
BeanUtils.copyProperties(wareOrderTaskDetailEntity,stockDetailTo);
stockLockedTo.setDetail(stockDetailTo);

在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类的submitOrder方法上删除@GlobalTransactional注解

5、测试
重启GulimallOrderApplication服务和GulimallWareApplication服务,截断gulimall_oms数据库的oms_order订单表和oms_order_item订单项表,将gulimall_wms数据库的wms_ware_sku库存表的stock_locked被锁库存属性全部修改为0,表示没有库存被锁住。

登录后,在 http://order.gulimall.com/toTrade 页面里,点击提交订单,此时会报/ by zero错误。可以看到在gulimall_oms数据库里,oms_order表和oms_order_item表回滚了。在gulimall_wms数据库里,wms_ware_sku表里已经锁定库存了,wms_ware_order_task已经保存工作单了,wms_ware_order_task_detail里已经保存工作单详情了(还没有实现解锁库存功能,所以被锁的库存不会解锁,这是正常的)

2、解锁库存
1、返回订单状态
在gulimall-order模块的com.atguigu.gulimall.order.controller.OrderController类里添加getOrderStatus方法用于返回订单的状态
/**
* 返回订单状态
*
* @return
*/
@GetMapping("/status/{orderSn}")
public R getOrderStatus(@PathVariable("orderSn") String orderSn) {
OrderEntity orderEntity = orderService.getOrderStatusByOrderSn(orderSn);
return R.ok().put("data", orderEntity);
}

在gulimall-order模块的com.atguigu.gulimall.order.service.OrderService接口里添加getOrderStatusByOrderSn抽象方法
OrderEntity getOrderStatusByOrderSn(String orderSn);

在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类里实现getOrderStatusByOrderSn方法
@Override
public OrderEntity getOrderStatusByOrderSn(String orderSn) {
LambdaQueryWrapper<OrderEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(OrderEntity::getOrderSn,orderSn);
return this.getOne(lambdaQueryWrapper);
}

在gulimall-ware模块的com.atguigu.gulimall.ware.feign包里添加OrderFeignService接口,用于远程调用订单模块
package com.atguigu.gulimall.ware.feign;
import com.atguigu.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* @author 无名氏
* @date 2022/8/19
* @Description:
*/
@FeignClient("gulimall-order")
public interface OrderFeignService {
@GetMapping("order/order/status/{orderSn}")
public R getOrderStatus(@PathVariable("orderSn") String orderSn);
}

在gulimall-ware模块的com.atguigu.gulimall.ware.vo包里新建OrderVo类,用于封装订单包含的信息

在gulimall-common模块的com.atguigu.common包下新建enums文件夹,在enums文件夹里新建OrderStatusEnum枚举类,用于表示订单状态
package com.atguigu.common.enums;
public enum OrderStatusEnum {
CREATE_NEW(0,"待付款"),
PAYED(1,"已付款"),
SENDED(2,"已发货"),
RECIEVED(3,"已完成"),
CANCLED(4,"已取消"),
SERVICING(5,"售后中"),
SERVICED(6,"售后完成");
private Integer code;
private String msg;
OrderStatusEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public String getMsg() {
return msg;
}
}

在gulimall-ware模块的src/main/resources/application.properties配置文件里添加如下配置,设置RabbitMQ消息手动确认接收
#手动ack
spring.rabbitmq.listener.simple.acknowledge-mode=manual

2、解锁库存
在gulimall-ware模块的com.atguigu.gulimall.ware包里新建listener文件夹,在listener文件夹里新建StockReleaseListener类,用于监听stock.release.stock.queue库存释放队列的消息
package com.atguigu.gulimall.ware.listener;
import com.atguigu.common.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;
/**
* @author 无名氏
* @date 2022/8/19
* @Description:
*/
@Service
@RabbitListener(queues = "stock.release.stock.queue")
public class StockReleaseListener {
@Autowired
WareSkuService wareSkuService;
/**
* 库存自动解锁
* 1)、下订单成功,订单过期没有支付被系统自动取消、被用户手动取消。都要解锁库存
* 2)、下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。
*
* @param stockLockedTo
* @param message
*/
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo stockLockedTo, Message message, Channel channel) throws IOException {
System.out.println("收到解锁库存的消息");
try {
wareSkuService.unLockStock(stockLockedTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}catch (Exception e){
e.printStackTrace();
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}

在gulimall-ware模块的com.atguigu.gulimall.ware.service.WareSkuService接口里添加unLockStock方法
void unLockStock(StockLockedTo stockLockedTo);

在gulimall-ware模块的com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl类里实现unLockStock方法
@Transactional(rollbackFor = Exception.class)
@Override
public void unLockStock(StockLockedTo stockLockedTo) {
//工作单详情
StockLockedTo.StockDetailTo detail = stockLockedTo.getDetail();
WareOrderTaskDetailEntity wareOrderTaskDetailEntity = wareOrderTaskDetailService.getById(detail.getId());
if (wareOrderTaskDetailEntity == null) {
//工作单详情里没有数据,无需解锁,确认收到消息
return;
}
/**
* 解锁库存
* 1、没有订单:证明锁定库存后面的业务出问题了,这种情况需要解锁库存
* 2、有订单:如果有订单需要判断订单状态,如果订单状态为`未支付`或`用户主动取消` 这时需要解锁库存
*/
//库存工作单id
Long wareOrderTaskId = stockLockedTo.getId();
WareOrderTaskEntity wareOrderTaskEntity = wareOrderTaskService.getById(wareOrderTaskId);
R r = orderFeignService.getOrderStatus(wareOrderTaskEntity.getOrderSn());
if (r.getCode() != 0) {
//消息拒绝以后重新放到队列里面,让别人继续消费解锁。
//channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
throw new RuntimeException("获取订单状态异常");
}
Object data = r.get("data");
OrderVo orderVo = null;
if (data != null) {
String s = JSON.toJSONString(data);
orderVo = JSON.parseObject(s, OrderVo.class);
}
//没有订单或订单状态为待付款或取消 并且工作单的锁定状态为已锁定
if ((data==null || OrderStatusEnum.CANCLED.getCode().equals(orderVo.getStatus())
|| OrderStatusEnum.CREATE_NEW.getCode().equals(orderVo.getStatus()))
&& wareOrderTaskDetailEntity.getLockStatus()==1) {
this.unLockStock(detail.getSkuId(),detail.getWareId(),detail.getSkuNum(),detail.getId());
}
}
private void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId){
//库存解锁
//update wms_ware_sku set stock_locked = stock_locked - 1 where sku_id = 1 and ware_id = 1
wareSkuDao.unLockStock(skuId,wareId,num);
//更新库存工作单状态
WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity();
entity.setId(taskDetailId);
//已解锁
entity.setLockStatus(2);
wareOrderTaskDetailService.updateById(entity);
}

在gulimall-ware模块的com.atguigu.gulimall.ware.dao.WareSkuDao接口里添加unLockStock方法
void unLockStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("num") Integer num);

在gulimall-ware模块的src/main/resources/mapper/ware/WareSkuDao.xml文件里添加id为unLockStock的sql
<update id="unLockStock">
update gulimall_wms.wms_ware_sku
set stock_locked = stock_locked - #{num} where sku_id = #{skuId} and ware_id = #{wareId}
</update>

删掉gulimall-ware模块的com.atguigu.gulimall.ware.config.MyRabbitConfig配置类的listener方法
@RabbitListener(queues = "stock.release.stock.queue")
public void listener(Message message){
}

3、没有收到消息
调试时发现延迟队列一直没有收到消息

查看gulimall-ware模块的com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl类的orderLockStock方法,可以看到是这里的交换机名称写错了,果然还是得用枚举或常量

在gulimall-ware模块的com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl类的orderLockStock方法里,把"order-event-exchange"改为"stock-event-exchange"

4、远程调用失败
然后老师也说了,feign远程调用会失败sun.net.www.http.HttpClient(http://auth.gulimall.com/login.html),失败的原因就是没有进行登录(配置的访问GulimallOrderApplication服务所有请求都需要登录)

在gulimall-order模块的com.atguigu.gulimall.order.interceptor.LoginUserInterceptor类的preHandle方法的开头添加如下代码,直接放行order/order/status/开头的请求
String uri = request.getRequestURI();
boolean match = new AntPathMatcher().match("order/order/status/**", uri);
if (match){
return true;
}

重启GulimallOrderApplication服务后,还是报了feign的错误
feign.codec.DecodeException: Could not extract response: no suitable HttpMessageConverter found for response type [class com.atguigu.common.utils.R] and content type [text/html;charset=UTF-8]
at feign.SynchronousMethodHandler.decode(SynchronousMethodHandler.java:180)
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:140)
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:78)
at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:103)
at com.sun.proxy.$Proxy108.getOrderStatus(Unknown Source)
at com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl.unLockStock(WareSkuServiceImpl.java:240)
at com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl$$FastClassBySpringCGLIB$$422f6383.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)

调试后,发现还是跳转到了登录页

在gulimall-order模块的com.atguigu.gulimall.order.interceptor.LoginUserInterceptor类的preHandle方法的开头,可以看到我们访问的是/order/order/status/202208192040228431560607606966325249,而我们放行的是以"order/order/status/**"开头的请求,所以还是被拦截了

在gulimall-order模块的com.atguigu.gulimall.order.interceptor.LoginUserInterceptor类的preHandle方法的开头,将刚刚配置的"order/order/status/**"修改为"/order/order/status/**"即可

5、再次测试
重启GulimallOrderApplication服务和GulimallWareApplication服务,截断gulimall_oms数据库的oms_order订单表和oms_order_item订单项表,将gulimall_wms数据库的wms_ware_sku库存表的stock_locked被锁库存属性全部修改为0,表示没有库存被锁住。

登录后,在 http://order.gulimall.com/toTrade 页面里,点击提交订单,此时会报/ by zero错误。可以看到在gulimall_oms数据库里,oms_order表和oms_order_item表回滚了。在gulimall_wms数据库里,wms_ware_sku表里已经锁定库存了,wms_ware_order_task已经保存工作单了,wms_ware_order_task_detail里已经保存工作单详情了

等到了规定的时间还没有支付后,处理解锁库存的接口收到了解锁库存的消息,gulimall_wms数据库的wms_ware_order_task_detail表将刚新增的数据的lock_status字段变为2(状态由已锁定变为已解锁),wms_ware_sku表里已锁定的库存也重新被解锁了

3、释放订单
1、添加关闭订单方法

剪切gulimall-order模块的com.atguigu.gulimall.order.config.MyMQConfig类的listener方法
/**
* com.atguigu.gulimall.order.web.HelloController类的createOrderTest方法发送消息
* @param orderEntity
* @param channel
* @param message
* @throws IOException
*/
@RabbitListener(queues = "order.release.order.queue")
public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException, InterruptedException {
Thread.sleep(5*1000);
System.out.println("收到过期的订单信息,准备关闭订单=>"+orderEntity.getOrderSn()+"时间=>"+orderEntity.getModifyTime());
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}

在gulimall-order模块的com.atguigu.gulimall.order包里新建listener文件夹,在listener文件夹里新建OrderCloseListener,将刚刚剪切的代码粘贴到这里,并稍加改造。
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;
/**
* @author 无名氏
* @date 2022/8/20
* @Description:
*/
@Service
@RabbitListener(queues = "order.release.order.queue")
public class OrderCloseListener {
@Autowired
OrderService orderService;
/**
* com.atguigu.gulimall.order.web.HelloController类的createOrderTest方法发送消息
* @param orderEntity
* @param channel
* @param message
* @throws IOException
*/
@RabbitHandler
public void listener(OrderEntity orderEntity, Channel channel, Message message) throws IOException, InterruptedException {
System.out.println("收到过期的订单信息,准备关闭订单=>"+orderEntity.getOrderSn()+"时间=>"+orderEntity.getModifyTime());
try {
orderService.closeOrder(orderEntity);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
e.printStackTrace();
}
}
}

在gulimall-order模块的com.atguigu.gulimall.order.service.OrderService接口里添加closeOrder方法
void closeOrder(OrderEntity orderEntity);

在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl实现类里实现closeOrder方法
@Override
public void closeOrder(OrderEntity entity) {
OrderEntity orderEntity = this.getById(entity.getId());
if (OrderStatusEnum.CREATE_NEW.getCode().equals(orderEntity.getStatus())) {
OrderEntity update = new OrderEntity();
update.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(update);
}
}

在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类的submitOrder方法里,注释掉int i = 10/0;,并在下面添加rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",orderCreateTo.getOrder());代码
//int i = 10/0;
//订单创建成功,发消息给RabbitMQ
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",orderCreateTo.getOrder());

2、释放订单
有可能订单创建成功后,由于各种原因,订单解锁比库存解锁后执行,因此订单解锁后可以给库存解锁发一条消息,告知库存服务及时解锁库存

这次我们实现的业务功能为下面画的红色方框的部分

在gulimall-order模块的com.atguigu.gulimall.order.config.MyMQConfig类里添加orderReleaseOtherBinding方法
@Bean
public Binding orderReleaseOtherBinding(){
return new Binding("stock.release.stock.queue", Binding.DestinationType.QUEUE,
"order-event-exchange","order.release.other.#",null);
}

复制gulimall-order模块的com.atguigu.gulimall.order.entity.OrderEntity类,粘贴到gulimall-common模块的com.atguigu.common.to包下,并重命名为OrderTo

在gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类里添加closeOrder方法
@Override
public void closeOrder(OrderEntity entity) {
OrderEntity orderEntity = this.getById(entity.getId());
if (OrderStatusEnum.CREATE_NEW.getCode().equals(orderEntity.getStatus())) {
OrderEntity update = new OrderEntity();
update.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(update);
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderEntity,orderTo);
orderTo.setStatus(OrderStatusEnum.CANCLED.getCode());
rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
}
}

在gulimall-ware模块的com.atguigu.gulimall.ware.listener.StockReleaseListener类里添加handleStockLockedRelease方法
@RabbitHandler
public void handleStockLockedRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
System.out.println("订单关闭,准备解锁库存");
try {
wareSkuService.unLockStock(orderTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}catch (Exception e){
e.printStackTrace();
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}

在gulimall-ware模块的com.atguigu.gulimall.ware.service.WareSkuService接口里添加unLockStock(OrderTo orderTo);方法
void unLockStock(OrderTo orderTo);

修改gulimall-ware模块的com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl类的unLockStock(StockLockedTo stockLockedTo)方法,将这个|| OrderStatusEnum.CREATE_NEW.getCode().equals(orderVo.getStatus())删掉

在gulimall-ware模块的com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl类里实现unLockStock(OrderTo orderTo)方法
/**
* 防止订单服务卡顿,导致订单状态消息一直改不了,库存消息优先到期。查订单状态新建状态,什么都不做就走了。
* 导致卡顿的订单,永远不能解锁库存
*
* @param orderTo
*/
@Override
public void unLockStock(OrderTo orderTo) {
String orderSn = orderTo.getOrderSn();
WareOrderTaskEntity orderTaskEntity = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);
Long taskId = orderTaskEntity.getId();
List<WareOrderTaskDetailEntity> orderTaskDetailEntities = wareOrderTaskDetailService.getOrderTaskDetailsByTaskId(taskId);
for (WareOrderTaskDetailEntity entity : orderTaskDetailEntities) {
this.unLockStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum(),entity.getId());
}
}

3、获取订单详细信息
在gulimall-ware模块的com.atguigu.gulimall.ware.service.WareOrderTaskService接口里添加getOrderTaskByOrderSn抽象方法
WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn);

在gulimall-ware模块的com.atguigu.gulimall.ware.service.impl.WareOrderTaskServiceImpl类里实现getOrderTaskByOrderSn方法
@Override
public WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn) {
LambdaQueryWrapper<WareOrderTaskEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(WareOrderTaskEntity::getOrderSn,orderSn);
return this.getOne(lambdaQueryWrapper);
}

4、获取工作单详情
在gulimall-ware模块的com.atguigu.gulimall.ware.service.WareOrderTaskDetailService接口里添加getOrderTaskDetailsByTaskId方法
List<WareOrderTaskDetailEntity> getOrderTaskDetailsByTaskId(Long taskId);

在gulimall-ware模块的com.atguigu.gulimall.ware.service.impl.WareOrderTaskDetailServiceImpl类里实现getOrderTaskDetailsByTaskId方法
@Override
public List<WareOrderTaskDetailEntity> getOrderTaskDetailsByTaskId(Long taskId) {
LambdaQueryWrapper<WareOrderTaskDetailEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(WareOrderTaskDetailEntity::getTaskId,taskId)
.eq(WareOrderTaskDetailEntity::getLockStatus, 1);
return this.list(lambdaQueryWrapper);
}

5、测试
测试时发现其他都正确,就是状态一直更新不成功

查看gulimall-order模块的com.atguigu.gulimall.order.service.impl.OrderServiceImpl类的closeOrder方法,发现更新时忘记写id了,在OrderEntity update = new OrderEntity();创建OrderEntity类对象后设置要更新的订单id
update.setId(entity.getId());

6、情况一
(普通情况,先收到订单关闭消息,再收到解锁库存的消息,此时订单先关闭,订单关闭后通知了解锁库存服务,已将库存解锁,收到解锁库存的消息后库存已解锁):这个视频在typora里无法播放
[//]: # (<video src="video/6.2.4.3.6.mp4"></video>)
7、情况二
(极端情况,由于网络原因或其他原因,先收到解锁库存的消息,此时不做处理,后收到订单关闭消息,订单关闭后再通知库存服务让其解锁库存)
我们可以设置订单关闭的时间大于库存释放时间,即订单关闭比库存解锁时间晚,用来模拟这种极端情况
在gulimall-order模块的com.atguigu.gulimall.order.config.MyMQConfig类的orderDelayQueue方法里,修改key为x-message-ttl的value
arguments.put("x-message-ttl", TimeUnit.MINUTES.toMillis(2));

在gulimall-ware模块的com.atguigu.gulimall.ware.config.MyRabbitConfig类的stockDelayQueue方法里,修改key为x-message-ttl的value
arguments.put("x-message-ttl", TimeUnit.MINUTES.toMillis(1));

8、测试
启动GulimallWareApplication服务后报了如下的错误,这是因为我们在代码里修改了队列的配置,修改后的2配置与RabbitMQ里队列的配置不一致导致报错了,我们只需删掉RabbitMQ里这些队列,让其自动再重新创建即可
2022-08-22 21:08:35.871 ERROR 7472 --- [.168.56.10:5672] o.s.a.r.c.CachingConnectionFactory : Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'x-message-ttl' for queue 'stock.delay.queue' in vhost '/': received '60000' but current is '120000', class-id=50, method-id=10)
2022-08-22 21:08:37.882 ERROR 7472 --- [.168.56.10:5672] o.s.a.r.c.CachingConnectionFactory : Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'x-message-ttl' for queue 'stock.delay.queue' in vhost '/': received '60000' but current is '120000', class-id=50, method-id=10)
2022-08-22 21:08:41.888 ERROR 7472 --- [.168.56.10:5672] o.s.a.r.c.CachingConnectionFactory : Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'x-message-ttl' for queue 'stock.delay.queue' in vhost '/': received '60000' but current is '120000', class-id=50, method-id=10)
2022-08-22 21:08:46.892 ERROR 7472 --- [.168.56.10:5672] o.s.a.r.c.CachingConnectionFactory : Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'x-message-ttl' for queue 'stock.delay.queue' in vhost '/': received '60000' but current is '120000', class-id=50, method-id=10)

删掉RabbitMQ里的stock.delay.queue队列

删掉RabbitMQ里的order.delay.queue队列

此时可以看到即便是特殊情况,我们也能很好的进行处理,这个视频在typora里无法播放
[//]: # (<video src="video/6.2.4.3.8.mp4"></video>)
9、复原
删掉RabbitMQ里的order.delay.queue队列

删掉RabbitMQ里的stock.delay.queue队列

在gulimall-order模块的com.atguigu.gulimall.order.config.MyMQConfig类的orderDelayQueue方法里,重新修回key为x-message-ttl的value
arguments.put("x-message-ttl", TimeUnit.MINUTES.toMillis(1));

在gulimall-ware模块的com.atguigu.gulimall.ware.config.MyRabbitConfig类的stockDelayQueue方法里,重新修改回key为x-message-ttl的value
arguments.put("x-message-ttl", TimeUnit.MINUTES.toMillis(2));

4、如何保证消息可靠性
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、消息积压
消费者宕机积压
消费者消费能力不足积压
发送者发送流量太大
上线更多的消费者,进行正常消费
上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理
5、可能发生的情况
情况一
消息发送出去了,但由于网络问题没有抵达Broker消息代理(这里的消息代理指的是RabbitMQ)。我们在发送消息前可以先记录日志,定时扫描日志,及时发现没有抵达到消息代理的消息
@Override
public void closeOrder(OrderEntity entity) {
OrderEntity orderEntity = this.getById(entity.getId());
if (OrderStatusEnum.CREATE_NEW.getCode().equals(orderEntity.getStatus())) {
OrderEntity update = new OrderEntity();
update.setStatus(OrderStatusEnum.CANCLED.getCode());
this.updateById(update);
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderEntity,orderTo);
orderTo.setStatus(OrderStatusEnum.CANCLED.getCode());
try {
//TODO 保证消息一定会发送出去,每一个消息都可以做好日志记录(给数据库保存每一个消息的详细信息)。
//定期扫描数据库将失败的消息再发送一遍;
rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo);
}catch (Exception e){
//将没法送成功的消息进行重试发送。
e.printStackTrace();
}
}
}

我们也可以在gulimall_oms模块里新建mq_message表,在消息发送前将消息状态设置为已发送,然后定期检查这张表,将订单状态一直为已发送的消息再次发送。(一直为已发送表示有可能是消息没有抵达)
CREATE TABLE `mq_message` (
`message_id` CHAR ( 32 ) NOT NULL,
`content` text,
`to_exchane` VARCHAR ( 255 ) DEFAULT NULL,
`routing_key` VARCHAR ( 255 ) DEFAULT NULL,
`class_type` VARCHAR ( 255 ) DEFAULT NULL,
`message_status` INT ( 1 ) DEFAULT '0' COMMENT '0-新建 1-已发送 2-错误抵达 3-已抵达',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY ( `message_id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4

情况二
消息抵达Broker,Broker要将消息写入磁盘(持久化)才算成功。此时Broker尚未持久化完成,然后突然宕机了。
可以在publisher消息的发布者里加入确认回调机制,确认成功的消息,修改数据库消息状态。

情况三
自动ACK的状态下。消费者收到消息,但没来得及消费消息然后宕机,导致消息没有被处理。
一定开启手动ACK,消费成功后才移除消息,失败或者没来得及处理就noAck并重新入队(还要注意消息重复消费的问题,可以给消息设置唯一id,防止消息被重复消费)

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