MQ基础
MQ提供接受消息、存储消息、发送消息的功能
MQ的好处
- 处理高并发任务 比如有一个系统每秒只能支持10000的处理量,但是高峰时期达到了20000,那么就用MQ去处理,先处理10000,再处理10000,最后统一返回结果。
- 应用解耦 有些系统有几个子系统,比如创建订单后,耦合调用支付系统、库存系统、订单系统。如果其中一个系统出了问题,要花一段时间去解决,那么就会造成下单异常。但是利用MQ,可以解除系统之间的互相调用,如果其中一个系统出了问题,消息会缓存在MQ中,下单操作可以完成,等出问题的系统恢复后继续处理。
- 处理分布式事务 MQ有多次提交的特点,在失败的情况下可以多次重试或者回滚,保证事务一致性。
- 异步处理 有些服务是异步的,比如A调用B,如果B需要花很长时间,A要知道B什么时候完成,有两种方式,要么每隔一段时间调用B的api查询B,要么等B结束后callback通知A。有了MQ后就可以B通知MQ,然后MQ把消息发送给A。
MQ的类型
- ActiveMQ
- Kafka 吞吐量最高,适合收集日志。
- RabbitMQ 中小公司用的最多。
- RocketMQ 阿里产品,适合金融场景。
安装RabbitMQ
我这里以mac为例
brew install rabbitmq
brew services start rabbitmq;
rabbitmq-plugins enable rabbitmq_management
输入localhost:15672
账号密码:guest guest
RabbitMQ整体架构
- Publisher 发布者
- exhange 交换机
- queue 队列
- consumer 订阅者
- 一个MQ可以创建多个VirtualHost虚拟主机,起到数据隔离的作用。
客户端快速入门
发送消息
-
新建队列
-
binding关联交换机
-
发送消息
数据隔离
此时只有一个默认的vh
添加用户
此时admin没有vh的操作权限,需要用他的账号创建一个vh
每个vh都有自己的交换机,每个user只能操作自己的vh,体现了数据隔离
JAVA RabbitMQ客户端
RabbitMQ的通信协议是AMQP(Advanced Message Queuing Protocol)高级消息队列协议 而Spring开发了一套基于AMQP协议的api规范SpringAMQP,此外SpringRabbit是SpringAMQP的唯一具体实现。
快速入门
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
- 配置MQ客户端信息
spring:
rabbitmq:
host: localhost
port: 5672
virtual-host: /
username: guest
password: guest
- 发送消息,利用RabbitTemplate工具类
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void test(){
String queueName="hello.queue1";
String msg="How Are You?";
rabbitTemplate.convertAndSend(queueName,msg);
}
- 接受消息
@Component
public class Listener {
@RabbitListener(queues = "hello.queue1")
public void listen(String msg){
System.out.println(msg);
}
}
WorkQueue 任务模型
让多个消费者绑定一个队列
在默认情况下,一个queue绑定多个消费者,会采用轮询机制发送给消费者,不会管有没有消费者是否已经接受完消息,会导致消息堆积。
@RabbitListener(queues = "workQueue")
public void listen1(String msg) throws InterruptedException {
System.out.println("消费者1接受到消息:"+msg);
Thread.sleep(20);
}
@RabbitListener(queues = "workQueue")
public void listen2(String msg) throws InterruptedException {
System.err.println("消费者2接受到消息:"+msg);
Thread.sleep(200);
}
解决方案:
配置加上
listener:
simple:
prefetch: 1
每次只处理一条消息
能者多劳
交换机
如果没有交换机,直接通过queue发送消息,只有一个服务收到消息。
Fanout交换机
广播模式,会将消息发给每一个与之绑定的queue
@Test
public void fanouttest(){
String exchange="test.fanout";
String msg="How Are You?";
rabbitTemplate.convertAndSend(exchange,null,msg);
}
@RabbitListener(queues = "fanout.q1")
public void fanout1(String msg) throws InterruptedException {
System.out.println("fanout消费者1接受到消息:"+msg);
}
@RabbitListener(queues = "fanout.q2")
public void fanout2(String msg) throws InterruptedException {
System.err.println("fanout消费者2接受到消息:"+msg);
}
Direct交换机
定向路由给指定的queue(对暗号)
绑定时指定routingKey
@Test
public void directtest(){
String exchange="test.direct";
String msg="How Are You?";
rabbitTemplate.convertAndSend(exchange,"red",msg);
}
@RabbitListener(queues = "direct.q1")
public void direct1(String msg) throws InterruptedException {
System.out.println("direct消费者1接受到消息:"+msg);
}
@RabbitListener(queues = "direct.q2")
public void direct2(String msg) throws InterruptedException {
System.err.println("direct消费者2接受到消息:"+msg);
}
Topic交换机
与Direct不同在于,Topic的routingKey可以指定多个,以.分隔,并且可以用通配符#(0个或多个)或*(一个) 比如有RoutingKey:
- China.news
- China.weather
- Japan.news
- Japan.weather
那么可以指定RoutingKey:#.weather(所有国家的天气)
@Test
public void topictest(){
String exchange="test.topic";
String msg="china.news";
rabbitTemplate.convertAndSend(exchange,"China.news",msg);
}
@RabbitListener(queues = "topic.q1")
public void topic1(String msg) {
System.out.println("topic消费者1接受到消息:"+msg);
}
@RabbitListener(queues = "topic.q2")
public void topic2(String msg) {
System.out.println("topic消费者2接受到消息:"+msg);
}
Java声明队列和交换机
@Configuration
public class FanoutConfiguration {
@Bean
public FanoutExchange fanoutExchange(){
// 创建交换机
return new FanoutExchange("hmall.fanout2");
}
@Bean
public Queue fanoutQueue3(){
// 创建queue
return new Queue("fanout.queue3");
}
@Bean
public Binding fanoutBinding3(Queue fanoutQueue3, FanoutExchange fanoutExchange){
// 绑定方式1
return BindingBuilder.bind(fanoutQueue3).to(fanoutExchange);
}
@Bean
public Queue fanoutQueue4(){
return new Queue("fanout.queue4");
}
@Bean
public Binding fanoutBinding4(){
// 绑定方式2
return BindingBuilder.bind(fanoutQueue4()).to(fanoutExchange());
}
}
但是这种方式比较麻烦。 基于注解
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2", durable = "true"),
exchange = @Exchange(name = "hmall.direct", type = ExchangeTypes.DIRECT),
key = {"red", "yellow"}
))
public void listenDirectQueue2(String msg) throws InterruptedException {
System.out.println("消费者2 收到了 direct.queue2的消息:【" + msg +"】");
}
消息转换器
默认情况下发送map类型会转换成字节码。
引入依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
@SpringBootApplication
public class PublisherApplication {
public static void main(String[] args) {
SpringApplication.run(PublisherApplication.class);
}
@Bean
public MessageConverter jacksonMessageConvertor(){
return new Jackson2JsonMessageConverter();
}
}
这样获得的消息就是map类型。