为什么会有这篇文章
还是为找工作,公司项目里面用到MQ,我也写到简历里了,但是面试官一问,我就说没做过,不对于两年半的我来说,不太容易找到工作;最近在b站学SpringCloud微服务开发与实战,记笔记,防止忘了;
如有错误,请指正;
b站微服务开发中mq课程背景

同步调用,异步调用
同步
余额支付: 1扣余额----2更新支付状态----3更新订单状态----4短信通知
扣余额和更新支付状态之间有关系,但是支付成功以后,更新订单状态,发送短信,增加用户积分之间不存在必要的关联,如果依次执行,耗时很长,扩展性也很差

异步
异步调用通常基于消息通知的方式,包含三个角色
消息发送者,消息接收者,消息代理
(商家),(顾客),(快递站)
上面的支付业务,就不用直接通过OpenFeign调用交易服务,而是发消息通知给代理,让代理去;
异步的问题,不能立即得到调用结果,不确定下游业务执行是否成功,业务安全依赖于Broker的可靠性
MQ
MQ(MessageQueue)中文是消息队列,字面来看就是存放消息的队列。也就是异步调用中的Broker。
| RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
| 公司/社区 | Rabbit | Apache | 阿里 | Apache |
| 开发语言 | Erlang | Java | Java | Scala&Java |
| 协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,STOMP,REST,XMPP,AMQP | 自定义协议 | 自定义协议 |
| 可用性 | 高 | 一般 | 高 | 高 |
| 单机吞吐量 | 一般 | 差 | 高 | 非常高 |
| 消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
| 消息可靠性 | 高 | 一般 | 高 | 一般 |
RabbitMQ
安装,部署
可以参考b站黑马MQ入门-05.RabbitMQ-安装部署_哔哩哔哩_bilibili
本笔记也是基于该视频书写,虎哥也有笔记,可以参考Docs
https://b11et3un53m.feishu.cn/wiki/OQH4weMbcimUSLkIzD6cCpN0nvc
rabbitmq,不同项目有不同的virtualHost,类似VMware可以有不同虚拟机
先熟悉一下控制台
先熟悉一下mq
在Queueu页面可以手动添加一个queue
我们使用amq.fanout名称的交换机,点进去

交换机只负责转发消息,没有存储能力,路由失败消息就会丢失
交换机需要绑定队列
Bindings:绑定队列
Publish message:发送消息

点击publish messages 就给队列发送消息了

这时候可以overview,看到消息被发送

看到两个队列都收到消息,相当于广播

点进队列,能看到发送的消息

数据隔离操作

先创建一个用户,但是 Can access virtual hosts 是空的,需要创建虚拟主机并绑定

如果登录用户是新建的hmall,创建虚拟主机,就直接绑定到自己下面


交换机也会新增一份,右上角可以选virtual host来筛选

Java客户端
AMQP, advanced message queuing protocol,是一种标准协议,与语言和平台无关
Spring AMQP是一套基于AMQP的API规范,提供了模板来发送,接受消息
可以下载本笔记,资源里面的mq-demo查看
快速入门
先去除,exchange,只有queue体验收发消息的过程

控制台创建一个queue

消息发送
首先配置MQ地址,在publisher服务的application.yml中添加配置:
spring:
rabbitmq:
host: 192.168.150.101 # 你的虚拟机IP
port: 5672 # 端口
virtual-host: /hmall # 虚拟主机
username: hmall # 用户名
password: 123 # 密码
然后在publisher服务中编写测试类SpringAmqpTest,并利用RabbitTemplate实现消息发送:
@SpringBootTest
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSimpleQueue() {
// 队列名称
String queueName = "simple.queue";
// 消息
String message = "hello, spring amqp!";
// 发送消息
rabbitTemplate.convertAndSend(queueName, message);
}
}
测试以后发现,生产者发送了消息

