异步处理
的内容。应用解耦
没有使用消息队列:订单系统 调用 库存系统接口,库存系统维护升级,那么订单系统调用的接口也要维护升级, 所以很麻烦!
使用消息队列:订单系统 无需依靠着库存系统的接口,只需要给消息队列发送消息,库存系统去消息队列消费消息即可,实现应用上的解耦。
流量控制
(也叫做 流量削峰)业务中涉及到某一时间段内,有大量请求需要处理,可以通过消息队列来做到流量控制。
例如:电商系统里面的 秒杀业务。
消息服务中两个重要概念:
消息代理(message broker)和 目的地(destination)
当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地。
消息队列主要有两种形式的目的地
:
消息队列的 两种协议:(遵循了协议就能够调用 消息队列中间件)
Spring 集成支持:
Spring Boot 继承自动配置:
市面上的MQ产品:ActiveMQ、RabbitMQ、RocketMQ、Kafka
本次项目采用AMQP的Rabbitmq来做消息队列。
Message:消息,由消息头和消息体组成。消息体是不透明的,消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
Publisher:消息的生产者,也是一个向交换器发布消息的客户端应用程序。
Exchange:交换器,用来接受生产者发送的消息并将这些消息路由给服务器中的队列。四种交换器类型:direct(默认)、fanout、topic、headers,不同类型转发消息的策略也不同。
Queue:消息队列。
Binding:绑定,用于消息队列和交换机之间的关联。
Connection:网络连接,比如:TCP连接。
Channel:信道,多路复用连接中的独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP命令都是通过信道发出去的,不管是发布消息、订阅队列还是接受消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念,以服用一条TCP连接。
Consumer:消费者
Virtual Host:虚拟主机,表示一批交换器、消息队列和相关对象。每个虚拟主机本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列、交换器、绑定和权限机制。目的:起到隔离效果,比如:一个RabbitMQ中,有一台针对Java调用的虚拟主机,有一台针对python调用的虚拟主机,这样Java虚拟主机如果出现问题,也不会影响python这台(也有按照开发、测试、生产环境来的)。
Broker:表示消息队列服务器实体。
# 1. 启动 rabbitmq:management 容器
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
# 2. 自动重启
docker update rabbitmq --restart=always
rabbitmq:management 集成了:web管理后台的端口。
端口 解释:
Overview 概述:
Admin 管理:
Exchanges 交换机:
Queues 队列:
Direct Exchange:直接模式,直接 交换机。
Fanout Exchange:广播模式, 扇形 交换机
Topic Exchange:主题模式 , 主题 交换机
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
package com.atguigu.gulimall.order;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
// 启动 注解
@EnableRabbit
@SpringBootApplication
public class GulimallOrderApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallOrderApplication.class, args);
}
}
# rabbitmq 配置信息
spring.rabbitmq.host=www.gulimall.com
spring.rabbitmq.port=5672
spring.rabbitmq.virtual-host=/
package com.atguigu.gulimall.order;
import com.atguigu.gulimall.order.entity.OrderReturnReasonEntity;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.core.AmqpAdmin;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;
@SpringBootTest
class GulimallOrderApplicationTests {
@Autowired
AmqpAdmin amqpAdmin;
@Autowired
RabbitTemplate rabbitTemplate;
// 发送message
@Test
public void sendMessage(){
OrderReturnReasonEntity orderReturnReasonEntity = new OrderReturnReasonEntity();
orderReturnReasonEntity.setId(1l);
orderReturnReasonEntity.setCreateTime(new Date());
orderReturnReasonEntity.setName("测试实体类");
// 1. 发送消息
// 如果发送的消息是个对象,我们会使用序列化机制,将对象写出去。对象必须实现Serializable!!
rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",orderReturnReasonEntity);
}
// 创建交换机
@Test
void createExchange() {
// 参数:名字、是否持久化、是否自动删除
DirectExchange directExchange = new DirectExchange("hello-java-exchange",true,false);
amqpAdmin.declareExchange(directExchange);
}
// 创建队列
@Test
void createQueue() {
Queue queue = new Queue("hello-java-queue",true,false,false);
amqpAdmin.declareQueue(queue);
}
// 创建绑定
@Test
void createBinding() {
// 将exchange指定的交换机和destination目的地进行绑定,使用routingKey作为指定的路由键
Binding binding = new Binding(
"hello-java-queue", // 目的地
Binding.DestinationType.QUEUE, // 目的地类型
"hello-java-exchange", // 指定交换机
"hello.java", // 路由key
null // 参数
);
amqpAdmin.declareBinding(binding);
}
}
对于实体类序列化,默认为jdk序列化:
可以通过配置消息转换来实现Jackson2JSON序列化:
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: xuyanbo
* @description: TODO
* @date: 2023/10/27 14:43
*/
@Configuration
public class MyRabbitConfig {
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
@RabbitListener注解,参数:
@RabbitListener注解:标注在 类 或者 方法上。(看源码注解能看出)
@RabbitHandler注解:标注在 方法上。
被@RabbitListener注解 ,标注方法上
:
注意:要引入rabbitmq的Channel。
package com.atguigu.gulimall.order.service.impl;
import com.alibaba.fastjson.JSON;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;
import com.atguigu.gulimall.order.dao.OrderItemDao;
import com.atguigu.gulimall.order.entity.OrderItemEntity;
import com.atguigu.gulimall.order.entity.OrderReturnReasonEntity;
import com.atguigu.gulimall.order.service.OrderItemService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import com.rabbitmq.client.Channel;
import java.util.Map;
@Service("orderItemService")
public class OrderItemServiceImpl extends ServiceImpl<OrderItemDao, OrderItemEntity> implements OrderItemService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<OrderItemEntity> page = this.page(
new Query<OrderItemEntity>().getPage(params),
new QueryWrapper<OrderItemEntity>()
);
return new PageUtils(page);
}
/
* @RabbitListener参数:
* queues:声明需要监听的所有队列。
*
* 被标注方法参数:
* 第一个参数 Message message:原生消息相信信息,消息头 + 消息体
* 第二个参数 T<发送的消息的类型> T content :消息体里面对应的实体信息(一般为实体类)
* 第三个参数 Channel channel : 当前传输数据的通道。(有问题,没有Channel类)
*/
@RabbitListener(queues = {"hello-java-queue"})
public void receiveMessage(Message message, OrderReturnReasonEntity content, Channel channel) {
// body里面存储的是:发送的消息内容
byte[] body = message.getBody();
// fixme 这种方式太麻烦,可以用参数直接来接受消息体里面的内容。
// OrderReturnReasonEntity orderReturnReasonEntity = JSON.parseObject(body.toString(), OrderReturnReasonEntity.class);
System.out.println("body:" + content);
// properties存储的是:发过来的消息头属性
MessageProperties properties = message.getMessageProperties();
System.out.println("接收到消息...内容:" + message.toString() + ",类型:" + message.getClass());
}
}
Tip:Queue:可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一个收到此消息场景:
- 订单服务启动多个;同一个消息,只能有一个客户端收到。
- 单个服务,一次只能处理一个消息,等消息完全处理完,方法运行结束,才可以接受到下一个消息。
被@RabbitListener注解 ,标注类上
参数:
碰壁了,此处有问题,没有Channel类可能跟配置序列化有关系。
package com.atguigu.gulimall.order.service.impl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;
import com.atguigu.gulimall.order.dao.OrderItemDao;
import com.atguigu.gulimall.order.entity.OrderEntity;
import com.atguigu.gulimall.order.entity.OrderItemEntity;
import com.atguigu.gulimall.order.entity.OrderReturnReasonEntity;
import com.atguigu.gulimall.order.service.OrderItemService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import com.rabbitmq.client.Channel;
import java.util.Map;
@RabbitListener(queues = {"hello-java-queue"})
@Service("orderItemService")
public class OrderItemServiceImpl extends ServiceImpl<OrderItemDao, OrderItemEntity> implements OrderItemService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<OrderItemEntity> page = this.page(
new Query<OrderItemEntity>().getPage(params),
new QueryWrapper<OrderItemEntity>()
);
return new PageUtils(page);
}
/
* @RabbitListener参数:
* queues:声明需要监听的所有队列。
*
* 被标注方法参数:
* 第一个参数 Message message:原生消息相信信息,消息头 + 消息体
* 第二个参数 T<发送的消息的类型> T content :消息体里面对应的实体信息(一般为实体类)
* 第三个参数 Channel channel : 当前传输数据的通道。(有问题,没有Channel类)
*/
@RabbitHandler
public void receiveMessageOrderReturnReasonEntity(Channel channel, Message message, OrderReturnReasonEntity content) {
System.out.println("接收到消息..." + content);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消息处理完成..." + content);
}
@RabbitHandler
public void receiveMessageForOrderEntity(Channel channel, Message message,OrderEntity content) {
System.out.println("接收到消息..." + content);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消息处理完成..." + content);
}
}
@PostConstruct注解:相当于创建完当前这个对象后,之后调用的方法。可以翻译为:构造器之后。
例如:下面就是等 MyRabbitConfig 创建实例后,执行initRabbitTemplate方法,给RestTemplate对象配置相关内容。
package com.atguigu.gulimall.order.config;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
/
* @author: xuyanbo
* @description: TODO
* @date: 2023/10/27 14:43
*/
@Configuration
public class MyRabbitConfig {
@Autowird // 此处报错,闭环错误。
RabbitTemplate rabbitTemplate;
// 使用JSON序列化机制,进行消息转换
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
/
* @PostConstruct注解:相当于创建完MyRabbitConfig对象,之后调用的方法。翻译为构造器之后。
*/
@PostConstruct
public void initRabbitTemplate(){
// 设置确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("confirm...");
System.out.println("CorrelationData:" + correlationData);
System.out.println("ack:" + ack);
System.out.println("cause:" + cause);
}
});
}
}
@Primary注解在Spring框架中表示当有多个相同类型的bean时,使用该注解赋予bean更高的优先级。比如在Spring IOC容器中有多个相同类型的bean时,当要注入该类型的bean,就可以使用@Primary来标注注入bean的优先,优先级高的bean先被注入。
事务消息:
为了保证消息不丢失,可靠抵达,可以使用事务消息,但是性能却下降250倍,所以事务消息是不推荐使用的,为此引入确认机制
。
先看官方文档:Reliability Guide — RabbitMQ
p(provider) -> b(Broker):需要 confirmCallback
e(Exchange) -> q(Queue):需要 returnCallback
q(Queue) -> c(Consumer):需要 ack
## 开启发送端消息抵达Broker确认(使用的SpringBoot版本不支持弃用了)
# spring.rabbitmq.publisher-confirms=true
## 开启发送端消息抵达Queue确认
spring.rabbitmq.publisher-returns=true
## 只要消息抵达Queue,就会异步发送优先回调returnfirm
spring.rabbitmq.template.mandatory=true
spring.rabbitmq.publisher-confirm-type=correlated
在Spring框架中,spring.rabbitmq.publisher-confirms 属性是用于开启或关闭消息发布确认的。从Spring AMQP 2.0开始,这个属性已经被弃用,并推荐使用 spring.rabbitmq.confirm-interval 和 spring.rabbitmq.publisher-returns 这两个新的属性来代替。
package com.atguigu.gulimall.order.config;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/
* @author: xuyanbo
* @description: TODO
* @date: 2023/10/27 14:43
*/
@Configuration
public class MyRabbitConfig {
RabbitTemplate rabbitTemplate;
@Primary
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
this.rabbitTemplate = rabbitTemplate;
rabbitTemplate.setMessageConverter(messageConverter());
initRabbitTemplate();
return rabbitTemplate;
}
// 使用JSON序列化机制,进行消息转换
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
public void initRabbitTemplate(){
// 设置确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/
* 第一个参数 correlationData:当前消息的唯一关联数据(这个是消息的唯一ID)
* 第二个参数 ack:消息是否成功收到
* 第三个参数 cause:失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("confirm...correlationData:" + correlationData + ",ack:" + ack + ",cause:" + cause);
}
});
}
}
## 开启发送端消息抵达Broker确认(使用的SpringBoot版本不支持弃用了)
# spring.rabbitmq.publisher-confirms=true
## 开启发送端消息抵达Queue确认
spring.rabbitmq.publisher-returns=true
## 只要消息抵达Queue,就会异步发送优先回调returnfirm
spring.rabbitmq.template.mandatory=true
## 手动ack消息,不使用默认的消费端确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual
@Autowired
RabbitTemplate rabbitTemplate;
// new CorrelationData(UUID.randomUUID().toString()) 添加消息唯一ID,方便测试
rabbitTemplate.convertAndSend(
"hello-java-exchange",
"hello.java",
orderEntity,
new CorrelationData(UUID.randomUUID().toString()));
package com.atguigu.gulimall.order.config;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/
* @author: xuyanbo
* @description: TODO
* @date: 2023/10/27 14:43
*/
@Configuration
public class MyRabbitConfig {
RabbitTemplate rabbitTemplate;
@Primary
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
this.rabbitTemplate = rabbitTemplate;
rabbitTemplate.setMessageConverter(messageConverter());
initRabbitTemplate();
return rabbitTemplate;
}
// 使用JSON序列化机制,进行消息转换
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
public void initRabbitTemplate(){
// 设置确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/
* 1、 只要消息抵达Broker就ack=true
* 第一个参数 correlationData:当前消息的唯一关联数据(这个是消息的唯一ID)
* 第二个参数 ack:消息是否成功收到
* 第三个参数 cause:失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("confirm...correlationData:" + correlationData + ",ack:" + ack + ",cause:" + cause);
}
});
// 设置消息抵达队列的确认回调
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
/
* 该方法只有失败才会调用,成功不会被调用。
* ReturnedMessage对象里面的参数解释:
* private final Message message; 投递失败的消息详情信息
* private final int replyCode; 回复的状态码
* private final String replyText; 回复的文本内容
* private final String exchange; 当时这个消息发给哪个交换机
* private final String routingKey; 当时这个消息用哪个路由键
*/
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
System.out.println("失败 消息,Message:" + returnedMessage.getMessage()
+ ",replyCode:" + returnedMessage.getReplyCode() + ",replyText:" + returnedMessage.getReplyText()
+ ",exchange:" + returnedMessage.getExchange() + ",routingKey:" + returnedMessage.getRoutingKey());
}
});
}
}
失败测试不成功,待解决。
Tip:ack全称:acknowledge 收到通知。
## 将ack确认设置为手动模式
spring.rabbitmq.listener.simple.acknowledge-mode=manual
package com.atguigu.gulimall.order.service.impl;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;
import com.atguigu.gulimall.order.dao.OrderItemDao;
import com.atguigu.gulimall.order.entity.OrderEntity;
import com.atguigu.gulimall.order.entity.OrderItemEntity;
import com.atguigu.gulimall.order.entity.OrderReturnReasonEntity;
import com.atguigu.gulimall.order.service.OrderItemService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Map;
@RabbitListener(queues = {"hello-java-queue"})
@Service("orderItemService")
public class OrderItemServiceImpl extends ServiceImpl<OrderItemDao, OrderItemEntity> implements OrderItemService {
@RabbitHandler
public void receiveMessageOrderReturnReasonEntity(Channel channel, Message message, OrderReturnReasonEntity content) {
System.out.println("接收到消息..." + content);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消息处理完成..." + content);
long deliveryTag = message.getMessageProperties().getDeliveryTag();
System.out.println("deliveryTag: " + deliveryTag);
/
* channel.basicAck 等同于 签收获取,手动确认。
* 第一个参数:可以理解为一个货物标签,就是消息的标签,是哪个消息被确认了。
* 第二个参数:是否批量确认。
*/
// 签收货物,非批量模式
try {
if (deliveryTag % 2 == 0) {
// 肯定确认
channel.basicAck(deliveryTag, false);
} else {
// 否定确认
/
* channel.basicNack 等同于 拒绝签收,拒绝确认。
* 第一个参数:可以理解为一个货物标签,就是消息的标签,是哪个消息被确认了。
* 第二个参数:是否批量确认。
* 第三个参数:是否重新回归队列。
*/
channel.basicNack(deliveryTag,false,true);
// channel.basicReject(deliveryTag,true);// 效果一样,但是不能批量操作。 }
} catch (IOException e) {
// 网络中断
e.printStackTrace();
}
}
}
在电商系统中,订单中心很重要,涉及到3流,分别是信息流、资金流、物流。订单中心就相当于是三者的中间整合商。
订单的作用:把感兴趣的商品整合一起,生成一个支付单,然后完成一个发货的物流过程。
所以,订单模块是电商系统的枢纽,在订单这个环节商需求获取多个模块的数据和信息。同时对这多个信息进行加工处理,流向下个环节。
订单总流程:
订单生成 流程包括:
update `wms_ware_sku`
set stock_locked = stock_locked + #{num}
where sku_id = #{skuId}
and ware_id = #{wareId}
and stock - stock_locked >= #{num}
package com.atguigu.gulimall.order.interceptor;
import com.atguigu.common.constant.AuthServerConstant;
import com.atguigu.common.vo.MemberRespVo;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/
* @author: xuyanbo
* @description: TODO
* @date: 2023/10/31 11:37
*/
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
MemberRespVo userInfo = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (userInfo != null) {
loginUser.set(userInfo);
return true;
} else {
// 没登录去登录
request.getSession().setAttribute("msg","请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
package com.atguigu.gulimall.order.config;
import com.atguigu.gulimall.order.interceptor.LoginUserInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/
* @author: xuyanbo
* @description: TODO
* @date: 2023/10/31 11:39
*/
@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {
@Autowired
LoginUserInterceptor interceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor).addPathPatterns("/");
}
}
解决办法:
feign在发起远程调用之前,会经过一大堆的拦截器,我们也可以添加一个拦截器,将相关信息维护上:
RequestContextHolder的原理是ThreadLocal
package com.atguigu.gulimall.order.config;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/
* @author: xuyanbo
* @description: TODO
* @date: 2023/10/31 14:56
*/
@Configuration
public class GuliFeignConfig {
// 添加一个拦截器,并且声明名字
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 1. 使用RequestContextHolder拿到刚进来的这个请求
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 2. 同步请求头数据,Cookie
String cookie = request.getHeader("Cookie");
// 3. 给Feign的请求里面,同步当前请求的cookie信息
template.header("Cookie",cookie);
}
};
}
}
因为,RequestContextHolder 的原理是 ThreadLocal ,当我们使用异步的方式进行Feign的远程调用,相当于创建了多个子线程,而不是主线程了,这时RequestInterceptor拦截器里面的RequestContextHolder就无法获取到请求的相关信息了,因为请求信息在主线程的RequestContextHolder中。
解决办法:
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
System.out.println("主线程..." + Thread.currentThread().getId());
// 主线程
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
System.out.println("member子线程..." + Thread.currentThread().getId());
// 在member子线程,设置请求信息
RequestContextHolder.setRequestAttributes(requestAttributes);
// 1. 远程查询所有的收货地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
confirmVo.setAddress(address);
}, executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
System.out.println("cart子线程..." + Thread.currentThread().getId());
// 在cart子线程,设置请求信息
RequestContextHolder.setRequestAttributes(requestAttributes);
// 2. 远程查询购物车所有选中的购物项
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
}, executor);
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
// 3. 查询用户积分
Integer integration = memberRespVo.getIntegration();
confirmVo.setIntegration(integration);
// 4. 其他数据自动计算
// TODO 5. 防重令牌
return confirmVo;
}
幂等性概念:幂等性是指对同一个资源的多个请求,在业务逻辑上具有相同的结果。
场景案例:
哪些情况需要防止:
什么情况需要幂等?
例如:
select * from tableA from id = ?
update tab1 set col1 = 1 where col2 = 2
delete from user where userId = 1
insert into user(userId,name) values (1,0) userId作为唯一主键,只会插入一条用户数据也是具备幂等性的。
无论执行多少次,结果都一样,不会改变状态,这些就是天然幂等的,具有幂等性的。
update tab1 set col1 = col1 + 1 where col2 = 2 ,每次操作执行结果都会发生变化,这就不是幂等性的。
insert into user(userId,name) values (1,0) userId,name都不是唯一主键,可以重复,这样的也是不具备幂等性的。
例如:本项目,就是thymleaf渲染页面前,就给页面封装了一个防重令牌(token),这样就是前端一个令牌,后端redis存了一个令牌。
危险性:
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
// 1. 验证令牌[令牌的对比和删除必须保证原子性]
// 0 令牌失败,1删除成功
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String orderToken = vo.getOrderToken();
Long result = redisTemplate.execute(
new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()),
orderToken
);
// 原子验证令牌和删除令牌
if (result == 0l) {
// 令牌验证失败
return response;
} else {
// 令牌验证成功
// 去创建订单,验令牌,验价格,锁库存...
}
悲观锁使用时,一般伴随着事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。另外注意的时,id字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会非常麻烦。
该方法适合更新的场景中:(带版本号)
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条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。
乐观锁主要使用于处理读多写少的问题。
如果多个机器可能在同一时间同时处理相同的数据,比如:多台机器定时任务都拿到了相同数据处理,我们就可以加分布式锁,锁定此数据,处理完成后释放锁。获取到锁的必须先判断这个数据是否背处理过。
插入数据,按照唯一索引来进行插入,比如:订单号,这样相同的订单不可能有两条记录插入。
很多数据需要处理,只能被处理一次,比如:我们可以计算数据的MD5将其放入redis的set,每次处理数据,先看这个MD5是否已经存在,存在就不处理。
使用订单号orderNo作为去重表的唯一索引,把唯一索引插入去重表,在进行业务操作,且他们在同一事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。这里注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。
调用接口时,生成一个唯一id,redis将数据保存到集合中(去重),存在即处理过,可以使用nginx设置每一个请求的唯一id:
proxy_set_header X-Request-Id $request_id;
特别是 Feign服务,触发重发请求,拿着以前的老请求再重新发,这样可以给这个请求设置一个全局唯一ID,就算重复发了,也能检测出来是否处理过。
Tips:也适用于链路追踪
本地事务:在分布式系统,只能控制自己的回滚,控制不了其他服务的回滚。
分布式事务:最大的问题就是:网络问题 + 分布式机器。
数据库事务的四个特性:ACID
事务的隔离级别:
事务的传播行为:
同一对象内事务方法互调默认失效,原因 绕过了代理对象,事务使用代理对象来控制的:
解决:使用代理对象来调用事务方法。
OrderServiceImpl orderService = (OrderServiceImpl)AopContext.currentProxy();
orderService.b(); // 调用orderService对象的b方法
orderService.c(); // 调用orderService对象的c方法。
CAP原则又称为CAP定理:
CAP原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。
分布式系统里面,分区容错肯定是要满足的,然而 一致性 和 可用性 这二者是相互冲突的。
分布式系统中实现一致性的raft算法:Raft (采用领导发布的效果原理)
还有paxos算法。
Tip:总结:一般市场上,都是基于AP的,无法保证c(强一致性),但是可以保证最终一致性。
是对CAP理论的延伸,思想是即使无法做到强一致性(CAP的一致性就是强一致性
),但可以采用适当的采用弱一致性,即最终一致性
。
BASE是指:
场景:不适用于高并发,适用于一般分布式事务,该模式已经被取代延申。
数据库支持的 2PC【2 phase commit 二阶提交】,又叫做 XA Transactions。
MySQL 从 5.5 版本开始支持,SQL Server 2005 开始支持,Oracle 7 开始支持。
其中,XA 是一个两阶段提交协议,该协议分为以下两个阶段:
第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是 否可以提交.
第二阶段:事务协调器要求每个数据库提交数据。 其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务 中的那部分信息。
后来,出现了3PC模式,了解即可。
场景:不推荐在高并发场景下,也是常用的分布式事务解决。
刚性事务:遵循 ACID 原则,强一致性。
柔性事务:遵循 BASE 理论,最终一致性;
与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。
分三个阶段:
第一阶段 prepare 行为:调用各个服务的 Try 逻辑。
第二阶段 commit 行为:调用各个服务的 Confirm 逻辑。
第三阶段 rollback 行为:有一个服务异常,则进行回滚操作 调用各个服务的 Cancel 逻辑。
此处,所谓的 补偿 ,举个例子:try逻辑是 某个数据-2 了,那么Cancel 补偿逻辑里面就是 +2 .
场景:基于消息服务的,适用高并发场景。
按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。这种方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合 MQ 进行实现,例如:通过 MQ 发送 http 请求,设置最大通知次数。达到通知次数后即不再通知。
案例:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对账文件),支付宝的支付成功异步回调
就是不断的通知,告诉你结果。
场景:基于消息服务的,适用高并发场景。
实现:业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。
也是出现问题,发送消息,服务接受到消息后就进行回滚,与上面那个相比多了消息这一步。
官方地址:https://seata.io/zh-cn/
Seata是一款开源的分布式事务解决方案,致力于提高高性能和简单易用的分布式事务服务。提供了多种模式:AT、TCC、SAGA 和 XA事务模式。
AT:auto自动模式
TCC:Try、Confirm、Cancel
Seata很好理解,首先,弄明白下面三个名词:
TC:事务协调者,维护全局和分支。
TM:事务管理器,处理全局事务的 开始、提交、回滚操作。(所谓的全局事务是 谁是主业务发起的远程调用,那么TM就在谁的上面。)
RM:资源管理器,维护分支事务。
SEATA AT 模式需要 UNDO_LOG 表:(文档描述很清楚)
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
register.conf 注册相关配置:
config 配置配置中心,type也是用的什么类型的配置中心:
默认:file类型,seata服务默认有个file.conf文件。也可以改成nacos。
store 配置:配置seata的存储方式。
不同版本可能不太一样,但是效果都差不多的:
package com.atguigu.gulimall.order.config;
import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
/
* @author: xuyanbo
* @date: 2023/11/2 18:57
*/
@Configuration
public class MySeataConfig {
// 数据源自带的配置属性
@Autowired
DataSourceProperties dataSourceProperties;
@Bean
public DataSource dataSource(DataSourceProperties dataSourceProperties){
// 根据源码创建数据源以及设置相关配置
HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
if (StringUtils.hasText(dataSourceProperties.getName())){
dataSource.setPoolName(dataSourceProperties.getName());
}
// 用seata的代理数据源,来配置即可。
return new DataSourceProxy(dataSource);
}
}
Tips:seata.tx-service-group=服务名 和 service.vgroup-mapping.服务名 的配置,用来映射seata-server的识别。
spring:
datasource:
username: root
password: root
url: jdbc:mysql://www.gulimall.com:3306/gulimall_oms
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
mapper-locations: classpath:/mapper//*.xml
global-config:
db-config:
id-type: auto # 主键自增
server:
port: 9000
# Seata 配置
seata:
tx-service-group: gulimall-order #这里每个服务都是对应不同的映射名,在配置中心可以看到
registry:
type: nacos
nacos:
server-addr: localhost:8848
group: DEFAULT_GROUP
service:
vgroup-mapping:
#这里也要注意 key为映射名,
gulimall-order: default
@GlobalTransactional
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
confirmVoThreadLocal.set(vo);
...
Seata 默认是AT模式:是2PC的演变,属于补偿性质。
官方案例都已经给出,几种模式的案例:
AT模式:相当于自动解锁,不适用于高并发的分布式事务,仅仅适用于一般的分布式事务。
要根据实际情况来,应用属于自己的分布式事务。
因为,订单服务属于高并发服务,使用其他分布式方案可能会出现严重问题。
因此,考虑使用 可靠消息 + 最终一致性方案
,进而保证高并发。
场景:比如未付款订单,超过一定时间后,系统自动取消订单并释放占有物品。
旧版本:Spring的schedule定时任务轮询数据库
缺点:消耗系统内存、增加了数据库的压力、存在较大的时间误差。
新版本 RabbitMQ版本解决:rabbitmq的消息TTL和死信Exchange结合。
消息的TTL(Time To Live)就是消息的存活时间。
RabbitMQ可以对队列
和消息
分别设置TTL。
理解DLX(Dead Letter Exchanges):
什么是死信(消息)?
效果图:
Tips:
推荐使用的是队列设置过期时间
。因为,在消息设置过期时间中,rabbitmq采用惰性检查机制
。例如:第一个消息5分钟过期,第二个消息1分钟过期,那么第二个消息就必须等第一个消息过期了才能被检测到。
基本上是每一个微服务对应一个交换机就够了,交换机命名方式如:服务名-事件-exchange 对应的一系列服务。
业务流程图:
画设计流程图:(与业务流程图一个效果)
Tips:注意,交换机是被复用了,不要严格定义什么死信交换机之类的。其实,都是普通队列和交换机,只不过复用出了不同效果而已。
SpringCloud集成RabbitMQ了相关内容,直接通过@Bean进行注入,创建即可:
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: xuyanbo
* @date: 2023/11/3 14:13
*/
@Configuration
public class MyMQConfig {
/
* 通过@Bean方式,将Binding、Queue、Exchange 自动创建RabbitMQ对应的交换机、队列、绑定等。
*/
// 创建死信队列
@Bean
public Queue orderDelayQueue(){
Map<String, Object> arguments = new HashMap<>();
/
* x-dead-letter-exchange 绑定死信交换机
* x-dead-letter-routing-key 绑定死信路由
* x-message-ttl 绑定过期时间
*/
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 queue = new Queue(
"order.delay.queue",
true,
false,
false,
arguments
);
return queue;
}
// 普通队列
@Bean
public Queue orderReleaseOrderQueue(){
Queue queue = new Queue(
"order.release.order.queue",
true,
false,
false
);
return queue;
}
// 声明交换机(根据路由复用了,区分好路由和绑定关系即可)
@Bean
public Exchange orderEventExchange(){
TopicExchange topicExchange = new TopicExchange(
"order-event-exchange",
true,
false
);
return topicExchange;
}
// 声明绑定死信队列关系
@Bean
public Binding orderCreateOrderBingding(){
return new Binding(
"order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null
);
}
// 声明绑定正常消费队列关系
@Bean
public Binding orderReleaseOrderBingding(){
return new Binding(
"order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null
);
}
}
Tips:RabbitMQ中,已经创建了Binding、Queue、Exchange ,在重新启动微服务执行@Bean时,并不会重新创建也不会修改属性之类的。
解决办法:手动删除即可。
这样就可以测试一下,延迟队列的效果:
// 1. 随便写个接口:
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("/test/orderCreate")
@ResponseBody
public String createOrderTest(){
// 订单下单成功
OrderEntity entity = new OrderEntity();
entity.setOrderSn(UUID.randomUUID().toString());
entity.setModifyTime(new Date());
rabbitTemplate.convertAndSend(
"order-event-exchange",
"order.create.order",
entity
);
return "ok";
}
// 2. 写个监听器测试是否成功
@RabbitListener(queues = "order.release.order.queue")
public void listener(OrderEntity entity, Channel channel , Message message) throws IOException {
System.out.println("收到的过期的订单信息,准备关闭订单:" + entity.getOrderSn());
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
spring.rabbitmq.host=www.gulimall.com
spring.rabbitmq.virtual-host=/
package com.atguigu.gulimall.ware.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: xuyanbo
* @description: TODO
* @date: 2023/10/27 14:43
*/
@Configuration
public class MyRabbitConfig {
// 使用JSON序列化机制,进行消息转换
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
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.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/
* @author: xuyanbo
* @description: TODO
* @date: 2023/10/27 14:43
*/
@Configuration
public class MyRabbitConfig {
// 使用JSON序列化机制,进行消息转换
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
// fixme 这个监听的作用就是为了触发一下rabbitmq,触发成功就会创建这些交换机或者绑定之类的。
@RabbitListener(queues = "stock.release.stock.queue")
public void handler(){
}
// 库存服务的默认交换机
@Bean
public Exchange stockEventExchange(){
return new TopicExchange(
"stock-event-exchange",
true,
false
);
}
// 普通队列
@Bean
public Queue stockReleaseStockQueue(){
return new Queue(
"stock.release.stock.queue",
true,
false,
false
);
}
// 延迟队列(死信队列)
@Bean
public Queue stockDelayQueue(){
Map<String, Object> arguments = new HashMap<>();
/
* x-dead-letter-exchange 绑定死信交换机
* x-dead-letter-routing-key 绑定死信路由
* x-message-ttl 绑定过期时间
*/
arguments.put("x-dead-letter-exchange","stock-event-exchange");
arguments.put("x-dead-letter-routing-key","stock.release");
// 2分钟
arguments.put("x-message-ttl",120000);
return new Queue(
"stock.delay.queue",
true,
false,
false,
arguments
);
}
// 正常队列的绑定关系
@Bean
public Binding stockReleaseBinding(){
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
);
}
}
Tips:记得添加一个监听方法,不然没办法触发一下rabbitmq创建这些交换机或者绑定之类的。
库存解锁的场景:
查询数据库关于这个订单的锁定库存信息。两种情况:
还有重要一点,Rabbitmq监听必须设置为手动ack模式:
只要解锁库存的消息失败。一定告诉服务解锁失败,一定要启动手动ack模式,
# 配置:spring.rabbitmq.listener.simple.acknowledge-mode=manual
# 代码:channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
案例代码:
package com.atguigu.gulimall.ware.listener;
import com.atguigu.common.to.mq.StockLockedTo;
import com.atguigu.gulimall.ware.service.WareSkuService;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
/
* @author: xuyanbo
* @description: TODO
* @date: 2023/11/4 10:36
*/
@Service
@RabbitListener(queues = "stock.release.stock.queue")
public class StockReleaseListener {
@Autowired
WareSkuService wareSkuService;
/
* 1. 库存自动解锁:
* 下订单成功,库存锁定成功。接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁。
* 2. 订单失败。
* 锁库存失败
*
* 只要解锁库存的消息失败。一定告诉服务解锁失败,一定要启动手动ack模式, channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);。
* @param to
* @param message
*/
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo to, Message message , Channel channel) throws IOException {
// 这样抛出异常等同于消息没有处理成功,进行拒绝并重新放回队列。
// 没有抛出异常等同于消息处理成功,ack手动返回true
try {
System.out.println("收到解决库存的消息");
wareSkuService.unlockStock(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
/
* 解锁
* 1. 查询数据库关于这个订单的锁定库存信息。
* 两种情况:
* 有:证明库存锁定成功了。
* 解锁:看订单情况。
* 1. 没有这个订单。必须解锁。
* 2. 有这个订单。就要看订单状态。
* 已取消:解锁库存。
* 没取消:不能解锁。
* 没有:库存锁定失败了,库存回滚了。这种情况无需解锁。
*/
@Override
public void unlockStock(StockLockedTo to) {
StockDetailTo detail = to.getDetail();
Long detailId = detail.getId();
WareOrderTaskDetailEntity byId = orderTaskDetailService.getById(detailId);
if (byId != null) {
// 解锁
Long id = to.getId();
WareOrderTaskEntity taskEntity = orderTaskService.getById(id);
String orderSn = taskEntity.getOrderSn(); // 根据订单号查询订单状态
R r = orderFeignService.getOrderStatus(orderSn);
if (r.getCode() == 0) {
// 订单数据返回成功
OrderVo data = r.getData(new TypeReference<OrderVo>() {
});
// 订单不存在 或者 4订单已经取消 都要解锁库存
if (data == null || data.getStatus() == 4) {
// 订单已经被取消了。才能解锁库存
if (byId.getLockStatus() == 1) {
unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);
}
}
} else {
// 消息拒接以后重新放到队列里面,让别人继续消息解锁
throw new RuntimeException("远程服务失败");
}
} else {
// 无需解锁
}
}
private void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId) {
// 库存解锁
wareSkuDao.unlockStock(skuId, wareId, num);
// update `wms_ware_sku` set stock_locked = stock_locked - #{num}
// where sku_id = #{skuId} and ware_id = #{wareId}
// 更新库存工作单的状态
WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity();
entity.setId(taskDetailId);
entity.setLockStatus(2); // 变为已解锁
orderTaskDetailService.updateById(entity);
}
整体流程也是通过rabbitmq监听器 等信息实现:
解决办法:
// 发给MQ一个
try {
// TODO 保证消息一定会发送出去,每一个消息都可以做好日志记录。给数据库保存每一个消息的详细信息。
// TODO 定期扫描数据库将失败的消息再发送一遍。
// 执行这个方法的时候,执行完了,网络延迟或者失败了。就要抛出异常,走catch。
rabbitTemplate.convertAndSend(
"order-event-exchange",
"order.release.other",
orderTo
);
} catch (Exception e) {
// TODO 将没发送成功的消息进行重试发送。
...
}
可以设置一个MQ消息表
,将每一个消息保存下来,定期扫描数据库将失败的消息再发送一遍
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 AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COMMENT='MQ消息表';
解决方式:
publisher也必须加入确认回调机制,通过生成者和消费者的确认机制解决该问题
,确认成功的消息,修改数据库消息状态。
解决方式:
一定开启手动ACK,消费成功才移除,失败或者没来的及处理就noAck并重新入队。
消息重复场景:
解决方法:
幂等性
的。比如:扣库存有工作单的状态标志。防重表(redis/mysql)
,发送消息每一个都有业务的唯一标识,处理过就不用处理过了。redelivered字段
,可以获取是否是被重新投递过来的,而不是第一次投递过来的。消费积压场景:
解决方式: