写点什么

面试官:Redis 如何实现延迟任务?

作者:王磊
  • 2024-04-09
    陕西
  • 本文字数:4404 字

    阅读完需:约 14 分钟

面试官:Redis如何实现延迟任务?

延迟任务(Delayed Task)是指在未来的某个时间点,执行相应的任务。也就是说,延迟任务是一种计划任务,它被安排在特定的时间后执行,而不是立即执行。


延迟任务的常见使用场景有以下几个:


  1. 定时发送通知或消息

  2. 发送定时短信、邮件或应用内消息,如注册确认、订单状态更新、促销活动通知等。

  3. 定时推送新闻、天气预报、股票价格等实时信息。

  4. 异步处理和后台任务

  5. 将耗时的操作安排为延迟任务,避免阻塞主线程或用户界面,提高系统的响应性能。

  6. 执行批量数据处理,如日志分析、数据报表生成等。

  7. 缓存管理和过期处理

  8. 定时清理过期的缓存数据,释放存储空间。

  9. 更新缓存中的数据,保持数据的时效性和准确性。

  10. 计划任务和定时调度

  11. 在特定时间执行系统维护任务,如数据库备份、系统更新等。

  12. 定时启动或关闭服务,以节约资源或满足业务需求。

  13. 订单和支付处理

  14. 在用户下单后的一段时间内,如果用户未支付,则自动取消订单。

  15. 定时检查订单的支付状态,并更新相应的订单信息。

  16. 重试和失败恢复机制

  17. 当某个操作失败时,可以在延迟一段时间后自动重试,以提高成功率。

  18. 实现分布式锁的超时释放,避免死锁情况。

  19. 提醒和日程管理

  20. 设置日程提醒,如会议、生日、纪念日等。

  21. 定时提醒用户完成任务或进行某项活动。

  22. 定时数据采集和上报

  23. 定期从传感器、设备或外部系统中采集数据。

  24. 定时上报应用的使用情况、统计数据或用户行为分析。

Redis 如何实现延迟任务?

Redis 本身并没有直接提供延迟任务的功能,但可以通过一些策略和手段,在 Redis 中手动实现延迟任务


使用 Redis 实现延迟任务的主要手段有以下几个:


  1. 使用过期键的事件通知执行延时任务:开启过期键通知,当 Redis 中键值过期时触发时间,在事件中实现延迟代码,但因为 Redis 的 Key 过期时不会被及时删除,所以这个过期事件也不保证可以立即触发,所以此方式很少用来实现延迟任务(因为极其不稳定)。

  2. 使用 ZSet 执行延时任务:在 ZSet 中插入延迟执行时间和任务 ID,如下命令所示:


ZADD delay_tasks <timestamp> <task_id>
复制代码


然后,启动一个后台线程或者定时任务,定期通过 ZRANGEBYSCORE 命令从有序集合中获取已到达执行时间的任务,即分数小于或等于当前时间的任务,进行执行即可实现延时任务。


  1. 使用 Redisson 执行延迟任务:在 Redisson 框架中,提供了一个 RDelayedQueue 用于实现延迟队列,使用简单方便,推荐使用。


具体实现如下。

1.过期键通知事件实现

Redis 提供了键空间通知功能,当某个键发生变化(过期)时,可以发送通知。你可以结合 EXPIRE 过期命令和键空间通知来实现延迟任务。


当为某个键设置过期时间时,一旦该键过期,Redis 会发送一个通知。你可以订阅这个通知,并在接收到通知时执行任务。但这种方法可能不够精确,且依赖于 Redis 的内部机制。


它的实现步骤是:


  1. 设置开启 Redis 过期键通知事件,可以通过执行“CONFIG SET notify-keyspace-events KEA”命令来动态开启键空间通知功能,而无需重启 Redis 服务器。

  2. 设置过期键,可以通过命令“SET mykey "myvalue" EX 3”设置某个 key 3 秒后过期(3s 后执行)。

  3. 编写一个监听程序来订阅 Redis 的键空间通知。这可以通过使用 Redis 的发布/订阅功能来实现,具体实现代码如下(以 Jedis 框架使用为例):