接受消息
消费者和生产者,导入依赖,配置参数一样。。这里省略
@RabbitListener(queues = "simple.queue") 通过注解配置要监听的队列名称,将来SpringAMQP会把消息传递到当前方法
@Slf4j
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) throws InterruptedException {
log.info("spring 消费者接收到消息:【" + msg + "】");
}
}
然后启动该项目,就能收到消息
Work Queues
任务模型,多个消费者,绑定一个队列,共同消费队列中的消息
生产者发送消息,是会被1还是2,消费掉????
rabbit控制台,新增一个 work.queue队列
代码中,添加两个消费者
@RabbitListener(queues = "work.queue")
public void listenWorkQueueMessage1(String msg) throws InterruptedException {
System.out.println("消费者1号, 接收到消息:【" + msg + "】,"+ LocalTime.now());
}
@RabbitListener(queues = "work.queue")
public void listenWorkQueueMessage2(String msg) throws InterruptedException {
System.err.println("消费者2号, 接收到消息:【" + msg + "】,"+ LocalTime.now());
}
生产者这里,新增一个发送50条数据的test
@Test
public void testWorkQueue() throws InterruptedException {
// 队列名称
String queueName = "work.queue";
// 消息
String message = "hello, message_";
for (int i = 0; i < 50; i++) {
// 发送消息,每20毫秒发送一次,相当于每秒发送50条消息
rabbitTemplate.convertAndSend(queueName, message + i);
Thread.sleep(20);
}
// 发送消息
rabbitTemplate.convertAndSend(queueName, message);
}
同一个消息,只会被一个消费者处理;这里只是测试用的,写两个方法,实际中是部署多个实例;

给1号,Thread.sleep(25); 2号Thread.sleep(200);模拟性能不一样
任务还是均匀分配

在消费者中配置,控制消费者预取的消息数量,实现能者多劳
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息

交换机
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
交换机的类型有四种:
-
Fanout:广播,将消息交给所有绑定到交换机的队列。我们最早在控制台使用的正是Fanout交换机
-
Direct:订阅,基于RoutingKey(路由key)发送给订阅了消息的队列
-
Topic:通配符订阅,与Direct类似,只不过RoutingKey可以使用通配符
-
Headers:头匹配,基于MQ的消息头匹配,用的较少。
Fanout交换机
会路由到每个队列,意思是,queue1和queue2都处理同样一份消息

声明队列fanout.queue1,fanout.queue2和交换机hmall.fanout,记得选类型

给交换机绑定队列

代码、中监听两个消费者

消费者代码,这里routingKey暂时为空

两个消费者都受到消息了,广播模式,一条消息发给多个队列
Direct交换机
queue和exchange绑定得key一样,就投递给谁;

先创建两个queue

创建direct交换机

交换机绑定queue,并设置routingkey

编写消费者代码

编写生产者代码,先试一下,routingkey是red的

发送blue

Topic交换机
就是bindingkey有通配符,类似路由

先创建两个队列

在创建交换机,创建时候,类型选topic

交换机绑定,并设置routing key

编写消费者

发送给消息,china.new 两个队列都符合,都会被匹配到


声明式队列,交换机
上面的测试都是通过,控制台设置的,不符合实际开发;下面通过Bean来声明
SpringAMQP提供了几个类,用来声明队列、交换机及其绑定关系:
Queue:用于声明队列,可以用工厂类QueueBuilder构建
Exchange:用于声明交换机,可以用工厂类ExchangeBuilder构建
Binding:用于声明队列和交换机的绑定关系,可以用工厂类BindingBuilder构建
一般在消费者中申明,新建config类
@Configuration
public class FanoutConfiguration {
@Bean
public FanoutExchange fanoutExchange(){
//return new FanoutExchange("hmall.fanout");
return ExchangeBuilder.fanoutExchange("hmall.fanout").build();
}
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
//return QueueBuilder.durable("fanout.queue1").build();
}
@Bean
public Binding fanoutQueueBinding(Queue fanoutQueue1, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.queue2");
//return QueueBuilder.durable("fanout.queue2").build();
}
@Bean
public Binding fanoutQueueBinding2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
发现控制台有了

