回顾:
在RabbitMQ初识中了解到
- RabbitMQ 是一个由Erlang语言开发的AMQP协议的开源实现,能够实现异步消息处理。是一个消息代理,接收和转发消息。
- AMQP(Advanced Message Queue Protocol 高级消息队列协议) 有一个AMQ Model(包含 Exchange,Message Queue, binding),rabbitMQ就是这个model的一个实现
- rabbitMQ 的exchange有三种类型 direct(point-to-point),topic(publish-subscribe), fanout(mulitcast), headers
- RabbitMQ的工作模式有:简单模式(不使用exchange),work queue(不使用exchange), publish/subscribe(exchange = fanout), routing(exchange = direct), topic(exchange = topic), RPC, publisher confirms
- MQ优势:
1、应用解耦,提高系统的容错性和可维护性
2、异步提速,提升用户体验和系统吞吐量
3、削峰填谷,提高系统稳定性- MQ劣势:
1、系统可用性降低, MQ挂掉会影响整个系统的使用
2、系统复杂度提高,由之前的同步调用转为异步调用
3、一致性问题,某一方消息消费失败会造成数据不一致问题
RabbitMQ 高级特性
- 消息的可靠性 【producer -> Broker】
- Consumer ACK 【Broker -> consumer】
- 消费端限流 【消费端处理消息的速率】
- TTL 【过期时间】
- 死信队列
- 延迟队列 【RabbitMQ没有提供延迟队列,需要使用TTL+死信队列 实现】
- 日志与监控
1. 消息的可靠性 【producer -> Broker】
以防消息传递过程中消息丢失造成消息没有发送到mq
生产者到queue 控制消息投递可靠性有两种模式
1、confirm模式(确认模式)[publisher —> exchange, exchange收到消息后调用confirmCallback回调函数告诉生成者]
开启确认模式: publisher-confirms = “true”
在rabbitTemplate 定义 confirmCallback回调函数
rabbitmq.setConfirmCallback(….);
2、return模式(退回模式)[exchange —> queue, 消息没有路由到queue,调用returnCallback回调函数告诉消息发送方]
开启回退模式 , 设置returnCallBack : publisher-returns = “true”
设置exchange处理消息的模式, 默认是如果消息没有路由到queue则直接丢弃消息;如果开始回退模式,如果消息没有路由到queue,会返回给消息发送方returnCallBack()
rabbitmq.setReturnCallback(….);
2. Consumer ACK 【Broker -> consumer】
RabbitMQ中有一个ACK机制,默认情况下消费者接收到到消息,RabbitMQ会自动提交ACK,之后这条消息就不会再发送给消费者了。
以防消息在消费过程中丢失,造成消息没有被消费
消费端收到消息后的确认方式
1、自动确认: acknowledge = “none”(默认)
2、手动确认: acknowledge = “manual”, 调用channel.basicAck(),手动签收,出现异常则调用channel.basicNack()方法,让其自动重新发消息
设置手动确认: acknowledge = “manual”
让监听器实现 channelAwareMessageListener接口
消息处理成功,则调用channel的basicAck()签收,消息处理失败,则调用Channel的basicNack()拒收,broker重发消息给consumer
3、根据异常情况确认: acknowledge = “auto”
3. 消费端限流 【消费端处理消息的速率】
1、确保ack机制是手动确认, acknowledge = “manual”
2、listener-container 配置属性 perfetch = 1 表示消费端每次从mq拉取一条消息,直到手动确认消息消费完毕后才会继续拉取下一条
通过perfetch限制每次消费条数达到限流效果。如果不设置会一次性全部拉取
4. TTL 【过期时间】
Time To Live 存活时间
当消息到达存活时间后还没有被消息会自动清除
可以对单条消息message设置存活时间 expiration
也可以对整个队列queue设置存活时间 x-message-ttl
如果同时对 message和queue设置了ttl,时间短的生效
5. 死信队列
DLX(Dead Letter Exchange),又名死信交换机
消息什么时候会成为死信消息呢?
1、队列消息长度达到限制之后,比如消息长度限制是10条,那第11条及以后的消息都是死信消息
2、消费者拒收的消息,requeue = false 时basicNack()或basicReject()并不会把这条被拒的消息放回到原消息队列中
3、原队列存在消息过期设置,消息达到超时时间未被消费,此条消息也会成为死信消息
如何绑定死信队列?
通过设置 x-dead-letter-exchange 和 x-dead-letter-routing-key
6. 延迟队列 【RabbitMQ没有提供延迟队列,需要使用TTL+死信队列 实现】
延迟队列,是消息不会被立即消费,只有达到指定时间后才会被消费。
使用场景:
1、下单后30分钟未支付,取消订单,回滚库存
2、新用户注册7天后发短信问候
等
实现方式:
方法一:可以使用定时任务,隔一段时间扫描一下,统一处理,不太优雅,扫描时间设定不合理会存在时间误差问题
方法二:使用延迟队列给队列设置ttl,超过时间之后放入到死信队列DLX中,再绑定到queue,只需要监听私信队列,如果有消息就去拉取做判断然后处理即可
7. 日志与监控
在GUI界面使用 rabbitmq-tracing 或者 firehose
启用插件命令: rabbitmq-plugins enable rabbitmq-tracing
值得注意的是 开启后mq的性能会受影响,所以用完注意关闭!!!!
应用问题
消息幂等性
幂等性: 一次和多次请求某资源,对于资源本身应该具有同样的结果。即任意多次执行对资源本身所产生的影响均与一次执行产生的影响相同。
MQ中的幂等性: 消费多条相同的消息得到的结果与消费一次该条消息得到的结果是相同的。 比如 消费了两条账户扣款500元的消息,此时账户应该扣款500 而不是1000
什么时候可能会出现幂等性问题(消息重复消费问题)?
情况1:生产者把消息发送给mq了,mq成功接收后给生产者返回ack的时候网断了,此时生产者没有收到确认信息可能会认为没有发送成功,在网络恢复后又重新发送同一条消息给mq
情况2:消费者在消费消息的时候,mq将消息发送给消费者,消费者消费完后返回ack给mq,此时网络中断会导致mq收不到确认信息,mq认为消息未消费成功,可能会将该条消息发送给其他消费者或者等网络恢复后重新发送给该消费者,造成消息的重复消费
如何保障消息的幂等性呢?保证消息不被重复消费?
方法一:使用乐观锁机制,添加一个版本号,在每次更新时根据id和版本号判断,update 后version + 1 (更新的时候对比数据库中该条数据的版本号与读取时的版本号是否相等,相等则说明没有被修改过,可以update,不等说明被修改过不能update)
方法二:生成一个全局唯一与业务无关的id,插入时唯一键重复不会被insert成功
方法三:结合redis使用,redis set 命令如果数据已存在会覆盖旧值
消息可靠性保障
消息丢失的三种情况:
1、生产者弄丢消息:生产者在将数据发送到MQ的时候,可能由于网络等原因造成消息投递失
2、mq自身弄丢消息:未开启RabbitMQ的持久化,数据存储于内存,服务挂掉后队列数据丢失;开启了RabbitMQ持久化,消息写入后会持久化到磁盘,但是在落盘的时候挂掉了,不过这种概率很小
3、消费者弄丢消息:消费者刚接收到消息还没处理完成,结果消费者挂掉了
1、生产者弄丢消息解决办法有两种:
方法一:生产者在发送数据前开启RabbitMQ事务(同步)该方法因为采用了事务机制,会导致吞吐量下降,特别消耗性能
方法二:开启confirm模式(异步)开篇有提到publisher 到 queue保障消息可靠性的方法
回顾一下:
publisher —> exchange: confirm模式(确认模式) exchange接收到消息后调用confirmCallback告知生产者
exchange —> queue: return模式(退回模式) 消息没有路由到queue 调用 returnCallback告知mq
如果使用的是springboot,需要在application.yml中添加
实现confirm回调接口
生产者发送消息是设置confirm回调
2、MQ自身丢失消息解决办法:
创建queue时设置为持久化队列,可以保证RabbitMQ持久化queue的元数据,此时还是不会持久化queue里的数据。
发送消息时将消息的deliveryMode设置为持久化,此时queue中的消息才会持久化到磁盘。
同时设置queue和message持久化以后,RabbitMQ挂了再次重启会从磁盘上重启恢复queue,恢复这个queue 里的数据,保证数据不会丢失。
需要考虑到的是 即使开启了持久化,也可能会出现在消息在落盘时服务器挂掉的情况,导致消息根本没有进入到queue,可以考虑结合生产者confirm机制处理。持久化机制开启后,消息只有成功落盘时才会通过confirm回调通知生产者,可以看做生产者在生产消息时维护的是一个正在等待消息发送确认的队列,如果超过一定时间没有从confirm中收到返回则自动重发
3、消费者弄丢消息解决办法:
开篇有提到Consumer ACK机制,其实可以解决此问题
回顾:
queue —> consumer: 首先保证mq确认机制是手动确认机制 acknowledge = “manual”,
监听器监听 ChannelAwareMessageListener接口,
消息处理成功,则调用channel.basicAck()方法,
消息处理失败则调用channel.basicNack()或 channel.reject()方法拒收
RabbitMQ的 ack机制,在默认情况下,消费者接收到消息之后,RabbitMQ会自动提交ACK,之后这条消息就不会再发送给消费者了。
可以将这个ack的提交方式改为手动提交,每次处理完消息后,手动ack一下。
值得注意的是,可能会出现刚处理完消息还没有来得及手动ack消费者挂了,会导致mq以为消费者没有消费成功,重新发送此消息,造成消息的重复消费,不过只要保证消息的幂等性,重复消费也不会造成问题。
在springboot中修改application.yml配置文件更改为手动ack模式
消费端手动ack参考代码
参考:
RabbitMQ如何保证消息的幂等性、可靠性、顺序性-CSDN博客
保障消息可靠性 — 补偿机制
- 生产方先将业务数据入库 DB
- 发送消息给 mq 的queue1
- 过一段时间在发送同样的一条消息给mq queue3(发送延迟消息)
- consumer 监听queue1,如果queue1有消息则拉取消费
- consumer消费消息后再发一条确认消息给发送给 mq queue2
- 回调检查服务监听确认消息queue2
- 监听到queue2有消息则将消息写入到mq的数据库MDB
- 回调检查服务同事监听延迟消息队列 queue3
- 将queue3的消息与MDB中的消息对比,如果MDB中有该条消息说明消息已被消费,不做任何处理,如果MDB中没有该条消息说明消费失败,根据业务需求判断是否需要生产者重发消息,需要则调方法重新发送
- 定时器
使用一个定时任务定时检查业务数据库和MDB中的数据是否一致,在延迟消息和正常发送消息都失败的情况下做兜底方案,如果发现不一致,则调生产者方法重新发送
如何保证消息顺序执行?
什么是顺序错乱?
在 mysql 里增删改一条数据,对应出来了增删改 3 条 binlog 日志,接着这三条 binlog 发送到 MQ 里面,再消费出来依次执行,起码得保证人家是按照顺序来的吧?不然本来是:增加、修改、删除;你楞是换了顺序给执行成删除、修改、增加,不全错了么。
RabbitMQ顺序错乱的场景
场景一:一个queue 多个consumer消费会造成乱序。consumer从mq读取数据是有序的,但每个consumer的执行时间是不确定的,无法保证先读到消息的consumer一定会先完成操作,就会出现消息没有按顺序执行,造成数据顺序错误。
场景二:一个queue对应一个consumer, consumer里面进行了多线程消费,无法保证那个线程先完成操作,所以也会存在乱序执行问题
RabbitMQ如何保证消息的消费顺序
方法一:拆分多个queue,每个queue对应一个consumer,会多一些queue,同时可能会造成吞吐量下降
方法二:一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理
拓展 kafka顺序错乱的场景
场景一: kafka一个topic,一个partition,一个consumer,但是consumer内部进行多线程消费,这样数据也会出现顺序错乱问题。
场景二:具有顺序的数据写入到了不同的partition里面,不同的消费者去消费,但是每个consumer的执行时间是不固定的,无法保证先读到消息的consumer一定先完成操作,就会出现消息没有按顺序执行,造成数据顺序错误。
拓展 kafka如何保证消息的消费顺序
方法一:确保同一个消息发送到同一个partition,一个topic,一个partition,一个consumer,内部单线程消费。单线程吞吐量太低,一般不会用这个.
方法二:写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。
参考:
docs/high-concurrency/how-to-ensure-the-order-of-messages.md · shishan100/Java-Interview-Advanced - Gitee.com
Rabbitmq如何保证消息顺序执行 - -零 - 博客园