消息队列—高并发场景微服务实战(十二)

你好,我是程序员Alan,很高兴遇见你。

在前面的几篇文章我们使用Nacos实现了分布式系统的注册中心和配置中心,使用Feigns实现了远程服务调用,今天我们讲一下分布式系统的另一个重要模块—消息队列。

什么是消息队列

分布式架构中经常提到的消息队列,可以简单理解为一种传递消息的容器。消息的传递和接收者,我们称之为生产者和消费者,生产者和消费者可以集群部署,也可以只有一个实例。

消息队列—高并发场景微服务实战(十二)_第1张图片

DATA是生产和消费方传递的数据,数据格式可以是简单的字符串也可以是序列化后的复杂信息。队列是消息的载体,用于传输和保存消息,它和拥有数据结构中队列的特性,例如先进先出、优先级队列等等。

消息队列的应用场景

系统解耦

架构设计模式中有一个开闭原则,规定软件的实体应该对于扩展是开放的,但是对于修改是封闭的”,尽量保持系统之间的独立性,这里面蕴含的就是解耦思想。系统架构中引入消息队列,消费者和生产者地操作是完全独立地,能够很方便地对业务进行解耦,调用方只需要发送消息而不用关注下游逻辑如何执行。

消息队列—高并发场景微服务实战(十二)_第2张图片

异步处理

使用消息队列可以将请求异步化发送。异步处理地典型场景是流量削峰。那我们机票订购服务来说,节假日旅游旺季的流量峰值是很高的,如果我们的系统不能承载这么高的瞬间流量,就可以使用消息队列结合限流工具,设置系统阈值,将超过阈值的请求暂存在消息队列中,等待流量高峰过去再进行处理。

数据分发

消息队列有很多种订阅模式,例如发布/订阅消息队列,可以用来实现数据的分发。例如MySQL数据库对binlog订阅的处理,由于主库的binlog只有一份,但是下游的消费方可能有多个存放不同数据的库,这个时候就可以应用消息队列来实现数据的分发。

消息队列的消费模型

RabbitMQ , RocketMQ , Kafka 等常见的消息队列的消费模式都不太一样。本文我重点讲一下消息队列的两种基础模型,点对点和发布/订阅模式。

点对点模型

在点对点模型下,生产者向一个特定的消息队列发送消息,消费者再从这个消息队列中读取消息,没条消息只会被一个消费者处理。

消息队列—高并发场景微服务实战(十二)_第3张图片

发布/订阅模型

发布者将消息发送到主题,消息队列再接收到消息后发送给订阅者。如果多个消费者订阅了同一个主题,那么一条消费就会被多个消费者都消费一次。

消息队列—高并发场景微服务实战(十二)_第4张图片

顺序消费的难点

虽然队列是一个有序的数据结构,消息传递时顺序的,但是在实际的分布式场景下,消息的有序性是很难保证的,主要有以下几个困难:

  • 分布式的时钟问题

在分布式系统中,生产者、消费者、消息队列,可能分布在不同的机器上,不同的机器由于设备硬件不同,本身存在偏差,一天的误差可能有毫秒甚至秒级。由于时间会出现不一致,所以消费者和生产者就不能以消息发送和到达的时间戳作为时序判断标准。

  • 生产者和消费者集群问题

由于时间很难同步,所以生产者如果发送多个实例,那么消费者就无法通过生产者的发送时许来作为消息发送的有序判断。同时,在多个消费者的情况下即使队列内部是有序的,消息在分发过程不同消费者的顺序也难以全局统一,所以也无法实现绝对的顺序消费。

  • 消息重传等情况的影响

由于消息传输过程中可能会出现网络抖动等异常问题导致的发送失败,对于这种问题消息队列一般都会合理地重传。重传发生的时间是不可预知的,这也会导致消息的乱序。

顺序消费是否必要

从上文的介绍我们可以发现分布式系统消息队列本身很难保证绝对的顺序消费。既然这个问题很难解决没法直接正面的解决它,我们是否可以考虑绕过这个问题。技术最终还是服务于业务的,思考一下在你的业务中,是否需要绝对的顺序消费。

比如我们机票订购服务的订单状态消息通知业务,订单有创建成功、待支付、已付款、出票成功,这么几个状态,虽然他们的关系是递进的需要保证有序性,但我们只是要根据订单的状态通知客户,这个场景下我们可以忽略订单状态的顺序,甚至只关注是否出票成功。

也就是说虽然订单状态的流转需要考虑顺序,但是在这个具体的功能下,是可以不考虑消息时序的。

业务中如何实现有序消费

