1信息队列的作用
-
缓冲数据
信息队列可以作为一个缓冲区域。在很多系统中,数据的产生和处理速度是不一致的。例如,在一个电商系统中,用户下单的操作是频繁且不可预测的,这些订单信息就是数据。订单产生的速度可能会在促销活动期间非常快,而后台的库存管理系统、发货系统等处理订单的速度可能相对较慢。信息队列就可以先将这些订单信息缓存起来,防止系统因为瞬间涌入大量订单而崩溃。
以一个简单的消息队列 RabbitMQ 为例,当生产者(如电商网站的前端服务器)快速产生消息(订单消息)时,这些消息会被放入队列中,等待消费者(如库存管理服务器、发货服务器等)在合适的时间进行处理。
-
解耦系统组件
信息队列有助于解耦不同的系统组件。在一个复杂的软件系统中,各个模块之间的直接调用会导致紧密耦合。比如,一个在线支付系统,它包含用户认证模块、支付处理模块和银行接口模块。如果这些模块之间是直接调用关系,当银行接口因为维护等原因发生变更时,很可能会影响到支付处理模块和用户认证模块。
通过信息队列,用户认证模块完成认证后可以将支付请求信息放入队列,支付处理模块从队列中获取请求进行处理,再将与银行交互的信息放入另一个队列,银行接口模块从这个队列中获取信息与银行系统交互。这样,各个模块之间只依赖于信息队列,而不是直接相互依赖,使得系统更加灵活,方便组件的独立开发、升级和维护。
-
实现异步通信
信息队列能够支持异步通信方式。以一个文件上传系统为例,用户上传一个大型文件,传统的同步方式是用户上传请求发出后,一直等待服务器完成文件存储等后续操作,在这个过程中用户界面可能会出现卡顿或者无响应的情况。
利用信息队列,用户发出上传请求后,文件上传的任务信息被放入队列,服务器立即返回一个上传成功的提示给用户,然后后台的文件存储服务从队列中获取任务并进行文件存储操作。这样用户不需要等待文件存储的漫长过程,可以继续进行其他操作,提高了用户体验和系统的整体效率。
-
流量削峰填谷
在高并发场景下,信息队列可以进行流量削峰。比如,一个热门的新闻网站在发布重大新闻时,瞬间可能会有大量用户访问该新闻页面并进行评论、点赞等操作。如果没有信息队列,这些请求直接涌向服务器的数据库(用于存储评论、点赞数据等),数据库可能会因为承受不住巨大的负载而出现性能下降甚至崩溃。
有了信息队列,这些请求可以先进入队列,服务器按照自己的处理能力从队列中逐步取出请求进行处理,将流量高峰时期的请求均匀地分布在一段时间内处理,起到了削峰填谷的作用,保证系统的稳定运行。
2.调用的方式:
2.1.最简单的调用
publisher:
public class SpringAmqpTest {
//自动装配RabbitTemplate模板对象
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSimpleQueue() {
// 队列名称
String queueName = "simple.queue";
// 消息
String message = "hello, spring amqp!";
// 发送消息
rabbitTemplate.convertAndSend(queueName, message);
}
}
consumer:
@Component
public class SpringRabbitListener {
//监听名称simple.queue队列,Spring只要接收到该队列的消息就会接收消息
@RabbitListener(queues = "simple.queue") //对应publisher的队列名称
//Spring自动将接收的消息给方法参数msg
public void listenSimpleQueueMessage(String msg) throws InterruptedException {
System.out.println("spring 消费者接收到消息:【" + msg + "】");
}
}
我们通常把consumer定义为listener
2.2.最常用的方式:Direct
在Direct模型下:
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个
RoutingKey
(路由key) - 消息的发送方在 向 Exchange发送消息时,也必须指定消息的
RoutingKey
。 - Exchange不再把消息交给每一个绑定的队列,而是根据消息的
Routing Key
进行判断,只有队列的Routingkey
与消息的Routing key
完全一致,才会接收到消息
交换机的名称在publis出定义
队列的名称,RoutingKey,绑定交换机都在consumer处定义
publisher:
public void testSendDirectExchange() {
// 交换机名称
String exchangeName = "itcast.direct";
// 消息
String message = "红色警报!日本乱排核废水,导致海洋生物变异,惊现哥斯拉!";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "red", message);
}
//都能收到
public void testSendDirectExchange() {
// 交换机名称
String exchangeName = "itcast.direct";
// 消息
String message = "蓝色警报!日本乱排核废水,导致海洋生物变异,惊现哥斯拉!";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "blue", message);
}
//只有绑定了蓝色RoutingKey的才能收到
public void testSendDirectExchange() {
// 交换机名称
String exchangeName = "itcast.direct";
// 消息
String message = "黄色警报!日本乱排核废水,导致海洋生物变异,惊现哥斯拉!";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "yellow", message);
}
//只有绑定了黄色的RoutingKey才能
consumer:
/*
TODO:
1. value = @Queue(name = "direct.queue1") 表示绑定的第一个队列
2.exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT) 表示交换机名和类型
3.key = {"red", "blue"} 表示路由key
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
key = {"red", "blue"}
))
public void listenDirectQueue1(String msg){
System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),
key = {"red", "yellow"}
))
public void listenDirectQueue2(String msg){
System.out.println("消费者接收到direct.queue2的消息:【" + msg + "】");
}
3.实际开发的案例:
1.使用springmvc绑定好前端的操作
@ApiOperation("免费课立刻报名接口")
@PostMapping("/freeCourse/{courseId}")
public PlaceOrderResultVO enrolledFreeCourse(@ApiParam("免费课程id") @PathVariable("courseId") Long courseId) {
return orderService.enrolledFreeCourse(courseId);
}
2.实现类中实现publisher的操作
//封装了一个send方法,实际调用的还是convertAndSend 在此处定义了交换机
public <T> void send(String exchange, String routingKey, T t) {
log.debug("准备发送消息,exchange:{}, RoutingKey:{}, message:{}", exchange, routingKey, t);
// 1.设置消息标示,用于消息确认,消息发送失败直接抛出异常,交给调用者处理
String id = UUID.randomUUID().toString(true);
CorrelationData correlationData = new CorrelationData(id);
// 2.设置发送超时时间为500毫秒
rabbitTemplate.setReplyTimeout(500);
// 3.发送消息,同时设置消息id
rabbitTemplate.convertAndSend(exchange, routingKey, t, processor, correlationData);
}
//具体使用rabbitmq的地方,调用了send方法
rabbitMqHelper.send(
MqConstants.Exchange.ORDER_EXCHANGE,
MqConstants.Key.ORDER_PAY_KEY,
OrderBasicDTO.builder()
.orderId(orderId)
.userId(userId)
.courseIds(cIds)
.finishTime(order.getFinishTime())
.build()
);
3.consumer:
@Slf4j
@Component
@RequiredArgsConstructor
public class LessonChangeListener {
private final ILearningLessonService lessonService;
/**
* 监听订单支付或课程报名的消息
* @param order 订单信息
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "learning.lesson.pay.queue", durable = "true"),
exchange = @Exchange(name = MqConstants.Exchange.ORDER_EXCHANGE, type = ExchangeTypes.TOPIC),
key = MqConstants.Key.ORDER_PAY_KEY
))
public void listenLessonPay(OrderBasicDTO order){
// 1.健壮性处理
if(order == null || order.getUserId() == null || CollUtils.isEmpty(order.getCourseIds())){
// 数据有误,无需处理
log.error("接收到MQ消息有误,订单数据为空");
return;
}
// 2.添加课程
log.debug("监听到用户{}的订单{},需要添加课程{}到课表中", order.getUserId(), order.getOrderId(), order.getCourseIds());
lessonService.addUserLessons(order.getUserId(), order.getCourseIds());
}
}
// 2.添加课程
log.debug(“监听到用户{}的订单{},需要添加课程{}到课表中”, order.getUserId(), order.getOrderId(), order.getCourseIds());
lessonService.addUserLessons(order.getUserId(), order.getCourseIds());
}
}
这里consumer会接收到publisher 发来的信息,进行处理