import redis.clients.jedis.Jedis;  import redis.clients.jedis.JedisPubSub;    public class RedisKeyspaceNotifier {        public static void main(String[] args) {          // 创建Jedis实例          Jedis jedis = new Jedis("localhost", 6379);            // 配置键空间通知(通常这一步在Redis配置文件中完成,但也可以在运行时配置)          jedis.configSet("notify-keyspace-events", "KEA");            // 订阅键空间通知          jedis.subscribe(new KeyspaceNotificationSubscriber(), "__keyevent@0__:expired");      }        static class KeyspaceNotificationSubscriber extends JedisPubSub {            @Override          public void onMessage(String channel, String message) {              System.out.println("Received message from channel: " + channel + ", message: " + message);              // 在这里处理接收到的键空间通知              // 例如,如果message是一个需要处理的任务ID,你可以在这里触发相应的任务处理逻辑          }            @Override          public void onSubscribe(String channel, int subscribedChannels) {              System.out.println("Subscribed to channel: " + channel);          }            @Override          public void onUnsubscribe(String channel, int subscribedChannels) {              System.out.println("Unsubscribed from channel: " + channel);          }      }  }
复制代码


但因为 Redis 的 Key 过期时不会被及时删除,Redis 采用的是惰性删除和定期删除,所以这个过期事件也不保证可以立即触发,所以此方式很少用来实现延迟任务(因为极其不稳定)。

2.使用 ZSet 实现延迟任务

可以将任务及其执行时间作为成员和分数存储在 ZSET 中,然后,使用一个后台任务(如定时任务或守护进程)定期检查 ZSET,查找分数(即执行时间)小于或等于当前时间的成员,并执行相应的任务。执行后,从 ZSET 中删除该成员,具体实现代码如下:


import redis.clients.jedis.Jedis;    import java.util.Set;    public class RedisDelayedTaskDemo {        private static final String ZSET_KEY = "delayed_tasks";      private static final String REDIS_HOST = "localhost";      private static final int REDIS_PORT = 6379;        public static void main(String[] args) {          Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT);            // 添加延迟任务          addDelayedTask(jedis, "task1", System.currentTimeMillis() / 1000 + 5); // 5秒后执行          addDelayedTask(jedis, "task2", System.currentTimeMillis() / 1000 + 10); // 10秒后执行            // 模拟定时任务检查器          new Thread(() -> {              while (true) {                  try {                      // 检查并执行到期的任务                      checkAndExecuteTasks(jedis);                      Thread.sleep(1000); // 每秒检查一次                  } catch (InterruptedException e) {                      e.printStackTrace();                  }              }          }).start();      }        private static void addDelayedTask(Jedis jedis, String task, long executeTime) {          jedis.zadd(ZSET_KEY, executeTime, task);          System.out.println("Added task: " + task + " with execution time: " + executeTime);      }        private static void checkAndExecuteTasks(Jedis jedis) {          long currentTime = System.currentTimeMillis() / 1000;          Set<String> tasks = jedis.zrangeByScore(ZSET_KEY, 0, currentTime);            for (String task : tasks) {              jedis.zrem(ZSET_KEY, task); // 从有序集合中移除任务              executeTask(task); // 执行任务          }      }        private static void executeTask(String task) {          System.out.println("Executing task: " + task);          // 在这里添加实际的任务执行逻辑      }  }
复制代码


在这个示例中,我们首先使用 addDelayedTask 方法向 Redis 的有序集合中添加任务,并设置它们的执行时间。然后,我们启动一个线程来模拟定时任务检查器,它会每秒检查一次是否有任务到期,并执行到期的任务。

3.使用 Redisson 执行定时任务

在 Redisson 框架中,提供了一个 RDelayedQueue 用于实现延迟队列,使用简单方便,推荐使用,它的具体实现如下:


import org.redisson.Redisson;import org.redisson.api.RDelayedQueue;import org.redisson.api.RedissonClient;import org.redisson.config.Config;
import java.util.concurrent.TimeUnit;
public class RDelayedQueueDemo {
public static void main(String[] args) throws InterruptedException { // 创建 Redisson 客户端 Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RedissonClient redisson = Redisson.create(config);
// 获取延迟队列 RDelayedQueue<String> delayedQueue = redisson.getDelayedQueue("delayedQueue");
// 添加延迟任务 delayedQueue.offer("task1", 5, TimeUnit.SECONDS);
// 监听并处理延迟任务 Thread listenerThread = new Thread(() -> { while (true) { try { // 通过 take 方法等待并获取到期的任务 String task = delayedQueue.take(); System.out.println("Handle task: " + task); } catch (InterruptedException e) { break; } } }); listenerThread.start(); }}
复制代码


在上述示例中,我们首先创建了一个 Redisson 客户端,通过配置文件指定了使用单节点 Redis 服务器。然后,我们获取一个延迟队列 RDelayedQueue,并添加一个延迟任务,延迟时间为 5 秒,接着,我们通过线程监听并处理延迟队列中的任务。

4.Redis 实现延迟任务优缺点分析

优点:


  1. 轻量级与高性能:Redis 是一个内存中的数据结构存储系统,因此读写速度非常快。将任务信息存储在 Redis 中可以迅速地进行存取操作。

  2. 简单易用:Redis 的 API 简洁明了,易于集成到现有的应用系统中。


缺点:


  1. 精度有限:Redis 的延迟任务依赖于系统的定时检查机制,而不是精确的定时器。这意味着任务的执行可能会有一定的延迟,特别是在系统负载较高或检查间隔较长的情况下。

  2. 功能有限:与专业的任务调度系统相比,Redis 提供的延迟任务功能可能相对简单。对于复杂的任务调度需求,如任务依赖、任务优先级等,可能需要额外的逻辑来实现。

  3. 稳定性较差:使用 Redis 实现延迟任务没有重试机制和 ACK 确认机制,所以稳定性比较差。

  4. 单点故障风险:如果没有正确配置 Redis 集群或主从复制,那么单个 Redis 实例的故障可能导致整个延迟任务系统的瘫痪。

课后思考

Redisson 底层是如何实现延迟任务的?


本文已收录到我的面试小站 www.javacn.site,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。

用户头像

王磊

关注

公众号:Java中文社群 2018-08-25 加入

公众号:Java中文社群

评论

发布
暂无评论
面试官:Redis如何实现延迟任务?_Java_王磊_InfoQ写作社区