关于后端:消息队列如何处理重复消息

11次阅读

共计 3825 个字符,预计需要花费 10 分钟才能阅读完成。

在消息传递过程中,如果呈现传递失败的状况,发送方会执行重试,重试的过程中就有可能会产生反复的音讯。对应用音讯队列的业务零碎来说,如果没有对反复音讯进行解决,就有可能会导致系统的数据呈现谬误。

比如说,一个生产订单音讯,统计下单金额的微服务,如果没有正确处理反复音讯,那就会呈现反复统计,导致统计后果谬误。

你可能会问,如果音讯队列自身能保障音讯不反复,那应用程序的实现不就简略了?那有没有音讯队列能保障音讯不反复呢?

不可避免的音讯反复

在 MQTT 协定中,给出了三种传递音讯时可能提供的服务质量规范,这三种服务质量从低到高顺次是:

  • At most once: 至少一次。音讯在传递时,最多会被送达一次。换一个说法就是,没什么音讯可靠性保障,容许丢音讯。个别都是一些对音讯可靠性要求不太高的监控场景应用,比方每分钟上报一次机房温度数据,能够承受数据大量失落。
  • At least once: 至多一次。音讯在传递时,至多会被送达一次。也就是说,不容许丢音讯,然而容许有大量反复音讯呈现。
  • Exactly once:恰好一次。音讯在传递时,只会被送达一次,不容许失落也不容许反复,这个是最高的等级。

这个服务质量规范不仅实用于 MQTT,对所有的音讯队列都是实用的。咱们当初罕用的绝大部分音讯队列提供的服务质量都是 At least once,包含 RocketMQ、RabbitMQ 和 Kafka 都是这样。也就是说,音讯队列很难保障音讯不反复。

Kafka 反对的“Exactly once”和咱们刚刚提到的消息传递的服务质量规范“Exactly once”是不一样的,它是 Kafka 提供的另外一个个性,Kafka 中反对的事务也和咱们通常意义了解的事务有肯定的差别。在 Kafka 中,事务和 Excactly once 次要是为了配合流计算应用的个性

略微说一些题外话,Kafka 的团队是一个十分长于包装和营销的团队,你看他们很奇妙地用了两个所有人都十分相熟的概念“事务”和“Exactly once”来包装它的新的个性,实际上它实现的这个事务和 Exactly once 并不是咱们通常了解的那两个个性,然而你深刻理解 Kafka 的事务和 Exactly once 后,会发现其实它这个个性尽管和咱们通常的了解不一样,但的确和事务、Exactly once 有肯定关系。

这一点上,咱们都要学习 Kafka 团队。一个优良的开发团队,不仅要能写代码,更要能写文档,能写 Slide(PPT),还要能讲,会分享。对于每个程序员来说,也是一样的。

咱们把话题发出来,持续来说反复音讯的问题。既然音讯队列无奈保障音讯不反复,就须要咱们的生产代码可能承受“音讯是可能会反复的”这一现状,而后,通过一些办法来打消反复音讯对业务的影响。

用幂等性解决反复音讯问题

个别解决反复音讯的方法是,在生产端,让咱们生产音讯的操作具备幂等性。

幂等(Idempotence) 原本是一个数学上的概念,它是这样定义的:

如果一个函数 f(x) 满足:f(f(x)) = f(x),则函数 f(x) 满足幂等性。

这个概念被拓展到计算机领域,被用来形容一个操作、办法或者服务。一个幂等操作的特点是, 其任意屡次执行所产生的影响均与一次执行的影响雷同。

一个幂等的办法,应用同样的参数,对它进行屡次调用和一次调用,对系统产生的影响是一样的。所以,对于幂等的办法,不必放心反复执行会对系统造成任何扭转。

咱们举个例子来阐明一下。在不思考并发的状况下,“将账户 X 的余额设置为 100 元”,执行一次后对系统的影响是,账户 X 的余额变成了 100 元。只有提供的参数 100 元不变,那即便再执行多少次,账户 X 的余额始终都是 100 元,不会变动,这个操作就是一个幂等的操作。

再举一个例子,“将账户 X 的余额加 100 元”,这个操作它就不是幂等的,每执行一次,账户余额就会减少 100 元,执行屡次和执行一次对系统的影响(也就是账户的余额)是不一样的。

如果咱们零碎生产音讯的业务逻辑具备幂等性,那就不必放心音讯反复的问题了,因为同一条音讯,生产一次和生产屡次对系统的影响是齐全一样的。也就能够认为,生产屡次等于生产一次。

从对系统的影响后果来说:At least once + 幂等生产 = Exactly once。

那么如何实现幂等操作呢?最好的形式就是, 从业务逻辑设计上动手,将生产的业务逻辑设计成具备幂等性的操作。 然而,不是所有的业务都能设计成人造幂等的,这里就须要一些办法和技巧来实现幂等。

上面我给你介绍几种罕用的设计幂等操作的办法:

1. 利用数据库的惟一束缚实现幂等

例如咱们刚刚提到的那个不具备幂等个性的转账的例子:将账户 X 的余额加 100 元。在这个例子中,咱们能够通过革新业务逻辑,让它具备幂等性。