再试一下 direct exchange,这种绑定,一个bean只能设置,queue的一个routingkey绑定一个exchange;
@Configuration
public class DirectConfiguration {
@Bean
public DirectExchange directExchange(){
//return new FanoutExchange("hmall.direct");
return ExchangeBuilder.directExchange("hmall.direct").build();
}
@Bean
public Queue directQueue1(){
return new Queue("direct.queue1");
//return QueueBuilder.durable("direct.queue1").build();
}
@Bean
public Binding directQueueBindingRed(Queue directQueue1, DirectExchange directExchange){
return BindingBuilder.bind(directQueue1).to(directExchange).with("red");
}
@Bean
public Binding directQueueBindingBlue(Queue directQueue1, DirectExchange directExchange){
return BindingBuilder.bind(directQueue1).to(directExchange).with("blue");
}
}
接下来换@RabbitListener注解来声明队列和交换机
写在上面创建的SpringRabbitListener这个类里
@RabbitListener( bindings = @QueueBinding(
value = @Queue(name = "direct.queue1", durable = "true"),
exchange = @Exchange(name="hmall.direct",type = ExchangeTypes.DIRECT),
key = {"red", "blue"}
))
public void listenDirectQueueMessage1(String msg) throws InterruptedException {
System.err.println("消费者1号,direct.queue1, 接收到消息:【" + msg + "】,"+ LocalTime.now());
Thread.sleep(200);
}
@RabbitListener( bindings = @QueueBinding(
value = @Queue(name = "direct.queue2", durable = "true"),
exchange = @Exchange(name="hmall.direct",type = ExchangeTypes.DIRECT),
key = {"red", "yellow"}
))
public void listenDirectQueueMessage2(String msg) throws InterruptedException {
System.err.println("消费者2号,direct.queue2, 接收到消息:【" + msg + "】,"+ LocalTime.now());
Thread.sleep(200);
}
消息转换器
发送消息的时候,message是object类型的,那消息转换器会把消息转为什么类型呢
测试一下
控制台创建一个queue:
发一条map

获得消息,这是序列化以后的结果,默认的用jdk的ObjectOutputStream完成序列化,有安全风险,消息变得很大,传输不方便,可读性差,不推荐

用JSON 序列化来代替JDK序列化
publisher和consumer都引入jackson依赖,配置MessageConverter
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
为了方便,注册bean,添加到启动类里面
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
消息变json类型了

接受代码,就按发送得类型接受
@RabbitListener(queues = "object.queue")
public void listenObjectQueueMessage(Map<String, Object> msg) throws InterruptedException {
System.err.println("消费者号,object.queue, 接收到消息:【" + msg + "】,"+ LocalTime.now());
Thread.sleep(200);
}
![]()
黑马商场业务改造

先添加 依赖
<!--消息发送-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
抽取配置到nacos , 然后在bootstrap.yaml里面配置一下


在common-service服务里面,新建config配置类
@Configuration
public class Mqconfig {
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
在common服务里注册得,怎么被pay,trade服务扫描到,利用springboot的自动装配

trade-service 新增消费者代码
@Component
@RequiredArgsConstructor
public class PayStatusListener {
private final IOrderService orderService;
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "trade.pay.success.queue",durable = "true"),
exchange = @Exchange(name = "pay.direct"),
key = "pay.success"
))
public void listenPaySuccess(Long orderId) {
orderService.markOrderPaySuccess(orderId);
}
}
改造发送代码
// 5.修改订单状态
//tradeClient.markOrderPaySuccess(po.getBizOrderNo());
try{
rabbitTemplate.convertAndSend("pay.direct","pay.success",po.getBizOrderNo());
}catch (Exception e){
log.error("发送支付成功消息异常!",po.getBizOrderNo(),e);
}

3227

被折叠的 条评论
为什么被折叠?



