Redis 延時隊列詳解
一、什么是延時隊列
普通隊列: 消息一到就消費
延時隊列: 消息到了先放著,到指定時間再消費
普通隊列: [消息] → 立即消費
延時隊列: [消息] → 等 30 分鐘 → 到期 → 消費
場景舉例:
- 下單 30 分鐘未支付,自動取消
- 紅包 24 小時未領取,自動退回
- 會議開始前 5 分鐘,發(fā)送提醒
- 7 天后自動確認收貨
二、為什么用 ZSet 實現(xiàn)
| 數(shù)據(jù)類型 | 結構 | 能做延時隊列嗎 |
|---|---|---|
| List | 按插入排序 | ? 不支持按時間排序 |
| Set | 無序 | ? 沒法指定執(zhí)行時間 |
| ZSet | 按 score 排序 | ? score 存到期時間戳 |
# ZSet 天然適合: ZADD delay_queue 1718000000 "task_001" # score=到期時間戳 ZADD delay_queue 1718000300 "task_002" # 自動按時間排序
三、核心流程
生產(chǎn)者 Redis ZSet 消費者(定時任務)
────── ────────── ──────────────
XADD delay_q score = 到期時間戳
score=到期時間 member = 任務數(shù)據(jù)
┌─────────────────┐
│ 1718000000 task1 │ ← 最早到期
│ 1718000300 task2 │
│ 1718000600 task3 │
└─────────────────┘
↓
ZRANGEBYSCORE 0 當前時間
取到 task1(已到期)
↓
執(zhí)行 task1 → ZREM 刪除四、基礎實現(xiàn)(有并發(fā)問題)
// 生產(chǎn)者:投遞延時任務
$redis->zAdd('delay:orders', time() + 1800, json_encode([
'order_id' => 12345,
'action' => 'auto_cancel',
]));
// 消費者:每秒輪詢到期任務(有 BUG 的版本)
$now = time();
$tasks = $redis->zRangeByScore('delay:orders', 0, $now, ['limit' => [0, 1]]);
if ($tasks) {
$task = $tasks[0];
// ?? 這里有 BUG!如果同時多個消費者拿到同一條
$redis->zRem('delay:orders', $task);
processTask($task);
}問題在哪? ZRANGEBYSCORE 和 ZREM 是分開的,多個消費者可能同時拿到同一條任務!
五、Lua 腳本原子化(解決并發(fā))
-- 原子操作:查出到期任務 + 立刻刪除 + 返回
local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1], 'LIMIT', 0, 1)
if #tasks == 0 then
return nil
end
local task = tasks[1]
local removed = redis.call('ZREM', KEYS[1], task)
if removed == 1 then
return task -- 刪除成功,返回任務
else
return nil -- 被別的消費者搶了
endPHP 端調(diào)用:
$lua = <<<'LUA'
local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1], 'LIMIT', 0, 1)
if #tasks == 0 then return nil end
local task = tasks[1]
if redis.call('ZREM', KEYS[1], task) == 1 then
return task
else
return nil
end
LUA;
// 定時任務循環(huán)執(zhí)行
while (true) {
$task = $redis->eval($lua, ['delay:orders', time()], 1);
// ↑ KEYS 部分 ↑ key數(shù)量
if ($task) {
$data = json_decode($task, true);
echo "處理任務: {$data['order_id']}\n";
processTask($data);
} else {
sleep(1); // 沒任務就等一下
}
}六、完整實戰(zhàn):30 分鐘未支付自動取消
<?php
// ====== 生產(chǎn)者(下單時) ======
function createOrder($orderId) {
$redis = new Redis();
$redis->connect('127.0.0.1', 6380);
// 1. 創(chuàng)建訂單...
// 2. 投遞延時任務:30分鐘后自動取消
$delayAt = time() + 1800; // 30分鐘
$task = json_encode([
'order_id' => $orderId,
'action' => 'auto_cancel',
'create_at'=> date('Y-m-d H:i:s'),
]);
$redis->zAdd('delay:orders', $delayAt, $task);
echo "訂單 {$orderId} 已創(chuàng)建,30分鐘后未支付將自動取消\n";
}
// ====== 消費者(定時腳本) ======
$lua = <<<'LUA'
local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1], 'LIMIT', 0, 1)
if #tasks == 0 then return nil end
if redis.call('ZREM', KEYS[1], tasks[1]) == 1 then
return tasks[1]
end
return nil
LUA;
while (true) {
$task = $redis->eval($lua, ['delay:orders', time()], 1);
if ($task) {
$data = json_decode($task, true);
// 檢查訂單是否已支付
$order = getOrder($data['order_id']);
if ($order['status'] === 'unpaid') {
cancelOrder($data['order_id']);
echo "? 訂單 {$data['order_id']} 超時未支付,已自動取消\n";
} else {
echo "? 訂單 {$data['order_id']} 已支付,跳過\n";
}
} else {
sleep(1);
}
}七、ZSet 延時隊列 vs 其他方案
| 方案 | 原理 | 優(yōu)點 | 缺點 |
|---|---|---|---|
| ZSet | score=時間戳,輪詢?nèi)?/td> | 簡單,Redis 自帶 | 需要輪詢,精度秒級 |
| Redis 過期回調(diào) | key 過期觸發(fā)通知 | 不用輪詢 | 通知不可靠,可能丟失 |
| RabbitMQ 延時插件 | 消息自帶 TTL + 死信隊列 | 專業(yè)可靠 | 需要額外裝插件 |
| 數(shù)據(jù)庫輪詢 | 定時掃表 | 實現(xiàn)簡單 | 大量數(shù)據(jù)時很慢 |
八、ZSet 的 score 誰來賦值的
ZADD key score member
↑
你自己指定的
ZADD delay_queue 1718000000 "task_001"
# ↑ 時間戳就是 score,你自己算的
# score 決定了 ZSet 里的排序
排序規(guī)則: score 越小越靠前,所以過期時間越早的排最前面。
九、Set vs ZSet 能不能做延時隊列
| Set | ZSet | |
|---|---|---|
| 有序嗎 | ? 無序 | ? 按 score 排序 |
| 能查到期的嗎 | ? | ? ZRANGEBYSCORE 0 now |
| 能做延時隊列嗎 | 不行 | 可以 |
延時隊列的核心需求是"按時間排序、查到期任務",只有 ZSet 能做到。
十、面試常問
Q: 延時隊列為什么不用 List?
List 只能頭進頭出,無法按時間排序,不知道哪些消息到期了。
Q: 輪詢會不會性能差?
單次 ZRANGEBYSCORE + ZREM 是 O(log N),每秒輪詢對 Redis 壓力很小。10萬條延時任務也沒問題。
Q: 很大量級的延時任務怎么辦?
- 用多個 ZSet key 分桶(按分鐘/小時)
- 每個桶一個消費者線程
- 配合 Redis Cluster 分片
Q: 消息丟了怎么辦?
Redis 純內(nèi)存的話宕機會丟。重要業(yè)務建議:
- AOF 持久化
- 訂單狀態(tài)雙寫(Redis + DB),定時任務掃表兜底
Q: score 存毫秒級時間戳可以嗎?
可以,ZSet 的 score 是 double 浮點數(shù),毫秒時間戳完全放得下。
核心記?。篫Set 的 score 排序 + Lua 原子搶任務 = 可靠的延時隊列
到此這篇關于Redis 延時隊列詳解的文章就介紹到這了,更多相關Redis 延時隊列內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Redis?延時任務實現(xiàn)及與定時任務區(qū)別詳解
這篇文章主要為大家介紹了Redis?延時任務實現(xiàn)及與定時任務區(qū)別詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-06-06
Redis集群利用Redisson實現(xiàn)分布式鎖方式
這篇文章主要介紹了Redis集群利用Redisson實現(xiàn)分布式鎖方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05

