Redis 延迟任务队列:凌晨3点服务器报警的救星

18 阅读7分钟

Redis 延迟任务队列:凌晨3点服务器报警的救星

凌晨3点,你被手机的震动声惊醒,服务器报警提示某个重要任务没有按时完成。你迅速打开电脑,登录系统,发现任务调度服务又挂了。这次,你决定不再忍受这种痛苦,而是要寻找一个更可靠的解决方案。是的,我们可以用 Redis 来实现一个延迟任务队列,确保任务按时执行,避免这类问题再次发生。

在开始之前,确保你已经安装并运行了 Redis。你可以通过以下命令启动 Redis 服务器:

redis-server

场景背景

假设你正在运行一个电商系统,每天凌晨需要生成前一天的销售报告。这个过程不仅耗时,还可能消耗大量资源,影响其他服务的正常运行。你需要一个可靠的延迟任务队列来确保这个任务可以在每天的固定时间执行,而不影响系统的整体性能。

使用 Redis 实现延迟任务队列

Redis 本身并不直接支持延迟任务,但我们可以利用 Redis 的有序集合(Sorted Set)来实现这一功能。有序集合的每个元素都有一个分数(score),我们可以将任务的执行时间作为分数,将任务的数据作为元素值。通过这种方式,Redis 可以帮助我们管理和排序这些任务。

关键步骤

我们需要实现以下几个关键步骤:

  1. 将任务添加到延迟队列中
  2. 定期检查队列,找出需要执行的任务
  3. 执行任务并将结果记录或处理

步骤1:将任务添加到延迟队列中

我们可以使用 ZADD 命令将任务添加到有序集合中。假设我们要在明天凌晨3点执行一个生成销售报告的任务,可以这样操作:

import redis
import time

# 连接 Redis
r = redis.Redis(host='localhost', port=6379, db=0)

# 当前时间(秒)
current_time = int(time.time())

# 任务执行时间(明天凌晨3点)
execution_time = current_time + (24 * 60 * 60 - (current_time % (24 * 60 * 60)) + 3 * 60 * 60)

# 任务数据
task_data = '生成销售报告'

# 将任务添加到延迟队列
r.zadd('delayed_tasks', {task_data: execution_time})

# 检查任务是否成功添加
print(r.zrange('delayed_tasks', 0, -1, withscores=True))

步骤2:定期检查队列,找出需要执行的任务

我们可以使用一个定时任务(例如每分钟运行一次)来检查队列中是否有任务需要执行。这里我们使用 Python 的 schedule 库来实现定时任务:

import redis
import schedule
import time

# 连接 Redis
r = redis.Redis(host='localhost', port=6379, db=0)

def process_tasks():
    # 获取当前时间(秒)
    current_time = int(time.time())

    # 从延迟队列中取出所有分数小于等于当前时间的任务
    tasks_to_execute = r.zrangebyscore('delayed_tasks', 0, current_time, withscores=True)

    # 处理每个任务
    for task, score in tasks_to_execute:
        print(f'执行任务: {task.decode()},计划时间: {score}')
        # 执行任务的逻辑(这里假设调用一个函数来生成销售报告)
        generate_sales_report(task.decode())

        # 从队列中移除已经执行的任务
        r.zrem('delayed_tasks', task)

def generate_sales_report(task_data):
    # 模拟生成销售报告的逻辑
    print(f'生成销售报告: {task_data}')

# 每分钟检查一次任务队列
schedule.every(1).minutes.do(process_tasks)

# 启动调度器
while True:
    schedule.run_pending()
    time.sleep(1)

步骤3:执行任务并将结果记录或处理

generate_sales_report 函数中,你可以实现具体的任务逻辑。这里我们只是简单地打印任务数据,实际生产环境中,你可能需要调用数据库查询、生成文件、发送邮件等操作。

实际应用

假设你的电商系统每天凌晨3点需要生成前一天的销售报告,并将报告发送到指定邮箱。你可以将上述代码稍作修改,加入实际的业务逻辑:

import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from datetime import datetime

def generate_sales_report(task_data):
    # 模拟生成销售报告的逻辑
    report = f'销售报告: {task_data} - 生成时间: {datetime.now()}'

    # 将报告发送到指定邮箱
    send_email('sales_report@example.com', '销售报告', report)

def send_email(to_email, subject, body):
    from_email = 'your_email@example.com'
    from_password = 'your_password'

    msg = MIMEMultipart()
    msg['From'] = from_email
    msg['To'] = to_email
    msg['Subject'] = subject

    msg.attach(MIMEText(body, 'plain'))

    # 连接 SMTP 服务器并发送邮件
    server = smtplib.SMTP('smtp.example.com', 587)
    server.starttls()
    server.login(from_email, from_password)
    text = msg.as_string()
    server.sendmail(from_email, to_email, text)
    server.quit()

# 每分钟检查一次任务队列
schedule.every(1).minutes.do(process_tasks)

# 启动调度器
while True:
    schedule.run_pending()
    time.sleep(1)

进阶功能:任务重试

实际生产环境中,任务可能会由于各种原因失败。为了增加系统的可靠性,我们可以实现任务重试机制。我们可以将失败的任务重新添加到延迟队列中,并设置一个重试次数限制:

import redis
import schedule
import time

# 连接 Redis
r = redis.Redis(host='localhost', port=6379, db=0)

