消息队列
为什么使用消息队列
先说一下消息队列常见的使用场景吧,其实场景有很多,但是比较核心的有 3 个:解耦、异步、削峰。
解耦
问题:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GiOf9HR2-1618106328448)(https://github.com/Gaotrees/advanced-java/raw/master/images/mq-1.png)]
一个系统A为多个系统B,C,D提供数据,如果某个系统挂掉,该怎么办?存消息还是重发?
解决措施
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ai5YCmld-1618106328449)(https://github.com/Gaotrees/advanced-java/raw/master/images/mq-2.png)]
使用MQ,A系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可。如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。
总结:通过一个 MQ,Pub/Sub 发布订阅消息这么一个模型,A 系统就跟其它系统彻底解耦了。
异步
问题:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DnWW5o5y-1618106328450)(https://github.com/Gaotrees/advanced-java/raw/master/images/mq-3.png)]
A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,这是请求将近1秒,很慢。
解决措施:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0tc31JWu-1618106328451)(https://github.com/Gaotrees/advanced-java/raw/master/images/mq-4.png)]
如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms,对于用户而言,其实感觉上就是点个按钮,8ms 以后就直接返回了。
削峰
问题:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IAAN4JIQ-1618106328452)(https://github.com/Gaotrees/advanced-java/raw/master/images/mq-5.png)]
每秒并发请求数量突然会暴增到 5k+ 条,是系统是直接基于 MySQL 的,大量的请求涌入 MySQL,直接把SQL打死了,系统崩溃。
解决措施
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3dL9zJjC-1618106328453)(https://github.com/Gaotrees/advanced-java/raw/master/images/mq-6.png)]
使用 MQ,每秒 5k 个请求写入 MQ,A 系统每秒钟最多处理 2k 个请求,因为 MySQL 每秒钟最多处理 2k 个。A 系统从 MQ 中慢慢拉取请求,每秒钟就拉取 2k 个请求,不要超过自己每秒能处理的最大请求数量就 ok
缺点
系统可用性降低
系统引入的外部依赖越多,越容易挂掉。MQ 一挂,整套系统崩溃。
系统复杂度提高
怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?
一致性问题
A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。
如何保证消息队列的高可用?
RabbitMQ 的高可用性
单机模式
单机模式,就是 Demo 级别的,一般就是你本地启动了玩玩儿的😄,没人生产用单机模式。
普通集群模式(无高可用性)
没做到所谓的分布式,就是个普通集群。因为这导致你要么消费者每次随机连接一个实例然后拉取数据,要么固定连接那个 queue 所在实例消费数据,前者有数据拉取的开销,后者导致单实例性能瓶颈。
而且如果那个放 queue 的实例宕机了,会导致接下来其他实例就无法从那个实例拉取,如果你开启了消息持久化,让 RabbitMQ 落地存储消息的话,消息不一定会丢,得等这个实例恢复了,然后才可以继续从这个 queue 拉取数据。
镜像集群模式(高可用性)
在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像
如何保证消息不被重复消费?(消息队列如何保证幂等性)
-
比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update 一下好吧。
-
比如你是写 Redis,那没问题了,反正每次都是 set,天然幂等性。
-
比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。
如何保证消息的可靠性传输?
消息丢失的3种情况
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tKsmjVsa-1618106328453)(https://github.com/Gaotrees/advanced-java/raw/master/images/rabbitmq-message-lose.png)]
生产者弄丢了数据
问题
生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为网络问题,都有可能。
解决措施
事务机制:
开启事务,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务channel.txRollback
,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit
。
但是这样的话吞吐量下来了。
confirm 模式
可以开启 confirm
模式,在生产者那里设置开启 confirm
模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个 ack
消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个 nack
接口,告诉你这个消息接收失败,你可以重试。
RabbitMQ 弄丢了数据
必须开启 RabbitMQ 的持久化,就是消息写入之后会持久化到磁盘。
怕是 RabbitMQ 自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。
设置持久化有两个步骤:
- 创建 queue 的时候将其设置为持久化
这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。 - 第二个是发送消息的时候将消息的
deliveryMode
设置为 2
就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。
必须要同时设置这两个持久化才行
消费端弄丢了数据
问题:
主要是因为你消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那么就尴尬了,RabbitMQ 认为你都消费了,这数据就丢了。
解决措施
这个时候得用 RabbitMQ 提供的 ack
机制,简单来说,就是你必须关闭 RabbitMQ 的自动 ack
,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 ack
一把。
总结
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zbqJQ4Nb-1618106328454)(https://github.com/Gaotrees/advanced-java/raw/master/images/rabbitmq-message-lose-solution.png)]
如何保证消息的顺序性?
问题
应出来了增删改 3 条 binlog
日志,接着这三条 binlog
发送到 MQ 里面,再消费出来依次执行不然本来是:增加、修改、删除;你愣是换了顺序给执行成删除、修改、增加。
RabbitMQ:一个 queue,多个 consumer。比如,生产者向 RabbitMQ 里发送了三条数据,顺序依次是 data1/data2/data3,压入的是 RabbitMQ 的一个内存队列。有三个消费者分别从 MQ 中消费这三条数据中的一条,结果消费者2先执行完操作,把 data2 存入数据库,然后是 data1/data3。这不明显乱了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BqCH8yNv-1618106328454)(https://github.com/Gaotrees/advanced-java/raw/master/images/rabbitmq-order-01.png)]
解决方案
拆分多个 queue,每个 queue 一个 consumer,就是多一些 queue 而已,确实是麻烦点;或者就一个 queue 但是对应一个 consumer,然后这个consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SiKqA5YQ-1618106328454)(https://github.com/Gaotrees/advanced-java/raw/master/images/rabbitmq-order-02.png)]
如何解决消息队列的延时以及过期失效问题?
大量消息在 mq 里积压了几个小时了还没解决?
临时紧急扩容
-
先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。
-
新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。
-
然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。
-
接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。
-
等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。
mq 中的消息过期失效了
RabbtiMQ 是可以设置过期时间的,如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。
手动写程序,将丢失的数据一点点查出来,重新灌入MQ。
mq 都快写满了
临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,然后走第二个方案。
让你写一个消息队列,该如何进行架构设计?
mq 得支持可伸缩性,需要的时候快速扩容,就可以增加吞吐量和容量。设计分布式系统,增加机器,不就可以存放更多数据,提供更高的吞吐量了。
mq 的数据落地磁盘,落磁盘才能保证别进程挂了数据就丢了,做法:顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。