1、整章思维导图
https://gitmind.cn/app/doc/fa1831630
2、概述
-
概述:大多应用中,可通过消息服务中间件来提升系统异步通信、扩展解耦能力
-
消息服务中两个重要概念:消息代理(message broker)和目的地(destination)当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地
-
消息队列主要有两种形式的目的地
1)队列(queue):点对点消息通信(point-to-point)
2)主题(topic):发布(publish)/订阅(subscribe)消息通信
3、作用
-
异步处理
(1)串行处理方式: 用户发送注册请求后,服务器会先将注册信息写入数据库,依次发送注册邮件和短信消息,服务器只有在消息处理完毕后才会将处理结果返回客户端,这种串行处理消息的方式非常耗时,用户体验不友好
(2)并行处理方式: 用户发送注册请求后,将注册信息写入数据库,同时发送注册邮件和短信,最后返回客 户端,这种并行处理的方式在一定程度上提高了后台业务处理的效率,但如果遇到较为耗时的业务处理,仍 然显得不够友好
(3)消息服务处理方式: 可以在业务中嵌入消息服务进行业务处理,这种方式先将注册信息写入数据库,在 极短的时间内将注册信息写入消息队列后即可返回响应信息。此时前端业务不需要理会不相干的后台业务处 理,而发送注册邮件和短信的业务会自动读取消息队列中的相关消息进行后续业务处理
-
应用解耦
(1)传统处理方式: 如果使用传统方式处理订单业务,用户下单后,订单服务会直接调用库存服务接口进行库存更新,这种方式有一个很大的问题是:一旦库存系统出现异常,订单服务会失败导致订单丢失
(2)使用消息服务: 如果使用消息服务模式,订单服务的下订单消息会快速写入消息队列,库存服务会监听并读取到订单,从而修改库存。相较于传统方式,消息服务模式显得更加高效、可靠
-
流量削峰
(1)不适用消息服务: 在秒杀业务的场景需求,专门增设服务器来应对秒杀活动期间的请求瞬时高峰,但在非秒杀活动期间,这些多余的服务器和配置就显得有些浪费,如果不进行有效处理的话,秒杀活动瞬时高峰流量请求有可能压垮服务器
(2)使用消息服务: 在秒杀业务的场景需求是较为理想的解决方案,通过在应用前端加入消息服务,先将所有请求写入到消息队列,并限定一定的阈值,多余的请求直接返回秒杀失败,秒杀服务会根据秒杀规则从消息队列中读取并处理有限的秒杀请求
-
分布式事务管理
(1)分布式的理解: 分布式把项目分成多个微服务(每一个功能元素最终都是一个可独立替换和独立升级的软件单元,我的简单理解就是每一个功能元素一个小项目),每个微服务通过请求来获取需要的数据;分布式的难题就是如何保证每个微服务数据的一致性,目前较为可靠的处理方式是基于消息队列的二次提交,在失败的情况下可以进行多次的尝试,或者基于队列数据进行回滚操作
(2)传统方式: 在订单系统中写入订单支付成功信息后,在远程调用库存系统进行库存更新,一旦库存系统异常,很有可能导致库存更新失败而订单支付成功的情况,从而导致数据不一致
(3)消息服务方式: 订单支付成功后,写入消息列表,然后定时扫描消息表消息写入消息队列中,库存系统会立即读取消息队列中的消息进行库存更新,同时添加消息处理状态;接着,库存系统向消息队列中写入库存处理结果,订单系统会立即读取消息队列中的库存处理状态。如果库存服务处理失败,订单服务还会重复扫描并发送消息表中的消息,请求库存系统进行最终一致性的库存更新。如果处理成功,订单服务直接删除消息表数据,并写入到历史消息表
4、JMS和AMQP的区别
JMS(Java Message Service) | AMQP(Advanced Message Queuing Protocol) | |
---|---|---|
定义 | Java api | 网络线级协议 |
消息中间件 | AvtiveMQ、HornetMQ | RabbitMQ |
跨语言 | 否 | 是 |
跨平台 | 否 | 是 |
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层的协议标准;天然具有跨平台、跨语言特性。 |
5、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 表示消息队列服务器实体
-
工作模式:
(1)Work queues(工作队列模式)
在Work queues工作模式中,不需要设置交换器(RabbitMQ会使用内部默认交换器进行消息转换),需要指定唯一的消息队列进行消息传递,并且可以有多个消息消费者。在这种模式下,多个消息消费者通过轮询的方式依次接收消息队列中存储的消息,一旦消息被某一个消息者接收,消息队列会将消息移除,而接收并处理的消费者必须在消费完一条消息后再准备接收下一条消息
(2)Publish/Subscribe(发布订阅模式)
在Publish/Subscribe工作模式中,**必须先配置一个fanout类型的交换器,不需要指定对应的路由键,**同时会将消息路由到每一个消息队列上,然后每个消息队列都可以对相同的消息进行接收存储,进而由各自消息队列关联的消息者进行消费,就像异步,例如:注册成功后,向用户同时发短信和邮件
(3)Routing(路由模式)
在Routing工作模式中必须先配置一个direct类型的交换器,并指定不同的路由键值将对应的消息从交换器路由到不同的消息队列进行存储,例如:日志的收集处理,用户可以配置不同的路由键值分别对不同级别的日志信息进行分类处理
(4)Topices(通配符模式)
在Topices工作模式中,必须先配置一个topic类型的交换器,并指定不同的路由键值将对应的消息从交换器路由到不同的消息队列进行存储,然后由消费者进行各自消费,Topices模式与Rounting模式的主要不同在于:Topics模式设置的路由键是包含通配符的,其中**#匹配多个字符,匹配一个字符,然后与其他字符一起使用“.”进行连接*,从而组成动态路由键,例如:一些订阅客户只接收邮件消息,一些订阅客户只接收短信消息,那么可以根据客户需求进行动态路由匹配
(5)RPC
RPC工作模式与Work queues工作模式主体流程相似,都不需要设置交换器,需要指定唯一的消息队列进行消息传递。RCP模式与Work queues不同在于:RPC模式是一个回环结构,主要针对分布式框架的消息传递业务,客户端Client先发送消息到消息队列,远程服务端Server获取消息,然后写入另一个消息队列,先原始客户端Client响应消息处理结果,用于分布式事务管理
(6)Headers
Headers工作模式在RabbitMQ所支持的工作模式中是较为少用的一种模式,其具体流程与Routing工作模式有些相似。不过Headers工作模式时,必**须设置一个headers类型的交换器,而不需要设置路由键,**取而代之的是Properties属性配置中的headers头信息中使用key/value的形式配置路由规则
6、Spring Boot整合RabbitMQ
- 添加amqp的依赖启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
-
配置RabbitMQ
spring: rabbitmq: host: 121.199.34.225 # 如果不配置下面的,springboot会使用内部的rabbitmq path: 5672 username: guest password: guest virtual-host: /
-
Publish/Subscribe模式示例(基于API的方式)
(1)创建交换器、队列及其绑定
// 注入AmqpAdmin类 @Autowired AmqpAdmin amqpAdmin; @Test void create() { // 定义fanout类型的交换器 amqpAdmin.declareExchange(new FanoutExchange("fanout_exchange")); // 定义两个消息队列 amqpAdmin.declareQueue(new Queue("fanout_queue_email")); amqpAdmin.declareQueue(new Queue("fanout_queue_sms")); // 将消息队列与交换器绑定 // Binding(String destination, Binding.DestinationType destinationType, String exchange, String routingKey, @Nullable Map<String, Object> arguments) amqpAdmin.declareBinding(new Binding("fanout_queue_email",Binding.DestinationType.QUEUE,"fanout_exchange","",null)); amqpAdmin.declareBinding(new Binding("fanout_queue_sms",Binding.DestinationType.QUEUE,"fanout_exchange","",null)); }
(2)发送消息
注意:RabbitTemplate类会调用默认的格式转换SimpleMessageConverter类,这个类只支持字符串字节数组及序列化后的类,因此这里的实体类要实现序列化
@Autowired RabbitTemplate rabbitTemplate; @Test void sendMessage(){ // 实现序列化接口 User user = new User(); user.setId(1); user.setUsername("test"); rabbitTemplate.convertAndSend("fanout_exchange","",user); }
(3)自定义转换器(不管用什么方式都要配置)
因为实现JDK的序列化接口可是效果差,因此改为Json格式,原理跳转
@Configuration public class RabbitMQMessageConfig { @Bean public MessageConverter messageConverter(){ return new Jackson2JsonMessageConverter(); } }
(4.1)接收消息(第一种方式)
这种方式能实时监听消息队列的消息,一有消息马上输出
@Service public class RabbitMQService { // 这里@RabbitListener注解值用来接收消息,后面还能用于创建和绑定 @RabbitListener(queues = "fanout_queue_email") public void psubConsumerEmail(Message message){ // 消息里面包含了消息头等信息,这里只获取消息体,为byte[]类型 byte[] body = message.getBody(); // 转换为String类型 String s = new String(body); System.out.println("邮件业务接收到消息:"+s); } // 如果形参为User类型的也可以,springboot会帮我们自动转换,接收为User,否则为Json // @RabbitListener(queues = "fanout_queue_sms") // public void psubConsumerSms(User user){ // System.out.println("邮件业务接收到短信:"+user); // } @RabbitListener(queues = "fanout_queue_sms") public void psubConsumerSms(Message message){ byte[] body = message.getBody(); String s = new String(body); System.out.println("邮件业务接收到短信:"+s); } }
(4.2)接收消息(第二种方式)
这种方式只能手动的执行,不能实时的监听,因此不常用
void receiveMessage(){ // 调用RabbitTemplate的receiveAndConvert方法获取消息体信息 Object email = rabbitTemplate.receiveAndConvert("fanout_queue_email"); Object sms = rabbitTemplate.receiveAndConvert("fanout_queue_sms"); System.out.println(email); System.out.println(sms); }
-
Routing模式示例(基于配置类的方式)
(1)创建交换器、队列及其绑定
@Configuration public class RabbitMQConfig { //定义direct类型的交换器 @Bean public Exchange routing_exchange(){ return ExchangeBuilder.directExchange("routing_exchange").build(); } // 定义队列 @Bean public Queue routing_queue_error(){ return new Queue("routing_queue_error"); } @Bean public Queue routing_queue_all(){ return new Queue("routing_queue_all"); } // 消息队列与交换器绑定,每个路由键都要绑一次,比较麻烦下面用注解的比较方便 @Bean public Binding bindingError(){ return BindingBuilder.bind(routing_queue_error()).to(routing_exchange()).with("error").noargs(); } @Bean public Binding bindingInfo() { return BindingBuilder.bind(routing_queue_all()).to(routing_exchange()).with("info").noargs(); } @Bean public Binding bindingWaring() { return BindingBuilder.bind(routing_queue_all()).to(routing_exchange()).with("warning").noargs(); } @Bean public Binding bindingError1() { return BindingBuilder.bind(routing_queue_all()).to(routing_exchange()).with("error").noargs(); } }
(2)发送消息(也是要实现序列化)
void sendRouingError(){ rabbitTemplate.convertAndSend("routing_exchange","error","routing send error message"); rabbitTemplate.convertAndSend("routing_exchange","info","routing send all message"); }
(3)接收消息和上面的大同小异
@RabbitListener(queues = "routing_queue_all") public void rtConsumerError(Message message){ byte[] body = message.getBody(); String s = new String(body); System.out.println("routing_all测试:"+s); } @RabbitListener(queues = "routing_queue_error") public void rtConsumerError(Message message){ byte[] body = message.getBody(); String s = new String(body); System.out.println("routing_error测试:"+s); }
-
Topics(基于注解的方式)
(1)创建交换器、队列、绑定、监听接收消息
@RabbitListener(bindings = @QueueBinding(value = @Queue("topic_queue_email"),exchange = @Exchange(value = "topic_exchange",type = "topic"),key = "info.#.email.#")) public void topicConsumerEmail(String message){ System.out.println("topic_email测试"+message); } @RabbitListener(bindings = @QueueBinding(value = @Queue("topic_queue_sms"),exchange = @Exchange(value = "topic_exchange",type = "topic"),key = "info.#.sms.#")) public void topicConsumerSms(String message){ System.out.println("topic测试_sms"+message); }
(2)发送消息(也是要实现序列化)
// 同时发送邮件和短信,注意路由键 void topicPublisher(){ rabbitTemplate.convertAndSend("topic_exchange","info.email.sms","topic send email message"); }
7、Spring Boot整合RabbitMQ原理
-
自动配置类RabbitAutoConfiguration执行
-
RabbitAutoConfiguration会帮我们创建RabbitTemplate和AmqpAdmin
@Bean @ConditionalOnSingleCandidate(ConnectionFactory.class) @ConditionalOnMissingBean({RabbitOperations.class}) // 就是一个模板类帮我们操作RabbitMQ,比如说给RabbitMQ发送和接受消息 public RabbitTemplate rabbitTemplate(RabbitTemplateConfigurer configurer, ConnectionFactory connectionFactory) { RabbitTemplate template = new RabbitTemplate(); configurer.configure(template, connectionFactory); return template; } @Bean @ConditionalOnSingleCandidate(ConnectionFactory.class) @ConditionalOnProperty( prefix = "spring.rabbitmq", name = {"dynamic"}, matchIfMissing = true ) // 系统管理功能组件 @ConditionalOnMissingBean public AmqpAdmin amqpAdmin(ConnectionFactory connectionFactory) { return new RabbitAdmin(connectionFactory); }
-
RabbitTemplate类里面有帮我操作RabbitMQ的方法及一些默认的参数,将消息转换为Message类型,这里就要调用序列化机制,但最终底层都是调用send方法
-
AmqpAdmin类里面有帮我们管理RabbitMQ的方法,包括创建交换器、队列、绑定、解绑等等
8、Spring Boot整合RabbitMQ序列化原理
-
RabbitTemplate类里面默认使用SimpleMessageConverter类来实现序列化
private MessageConverter messageConverter = new SimpleMessageConverter();
-
SimpleMessageConverter类里面使用的是JDK自带的序列化接口
bytes = SerializationUtils.serialize(object);
-
但RabbitTemplate有setMessageConverter方法
public void setMessageConverter(MessageConverter messageConverter) { this.messageConverter = messageConverter; }
-
根据spring的ioc容器,通过注入bean的方式来将参数传进去,这里虽然有原有的赋值和调用构造函数的赋值,但最终都是bean的值,因此我吗可以自定义一个MessageConverter的对象,然后将该对象放入ioc容器中,然后让RabbitTemplate自动注入(实现自定义序列化代码)