首先,咱们能够限定,对于每个转账单每个账户只能够执行一次变更操作,在分布式系统中,这个限度实现的办法十分多,最简略的是咱们在数据库中建一张转账流水表,这个表有三个字段:转账单 ID、账户 ID 和变更金额,而后给转账单 ID 和账户 ID 这两个字段联结起来创立一个惟一束缚,这样对于雷同的转账单 ID 和账户 ID,表里至少只能存在一条记录。

这样,咱们生产音讯的逻辑能够变为:“在转账流水表中减少一条转账记录,而后再依据转账记录,异步操作更新用户余额即可。”在转账流水表减少一条转账记录这个操作中,因为咱们在这个表中事后定义了“账户 ID 转账单 ID”的惟一束缚,对于同一个转账单同一个账户只能插入一条记录,后续反复的插入操作都会失败,这样就实现了一个幂等的操作。咱们只有写一个 SQL,正确地实现它就能够了。

基于这个思路,不光是能够应用关系型数据库,只有是反对相似“INSERT IF NOT EXIST”语义的存储类零碎都能够用于实现幂等,比方,你能够用 Redis 的 SETNX 命令来代替数据库中的惟一束缚,来实现幂等生产。

2. 为更新的数据设置前置条件

另外一种实现幂等的思路是,给数据变更设置一个前置条件,如果满足条件就更新数据,否则回绝更新数据,在更新数据的时候,同时变更前置条件中须要判断的数据。这样,反复执行这个操作时,因为第一次更新数据的时候曾经变更了前置条件中须要判断的数据,不满足前置条件,则不会反复执行更新数据操作。

比方,刚刚咱们说过,“将账户 X 的余额减少 100 元”这个操作并不满足幂等性,咱们能够把这个操作加上一个前置条件,变为:“如果账户 X 以后的余额为 500 元,将余额加 100 元”,这个操作就具备了幂等性。对应到音讯队列中的应用时,能够在发消息时在音讯体中带上以后的余额,在生产的时候进行判断数据库中,以后余额是否与音讯中的余额相等,只有相等才执行变更操作。

然而,如果咱们要更新的数据不是数值,或者咱们要做一个比较复杂的更新操作怎么办?用什么作为前置判断条件呢?更加通用的办法是,给你的数据减少一个版本号属性,每次更数据前,比拟以后数据的版本号是否和音讯中的版本号统一,如果不统一就回绝更新数据,更新数据的同时将版本号 +1,一样能够实现幂等更新。

3. 记录并查看操作

如果下面提到的两种实现幂等办法都不能实用于你的场景,咱们还有一种通用性最强,适用范围最广的实现幂等性办法:记录并查看操作,也称为“Token 机制或者 GUID(全局惟一 ID)机制”,实现的思路特地简略:在执行数据更新操作之前,先检查一下是否执行过这个更新操作。

具体的实现办法是,在发送音讯时,给每条音讯指定一个全局惟一的 ID,生产时,先依据这个 ID 查看这条音讯是否有被生产过,如果没有生产过,才更新数据,而后将生产状态置为已生产。

原理和实现是不是很简略?其实一点儿都不简略,在分布式系统中,这个办法其实是十分难实现的。首先,给每个音讯指定一个全局惟一的 ID 就是一件不那么简略的事儿,办法有很多,但都不太好同时满足简略、高可用和高性能,或多或少都要有些就义。更加麻烦的是,在“查看生产状态,而后更新数据并且设置生产状态”中,三个操作必须作为一组操作保障原子性,能力真正实现幂等,否则就会呈现 Bug。

比如说,对于同一条音讯:“全局 ID 为 8,操作为:给 ID 为 666 账户减少 100 元”,有可能呈现这样的状况:

  • t0 时刻:Consumer A 收到条音讯,查看音讯执行状态,发现音讯未解决过,开始执行“账户减少 100 元”;
  • t1 时刻:Consumer B 收到条音讯,查看音讯执行状态,发现音讯未解决过,因为这个时刻,Consumer A 还未来得及更新音讯执行状态。

这样就会导致账户被谬误地减少了两次 100 元,这是一个在分布式系统中非常容易犯的谬误,肯定要引以为戒。

对于这个问题,当然咱们能够用事务来实现,也能够用锁来实现,然而在分布式系统中,无论是分布式事务还是分布式锁都是比拟难解决问题。

总结

本文次要介绍了通过幂等生产来解决音讯反复的问题,而后形容几种实现幂等操作的办法,能够利用数据库的束缚来避免反复更新数据,也能够为数据更新设置一次性的前置条件,来避免反复音讯,如果这两种办法都不适用于你的场景,还能够用“记录并查看操作”的形式来保障幂等,这种办法适用范围最广,然而实现难度和复杂度也比拟高,个别不举荐应用。

这些实现幂等的办法,不仅能够用于解决反复音讯的问题,也同样实用于,在其余场景中来解决反复申请或者反复调用的问题。比方,咱们能够将 HTTP 服务设计成幂等的,解决前端或者 APP 反复提交表单数据的问题;也能够将一个微服务设计成幂等的,解决 RPC 框架主动重试导致的反复调用问题。

本文由 mdnice 多平台公布

正文完
 0