RabbitMq实现数据过期提醒并且保证数据不丢失

目录

 一、业务场景

 二、技术选型

 1、常见的解决方案

 2、选择/丢弃原因

 三、实现方案

1、RabbitMq机制​编辑

2、防止消息丢失

3、实现过程 

4、补偿机制

5、数据一致性

四、结语


 一、业务场景

  • 在页面上用户会创建有开始时间和结束时间的数据,并且在快到结束时间的前3个月要提醒用户数据即将过期,然后在到了结束日期的时候要提醒用户数据已经过期,并且在未过期期间可以对此条数据的结束日期进行修改。

 二、技术选型

 1、常见的解决方案

  • 定时任务
  • RabbitMq死信队列
  • Redisson延迟队列
  • RocketMq延迟队列
  • RabbitMq延迟队列插件
  • 时间轮

 2、选择/丢弃原因

  • 定时任务具有很严重的时效性。例如当前设置的结束时间为一年后过期那么在设置定时任务的执行规则时,如果设置的过长(一个月执行一次)会导致当又创建一条新数据的结束时间为一天后的话,那么这条数据在一个月后才会提示过期。如果设置的过小(一小时执行一次),那么这条一年后才过期的数据在一年内每隔一小时就执行一次频繁的扫描数据表造成无用的资源消耗,所以pass掉定时任务方案。
  • 死信队列由于队列数据结构的特性先进先出。当创建了一条数据结束时间为一年后,又创建了一条结束时间为一个月后过期的数据时,当给两条mq消息设置单条过期时间后,一个月的先过期,但是由于在它之前创建的一年的期限的数据没有过期,所以不会主动发送到死信队列中,导致数据到时间没有过期的情况,也pass掉。RabbitMq实现数据过期提醒并且保证数据不丢失_第1张图片
  • Redisson延迟队列完全基于内存,由于公司Redis集群环境老崩溃(项目组运维能力问题,硬伤无法解决),所以pass。
  • RocketMq的延迟队列不支持自定义设置过期时间,只支持18个过期级别最大为2h,对于现在的业务场景不满足所以pass。
  • RabbitMq延迟队列是在3.6.x版本后支持的插件,可以设置自定义的TTL,但是实现方式对于普通的交换机存在差异,延迟队列优先放入mnesia表中,当快到设置的过期时间时在发送到队列中,本次采取的也是这一种方式实现过期提醒。

RabbitMq实现数据过期提醒并且保证数据不丢失_第2张图片

  • 时间轮算法,定义一个环状的slot队列,通过移动指针找到对应slot中的数据进行消费处理,具有很好的时效性可以控制精度,但是代码实现难度较大,对于本次任务时间紧,所以选择pass,以后有时间在尝试实现此方案。有兴趣的朋友也可以,深究一下。

 三、实现方案

1、RabbitMq机制RabbitMq实现数据过期提醒并且保证数据不丢失_第3张图片

当我们大概熟悉了消息传递的机制的时候可以分析出,出现消息丢失的阶段有:发送消息到交换机交换机到队列消费者捞取数据消费阶段。

2、防止消息丢失

刚才已经大概说了一下,可能会出现消息丢失的情况,下面再逐一分析。

  1. 发送消息到交换机:出现这种情况的原因可能是当程序发送消息到交换机时,此时RabbitMq出现异常导致无法收到消息或者传递时由于网络原因连接断开再或者又因为生产者填错了交换机名称导致消息丢失。此时可以利用RabbitMq的confirm机制,通过异步的回调将发送失败的消息重新入库或者保存下来。但此时也会存在一个问题触发了confirm回调函数后,项目刚好重启或者网络断开导致消息并没有持久化,也会导致消息丢失。所以需要在创建这条数据的同时就将要发送的这条消息放入表中保证在发送时不丢失。

    RabbitMq实现数据过期提醒并且保证数据不丢失_第4张图片

  2. 交换机到队列:由于采用的是延迟队列插件,它是先由交换机放入mnesia表中,到了结束时间后再发送到队列中,所以RabbitMq的Return机制再这种情况下及时成功发送到队列中还是会报:NO_ROUTE。

    原因:

        1、发送消息时不能保证原路由的队列还存在

        2、发送消息时之前与队列建立的链接是否还支持回调

    大白话讲就是等你到了结束时间的时候,还能找到我你就把消息给我,找不到我了那我就不要了。

     所以要选择其他方式来验证消息是否已经成功发到了队列中,这时就可以用到上一步已经持久化消息的状态来做判断,下文会具体解释如何用状态区分。

  3. 消费者捞取数据消费阶段:当消费者得到数据进行消费时,代码出现异常报错导致消费失败要将消费失败的数据重新记录回持久化表中,来保证再消费者端的数据不丢失。当然还要注意消费时的数据一致性,所以需要再消费时比对当前消息数据的结束时间和真实数据的结束时间是否一致,来进行下一步操作,是直接丢弃掉还是去提示用户此条数据过期。

    RabbitMq实现数据过期提醒并且保证数据不丢失_第5张图片

    注意:

        如果开启的是自动认证,再消费者收到消息的后就立马成功,这种是不安全的,因为消费者可能在业务中并没有执行成功就中断了。

        如果开启的是手动认证,需要当消费者线程由于某些原因产生异常进入的catch,这时仍需要手动提交ask,用来保证RabbitMq的UnAskEd不产生积压。

