消息乱序,重复,幂等性

语音遥控器和智能台灯项目复盘

Posted by Liangjf on September 10, 2020

语音遥控器和智能台灯项目复盘

遇到的问题

涉及问题: 消息乱序, 消息不重复消费, 接口幂等性

项目背景

对于这两个项目, 用户控制命令不能是重复响应, 而且控制命令不能是乱序的?

但是推送系统的消息队列使用的是nsq, 为什么用nsq? 因为架构简单分布式, 配置和运维简单, 而且大多的业务都是和顺序无关的.

想象一下, 开灯和关灯两个控制命令, 在终端的控制顺序是开灯在前, 关灯在后, 即最终表现状态是关灯; 而通过消息推送, 后端接收到的消息顺序是关灯在前, 开灯在后, 即最终表现状态为开灯. 和用户的意图完全违背了

所以对于消息需要满足有序. 那么再来看看是消息否需要严格的有序.

在分析之前, 先交代需求背景, 对于产品的命令响应时间不需要瞬间响应, 可以有一定的延迟(比如300ms), 仔细想想, 开个灯, 稍微延迟个几百毫秒, 其实用户是无感的. 所以正因为有这个可延时的特点给了我们操作的空间.

看以下场景:

用户关灯(A)->开灯(B)->关灯(C)->开灯(D), 因为后端设定300ms内的消息设为一批消息. 可能出现以下24种组合:

  • 关灯(A)->开灯(B)->关灯(C)->开灯(D)
  • 关灯(A)->关灯(C)->开灯(B)->开灯(D)
  • 关灯(A)->关灯(C)->开灯(D)->开灯(B)
  • 关灯(A)->开灯(D)->开灯(B)->关灯(C)

后端的对于用户命令的表现可以有2种:

  • 1.根据这批消息的最终表现状态来响应
  • 2.必须走一遍这批消息还原有序后的每个状态

消息乱序解决方案

第一种方案

给每一条消息打上时间戳, 对设定时间范围内的消息排序, 直接响应时间戳最大的消息(最终的消息), 并记录下当前批次最大的时间戳, 用于和后面批次的消息比较(因为时间批次的划分, 时间戳更小的消息可能在后面单独请求过来, 所以对比时间戳旧可以不响应那个消息了)

优点:

  • 1.实现简单
  • 2.可以验证后端响应终端消息的最终一致性

缺点:

  • 1.不能连续看到每个控制消息的表现状态(300ms的表现状态真的那么重要吗?个人觉得没那么重要, 而且如开关灯这种即使软件层面不限制频率, 固件层也会限制的)

第二种方案

和第一种方案一致, 只是修改为后端对排序好的消息列表的每个表现状态都响应. (个人认为, 如果不是产品的强制要求, 没必要这样实现)

优点:

1.响应了每个控制消息的表现状态, 可能硬件表现出来的状态和用户的操作是完全一致的

缺点:

1.也不算缺点, 就是对于短时间内的状态表现完全有序模拟出来感觉是不必要的

最后经过和产品的沟通, 并且对比竞品, 其实都是有一个延时差的, 因为此类智能硬件, 如果是联网, 肯定是和后端服务在通信的, 这里本身就有一个消息延时差,而且由于人的视觉暂留问题, 操作时间(谁会无聊到一直开灯关灯, 调节亮度等操作)等原因, 最终选择了第一种方案.

以上的方案解决了消息乱序的问题, 还有一个消息重复幂等性的问题.

消息重复和幂等性

因为引入了推送系统, 消息队列. 由于其为了保证at least one, 会有重试机制, 因此后端收到的消息有可能会有重复的.

解决方法

为每个消息经过消息系统时标上自增id。保存消费的消息的消息id, 消息来了先判断是否已消费过. 若已消费的就丢失不处理。有两种需求,一个是永久唯一性,一个是一段时间唯一性。我们的业务其实一段时间唯一性就足够了。因此使用了redis来缓存一段时间的消息的消息id,但是”一段时间”怎么个定义? 我是设置了10分钟, 正常来说, 消息的重复性时间间隔不会这么长的(除非后端出现问题, 造成消息积压在消息队列未消费, 并且redis的消息过期删除了, 会出现消息重复发送的问题)

但是后端是水平拓展实例的, 并且处理的业务比较轻, 出现异常的几率较低. 即使真的出现异常, 消息重复消费, 还有一个兜底, 判断消息到来的时间戳和消息的时间戳的差值, 如果大于5~10分钟, 就忽略不处理。

以上就是在处理语音项目, 智能台灯项目对于消息乱序, 消息重复, 幂等性等处理.