考察点: Sorted Set、ZRANGEBYSCORE、定时轮询、可靠性
🎬 开场:一个关于"闹钟"的故事
想象你是个健忘的人 🧠:
场景1:普通队列(立即执行)
你:今天要买牛奶!
大脑:收到!立即提醒!
你:不是现在,是下午3点!
大脑:不行,我只会立即提醒...😅
场景2:延迟队列(定时执行)
你:今天下午3点提醒我买牛奶
大脑:好的,设置闹钟,3点准时提醒!⏰
(到了3点)
大脑:该买牛奶了!
你:完美!😎
Redis延迟队列就是这样的"智能闹钟"! 📱
第一部分:什么是延迟队列? 🤔
1.1 延迟队列的定义
延迟队列(Delay Queue) = 消息在指定时间之后才能被消费
普通队列:
生产者 → [消息1][消息2][消息3] → 消费者(立即消费)
延迟队列:
生产者 → [消息1(3秒后)][消息2(10秒后)][消息3(60秒后)]
↓ ↓ ↓
等待3秒 等待10秒 等待60秒
↓ ↓ ↓
消费者 消费者 消费者
1.2 延迟队列的应用场景
场景1:订单超时取消 ⏱️
用户下单 → 30分钟未支付 → 自动取消订单
实现:
1. 用户下单时,将订单ID放入延迟队列(延迟30分钟)
2. 30分钟后,消息被取出
3. 检查订单状态,如果未支付,取消订单
场景2:定时发送通知 📧
用户预约体检 → 体检前1天发送提醒短信
实现:
1. 用户预约时,将提醒任务放入延迟队列
2. 延迟时间 = 体检时间 - 1天 - 当前时间
3. 到期后发送短信
场景3:延迟重试 🔄
调用第三方API失败 → 5秒后重试
实现:
1. 调用失败后,将任务放入延迟队列(延迟5秒)
2. 5秒后重新执行
3. 如果还失败,延迟10秒、30秒、60秒...(指数退避)
场景4:定时任务 📅
每天凌晨2点生成报表
实现:
1. 计算距离下次凌晨2点的时间差
2. 将任务放入延迟队列
3. 执行完后,再次添加(下一天凌晨2点)
1.3 延迟队列的要求
✅ 必须具备的特性:
- 支持延迟时间
- 到期后能被消费
- 保证消息不丢失
- 保证消息不重复消费(尽量)
第二部分:Redis实现延迟队列 🛠️
2.1 核心数据结构:Sorted Set
为什么用Sorted Set?
Sorted Set(有序集合)特点:
1. 成员唯一(不重复)
2. 按分数排序
3. 支持范围查询(ZRANGEBYSCORE)
完美匹配延迟队列需求:
- 成员 = 任务ID/消息内容
- 分数 = 执行时间戳
- 范围查询 = 取出到期的任务
2.2 基础实现(v1.0)
import redis
import time
import json
class DelayQueue:
def __init__(self, redis_client, queue_name="delay_queue"):
self.redis = redis_client
self.queue_name = queue_name
def push(self, message, delay_seconds):
"""
添加延迟任务
:param message: 任务内容
:param delay_seconds: 延迟秒数
"""
# 计算执行时间
execute_time = time.time() + delay_seconds
# 将任务添加到Sorted Set
# score = 执行时间戳
self.redis.zadd(
self.queue_name,
{json.dumps(message): execute_time}
)
print(f"✅ 任务已添加,将在 {delay_seconds} 秒后执行")
def pop(self):
"""
取出到期的任务
:return: 任务内容 或 None
"""
# 当前时间戳
now = time.time()
# 查询 score <= now 的任务(已到期)
tasks = self.redis.zrangebyscore(
self.queue_name,
min=0,
max=now,
start=0,
num=1 # 只取一个
)
if not tasks:
return None
task = tasks[0]
# 删除任务(防止重复消费)
self.redis.zrem(self.queue_name, task)
# 返回任务内容
return json.loads(task)
def consume(self):
"""
持续消费任务
"""
print("🚀 延迟队列启动,等待任务...")
while True:
# 取出任务
task = self.pop()
if task:
print(f"📨 收到任务: {task}")
# 处理任务
self.handle_task(task)
else:
# 没有任务,休息1秒
time.sleep(1)
def handle_task(self, task):
"""
处理任务(业务逻辑)
"""
print(f"✅ 执行任务: {task}")
# 使用示例
if __name__ == "__main__":
# 连接Redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# 创建延迟队列
queue = DelayQueue(r)
# 添加任务
queue.push({"orderId": "12345", "action": "cancel"}, delay_seconds=5)
queue.push({"orderId": "67890", "action": "cancel"}, delay_seconds=10)
# 消费任务
queue.consume()
运行效果:
✅ 任务已添加,将在 5 秒后执行
✅ 任务已添加,将在 10 秒后执行
🚀 延迟队列启动,等待任务...
(5秒后)
📨 收到任务: {'orderId': '12345', 'action': 'cancel'}
✅ 执行任务: {'orderId': '12345', 'action': 'cancel'}
(10秒后)
📨 收到任务: {'orderId': '67890', 'action': 'cancel'}
✅ 执行任务: {'orderId': '67890', 'action': 'cancel'}
2.3 问题1:并发竞争(多消费者)
问题:
两个消费者同时pop:
消费者1: ZRANGEBYSCORE → 获取task1
消费者2: ZRANGEBYSCORE → 获取task1(重复!)
消费者1: ZREM task1
消费者2: ZREM task1
结果:task1被重复处理!
解决方案:使用Lua脚本保证原子性
class DelayQueue:
def __init__(self, redis_client, queue_name="delay_queue"):
self.redis = redis_client
self.queue_name = queue_name
# Lua脚本:原子性地获取并删除任务
self.pop_script = """
local queue_name = KEYS[1]
local now = ARGV[1]
-- 获取一个到期的任务
local tasks = redis.call('ZRANGEBYSCORE', queue_name, 0, now, 'LIMIT', 0, 1)
if #tasks == 0 then
return nil
end
local task = tasks[1]
-- 删除任务
redis.call('ZREM', queue_name, task)
return task
"""
def pop(self):
"""
原子性地取出任务
"""
now = time.time()
# 执行Lua脚本
task = self.redis.eval(
self.pop_script,
1, # KEYS数量
self.queue_name, # KEYS[1]
now # ARGV[1]
)
if task:
return json.loads(task)
return None
2.4 问题2:任务丢失(消费者崩溃)
问题:
消费者取出任务 → 开始处理 → 崩溃(任务丢失!)
解决方案:ACK机制(确认机制)
class ReliableDelayQueue:
def __init__(self, redis_client, queue_name="delay_queue"):
self.redis = redis_client
self.queue_name = queue_name
self.processing_queue = queue_name + ":processing"
# Lua脚本:取出任务并加入处理队列
self.pop_script = """
local queue_name = KEYS[1]
local processing_queue = KEYS[2]
local now = ARGV[1]
local timeout = ARGV[2]
-- 获取任务
local tasks = redis.call('ZRANGEBYSCORE', queue_name, 0, now, 'LIMIT', 0, 1)
if #tasks == 0 then
return nil
end
local task = tasks[1]
-- 从延迟队列删除
redis.call('ZREM', queue_name, task)
-- 添加到处理队列(设置超时时间)
local timeout_time = tonumber(now) + tonumber(timeout)
redis.call('ZADD', processing_queue, timeout_time, task)
return task
"""
def pop(self, timeout=300):
"""
取出任务(带超时保护)
:param timeout: 处理超时时间(秒),默认5分钟
"""
now = time.time()
task = self.redis.eval(
self.pop_script,
2,
self.queue_name,
self.processing_queue,
now,
timeout
)
if task:
return json.loads(task)
return None
def ack(self, task):
"""
确认任务完成
"""
task_str = json.dumps(task)
self.redis.zrem(self.processing_queue, task_str)
print(f"✅ 任务已确认: {task}")
def retry_timeout_tasks(self):
"""
重试超时的任务(定时任务,每分钟执行一次)
"""
now = time.time()
# 获取超时的任务
timeout_tasks = self.redis.zrangebyscore(
self.processing_queue,
min=0,
max=now
)
if timeout_tasks:
print(f"⚠️ 发现 {len(timeout_tasks)} 个超时任务,重新加入队列")
for task in timeout_tasks:
# 从处理队列删除
self.redis.zrem(self.processing_queue, task)
# 重新加入延迟队列(立即执行)
self.redis.zadd(self.queue_name, {task: now})
def consume(self):
"""
消费任务
"""
print("🚀 可靠延迟队列启动...")
while True:
try:
# 取出任务
task = self.pop()
if task:
print(f"📨 收到任务: {task}")
try:
# 处理任务
self.handle_task(task)
# 确认任务完成
self.ack(task)
except Exception as e:
print(f"❌ 任务处理失败: {e}")
# 不ACK,任务会超时后重试
else:
# 没有任务,检查超时任务
self.retry_timeout_tasks()
time.sleep(1)
except Exception as e:
print(f"❌ 消费出错: {e}")
time.sleep(1)
def handle_task(self, task):
"""
处理任务
"""
# 模拟处理
time.sleep(2)
print(f"✅ 任务处理完成: {task}")
2.5 完整示例:订单超时取消
import redis
import time
import json
from datetime import datetime
class OrderCancelQueue:
"""订单超时取消延迟队列"""
def __init__(self, redis_client):
self.redis = redis_client
self.queue = ReliableDelayQueue(redis_client, "order:cancel:queue")
def create_order(self, order_id, timeout_minutes=30):
"""
创建订单
:param order_id: 订单ID
:param timeout_minutes: 超时时间(分钟)
"""
# 订单信息存储(模拟数据库)
order = {
"order_id": order_id,
"status": "unpaid", # 未支付
"create_time": time.time()
}
self.redis.hset(f"order:{order_id}", mapping=order)
# 加入延迟队列
task = {
"order_id": order_id,
"action": "cancel_if_unpaid"
}
self.queue.push(task, delay_seconds=timeout_minutes * 60)
print(f"📝 订单创建: {order_id}, {timeout_minutes}分钟后自动取消(如未支付)")
def pay_order(self, order_id):
"""
支付订单
"""
self.redis.hset(f"order:{order_id}", "status", "paid")
print(f"💰 订单已支付: {order_id}")
def handle_cancel_task(self, task):
"""
处理取消任务
"""
order_id = task["order_id"]
# 获取订单状态
status = self.redis.hget(f"order:{order_id}", "status")
if status == b"unpaid":
# 未支付,取消订单
self.redis.hset(f"order:{order_id}", "status", "cancelled")
print(f"❌ 订单已取消: {order_id} (超时未支付)")
else:
# 已支付,不取消
print(f"✅ 订单已支付: {order_id} (无需取消)")
def start_consumer(self):
"""
启动消费者
"""
print("🚀 订单取消队列启动...")
while True:
task = self.queue.pop()
if task:
try:
self.handle_cancel_task(task)
self.queue.ack(task)
except Exception as e:
print(f"❌ 处理失败: {e}")
else:
self.queue.retry_timeout_tasks()
time.sleep(1)
# 使用示例
if __name__ == "__main__":
r = redis.Redis(host='localhost', port=6379)
order_queue = OrderCancelQueue(r)
# 创建订单(30秒后超时)
order_queue.create_order("ORDER001", timeout_minutes=0.5) # 30秒
order_queue.create_order("ORDER002", timeout_minutes=1) # 60秒
# 10秒后支付第一个订单
time.sleep(10)
order_queue.pay_order("ORDER001")
# 启动消费者(在另一个进程/线程)
order_queue.start_consumer()
运行结果:
📝 订单创建: ORDER001, 0.5分钟后自动取消(如未支付)
📝 订单创建: ORDER002, 1分钟后自动取消(如未支付)
(10秒后)
💰 订单已支付: ORDER001
(30秒后)
📨 收到任务: {'order_id': 'ORDER001', 'action': 'cancel_if_unpaid'}
✅ 订单已支付: ORDER001 (无需取消)
(60秒后)
📨 收到任务: {'order_id': 'ORDER002', 'action': 'cancel_if_unpaid'}
❌ 订单已取消: ORDER002 (超时未支付)
第三部分:进阶优化 🚀
3.1 多队列(按优先级)
class PriorityDelayQueue:
"""支持优先级的延迟队列"""
def __init__(self, redis_client):
self.redis = redis_client
self.queues = {
"high": "delay_queue:high", # 高优先级
"medium": "delay_queue:medium", # 中优先级
"low": "delay_queue:low" # 低优先级
}
def push(self, message, delay_seconds, priority="medium"):
"""
添加任务
:param priority: high/medium/low
"""
execute_time = time.time() + delay_seconds
queue_name = self.queues[priority]
self.redis.zadd(
queue_name,
{json.dumps(message): execute_time}
)
def pop(self):
"""
按优先级取出任务
"""
now = time.time()
# 按优先级顺序检查
for priority in ["high", "medium", "low"]:
queue_name = self.queues[priority]
tasks = self.redis.zrangebyscore(
queue_name,
min=0,
max=now,
start=0,
num=1
)
if tasks:
task = tasks[0]
self.redis.zrem(queue_name, task)
return json.loads(task), priority
return None, None
3.2 批量消费(提高性能)
def pop_batch(self, batch_size=10):
"""
批量取出任务
"""
now = time.time()
# 获取多个任务
tasks = self.redis.zrangebyscore(
self.queue_name,
min=0,
max=now,
start=0,
num=batch_size
)
if not tasks:
return []
# 批量删除
self.redis.zrem(self.queue_name, *tasks)
return [json.loads(task) for task in tasks]
def consume_batch(self):
"""
批量消费
"""
while True:
tasks = self.pop_batch(batch_size=100)
if tasks:
print(f"📨 收到 {len(tasks)} 个任务")
# 并行处理
with ThreadPoolExecutor(max_workers=10) as executor:
executor.map(self.handle_task, tasks)
else:
time.sleep(1)
3.3 监控和统计
def get_stats(self):
"""
获取队列统计信息
"""
now = time.time()
# 总任务数
total = self.redis.zcard(self.queue_name)
# 到期任务数
ready = self.redis.zcount(self.queue_name, 0, now)
# 处理中任务数
processing = self.redis.zcard(self.processing_queue)
# 最早的任务
earliest = self.redis.zrange(self.queue_name, 0, 0, withscores=True)
stats = {
"total": total,
"ready": ready,
"processing": processing,
"waiting": total - ready
}
if earliest:
task, score = earliest[0]
wait_seconds = score - now
stats["next_task_in"] = max(0, wait_seconds)
return stats
# 使用
print(queue.get_stats())
# 输出:
# {
# "total": 1000,
# "ready": 50,
# "processing": 10,
# "waiting": 940,
# "next_task_in": 3.5
# }
第四部分:与专业消息队列对比 🥊
4.1 Redis vs RabbitMQ延迟队列
| 特性 | Redis | RabbitMQ |
|---|---|---|
| 实现难度 | ⭐⭐ 简单 | ⭐⭐⭐ 中等 |
| 延迟精度 | 秒级(轮询间隔) | 毫秒级 |
| 可靠性 | 需要自己实现ACK | 内置支持 |
| 性能 | 极高(10万+/秒) | 中等(1万/秒) |
| 持久化 | 支持(RDB/AOF) | 支持 |
| 适用场景 | 中小型应用 | 大型企业应用 |
4.2 Redis vs RocketMQ延迟消息
| 特性 | Redis | RocketMQ |
|---|---|---|
| 延迟级别 | 任意时间 | 固定18级 |
| 最大延迟 | 无限制 | 2小时 |
| 消息顺序 | 不保证 | 支持 |
| 事务 | Lua脚本 | 分布式事务 |
| 适用场景 | 灵活延迟 | 标准延迟 |
4.3 何时用Redis?
✅ 适合用Redis:
- 延迟时间需要灵活控制
- 数据量不是特别大(百万级以下)
- 已经有Redis,不想引入新组件
- 对性能要求高
- 可以接受秒级精度
❌ 不适合用Redis:
- 需要毫秒级精度
- 消息量特别大(千万级以上)
- 需要复杂的消息路由
- 需要强一致性保证
第五部分:生产环境最佳实践 💼
5.1 完整的生产级实现
import redis
import time
import json
import logging
from concurrent.futures import ThreadPoolExecutor
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class ProductionDelayQueue:
"""
生产级延迟队列
特性:
1. ACK机制(防止任务丢失)
2. 超时重试
3. 死信队列
4. 监控统计
5. 优雅关闭
"""
def __init__(self, redis_client, queue_name="delay_queue"):
self.redis = redis_client
self.queue_name = queue_name
self.processing_queue = queue_name + ":processing"
self.dead_letter_queue = queue_name + ":dlq"
self.retry_limit = 3
self.running = False
# Lua脚本
self._init_scripts()
def _init_scripts(self):
"""初始化Lua脚本"""
# 原子性pop脚本
self.pop_script = self.redis.register_script("""
local queue = KEYS[1]
local processing = KEYS[2]
local now = tonumber(ARGV[1])
local timeout = tonumber(ARGV[2])
local tasks = redis.call('ZRANGEBYSCORE', queue, 0, now, 'LIMIT', 0, 1)
if #tasks == 0 then
return nil
end
local task = tasks[1]
redis.call('ZREM', queue, task)
redis.call('ZADD', processing, now + timeout, task)
return task
""")
def push(self, message, delay_seconds, retry_count=0):
"""添加任务"""
execute_time = time.time() + delay_seconds
task_data = {
"message": message,
"retry_count": retry_count,
"create_time": time.time()
}
self.redis.zadd(
self.queue_name,
{json.dumps(task_data): execute_time}
)
logger.info(f"Task added: {message}, delay: {delay_seconds}s")
def pop(self, timeout=300):
"""取出任务"""
now = time.time()
task_str = self.pop_script(
keys=[self.queue_name, self.processing_queue],
args=[now, timeout]
)
if task_str:
return json.loads(task_str)
return None
def ack(self, task_data):
"""确认任务完成"""
task_str = json.dumps(task_data)
removed = self.redis.zrem(self.processing_queue, task_str)
if removed:
logger.info(f"Task acknowledged: {task_data['message']}")
return removed
def nack(self, task_data, delay_seconds=60):
"""
拒绝任务(重试)
"""
task_str = json.dumps(task_data)
self.redis.zrem(self.processing_queue, task_str)
retry_count = task_data.get("retry_count", 0) + 1
if retry_count <= self.retry_limit:
# 重新加入队列(指数退避)
delay = delay_seconds * (2 ** (retry_count - 1))
self.push(
task_data["message"],
delay_seconds=delay,
retry_count=retry_count
)
logger.warning(f"Task retrying: {task_data['message']}, retry: {retry_count}/{self.retry_limit}")
else:
# 超过重试次数,加入死信队列
self.redis.lpush(self.dead_letter_queue, task_str)
logger.error(f"Task moved to DLQ: {task_data['message']}")
def retry_timeout_tasks(self):
"""重试超时任务"""
now = time.time()
timeout_tasks = self.redis.zrangebyscore(
self.processing_queue,
min=0,
max=now
)
for task_str in timeout_tasks:
task_data = json.loads(task_str)
self.redis.zrem(self.processing_queue, task_str)
logger.warning(f"Task timeout, retrying: {task_data['message']}")
self.nack(task_data)
def get_stats(self):
"""获取统计信息"""
return {
"queue_size": self.redis.zcard(self.queue_name),
"processing": self.redis.zcard(self.processing_queue),
"dead_letter": self.redis.llen(self.dead_letter_queue)
}
def start(self, handler, num_workers=10):
"""
启动消费者
:param handler: 任务处理函数
:param num_workers: 工作线程数
"""
self.running = True
logger.info(f"Delay queue started with {num_workers} workers")
with ThreadPoolExecutor(max_workers=num_workers) as executor:
while self.running:
try:
task_data = self.pop()
if task_data:
# 提交任务到线程池
executor.submit(
self._process_task,
task_data,
handler
)
else:
# 检查超时任务
self.retry_timeout_tasks()
# 打印统计
if int(time.time()) % 60 == 0:
logger.info(f"Stats: {self.get_stats()}")
time.sleep(1)
except Exception as e:
logger.error(f"Consumer error: {e}")
time.sleep(1)
def _process_task(self, task_data, handler):
"""处理任务"""
try:
message = task_data["message"]
logger.info(f"Processing task: {message}")
# 执行业务逻辑
handler(message)
# 确认任务完成
self.ack(task_data)
except Exception as e:
logger.error(f"Task processing failed: {e}")
# 拒绝任务(重试)
self.nack(task_data)
def stop(self):
"""优雅关闭"""
logger.info("Stopping delay queue...")
self.running = False
# 使用示例
def handle_order_cancel(message):
"""订单取消处理器"""
order_id = message["order_id"]
print(f"Cancelling order: {order_id}")
time.sleep(2) # 模拟处理
print(f"Order cancelled: {order_id}")
if __name__ == "__main__":
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
queue = ProductionDelayQueue(r, "order:cancel")
# 添加任务
queue.push({"order_id": "ORDER001"}, delay_seconds=10)
queue.push({"order_id": "ORDER002"}, delay_seconds=20)
# 启动消费者
queue.start(handler=handle_order_cancel, num_workers=5)
5.2 监控脚本
import redis
import time
def monitor_delay_queue(redis_client, queue_name, interval=10):
"""监控延迟队列"""
while True:
stats = {
"queue_size": redis_client.zcard(queue_name),
"processing": redis_client.zcard(queue_name + ":processing"),
"dlq": redis_client.llen(queue_name + ":dlq")
}
# 获取即将执行的任务数(未来1分钟)
now = time.time()
upcoming = redis_client.zcount(queue_name, now, now + 60)
print(f"""
╔══════════════════════════════════╗
║ Delay Queue Monitor ║
╠══════════════════════════════════╣
║ Queue Size: {stats['queue_size']:>6} ║
║ Processing: {stats['processing']:>6} ║
║ Dead Letter: {stats['dlq']:>6} ║
║ Upcoming (1min): {upcoming:>6} ║
╚══════════════════════════════════╝
""")
time.sleep(interval)
🎓 总结:延迟队列选型
[需要延迟队列?]
|
┌──────┴──────┐
↓ ↓
[数据量多大?] [精度要求?]
| |
< 百万级 秒级可接受
↓ ↓
[Redis延迟队列] [Redis]
> 千万级 毫秒级
↓ ↓
[RocketMQ] [RabbitMQ]
记忆口诀 🎵
延迟队列有妙用,
定时任务它能行。
ZSet分数存时间,
到期任务自动触。
原子操作用Lua脚本,
并发竞争不用愁。
ACK机制防丢失,
超时重试更可靠。
死信队列兜底线,
重试次数要限制。
监控统计不能少,
生产环境要完善!
面试要点 ⭐
- 实现原理:Sorted Set,score存时间戳,ZRANGEBYSCORE取出
- 并发问题:Lua脚本保证原子性
- 可靠性:ACK机制、超时重试、死信队列
- 性能优化:批量消费、多线程处理
- 适用场景:订单超时、定时提醒、延迟重试
- 对比其他:vs RabbitMQ、RocketMQ的优劣
最后总结:
Redis延迟队列就像智能闹钟 ⏰:
- Sorted Set = 闹钟列表(按时间排序)
- 消费者 = 你(定时检查闹钟)
- ACK = 关闭闹钟(确认已响)
- 重试 = 贪睡模式(5分钟后再响)
记住:简单场景用Redis,复杂场景用专业MQ! 🎯
加油,分布式系统架构师!💪