消息队列----使用场景,重试补偿,事务补偿,幂等性,消息堆积,有序性,ACK机制

一、消息队列----应用场景

场景名称

场景描述

传统做法

消息队列做法

异步处理

用户注册后,需要发注册邮件和注册短信

1.串行的方式:

  信息写入数据库50ms + 发送注册短信50ms + 发送注册邮件50ms =》 150ms

2.并行方式:

  信息写入数据库50ms +【发送注册邮件的同时,发送注册短信】50ms=》100ms

信息写入数据库50ms + 【注册邮件,发送短信写入消息队列】0.0001ms =>50ms

注:因此写入消息队列的速度很快,基本可以忽略;

中心思想:引入消息队列,将不是必须的业务逻辑,异步处理

应用解耦

用户下单后,订单系统需要通知库存系统

消息队列----使用场景,重试补偿,事务补偿,幂等性,消息堆积,有序性,ACK机制_第1张图片

流量削峰

秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉;

用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,

则直接抛弃用户请求或跳转到错误页面

日志处理

将消息队列用在日志处理中;

(我一般关注把日志记录下来,不怎么关注日志后续的处理,所以这个用的不多)

消息通讯

消息通讯是指,【消息队列一般都内置了高效的通信机制】,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等

二、消息队列----优缺点

优点

缺点

解耦

引入复杂度(引入消息队列本身就有创建维护成本)

提速

暂时的不一致性(你把消息给队列后,默认它一定会成功执行的,但实际上不一定)

广播(一次生成,可多人订阅)

削峰

三、消息队列----重试补偿,事务补偿

问题场景

解决思想

解决办法

消费者已经收到消息或消费消息了,但因为网络中断没给mq发送ack,导致消息重发重复消费;

消费消息时先判断该消息是否已消费过(这个状态位如何存储读取?

发送消息时】给消息分配一个全局id,只要消费过该消息,将 < id,message>K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可

客户下单,若订单创建成功,库存扣减失败,如何回滚订单?在一台服务器上用事务能解决,但分布式如何处理?

既然不能把【订单创建】和【库存减扣】放到一个事务里,那就把【订单创建】和【库存减扣分身--消息事件表】放到一个事务里;

实现:

1.【订单创建】和【库存减扣分身--消息事件表】放到一个事务里;

2. 或者 消息事件表消费失败更新事件表状态,根据状态扔异常进行事务回滚;

3. 或者 库存服务定时扫描消息事件表,将未投递失败/消费 失败的消息进行消费,即补偿事务一致性

消息队列----使用场景,重试补偿,事务补偿,幂等性,消息堆积,有序性,ACK机制_第2张图片

四、消息队列----幂等性(如何保证 重复消费的结果 与 消费一次的结果是相同的)

解决办法

举例说明

利用数据库唯一约束

将订单表中的订单编号设置为唯一索引,创建订单时,根据订单编号就可以保证幂等

去重表

首先在去重表上建唯一索引,其次操作时把业务表和去重表放在同个本地事务中,如果出现重复消费,数据库会抛唯一约束异常,操作就会回滚

利用redis的原子性

每次操作都直接set到redis里面,然后将redis数据定时同步到数据库中

多版本(乐观锁)控制

此方案多用于【更新】的场景下。大体思路是:给业务数据增加一个版本号属性,每次更新数据前,比较当前数据的版本号是否和消息中的版本一致,如果不一致则拒绝更新数据,更新数据的同时将版本号+1

状态机机制

此方案多用于更新且业务场景存在多种状态流转的场景

token机制

生产者发送每条数据的时候,增加一个全局唯一的id,这个id通常是业务的唯一标识,比如订单编号。在消费端消费时,则验证该id是否被消费过,如果还没消费过,则进行业务处理。处理结束后,在把该id存入redis,同时设置状态为已消费。如果已经消费过了,则不进行处理。

redis原子性操作的实现原理在于redis底层使用单线程操作。设计者认为cpu不会成为性能的瓶颈,实际上是会的。

  1. Redis的原子性有两点:

    • 单个操作是原子性的

    • 多个操作也支持事务,即原子性,通过 MULTI 和 EXEC 指令包起来

  2. 原子操作的意思就是要么成功执行要么失败完全不执行。用现实中的转账比喻最形象,你转账要么成功,要么失败钱不动,不存在你钱转出去了,但收款方没收到这种成功一半失败一半的情况

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

五、消息队列----消息堆积(要么是发送端变快,要么是消费端变慢造成)

产生原因:

  1. Producer 端单位时间发送的消息增多,Consumer 端短时间内来不及消费;
  2. Producer 端单位时间发送的消息正常,Consumer 端因消费线程低效不能及时消费;

解决思想:

设计MQ系统的时候,一定要保证 Consumer 端的消费性能要高于 Producer 端的发送性能

发送端性能优化:

发送端性能低:检查是否因为业务逻辑耗时太久导致的 + 设置合适的 并发 和 批量 大小;

消费端性能优化:

消费端性能低:优化业务逻辑耗时 + 水平扩容 (扩充consumer端的 实例数 和 topic中的 partition 数)

六、消息队列----有序性(产生原因:多个消费者/多线程)

RabbitMQ 无序原因:

一个queue,多个consumer

消息队列----使用场景,重试补偿,事务补偿,幂等性,消息堆积,有序性,ACK机制_第3张图片

RabbitMQ无序解决办法:

拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点;

或者就一个queue,但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理

消息队列----使用场景,重试补偿,事务补偿,幂等性,消息堆积,有序性,ACK机制_第4张图片

kafka 无序原因:

一个topic,一个partition,一个consumer,但是内部多线程

消息队列----使用场景,重试补偿,事务补偿,幂等性,消息堆积,有序性,ACK机制_第5张图片

Kafka 无序解决办法:

一个topic,一个partition,一个consumer,内部单线程消费,写N个内存queue,然后N个线程分别消费一个内存queue即可

消息队列----使用场景,重试补偿,事务补偿,幂等性,消息堆积,有序性,ACK机制_第6张图片

七、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死信交换机。

消息队列----使用场景,重试补偿,事务补偿,幂等性,消息堆积,有序性,ACK机制_第7张图片

4、消息有哪几种情况成为死信

  • 消费者拒收消息(basic.reject/ basic.nack) ,并且没有重新入队 requeue=false
  • 消息在队列中未被消费,且超过队列或者消息本身的过期时间TTL(time-to-live)
  • 队列的消息长度达到极限
  • 结果:消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列,否则被清除;

 

你可能感兴趣的:(运行过程类,队列,java)