一、消息队列----应用场景
场景名称 | 场景描述 | 传统做法 | 消息队列做法 |
异步处理 | 用户注册后,需要发注册邮件和注册短信 | 1.串行的方式: 信息写入数据库50ms + 发送注册短信50ms + 发送注册邮件50ms =》 150ms 2.并行方式: 信息写入数据库50ms +【发送注册邮件的同时,发送注册短信】50ms=》100ms | 信息写入数据库50ms + 【注册邮件,发送短信写入消息队列】0.0001ms =>50ms 注:因此写入消息队列的速度很快,基本可以忽略; 中心思想:引入消息队列,将不是必须的业务逻辑,异步处理; |
应用解耦 | 用户下单后,订单系统需要通知库存系统 | ||
流量削峰 | 秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉; | 用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量, 则直接抛弃用户请求或跳转到错误页面 | |
日志处理 | 将消息队列用在日志处理中; (我一般关注把日志记录下来,不怎么关注日志后续的处理,所以这个用的不多) | ||
消息通讯 | 消息通讯是指,【消息队列一般都内置了高效的通信机制】,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等 |
二、消息队列----优缺点
优点 | 缺点 |
解耦 | 引入复杂度(引入消息队列本身就有创建维护成本) |
提速 | 暂时的不一致性(你把消息给队列后,默认它一定会成功执行的,但实际上不一定) |
广播(一次生成,可多人订阅) | |
削峰 |
三、消息队列----重试补偿,事务补偿
问题场景 | 解决思想 | 解决办法 |
消费者已经收到消息或消费消息了,但因为网络中断没给mq发送ack,导致消息重发重复消费; | 消费消息时先判断该消息是否已消费过(这个状态位如何存储读取?) | 【发送消息时】给消息分配一个全局id,只要消费过该消息,将 < id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可 |
客户下单,若订单创建成功,库存扣减失败,如何回滚订单?在一台服务器上用事务能解决,但分布式如何处理? | 既然不能把【订单创建】和【库存减扣】放到一个事务里,那就把【订单创建】和【库存减扣分身--消息事件表】放到一个事务里; 实现: 1.【订单创建】和【库存减扣分身--消息事件表】放到一个事务里; 2. 或者 消息事件表消费失败更新事件表状态,根据状态扔异常进行事务回滚; 3. 或者 库存服务定时扫描消息事件表,将未投递失败/消费 失败的消息进行消费,即补偿事务一致性 | |
四、消息队列----幂等性(如何保证 重复消费的结果 与 消费一次的结果是相同的)
解决办法 | 举例说明 |
利用数据库唯一约束 | 将订单表中的订单编号设置为唯一索引,创建订单时,根据订单编号就可以保证幂等; |
去重表 | 首先在去重表上建唯一索引,其次操作时把业务表和去重表放在同个本地事务中,如果出现重复消费,数据库会抛唯一约束异常,操作就会回滚 |
利用redis的原子性 | 每次操作都直接set到redis里面,然后将redis数据定时同步到数据库中 |
多版本(乐观锁)控制 | 此方案多用于【更新】的场景下。大体思路是:给业务数据增加一个版本号属性,每次更新数据前,比较当前数据的版本号是否和消息中的版本一致,如果不一致则拒绝更新数据,更新数据的同时将版本号+1 |
状态机机制 | 此方案多用于更新且业务场景存在多种状态流转的场景 |
token机制 | 生产者发送每条数据的时候,增加一个全局唯一的id,这个id通常是业务的唯一标识,比如订单编号。在消费端消费时,则验证该id是否被消费过,如果还没消费过,则进行业务处理。处理结束后,在把该id存入redis,同时设置状态为已消费。如果已经消费过了,则不进行处理。 |
redis原子性操作的实现原理在于redis底层使用单线程操作。设计者认为cpu不会成为性能的瓶颈,实际上是会的。
Redis的原子性有两点:
单个操作是原子性的
多个操作也支持事务,即原子性,通过
MULTI
和EXEC
指令包起来原子操作的意思就是要么成功执行要么失败完全不执行。用现实中的转账比喻最形象,你转账要么成功,要么失败钱不动,不存在你钱转出去了,但收款方没收到这种成功一半失败一半的情况
redis> MULTI # 标记事务开始 OK redis> INCR user_id # 多条命令按顺序入队 QUEUED redis> INCR user_id QUEUED redis> INCR user_id QUEUED redis> PING QUEUED redis> EXEC # 执行事务块内的多条命令,会按照先后顺序被放进一个队列当中,最后由 EXEC 命令原子性(atomic)地执行 1) (integer) 1 2) (integer) 2 3) (integer) 3 4) PONG
五、消息队列----消息堆积(要么是发送端变快,要么是消费端变慢造成)
产生原因: |
|
解决思想: | 设计MQ系统的时候,一定要保证 Consumer 端的消费性能要高于 Producer 端的发送性能 |
发送端性能优化: | 发送端性能低:检查是否因为业务逻辑耗时太久导致的 + 设置合适的 并发 和 批量 大小; |
消费端性能优化: | 消费端性能低:优化业务逻辑耗时 + 水平扩容 (扩充consumer端的 实例数 和 topic中的 partition 数) |
六、消息队列----有序性(产生原因:多个消费者/多线程)
RabbitMQ 无序原因: | 一个queue,多个consumer |
RabbitMQ无序解决办法: | 拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点; 或者就一个queue,但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理 |
kafka 无序原因: | 一个topic,一个partition,一个consumer,但是内部多线程 |
Kafka 无序解决办法: | 一个topic,一个partition,一个consumer,内部单线程消费,写N个内存queue,然后N个线程分别消费一个内存queue即可 |
七、ACK机制
1、什么是消息确认ACK。
答:如果在处理消息的过程中,消费者的服务器在处理消息的时候出现异常,那么可能这条正在处理的消息就没有完成消息消费,数据就会丢失。为了确保数据不会丢失,RabbitMQ支持消息确定-ACK。
2、ACK的消息确认机制。
答:ACK机制是消费者从RabbitMQ收到消息并处理完成后,反馈给RabbitMQ,RabbitMQ收到反馈后才将此消息从队列中删除。
如果一个消费者在处理消息出现了网络不稳定、服务器异常等现象,那么就不会有ACK反馈,RabbitMQ会认为这个消息没有正常消费,会将消息重新放入队列中。
如果在集群的情况下,RabbitMQ会立即将这个消息推送给这个在线的其他消费者。这种机制保证了在消费者服务端故障的时候,不丢失任何消息和任务。
消息永远不会从RabbitMQ中删除,只有当消费者正确发送ACK反馈,RabbitMQ确认收到后,消息才会从RabbitMQ服务器的数据中删除。
消息的ACK确认机制默认是打开的。
#消息发送交换机确认
spring.rabbitmq.publisher-confirms = true
#消息发送队列回调
spring.rabbitmq.publisher-returns = true
3、ACK确认的类型:发送确认,消费确认;
八、重试
消费端在处理消息过程中可能会报错,此时该如何重新处理消息呢?解决方案有以下两种。
-
在redis或者数据库中记录重试次数,达到最大重试次数以后消息进入死信队列或者其他队列,再单独针对这些消息进行处理;
-
使用spring-rabbit中自带的retry功能;
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: auto # 自动ack
retry:
enabled: true
max-attempts: 5
max-interval: 10000 # 重试最大间隔时间
initial-interval: 2000 # 重试初始间隔时间
multiplier: 2 # 间隔时间乘子,间隔时间*乘子=下一次的间隔时间,最大不能超过设置的最大间隔时间
九、死信队列+ TTL+死信交换机
1、什么是TTL
- time to live 消息存活时间
- 如果消息在存活时间内未被消费,则会被清除
- RabbitMQ支持两种ttl设置
- 单独消息进行配置ttl
- 整个队列进行配置ttl(居多)
2、什么是rabbitmq的死信队列
- 没有被及时消费的消息存放的队列
3、什么是rabbitmq的死信交换机
- Dead Letter Exchange(死信交换机,缩写: DLX)当消息成为死信后,会被重新发送到另⼀个交换机,这个交换机就是DLX死信交换机。
4、消息有哪几种情况成为死信
- 消费者拒收消息(basic.reject/ basic.nack) ,并且没有重新入队 requeue=false
- 消息在队列中未被消费,且超过队列或者消息本身的过期时间TTL(time-to-live)
- 队列的消息长度达到极限
- 结果:消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列,否则被清除;