17如何保证消息仅仅被消费一次(消息丢失+消息重复消费)

480 阅读3分钟

1.基本概念

使用消息队列

  • 对峰值流量削峰填谷
  • 对次要业务逻辑业务处理
  • 对不同模块做解耦合

image.png

消息队列引入的新问题:

  • 消息丢失
  • 消息重复

2.消息为什么会丢失

如果要保证消息只被消费一次,首先就要保证消息不会丢失。

可能丢失消息的场景:

  • 消息从生产者写入到消息队列的过程
  • 消息在消息队列中的存储场景
  • 消息被消费者消费的过程

image.png

2.1生产环节丢失

  • 问题:消息队列在独立的机器上,消息可能因为网络抖动导致消息丢失
    • 解决办法:每次消息发送失败后,消息重传(重试次数有限)
    • 缺点:可能造成消息重复

2.2消息队列的丢失(以kafka为例)

  • kafka:消息存储在磁盘上,为了避免不停的对磁盘随机IO,将消息写到操作系统的 Page Cache 中,然后再找合适的时机刷新到磁盘上。
  • 异步刷盘:时间达到阈值+累计一定的消息后,统一将内存中消息刷入磁盘
  • 问题:机器掉电或者异常重启,到此内存中的消息丢失
  • 解决办法
    • 将刷盘时间设置很短,每条消息都刷盘(不建议)
    • 考虑使用集群方式部署kafka服务,通过部署多个备份数据保证消息不丢失
      • Leader 负责消息的写入和消费
      • 多个 Follower 负责数据的备份
      • 其中Follower有一个ISR集合,主库异常后(从ISR从选举一个机器)
      • 仍然存在异步复制到Follower消息丢失
      • acks=all 当主库和所有ISR确认成功后才算成功
    • 集群方案>同步刷盘
    • 对于消息丢失有一定容忍度,可以不设置acks=all 
    • 消息丢失后,业务保证就好(log)(业务场景对消息丢失要求严格)

image.png

2.3消费者丢失

  • 消费者消费过程
    • 接收消息
    • 处理消息
    • 更新消费进度
      • 一定要等到消息结束后更新消费进度

3.如何保证消息只被消费一次

  • 避免消息丢失
    • 性能消耗
    • 消息重复消费
  • 即使消息重复消费需要保证消费是幂等的

3.1什么是幂等

多次执行同一个操作和执行一次操作,最终得到的结果是相同的(无状态的)

一件事儿无论做多少次都和做一次产生的结果是一样的,那么这件事儿就具有幂等性

3.2生产者消息幂等

原因:超时重传导致消息重复

解决:Kafka0.11 版本和 Pulsar 中都支持“producer idempotency”的特性,翻译过来就是生产过程的幂等性

实现原理:

  • 给每一个生产者一个唯一的 ID,并且为生产的每一条消息赋予一个唯一 ID,消息队列的服务端会存储 < 生产者 ID,最后一条消息 ID> 的映射
  • 某一个生产者产生新的消息时,消息队列服务端会比对消息 ID 是否与存储的最后一条 ID 一致,如果一致就认为是重复的消息,服务端会自动丢弃。

image.png

3.3消费者消息幂等

  • 通用层(AOP)
    • 为每条消息生成一个全局唯一ID
    • 每次处理消息前,判断消息是否被处理过
    • 幂等性由数据库保证(更严格的情况下需要使用事务)
  • 业务层
    • 可以增加乐观锁
    • 账号数据中增加一个版本号的字段,更新条件带上版本号
    • 由于重复消息版本号都是一致的,所以不会重复消费

4.总结

  • 消息丢失
    • 生产者重试
    • 消息队列集群配置
    • 更新消息状态在最后
  • 解决消息丢失引入消息重复性
  • 需要消费者保证消费的幂等性

方案设计看场景,这是一切设计的原则(因地制宜)