一, 什么是消息队列?
消息队列不知道大家看到这个词的时候, 会不会觉得它是一个比较高端的技术, 反正我是觉得它好像是挺牛逼的.
消息队列, 一般我们会简称它为 MQ(Message Queue), 嗯, 就是很直白的简写.
我们先不管消息 (Message) 这个词, 来看看队列(Queue). 这一看, 队列大家应该都熟悉吧.
队列是一种先进先出的数据结构.
在 Java 里边, 已经实现了不少的队列了:
那为什么还需要消息队列 (MQ) 这种中间件呢???
到这里, 大家可以先猜猜为什么要用消息队列 (MQ) 这种中间件, 下面会继续补充.
消息队列可以简单理解为: 把要传输的数据放在队列中.
科普:
把数据放到消息队列叫做生产者
从消息队列里边取数据叫做消费者
二, 为什么要用消息队列?
为什么要用消息队列, 也就是在问: 用了消息队列有什么好处. 我们看看以下的场景
2.1 解耦
现在我有一个系统 A, 系统 A 可以产生一个 userId
然后, 现在有系统 B 和系统 C 都需要这个 userId 去做相关的操作
写成伪代码可能是这样的:
- public class SystemA {
- // 系统 B 和系统 C 的依赖
- SystemB systemB = new SystemB();
- SystemC systemC = new SystemC();
- // 系统 A 独有的数据 userId
- private String userId = "Java3y";
- public void doSomething() {
- // 系统 B 和系统 C 都需要拿着系统 A 的 userId 去操作其他的事
- systemB.SystemBNeed2do(userId);
- systemC.SystemCNeed2do(userId);
- }
- }
结构图如下:
ok, 一切平安无事度过了几个天.
某一天, 系统 B 的负责人告诉系统 A 的负责人, 现在系统 B 的 SystemBNeed2do(String userId)这个接口不再使用了, 让系统 A 别去调它了.
于是, 系统 A 的负责人说 "好的, 那我就不调用你了.", 于是就把调用系统 B 接口的代码给删掉了:
- public void doSomething() {
- // 系统 A 不再调用系统 B 的接口了
- //systemB.SystemBNeed2do(userId);
- systemC.SystemCNeed2do(userId);
- }
又过了几天, 系统 D 的负责人接了个需求, 也需要用到系统 A 的 userId, 于是就跑去跟系统 A 的负责人说:"老哥, 我要用到你的 userId, 你调一下我的接口吧"
于是系统 A 说:"没问题的, 这就搞"
然后, 系统 A 的代码如下:
- public class SystemA {
- // 已经不再需要系统 B 的依赖了
- // SystemB systemB = new SystemB();
- // 系统 C 和系统 D 的依赖
- SystemC systemC = new SystemC();
- SystemD systemD = new SystemD();
- // 系统 A 独有的数据
- private String userId = "Java3y";
- public void doSomething() {
- // 已经不再需要系统 B 的依赖了
- //systemB.SystemBNeed2do(userId);
- // 系统 C 和系统 D 都需要拿着系统 A 的 userId 去操作其他的事
- systemC.SystemCNeed2do(userId);
- systemD.SystemDNeed2do(userId);
- }
- }
时间飞逝:
又过了几天, 系统 E 的负责人过来了, 告诉系统 A, 需要 userId.
又过了几天, 系统 B 的负责人过来了, 告诉系统 A, 还是重新掉那个接口吧.
又过了几天, 系统 F 的负责人过来了, 告诉系统 A, 需要 userId.
......
于是系统 A 的负责人, 每天都被这给骚扰着, 改来改去, 改来改去.......
还有另外一个问题, 调用系统 C 的时候, 如果系统 C 挂了, 系统 A 还得想办法处理. 如果调用系统 D 时, 由于网络延迟, 请求超时了, 那系统 A 是反馈 fail 还是重试??
最后, 系统 A 的负责人, 觉得隔一段时间就改来改去, 没意思, 于是就跑路了.
然后, 公司招来一个大佬, 大佬经过几天熟悉, 上来就说: 将系统 A 的 userId 写到消息队列中, 这样系统 A 就不用经常改动了. 为什么呢? 下面我们来一起看看:
系统 A 将 userId 写到消息队列中, 系统 C 和系统 D 从消息队列中拿数据. 这样有什么好处?
系统 A 只负责把数据写到队列中, 谁想要或不想要这个数据(消息), 系统 A 一点都不关心.
即便现在系统 D 不想要 userId 这个数据了, 系统 B 又突然想要 userId 这个数据了, 都跟系统 A 无关, 系统 A 一点代码都不用改.
系统 D 拿 userId 不再经过系统 A, 而是从消息队列里边拿. 系统 D 即便挂了或者请求超时, 都跟系统 A 无关, 只跟消息队列有关.
这样一来, 系统 A 与系统 B,C,D 都解耦了.
2.2 异步
我们再来看看下面这种情况: 系统 A 还是直接调用系统 B,C,D
代码如下:
- public class SystemA {
- SystemB systemB = new SystemB();
- SystemC systemC = new SystemC();
- SystemD systemD = new SystemD();
- // 系统 A 独有的数据
- private String userId ;
- public void doOrder() {
- // 下订单
- userId = this.order();
- // 如果下单成功, 则安排其他系统做一些事
- systemB.SystemBNeed2do(userId);
- systemC.SystemCNeed2do(userId);
- systemD.SystemDNeed2do(userId);
- }
- }
假设系统 A 运算出 userId 具体的值需要 50ms, 调用系统 B 的接口需要 300ms, 调用系统 C 的接口需要 300ms, 调用系统 D 的接口需要 300ms. 那么这次请求就需要 50+300+300+300=950ms
并且我们得知, 系统 A 做的是主要的业务, 而系统 B,C,D 是非主要的业务. 比如系统 A 处理的是订单下单, 而系统 B 是订单下单成功了, 那发送一条短信告诉具体的用户此订单已成功, 而系统 C 和系统 D 也是处理一些小事而已.
那么此时, 为了提高用户体验和吞吐量, 其实可以异步地调用系统 B,C,D 的接口. 所以, 我们可以弄成是这样的:
系统 A 执行完了以后, 将 userId 写到消息队列中, 然后就直接返回了(至于其他的操作, 则异步处理).
本来整个请求需要用 950ms(同步)
现在将调用其他系统接口异步化, 从请求到返回只需要 100ms(异步)
(例子可能举得不太好, 但我觉得说明到点子上就行了, 见谅.)
2.3 削峰 / 限流
我们再来一个场景, 现在我们每个月要搞一次大促, 大促期间的并发可能会很高的, 比如每秒 3000 个请求. 假设我们现在有两台机器处理请求, 并且每台机器只能每次处理 1000 个请求.
那多出来的 1000 个请求, 可能就把我们整个系统给搞崩了... 所以, 有一种办法, 我们可以写到消息队列中:
系统 B 和系统 C 根据自己的能够处理的请求数去消息队列中拿数据, 这样即便有每秒有 8000 个请求, 那只是把请求放在消息队列中, 去拿消息队列的消息由系统自己去控制, 这样就不会把整个系统给搞崩.
三, 使用消息队列有什么问题?
经过我们上面的场景, 我们已经可以发现, 消息队列能做的事其实还是蛮多的.
说到这里, 我们先回到文章的开头,"明明 JDK 已经有不少的队列实现了, 我们还需要消息队列中间件呢?" 其实很简单, JDK 实现的队列种类虽然有很多种, 但是都是简单的内存队列. 为什么我说 JDK 是简单的内存队列呢? 下面我们来看看要实现消息队列 (中间件) 可能要考虑什么问题.
3.1 高可用
无论是我们使用消息队列来做解耦, 异步还是削峰, 消息队列肯定不能是单机的. 试着想一下, 如果是单机的消息队列, 万一这台机器挂了, 那我们整个系统几乎就是不可用了.
所以, 当我们项目中使用消息队列, 都是得集群 / 分布式的. 要做集群 / 分布式就必然希望该消息队列能够提供现成的支持, 而不是自己写代码手动去实现.
3.2 数据丢失问题
我们将数据写到消息队列上, 系统 B 和 C 还没来得及取消息队列的数据, 就挂掉了. 如果没有做任何的措施, 我们的数据就丢了.
学过 Redis 的都知道, Redis 可以将数据持久化磁盘上, 万一 Redis 挂了, 还能从磁盘从将数据恢复过来. 同样地, 消息队列中的数据也需要存在别的地方, 这样才尽可能减少数据的丢失.
那存在哪呢?
磁盘?
数据库?
Redis?
分布式文件系统?
同步存储还是异步存储?
3.3 消费者怎么得到消息队列的数据?
消费者怎么从消息队列里边得到数据? 有两种办法:
生产者将数据放到消息队列中, 消息队列有数据了, 主动叫消费者去拿(俗称 push)
消费者不断去轮训消息队列, 看看有没有新的数据, 如果有就消费(俗称 pull)
3.4 其他
除了这些, 我们在使用的时候还得考虑各种的问题:
消息重复消费了怎么办啊?
我想保证消息是绝对有顺序的怎么做?
........
虽然消息队列给我们带来了那么多的好处, 但同时我们发现引入消息队列也会提高系统的复杂性. 市面上现在已经有不少消息队列轮子了, 每种消息队列都有自己的特点, 选取哪种 MQ 还得好好斟酌.
最后
本文主要讲解了什么是消息队列, 消息队列可以为我们带来什么好处, 以及一个消息队列可能会涉及到哪些问题. 希望给大家带来一定的帮助.
来源: http://developer.51cto.com/art/201904/595020.htm