def process_tasks():
    # 获取当前时间(秒)
    current_time = int(time.time())

    # 从延迟队列中取出所有分数小于等于当前时间的任务
    tasks_to_execute = r.zrangebyscore('delayed_tasks', 0, current_time, withscores=True)

    # 处理每个任务
    for task, score in tasks_to_execute:
        task_data = task.decode()
        print(f'执行任务: {task_data},计划时间: {score}')

        # 尝试执行任务
        try:
            generate_sales_report(task_data)
            # 从队列中移除已经成功执行的任务
            r.zrem('delayed_tasks', task)
        except Exception as e:
            print(f'任务执行失败: {e}')
            # 任务失败,记录失败次数
            fail_count = r.hincrby('task_failures', task_data, 1)
            print(f'任务 {task_data} 已失败 {fail_count} 次')

            # 如果失败次数超过限制,不再重试
            if fail_count > 3:
                print(f'任务 {task_data} 达到最大重试次数,不再重试')
            else:
                # 重新设置任务的执行时间,延迟5分钟重试
                retry_time = score + 5 * 60
                r.zadd('delayed_tasks', {task: retry_time})

def generate_sales_report(task_data):
    # 模拟生成销售报告的逻辑
    report = f'销售报告: {task_data} - 生成时间: {datetime.now()}'

    # 将报告发送到指定邮箱
    send_email('sales_report@example.com', '销售报告', report)

def send_email(to_email, subject, body):
    from_email = 'your_email@example.com'
    from_password = 'your_password'

    msg = MIMEMultipart()
    msg['From'] = from_email
    msg['To'] = to_email
    msg['Subject'] = subject

    msg.attach(MIMEText(body, 'plain'))

    # 连接 SMTP 服务器并发送邮件
    server = smtplib.SMTP('smtp.example.com', 587)
    server.starttls()
    server.login(from_email, from_password)
    text = msg.as_string()
    server.sendmail(from_email, to_email, text)
    server.quit()

# 每分钟检查一次任务队列
schedule.every(1).minutes.do(process_tasks)

# 启动调度器
while True:
    schedule.run_pending()
    time.sleep(1)

常见问题与优化

  1. 任务堆积:如果任务队列中的任务太多,可能会导致处理速度跟不上。可以考虑增加多个处理任务的消费者,或者优化任务执行的效率。
  2. 时间精度:Redis 的时间精度为秒,对于需要毫秒级精度的任务,可以考虑使用其他工具或库。
  3. 任务去重:为了防止重复任务,可以在添加任务时检查任务是否已经存在。可以使用 Redis 的 ZSCORE 命令来实现:
    if not r.zscore('delayed_tasks', task_data):
        r.zadd('delayed_tasks', {task_data: execution_time})
    

代码优化

为了提高代码的可读性和可维护性,我们可以将任务处理逻辑封装成一个类:

import redis
import schedule
import time
from datetime import datetime
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart

class DelayedTaskQueue:
    def __init__(self, host='localhost', port=6379, db=0):
        self.r = redis.Redis(host=host, port=port, db=db)

    def add_task(self, task_data, execution_time):
        if not self.r.zscore('delayed_tasks', task_data):
            self.r.zadd('delayed_tasks', {task_data: execution_time})

    def process_tasks(self):
        current_time = int(time.time())
        tasks_to_execute = self.r.zrangebyscore('delayed_tasks', 0, current_time, withscores=True)

        for task, score in tasks_to_execute:
            task_data = task.decode()
            print(f'执行任务: {task_data},计划时间: {score}')

            try:
                self.generate_sales_report(task_data)
                self.r.zrem('delayed_tasks', task)
            except Exception as e:
                print(f'任务执行失败: {e}')
                fail_count = self.r.hincrby('task_failures', task_data, 1)
                print(f'任务 {task_data} 已失败 {fail_count} 次')

                if fail_count > 3:
                    print(f'任务 {task_data} 达到最大重试次数,不再重试')
                else:
                    retry_time = score + 5 * 60
                    self.r.zadd('delayed_tasks', {task: retry_time})

    def generate_sales_report(self, task_data):
        report = f'销售报告: {task_data} - 生成时间: {datetime.now()}'
        self.send_email('sales_report@example.com', '销售报告', report)

    def send_email(self, to_email, subject, body):
        from_email = 'your_email@example.com'
        from_password = 'your_password'

        msg = MIMEMultipart()
        msg['From'] = from_email
        msg['To'] = to_email
        msg['Subject'] = subject

        msg.attach(MIMEText(body, 'plain'))

        server = smtplib.SMTP('smtp.example.com', 587)
        server.starttls()
        server.login(from_email, from_password)
        text = msg.as_string()
        server.sendmail(from_email, to_email, text)
        server.quit()

# 创建任务队列实例
task_queue = DelayedTaskQueue()

# 添加一个任务
task_queue.add_task('生成销售报告', int(time.time()) + 60)

# 每分钟检查一次任务队列
schedule.every(1).minutes.do(task_queue.process_tasks)

# 启动调度器
while True:
    schedule.run_pending()
    time.sleep(1)

结束语

通过上述步骤,你已经成功使用 Redis 实现了一个可靠的延迟任务队列。下次凌晨3点,你就可以安心睡觉了,因为系统会自动处理那些重要的任务。

如果你还需要更多工具来帮助你调试 Cron 表达式、正则表达式、JSON 格式化等,不妨试试 Hey Cron。这个免费在线工具网站提供了 Cron 表达式生成器、正则表达式生成器、中英互译、JSON 格式化、Base64 编码解码和时间戳转换等多种实用功能,助你高效开发和调试。希望这些工具能为你的开发工作带来便利!