六、订单
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
按钮即可获得消息并确认收到

RabbitMQ
5.8.2、整合RabbitMQ
1、整合RabbitMQ
1、引入在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进行序列化
Serializable
接口
1、实现让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、接收消息
Object
类型
1、参数指定为在需要监听队列的业务方法上标注@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

Message
类型
2、参数指定为由于接收到的消息的类型为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

Channel
类型参数
4、添加参数还可以传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、添加配置
thymeleaf
1、引入在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
了

confirmOrder
方法
2、修改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()));

order_sn
字段太长
6、重启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
里获取msg
msg
也能获取数据
在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
库存表里也没有库存被锁住,因此可以判断出分布式事务已经生效了。

AT
模式
6、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=nacos
file.conf
: 3、所有想要用到分布式事务的微服务使用seata
DataSourceProxy
代理 自己的数据源 4、每个微服务,都必须导入 registry. conf
file.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
字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的
