延遲任務(Delayed Task)是指在未來的某個時間點,執行相應的任務。也就是說,延遲任務是一種計劃任務,它被安排在特定的時間後執行,而不是立即執行。
延遲任務的常見使用場景有以下幾個:
- 定時發送通知或消息:
- 發送定時短信、郵件或應用內消息,如註冊確認、訂單狀態更新、促銷活動通知等。
- 定時推送新聞、天氣預報、股票價格等實時信息。
- 異步處理和後臺任務:
- 將耗時的操作安排爲延遲任務,避免阻塞主線程或用戶界面,提高系統的響應性能。
- 執行批量數據處理,如日誌分析、數據報表生成等。
- 緩存管理和過期處理:
- 定時清理過期的緩存數據,釋放存儲空間。
- 更新緩存中的數據,保持數據的時效性和準確性。
- 計劃任務和定時調度:
- 在特定時間執行系統維護任務,如數據庫備份、系統更新等。
- 定時啓動或關閉服務,以節約資源或滿足業務需求。
- 訂單和支付處理:
- 在用戶下單後的一段時間內,如果用戶未支付,則自動取消訂單。
- 定時檢查訂單的支付狀態,並更新相應的訂單信息。
- 重試和失敗恢復機制:
- 當某個操作失敗時,可以在延遲一段時間後自動重試,以提高成功率。
- 實現分佈式鎖的超時釋放,避免死鎖情況。
- 提醒和日程管理:
- 設置日程提醒,如會議、生日、紀念日等。
- 定時提醒用戶完成任務或進行某項活動。
- 定時數據採集和上報:
- 定期從傳感器、設備或外部系統中採集數據。
- 定時上報應用的使用情況、統計數據或用戶行爲分析。
Redis如何實現延遲任務?
Redis 本身並沒有直接提供延遲任務的功能,但可以通過一些策略和手段,在 Redis 中手動實現延遲任務。
使用 Redis 實現延遲任務的主要手段有以下幾個:
- 使用過期鍵的事件通知執行延時任務:開啓過期鍵通知,當 Redis 中鍵值過期時觸發時間,在事件中實現延遲代碼,但因爲 Redis 的 Key 過期時不會被及時刪除,所以這個過期事件也不保證可以立即觸發,所以此方式很少用來實現延遲任務(因爲極其不穩定)。
- 使用 ZSet 執行延時任務:在 ZSet 中插入延遲執行時間和任務 ID,如下命令所示:
ZADD delay_tasks <timestamp> <task_id>
然後,啓動一個後臺線程或者定時任務,定期通過 ZRANGEBYSCORE 命令從有序集合中獲取已到達執行時間的任務,即分數小於或等於當前時間的任務,進行執行即可實現延時任務。
- 使用 Redisson 執行延遲任務:在 Redisson 框架中,提供了一個 RDelayedQueue 用於實現延遲隊列,使用簡單方便,推薦使用。
具體實現如下。
1.過期鍵通知事件實現
Redis 提供了鍵空間通知功能,當某個鍵發生變化(過期)時,可以發送通知。你可以結合 EXPIRE 過期命令和鍵空間通知來實現延遲任務。
當爲某個鍵設置過期時間時,一旦該鍵過期,Redis 會發送一個通知。你可以訂閱這個通知,並在接收到通知時執行任務。但這種方法可能不夠精確,且依賴於 Redis 的內部機制。
它的實現步驟是:
- 設置開啓 Redis 過期鍵通知事件,可以通過執行“CONFIG SET notify-keyspace-events KEA”命令來動態開啓鍵空間通知功能,而無需重啓 Redis 服務器。
- 設置過期鍵,可以通過命令“SET mykey "myvalue" EX 3”設置某個 key 3 秒後過期(3s 後執行)。
- 編寫一個監聽程序來訂閱 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實現延遲任務優缺點分析
優點:
- 輕量級與高性能:Redis 是一個內存中的數據結構存儲系統,因此讀寫速度非常快。將任務信息存儲在 Redis 中可以迅速地進行存取操作。
- 簡單易用:Redis 的 API 簡潔明瞭,易於集成到現有的應用系統中。
缺點:
- 精度有限:Redis 的延遲任務依賴於系統的定時檢查機制,而不是精確的定時器。這意味着任務的執行可能會有一定的延遲,特別是在系統負載較高或檢查間隔較長的情況下。
- 功能有限:與專業的任務調度系統相比,Redis 提供的延遲任務功能可能相對簡單。對於複雜的任務調度需求,如任務依賴、任務優先級等,可能需要額外的邏輯來實現。
- 穩定性較差:使用 Redis 實現延遲任務沒有重試機制和 ACK 確認機制,所以穩定性比較差。
- 單點故障風險:如果沒有正確配置 Redis 集羣或主從複製,那麼單個 Redis 實例的故障可能導致整個延遲任務系統的癱瘓。
課後思考
Redisson 底層是如何實現延遲任務的?
本文已收錄到我的面試小站 www.javacn.site,其中包含的內容有:Redis、JVM、併發、併發、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、設計模式、消息隊列等模塊。