上面我们讨论了根据实际业务功能考虑顺序消费是否必要,现在我再讲一下业务处理中几种常见的实现有序的方式。注意这里说的有序也并不是绝对的有序。

  • 根据不同的业务场景,以发送端或者消费端时间戳为准

在机票秒杀的场景中,如果钥对秒杀的请求进行排队,就可以使用秒杀提交的服务端的时间戳,虽然不能保证所有节点的服务端时钟一致,但是在这个场景下我们不需要保证绝对的有序。

  • 每次消息发送时生成唯一递增的ID

消息有序的落到一个实例实现有序是简单的,但是分布式架构下落到不同的实例就很难保证有序了,如果非要保证有序,在每次写入消息时,可以考虑添加一个单调递增的序列ID,消费者进行消费时记录最大的序列ID,只消费超过当前最大序列ID的消息。

  • 通过缓存时间戳的方式

这种方式的机制和上面递增ID是一致的,即当生产者发送消息时,添加一个时间戳,消费端在处理消息时,通过缓存时间戳的方式,判断消息产生的时间是否最新,如果不是则丢弃。

消息幂等的理解

上面我们讨论完了有序消费难点,有序消费的必要和业务中有序消费的实现,最后我们再来考虑一下幂等。

幂等最早是数学上的一个概念,幂等函数指一个函数或方法,使用相同的参数执行多次,数据结果是一致的。

在CRUD的时候,Create操作很明显不是幂等的、Get操作是幂等的,Update操作可能幂等也可能不幂等。

  • Create操作不幂等很好理解,创建操作必然会影响资源本身。
  • 相反的Get操作,不会影响资源本身,虽然同一个查询条件可能会因为数据被修改而查询出不同的结果,但这不影响它是一个幂等操作。
  • 为什么说Update操作可能幂等,也可能不幂等,我们可以看下面两种情况

这个操作是幂等的

UPDATED order SET user = 'alan' WHERE id = 'air-999';

这个操作就不符合幂等性的要求

UPDATED order SET count = count+1 WHERE id = 'air-999';

业务上如何处理幂等

因为存在网络抖动等异常问题的存在,远程服务调用和消息消费的重试都会带来幂等问题,所以解决幂等问题的能力很重要。要保证消费幂等,最后还是要回到业务中来分析,结合具体的情况设计方案解决,这里我给出几个常用的处理方法。

  • 设计天然幂等业务

这点可以参考上面对CRUD操作的分析,实现业务时考虑可否天然幂等。允许重复调用和重试。

  • 利用数据库进行去重

以日志管理举例,在Log表中写入一条日志,我们可以把业务ID和修改时间戳做一个唯一索引进行约束。

  • 设置全局唯一ID

在消息投递时,给每条业务消息附加一个唯一的消息ID,然后消费端就可以通过这个ID去实现唯一性的消费。

消息投递的几种语义

业界有许多消息队列的应用协议规范消息的调用,其中也对消息投递标准做了一些约束。

  • At most once

消息在传递时,最多会被送达一次,在这种场景下,消息可能会丢,但绝不会重复传输,一般用于对消息可靠性没有太高要求的场景,比如一些允许数据丢失的日志报表、监控信息等。

  • At least once

消息在传递时,至少会被送达一次,在这种情况下,消息绝不会丢,但可能会出现重复传输。

绝大多数应用中,都是使用至少投递一次这种方式,同时,大部分消息队列都支持到这个级别,应用最广泛。

  • Exactly once

每条消息肯定会被传输一次且仅传输一次,并且保证送达,因为涉及发送端和生产端的各种协同机制,绝对的 Exactly once 级别是很难实现的,通用的 Exactly once 方案几乎不可能存在,可以参考分布式系统的「FLP 不可能定理」。

说点题外话

To a man with a hammer, everything looks like a nail. 拿着锤子的人看什么都像钉子

到本章节为止,《高并发场景微服务实战》我已经写了十二章了,展开讲了许多微服务架构中常用组件。提到这句谚语是希望你在设计架构方案的时候,不是为了设计而设计引入各种高大上的中间件,技术自嗨。方案的设计目的是实现业务目标。

系统中的元素越多,为了维持系统的平衡,需要付出的势能必然也越大。系统拆解的粒度越大,各个组件之间的耦合越小,但是解决的组件间协同问题也就越多。在系统设计时,要避免过度设计,把握技术方案的核心目的,在这个基础上进行针对性设计。

站在巨人的肩膀上

  • 邴越—分布式技术原理与实战45讲

你可能感兴趣的:(高并发场景微服务,java,数据库,开发语言)