【如何优雅的实现延迟消息多次提醒】方案集合

大家好,说到这个话题,可能大家一定脱口而出就是rabbitmq,这个肯定没问题,但是如果你们的业务技术栈引入的MQ并不是这个呢,比如Kakfa呢,总不能为了这个功能更换技术栈吧
所以这里本博主收集了几种常见的方案,也简单列举了优缺点,大家按照自己的业务场景去选择

方案一:DelayedQueue

这种是利用简单的延迟队列结构来实现的 ,这里就不展开细说了,不常用,因为这个是单价版的,现在业务基本都是分布式的,但是虽然是简单的 但是也是最重要的,因为利用这个数据结构实现了接下来我说的所有的方案

方案二:redis+key的过期监听器

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;

import java.util.UUID;

public class RedisDelayedNotifyDemo {

    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);

        // 消息ID
        String messageId = UUID.randomUUID().toString();

        // 消息内容
        String messageContent = "hello, world!";

        // 延迟时间(毫秒)
        long delayTime = 5000;

        // 将消息存入 Redis 中,并设置过期时间
        jedis.setex("delayed_messages:" + messageId, delayTime / 1000, messageContent);

        // 添加过期监听器
        jedis.psubscribe(new JedisPubSub() {
            @Override
            public void onPSubscribe(String pattern, int subscribedChannels) {
                System.out.println("Subscribed to " + pattern);
            }


当然代码比较粗糙呀,不建议大家直接copy,这里只是伪代码,可以按照这个方案去网上搜寻,思路一致的,写法高级点

但是这个缺点也很明显,无法实现多次提醒
Redis 实现 key 过期的方案只能触发一次监听器,因为 Redis 是基于发布/订阅模式实现的,当 key 过期时,Redis 会发布一个过期事件,订阅了该事件的客户端可以接收到事件并执行相应的操作。一旦监听器被触发,就需要重新设置 key 的过期时间来实现下一次提醒,这样需要频繁地更新 key,增加了 Redis 的负担和延迟
如果需要实现多次提醒怎么办?

方案三:Redisson+DelayedQueue(推荐)

import java.util.concurrent.TimeUnit;

import org.redisson.Redisson;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RDelayedQueue;
import org.redisson.api.RedissonClient;
import org.redisson.client.codec.StringCodec;
import org.redisson.config.Config;

public class RedisDelayedReminderExample {

    public static void main(String[] args) throws InterruptedException {
        Config config = new Config();
        config.useSingleServer()
            .setAddress("redis://127.0.0.1:6379")
            .setPassword("password");

        RedissonClient redisson = Redisson.create(config);
        RBlockingQueue<String> blockingQueue = redisson.getBlockingQueue("reminder-queue", new StringCodec());

        // 创建延迟队列
        RDelayedQueue<String> delayedQueue = redisson.getDelayedQueue(blockingQueue);

        // 充值会员提前7天提醒任务
        String userId = "user123";
        delayedQueue.offer(userId, 7, TimeUnit.DAYS);

        // 充值会员提前1天提醒任务
        delayedQueue.offer(userId, 1, TimeUnit.DAYS);

        // 监听器处理任务
        RedisDelayedReminderListener listener = new RedisDelayedReminderListener();
        blockingQueue.takeAsync().thenAccept(listener::onDelayedTask);
    }
}

class RedisDelayedReminderListener {

    public void onDelayedTask(String userId) {
        // 进行提醒操作
        System.out.println("Send reminder to user " + userId);
    }
}

上面的代码是加入了监听器逻辑,但是也可以不需要,看自己业务,重点不在这个

比如下面的

// 创建 Redisson 实例
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redissonClient = Redisson.create(config);

// 获取延迟队列和普通队列
RQueue<String> queue = redissonClient.getQueue("myQueue");
RDelayedQueue<String> delayedQueue = redissonClient.getDelayedQueue(queue);

// 将消息放入延迟队列,并设置多次提醒
String message = "Hello, World!";
long delayTime = 10000;  // 10 秒后第一次提醒
int repeatTimes = 3;     // 一共提醒 3 次
long interval = 5000;    // 每次间隔 5 秒
delayedQueue.offer(message, delayTime, repeatTimes, interval);

// 启动一个线程,从普通队列中获取消息,并处理
new Thread(() -> {
    while (true) {
        try {
            String msg = queue.take();
            System.out.println(msg);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}).start();

方案四:redisson+mapCache

在 Redisson 中,使用 MapCache 可以实现对 Map 类型的缓存操作。MapCache 是一个具有过期时间和最大缓存条目数限制的 Map,它可以自动将过期的缓存条目从缓存中移除,从而避免内存泄漏和数据膨胀问题。同时,MapCache 还提供了延迟失效功能,即在缓存条目失效前,可以通过修改缓存条目的 TTL (Time To Live) 值来延长缓存的生命周期。

为了实现延迟提醒功能,您可以在 MapCache 中存储需要提醒的事件信息,同时设置一个相应的 TTL 值。当 TTL 时间到期时,MapCache 会自动将该条目从缓存中删除。同时,您可以使用 Redisson 提供的监听器功能,在缓存条目失效时触发相应的事件通知操作,从而实现延迟提醒的功能。

下面是一个使用 MapCache 实现延迟提醒功能的简单示例代码:

// 初始化 Redisson 客户端
RedissonClient client = Redisson.create();

// 创建 MapCache 对象,并设置过期时间和最大缓存条目数
RMapCache<String, String> mapCache = client.getMapCache("myMapCache");
mapCache.setMaxSize(100);
mapCache.setMaxIdleTime(30, TimeUnit.MINUTES);

// 将需要提醒的事件信息存储到 MapCache 中,并设置相应的 TTL 值
mapCache.put("eventId", "eventInfo", 1, TimeUnit.HOURS);

// 添加 MapCache 监听器,用于在缓存条目失效时触发相应的事件通知操作
mapCache.addListener(new MapCacheListener<String, String>() {
    @Override
    public void onExpire(String key, String value) {
        // 在缓存条目失效时触发相应的事件通知操作
        System.out.println("Event expired: " + key);
    }
});

在上面的示例代码中,我们首先初始化了 Redisson 客户端,然后创建了一个名为 “myMapCache” 的 MapCache 对象,并设置了最大缓存条目数为 100,最大闲置时间为 30 分钟。接下来,我们将需要提醒的事件信息存储到 MapCache 中,并设置了一个 TTL 值为 1 小时。最后,我们添加了一个 MapCache 监听器,在缓存条目失效时触发相应的事件通知操作。

但是这个缺点明显,因为是单机的,因为这里的mapcache是本地缓存

方案四 redis中key过期的事件触发

这个地方只是简单的代码片段演示,详细请看redis中key过期的事件触发
这个和方案二类似 都是利用redis中的key失效之后触发监听事件
但是区别就是 方案二 每个redis调用函数的地方一个个去添加监听事件 ,这样一来在一个项目中 ,代码使用的地方就多了,不利于管理,不利于管理,而且方案二还是使用老版的jedis去添加监听函数的 ,还不如使用下面的redisson,比如

RMapCache<String, String> mapCache = client.getMapCache("myMapCache");
mapCache.setMaxSize(100);
mapCache.setMaxIdleTime(30, TimeUnit.MINUTES);

// 将需要提醒的事件信息存储到 MapCache 中,并设置相应的 TTL 值
mapCache.put("eventId", "eventInfo", 1, TimeUnit.HOURS);

// 添加 MapCache 监听器,用于在缓存条目失效时触发相应的事件通知操作
mapCache.addListener(new MapCacheListener<String, String>() {
    @Override
    public void onExpire(String key, String value) {
        // 在缓存条目失效时触发相应的事件通知操作
        System.out.println("Event expired: " + key);
    }
  当然 这个mapCache 只是一个创建桶的对象,这个地方可以创建其他的,比如 RList,或者 对象桶都可以,然后继续调用addListener函数即可

话说回来,这个地方反正们每处代码都这样调用的话,就不利于统一管理了,比好比统一异常收集一个意思

@Component
@Slf4j
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

  @Autowired
  private KafkaProducerService kafkaProducerService;

  public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
    super(listenerContainer);
  }

  /**
   * 针对 redis 数据失效事件,进行数据处理
   * @param message
   * @param pattern
   */
  @Override
  public void onMessage(Message message, byte[] pattern){
    if(message == null || StringUtils.isEmpty(message.toString())){
      return;
    }
    String content = message.toString();
    //key的格式为  flag:时效类型:运单号 示例如下
    try {
      if(content.startsWith(AbnConstant.EMS)){
        kafkaProducerService.sendMessageSync(TopicConstant.EMS_WAYBILL_ABN_QUEUE,content);
      }else if(content.startsWith(AbnConstant.YUNDA)){
        kafkaProducerService.sendMessageSync(TopicConstant.YUNDA_WAYBILL_ABN_QUEUE,content);
      }
    } catch (Exception e) {
      log.error("监控过期key,发送kafka异常,",e);
    }
  }
}
这种是管理所有的监听key失效的地方

tips:

但是也是有缺点的:比如我们使用key监听之后的处理逻辑不一致的时候 就需要上面的那样调用就好些了
反正各有利弊,无论哪种都是和今天说的如何实现延迟消息多次提醒不合适的
,因为一个key我需要多次提醒,比如会议开始前一天提醒,当天提醒,提前半小时都需要分别推送提醒怎么办呢,总不能用上面的方式每次失效之后
然后又重新set一个时间进去吧,这样太浪费CPU了,而且你还不能分别set三种key进去,因为我需要的肯定是一种key,此时就需要使用到方案三
还有一个问题:比如我set的时候是使用redis的延迟队列,而超时的时候缺使用上面的方案,也就是实现了KeyExpirationEventMessageListener,这个方案呢,有个缺点就是实现了这个接口意味着redis中所有的key失效都会触发,但是目前我只关心延迟队列的过期key,就会导致没必要的性能消化,有些key并不需要放置延迟队列,真正的需求肯定只需要关注延迟队列的key的TTL,比如
RQueue queue = redissonClient.getQueue(“myQueue”);
RDelayedQueue delayedQueue = redissonClient.getDelayedQueue(queue);

方案五 redis的zset实现延迟队列

详细请看redis的zset实现延迟队列
这个方案之所以提出来就是和方案四做一个对比的,但是这几种都无法满足今天标题的需求,如果只是简单的监听 ,都是可以的,看自己的业务需求吧

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值