一、常见的使用场景
1.异步处理
- 可以更快地返回结果;
- 减少等待,自然实现了步骤之间的并发,提升系统总体的性能。
2.流量控制
使用消息队列隔离网关和后端服务,以达到流量控制和保护后端服务的目的。
- 网关在收到请求后,将请求放入请求消息队列;
- 后端服务从请求消息队列中获取 APP 请求,完成后续处理过程,然后返回结果。
这种设计的优点是:能根据下游的处理能力自动调节流量,达到“削峰填谷”的作用。
缺点是:增加了系统调用链环节,导致总体的响应时延变长。
上下游系统都要将同步调用改为异步消息,增加了系统的复杂度。
还可以用消息队列实现一个令牌桶,更简单地进行流量控制。
令牌桶控制流量的原理是:单位时间内只发放固定数量的令牌到令牌桶中,规定服务在处理请求之前必须先从令牌桶中拿出一个令牌,如果令牌桶中没有令牌,则拒绝请求。这样就保证单位时间内,能处理的请求不超过发放令牌的数量,起到了流量控制的作用。
实现的方式也很简单,不需要破坏原有的调用链,只要网关在处理 APP 请求时增加一个获取令牌的逻辑。
令牌桶可以简单地用一个有固定容量的消息队列加一个“令牌发生器”来实现:令牌发生器按照预估的处理能力,匀速生产令牌并放入令牌队列(如果队列满了则丢弃令牌),网关在收到请求时去令牌队列消费一个令牌,获取到令牌则继续调用后端服务,如果获取不到令牌则直接返回失败。
3.服务解耦
所有的电商都选择用消息队列来解决类似的系统耦合过于紧密的问题。引入消息队列后,订单服务在订单变化时发送一条消息到消息队列的一个主题 Order 中,所有下游系统都订阅主题 Order,这样每个下游系统都可以获得一份实时完整的订单数据。
无论增加、减少下游系统或是下游系统需求如何变化,订单服务都无需做任何更改,实现了订单服务与下游服务的解耦。
4.其他
作为发布 / 订阅系统实现一个微服务级系统间的观察者模式;
连接流计算任务和数据;
用于将消息广播给大量接收者。
二、消息队列的选择
- rabbitmq:
优点:轻量,迅捷,容易部署和使用,拥有灵活的路由配置,它在生产者(Producer)和队列(Queue)之间增加了一个 Exchange 模块
缺点:性能和吞吐量较差,不易进行二次开发 - rocketmq:
优点:性能好,稳定可靠,有活跃的中文社区,延迟低
缺点:兼容性较差,生态少 - kafka:
优点:拥有强大的性能及吞吐量,兼容性很好
缺点:由于“攒一波再处理”导致延迟比较高
如果说,消息队列并不是你将要构建系统的主角之一,你对消息队列功能和性能都没有很高的要求,只需要一个开箱即用易于维护的产品,建议使用 RabbitMQ。
如果你的系统使用消息队列主要场景是处理在线业务,比如在交易系统中用消息队列传递订单,那 RocketMQ 的低延迟和金融级的稳定性是你需要的。
如果你需要处理海量的消息,像收集日志、监控信息或是前端的埋点这类数据,或是你的应用场景大量使用了大数据、流计算相关的开源产品,那 Kafka 是最适合你的消息队列。
三、事务消息
消息队列中的事务,主要解决的是消息生产者和消息消费者的数据一致性问题。
订单系统创建订单后,发消息给购物车系统,将已下单的商品从购物车中删除。
创建订单和发送消息这两个步骤要么都操作成功,要么都操作失败,不允许一个成功而另一个失败的情况出现。
对于分布式系统来说,严格的实现 ACID 这四个特性几乎是不可能的,或者说实现的代价太大。
事务消息适用的场景主要是那些需要异步更新数据,并且对数据实时性要求不太高的场景。
事务消息需要消息队列提供相应的功能才能实现,Kafka 和 RocketMQ 都提供了事务相关功能。
- 首先,订单系统在消息队列上开启一个事务。然后订单系统给消息服务器发送一个“半消息”,这个半消息不是说消息内容不完整,它包含的内容就是完整的消息内容,半消息和普通消息的唯一区别是,在事务提交之前,对于消费者来说,这个消息是不可见的。
- 半消息发送成功后,订单系统就可以执行本地事务了,在订单库中创建一条订单记录,并提交订单库的数据库事务。然后根据本地事务的执行结果决定提交或者回滚事务消息。如果订单创建成功,那就提交事务消息,购物车系统就可以消费到这条消息继续后续的流程。如果订单创建失败,那就回滚事务消息,购物车系统就不会收到这条消息。这样就基本实现了“要么都成功,要么都失败”的一致性要求。
如果提交事务消息时失败了怎么办?对于这个问题,Kafka 和 RocketMQ 给出了 2 种不同的解决方案。
- Kafka 的解决方案比较简单粗暴,直接抛出异常,让用户自行处理。我们可以在业务代码中反复重试提交,直到提交成功,或者删除之前创建的订单进行补偿。
- 在 RocketMQ 中的事务实现中,增加了事务反查的机制来解决事务消息提交失败的问题。如果 Producer 也就是订单系统,在提交或者回滚事务消息时发生网络异常,RocketMQ 的 Broker 没有收到提交或者回滚的请求,Broker 会定期去 Producer 上反查这个事务对应的本地事务的状态,然后根据反查结果决定提交或者回滚这个事务。
为了支撑这个事务反查机制,我们的业务代码需要实现一个反查本地事务状态的接口,告知 RocketMQ 本地事务是成功还是失败。
一定是先提交本地事务,再提交消息,以免出现消息发送了但本地事务失败,撤不回消息,产生数据不一致。
思考题
1、在消费的时候,为了保证消息的不丢失和严格顺序,每个队列只能串行消费,无法做到并发,否则会出现消费空洞的问题。那如果放宽一下限制,不要求严格顺序,能否做到单个队列的并行消费呢?如果可以,该如何实现?
答:并行消费时,如果靠前的消息失败,可以单独放到重试队列,offset 仍然放到最后的消费位置。让重试队列的消息优先被消费。
2、RocketMQ 的这种事务消息是不是完整地实现了事务的 ACID 四个特性?如果不是,哪些特性没有实现?
答:
A:本地事物的操作1,与往消息队列中生产消息的操作2,是两个分离的操作,不符合对原子性的定义;
C:由于操作消息队列属于异步操作,在数据一致性上,只能保证数据的最终一致性。若对于时效性要求很高的系统来说,事物消息不是数据一致的;但对于时效性要求不高的系统来说,他就是数据一致的。我认为,用不同的业务视角来看问题,会有不同的答案;
I:隔离性上,由于事物消息是分两步操作的,本地事物提交后,别的事物消息就已经可以看到提交的消息了。所以,不符合隔离性的定义;
D:持久性上,rocketMq上支持事物的反查机制,但我不太清楚“半消息”是存储在磁盘中,还是内存里。若存储在磁盘中,那就支持持久性,即使事物消息提交后,发生服务突然宕机也不受影响;若存储在内存中,则无法保证持久性。
参考资料:李玥——消息队列高手课