3、实现过程 

上面已经分析出了可能出现消息丢失情况,并对单独情况提出解决方案,下面结合业务来分析。

  • 因为用户创建的数据每一条可能都有不同的结束时间,有长有短,短的可能几天长的可能几年,虽然RabbitTemplate给我提供了可以设置单独一条消息的TTL的方法。
    message.getMessageProperties().setDelay()
    但是观察底层代码发现此方法接收的参数是Integer类型的,由于Integer最大到2^31-1转为天数大概为25天,并不满足创建的结束时间很长的数据,所以不能在一创建数据的同时就将消息设置TTL发送到队列中。对于这种情况我的解决方案为,可以再这条数据快到了要提醒时间的前10天内放入队列中。
  • 因为存在两个状态:即将过期已到期,结合这俩个状态和提前发送到队列的时间可以画出一个线段图。

    在一条数据创建完成时共会经历5个阶段:待处理、成功放入即将过期队列、成功放入过期队列、处理完成、消费失败。
  • 在用户创建数据的时候,会有几种情况分别为:RabbitMq实现数据过期提醒并且保证数据不丢失_第6张图片

    当创建的数据已经是过期的,无需处理,在创建的时候就会将状态修改为已过期,并且默认处于“成功发送到过期队列阶段”。

4、补偿机制

由于无法确保消费者一定会消费成功,所以生产者定时任务需要捞取5个阶段的数据分别为:

  1. 还有10天就即将过期的待处理状态的数据
  2. 已经过了即将过期时间但还没到过期时间前10天同时还没有被生产者定时任务捞取过的数据
  3. 还有10天就过期的并且状态为成功放入即将过期队列状态的数据
  4. 已经过了过期时间的数据
  5. 消费失败的数据

在第一种数据处理时直接放入Mq中,将状态改为“成功放入即将过期队列中”就结束。

在第二种数据处理时需要对比真实数据的状态是否已经修改,如果没有修改证明消费者在消费时出现了问题意外断开都没有进入catch 或者 压根没消费到这条数据。此时再由定时任务层面直接修改数据状态为即将过期,并且修改持久化表中的定时任务捞取状态为以捞取过,防止下次再拿到无用数据。

在第三种数据处理时和第一步类似,直接放入Mq中,将状态改为“成功放入过期队列中”就结束。

在第四种数据处理时和第二步类似,校验真实数据的状态是否已经修改,如果没有修改直接由定时任务触发修改数据状态为已过期,并且修改持久化表中的状态为“处理完成”,以便以后捞取不到。

在第五次数据处理时,因为已经消费失败了,证明此条数据在消费者层面发生了异常进入了catch中,肯定是已经即将过期或过期的数据,直接修改数据状态即将过期或已过期,在修改持久化表中的状态为“成功放入即将过期队列中”或“成功放入过期队列中”,以便继续流程。

5、数据一致性

在消费者和定时任务层面做的数据一致性比对,需要在数据创建时将数据的唯一id和创建到期时间放入redis中,减少访问数据库的次数,并设置过期时间保证redis数据的最终一致性。

四、结语

这个方案也是在开发的时候整理的,可能也有漏想或者值得优化的地方,我分别从RabbitMq的角度、Mysql连接、程序异常断开或部署的角度分析数据会丢失的问题。也欢迎给我提出问题在展开探讨。

你可能感兴趣的:(java,mybatis,rabbitmq,mysql)