Java架构直通车——MQ 生产端可靠性投递和消费端幂等性保障方案

什么是生产端的可靠性投递

生产端要做到可靠性投递,需要以下几点:

  1. 保障消息的成功发出
  2. 保障MQ节点的成功接收
  3. 发送端收到MQ节点(Broker)的确认应答
  4. 完善消息的补偿机制:也就是上面三个步骤失败了的补偿机制。

生产端可靠性投递的解决方案:


  • 消息落库,对消息状态进行打标

在消息发送的时候,把消息持久化到数据库中,然后消息有个状态:比如说,刚发送的时候可能说消息叫做发送中,收到回送响应后把消息状态进行变更,比如说打标为已发送。
那么对于没有响应的消息,可以进行轮询操作,抓取没有成功的消息进行重新发送,做一个最大努力尝试的次数即可。

在这里插入图片描述

  • step1:进行数据的入库,包括对业务的入库和对消息的入库,比如说订单服务,包括对订单业务数据的存储(保存订单消息或者创建订单)以及对订单消息的存储。如果这两个持久化操作抛出异常,快速失败即可。
  • step2:producer发送消息给broker。
  • step3:broker应答结果给producer。Confirm Listener异步的去监听Broker回送的这条响应。
  • step4:更新数据库的消息状态,表示确认消息发送成功。
  • step5:使用分布式定时任务去抓取数据库中不成功的消息(一般来讲使用分布式的任务,防止重复抓取)。
  • step6:消息重发,设置重发次数。
  • step7:消息重发次数过多,表明消息投递失败。这时候可以给出一个补偿系统去获取这些消息失败的原因(当然也可以人工去操作)。

这种方式有个缺点:
在数据持久化的时候, 需要经过两次磁盘写操作(业务消息入库和消息信息入库)。并且后续需要update。(一般来说不需要事务,事务会造成严重的性能瓶颈)。在高并发场景下,这种解决方案不合适。


  • 消息的延迟投递,做消息的二次确认、回调检查

这种方式是比较好的可靠性投递机制。这种方式主要是为了减少数据库持久化的操作

在这里插入图片描述

  • step1:upstream service也就是上游服务(生产端),首先会将业务数据持久化(一般来说不加事务),落库完了之后把消息发到broker。
  • step2:第二条消息是延迟消息投递检查,设置一个消息发送的延迟时间,比如设置5min,延迟5min后发送消息。这里的延迟消息虽然和第一次放松的消息在业务上说的是一个事情,但是投递的队列不是一个队列。
  • step3:downstream service也就是下游服务(消费端),收到消息并处理。
  • step4:下游服务消息处理完成之后,向broker发送一个confirm的消息,这个confirm消息也是一个消息队列。也就是处理成功的消息会被编辑成为一个新的消息投递到broker。
  • step5:callback service也就是回调服务充当了之前的Message DB的角色,回调服务去监听下游服务发送的消息,然后回调服务做消息的持久化服务。
  • step 6:回调服务监听延迟消息的队列,5min后延迟消息发送到该队列,回调服务监听到了该消息,并在Message DB数据库中进行检查,发现DB里收到了这个数据,不需要组任何事情。如果发现没有查到这个message,那么进行一个RPC消息,通知上游再次发送这个消息。

这里是异步做了消息的持久化操作,这个回调服务就相当于一个补偿服务,在主流程中减少了一次数据持久化操作,由回调服务实现。

消费端幂等性保障

在海量的订单产生的业务高峰期,如何避免消息的重复消费问题呢?
消费端实现幂等性,就意味着我们的消息永远不会消费多次,即使收到了多条一样的消息。

业内主要的幂等性实现方案:

  1. 唯一ID+指纹码 机制,利用数据库主键去重

生成了唯一ID还不够(一般是用户ID),非得加上指纹码的原因在于,为了应对用户在一瞬间的频繁操作,这个指纹码可能是我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性。
然后就利用查询语句进行判断这个id+指纹码是否存在数据库中,比如使用select count(1) from t_order where id= 唯一ID+指纹码。如果count(1)结果是0,说明还没有被消费过;否则已经被消费过了。

这种方式的好处是:容易实现。
坏处是:高并发下有数据库写入的性能瓶颈
解决方案是为了应对数据库写入的性能瓶颈,可以跟进ID进行分库分表的算法路由。

  1. 利用Redis原子性实现

Redis实现幂等性需要考虑以下问题:
第一:是否要进行数据落库,如果进行落库的化,如何做到数据库和Redis缓存之间的原子性?
比如可能出现下面的场景,第一次消息过来的时候,Redis写入成功,在数据库上写入失败了;第二次消息过来的时候Redis显示已经处理过,但是数据库写入不成功。
Redis和数据库加事务不能解决上面的问题,因为Redis和数据库不是一个数据源,不可能在一个事务里,没有办法做到原子性。

第二:如果不进行落库,都存储在缓存中,怎么去设置定时同步的策略呢?
因为消息处理结果不可能一直都放在缓存里,肯定是要把数据持久化的,如何去设置这个定时同步策略呢?
比如还会有下面的场景出现,在同步之前缓存挂掉了,怎么来做到这个缓存的可靠性保障呢?

这些都是需要Redis来考虑的关键问题,以后会详细补充。

发布了392 篇原创文章 · 获赞 326 · 访问量 16万+

猜你喜欢

转载自blog.csdn.net/No_Game_No_Life_/article/details/104479642