1.消息队列
-
消息队列是指在消息传输过程中保存消息的容器,可以帮助不同进程 / 应用之间异步通信。
-
最关键的三个优点,异步、削峰、解耦。
-
缺点:
- 系统的可用性降低:如果消息队列一挂那就全崩了
- 系统的复杂性提高:一系列问题,例如防止消息丢失,消息的重复消费,消息传递的顺序性。
- 一致性问题:就拿异步举例子,如果消息队列里面异步处理的系统失败了,但是信息在写入数据库时早就返回了成功,那该怎么办
-
1.1异步处理
场景说明:用户注册后发送注册邮件和注册短信
-
传统做法
- 串行:只有等全部任务执行完才会拿到返回结果
- 并行:
- 串行:只有等全部任务执行完才会拿到返回结果
-
消息队列:将无关紧要的业务进行异步处理,不需要等待异步处理的结果
1.2应用解耦
场景说明:双11下单
-
传统做法
-
在用户下单后,订单系统通过调用库存系统的接口来通知
- 缺点:如果库存系统出现问题,订单就会失败,耦合度太高。
-
-
引入消息队列
-
订单系统:用户下单后,订单系统先进行持久化,然后将消息写入消息队列,返回用户订单下单成功。
-
库存系统:订阅下单的消息,获取下单消息,进行库操作。
- 即使库存系统出现问题,这个订单也不会丢失。
-
1.3流量削峰
场景:秒杀活动
作用:
- 通过控制消息流向下一个处理系统的速率来进行解耦。
- 可以缓解短时间的高流量压垮应用
2.ActiveMQ,RocketMQ,RabbitMQ,Kafka
-
ActiveMQ
- 社区对ActiveMQ的维护较少,高吞吐的场景较少使用。
-
Kafka:为大数据而生,在大数据领域以及日志采集时被广泛使用,大型公司建议用,非常契合日志采集。
- 优点:高吞吐吞吐量达到百万级,可用性高,性能好,一个数据多个副本,数据难丢失。
- 缺点:社区更新慢,消费不支持重试。
-
RocketMQ:出自阿里巴巴(应用于阿里的双11),为金融互联网而生,对于可靠性要求高的场景例如(订单处理,流量削峰,进行大量交易时)
- 优点:吞吐量达到10万级,可用性高,消息可以做到0丢失,支持10亿级别的消息堆积,消息堆积对性能影响很小,java语言实现。
- 缺点:支持的客户端语言不多目前支持java和部分c++。
-
RabbitMQ:适合数据量没有那么大。中小型公司可以选择功能比较完整的RabbitMQ
- 优点:性能好,吞吐达到万级,支持多种语言,社区比较活跃,更新频率较快,资源占用低,配置简单开箱即用,能够支持多种交换器,支持多种协议。
- 缺点:消息堆积能力一般,吞吐量一般,不适合超大规模数据流
3.消息队列协议
协议:在TCP/IP协议基础之上构建的一种约定成的规范和机制,是为了让客户端进行沟通和通讯,并且这种协议必须具有持久性,高可用,高可靠的性能。因为TCP/IP协议太简单,并不能承载消息的内容和载体,因此在此之上增加了一些内容。 消息的中间件负责数据的传递,存储,和分发消费三个部分,数据的存储和分发的过程需要遵循某种规范而这些规范就称为协议。
所谓协议是指:
1. 计算机底层操作系统和应用程序通讯时共同遵守的约定,只有遵循共同的约定和规范,系统和底层操作系统之间才能相互交流。
2. 和一般的网络应用程序不同,它主要负责数据的接收和传递,所以性能比较的高。
3. 协议对数据格式和计算机之间交换数据都必须严格遵守规范。
4.Work Queues
工作队列(又称任务队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。相反我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当有多个工作线程时,这些工作线程将一起处理这些任务,默认情况下采用的轮询策略。
5.rabbitMQ里的订阅模型的概念
- publisher:生产者,将消息发送到交换机里
- exchange:交换机,消息的中转站根据交换机的类型将消息递交到对应的队列中
- queue:队列,存储消息要和交换机绑定
- consumer:消费者,监测消息队列中的消息
交换机的类型
- fanout:广播,将发送者发送的消息发送到与交换机绑定的所有队列中
- direct:订阅,根据路由键(RoutingKey)发送到绑定的队列中
- topic:通配符订阅,与direct相似,只不过进行路由键匹配的时候可以使用通配符 "*"(只占用一个字符), "#"(占用多个或一个字符) 等
- header:头匹配,根据MQ的消息头进行匹配
如果这里MQ通知失败,支付服务中支付流水显示支付成功,而交易服务中的订单状态却显示未支付,数据出现了不一致
6.确保消息的可靠性
- 确保生产者一定把消息发送到MQ
- 确保MQ不会将消息丢失
- 确保消费者一定处理消息
6.1消息发送的可靠性
-
当生产者发送消息时出现网络波动这时可以使用生产者重试机制,SpringAMQP的重试是阻塞式的重试,会阻塞线程
spring: rabbitmq: connection-timeout: 1s # 设置MQ的连接超时时间 template: retry: enabled: true # 开启超时重试机制 initial-interval: 1000ms # 失败后的初始等待时间 multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier max-attempts: 3 # 最大重试次数 -
在网络顺畅的情况下采用生产者确认机制
spring: rabbitmq: publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型 publisher-returns: true # 开启publisher return机制type的类型有三种
- none:关闭confirm机制
- simple:同步阻塞等待MQ的回执
- correlated(推荐):MQ异步回调返回回执
6.2MQ的可靠性
消息到达MQ以后,如果MQ不能及时保存,那就会导致消息丢失,所以MQ的可靠性很重要。
6.2.1数据的持久化
-
交换机的持久化
- 在设置交换机时设置为durable
-
队列的持久化
- 设置队列的时候设置为durable
-
消息的持久化
- 也可以在控制台中设置
6.2.2lazyQueue
为了解决消息堆积的问题引入了lazyQueue
- 接收到消息后直接存入磁盘而非内存
- 消费者要消费消息时才会从磁盘中读取并加载到内存
- 支持百万条的消息存储
6.3消费者的可靠性
6.3.1消费者确认机制
-
ack:成功处理消息,RabbitMQ从队列中删除该消息
-
SpringAMQP设置了三种ACK模式
-
none:不处理,将消息传给消费者后立刻ack,消息会从MQ中删除。不安全,不建议使用。
-
manual:手动模式需要调用api发送ack或reject
-
auto:业务能够正常执行就自动返回ack,业务出现异常时根据异常类型返回结果
- 业务异常,自动返回nack
- 消息处理或校验异常,返回reject
-
-
-
nack:消息处理失败,RabbitMQ需要再次投递该消息
-
reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息
6.3.2失败重试机制
为了应对队列一直重新投递没被消费的消息的情况,SpringAMQP引入了失败重试的设置
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000ms # 初次的失败等待时长为1秒
multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数
stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
6.3.3失败处理策略
在使用失败重试机制后,如果达到最大重试次数消息会被丢弃。在某些对于消息可靠性要求较高的情况就不太合适了。
因此Spring允许我们自定义重试次数耗尽的消息处理策略,这个策略是MessageRecovery接口来定义
- RejectAndDontRequeueRecover: 重试耗尽后,直接reject,丢弃消息。
- ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队。
- RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机。
6.3.4业务幂等性
为了保证业务的幂等性有以下两种方法
-
唯一消息ID
-
每一条消息都生成一个唯一的id,与消息一起投递给消费者
-
消费者在接收到消息后处理自己的业务,业务处理成功后将ID保存到数据库
-
如果下次接收到相同消息,去数据库查询判断是否存在,存在则为重复消息放弃处理
@Bean public MessageConverter messageConverter(){ // 1.定义消息转换器 Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter(); // 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息 jjmc.setCreateMessageIds(true); return jjmc; }
-
-
业务状态判断(推荐)
- 例如支付修改订单业务里,先判断要修改的订单是否存在载更新数据
6.3.5兜底方案
如果MQ通知不一定能到对应的服务里,那么那个服务就必须自己出动去查询支付状态,这样即使MQ通知失败,也能通过主动查询来保证订单状态一致。
一般都是使用的定时任务进行定期查询,并判断相应的状态
7.延迟消息
在电商支付业务场景中,对于一些库存有限的商品,通常都会在下单后立刻扣除库存。
但这样存在一个问题,如果用户一直不支付,那就会一直占用库存资源导致其他客户无法正常交易。
因此,电商中通常的做法是:对于一定时间未支付的订单,应该立刻取消订单并释放库存。
那我们怎么才能准确的实现在下单后第30分钟去检查支付状态呢?
这时我们要使用延迟任务,而延迟任务最简单的方案是利用MQ的延迟消息。
RabbitMQ实现延迟消息的方案:
- 死信交换机 + TTL
- 延迟消息插件
7.1私信交换机和延迟消息
7.1.1私信交换机
死信
- 消费者basic.reject和basic.nack声明消费失败,并且requeue参数设置为false
- 消息是一个过期消息,超时无人消费
- 要投递的队列消息满了,无法投递
如果一个队列中的消息已经成为死信,这个队列通过dead-letter-exchange指定一个交换机,队列中的死信会投递到这个交换机中,而这个交换机称为死信交换机。
死信交换机的作用
1.收集因处理失败而拒绝的消息
2.收集因队列满了而拒绝的消息
3.收集TTL到期的消息
7.1.2延迟消息
7.2DelayExchange插件
7.3订单状态同步
在实际业务中大多数用户在一分钟以内就支付了,如果我们还是在30分钟时才去检测订单状态,那就会消耗MQ资源。因此我们应该设置好多检测几次订单状态,而不是在第30分钟时才检测。当途中检测到了订单是已支付状态,那就取消后续的检测。