【Flask 进阶】3 从同步到异步:基于 Redis 任务队列解决 API 高并发与长耗时任务阻塞

在之前的章节中,我们实现了🐼基于文件/数据库授权的 Flask API。这种模式在处理"毫秒级"任务(如简单的签名生成、数据查询)时表现良好。

🐬然而在真实的生产环境中,可能面临:

a)长耗时任务: 如果核心算法涉及深度学习推理、大规模数据转换或第三方API调用,单次处理时间可能长达几十秒,在同步架构下,这意味着整个后端线程会被该请求死死占用;

b)高并发压力:请求量激增时,即使每个任务都耗时很短,有限的数据库连接和服务器线程也会瞬间填满,导致系统响应剧烈抖动甚至瘫痪。

异步任务队列架构

当 API 面临耗时较长的核心算法或极高的瞬时请求压力时,传统的"同步阻塞"模式会导致服务器资源迅速耗尽。为此,我们引入了如下图所示的异步解耦架构

🙋**调用者:**携带参数发起请求

💻**API:**负责"接单"。

++①任务入队++ :将任务参数包装成一个消息,推送到任务队列(Redis或RabbitMQ)中;

++②响应凭证++ :迅速向调用者返回一个响应(通常包含一个 task_id),告知:"任务已受理,正在处理"。此时 API与调用者的连接立即断开,释放资源去接待下一个用户。

📁**任务队列:**作为消息的中转站,负责暂存所有待处理的任务。

📔**worker:**一个或多个独立的 worker进程:

① 持续监听任务队列。一旦空闲,就从任务队列中取出一个任务

② 处理完成后将结果写入结果队列(Result Backend),并标记任务状态"已完成"。

📝**结果获取:**调用者通过之前拿到的task_id 回来查询任务进度,或者由系统通过Webhook 等方式主动将结果推送给调用者。

1 API 实现

python 复制代码
import json
import uuid
import redis
from flask import Flask, request, jsonify

# 创建按一个Flask实例对象
app = Flask(__name__)

@app.route("/task", methods=["POST"])
def task():
    '''
    请求的数据格式要求: {"ordered_string": "......"}
    '''
    ordered_string = request.json.get('ordered_string')
    if not ordered_string:
        return jsonify({"status":False, "error":"参数错误"})

    # 创建任务 ID
    tid = str(uuid.uuid4())

    # 1.加入redis任务队列
    task_dict = {'tid': tid, 'data': ordered_string}
    REDIS_CONN_PARAMS = {
        "host": "127.0.0.1",
        "password": "123456",
        "port": 6379,
        "encoding": 'utf-8'
    }
    conn = redis.Redis(**REDIS_CONN_PARAMS)
    conn.lpush('spider_task_list', json.dumps(task_dict))

    # 2. 返回给调用者
    return jsonify({'status':True, 'data':tid, 'message':'正在处理中,预计1分钟完成'})

if __name__ == '__main__':
    app.run()

注意运行项目时要启动 Redis 服务:

python 复制代码
redis-server.exe redis.windows.conf

// 运行效果:


2 Worker 实现

角色职责:去队列中获取任务,执行并写入到结果队列。

python 复制代码
import hashlib
import json
import redis

def get_task():
    REDIS_CONN_PARAMS = {
        "host": "127.0.0.1",
        "password": "123456",
        "port": 6379,
        "encoding": "utf-8"
    }
    conn = redis.Redis(**REDIS_CONN_PARAMS)
    data = conn.brpop("spider_task_list", timeout=10)
    if not data:
        return None
    return json.loads(data[1].decode('utf-8'))

def set_result(tid, value):
    # 建立连接
    REDIS_CONN_PARAMS = {"host": "127.0.0.1", "password": "123456", "port": 6379, "encoding": "utf-8"}
    conn = redis.Redis(**REDIS_CONN_PARAMS)
    # 存入结果
    conn.hset("spider_task_result", tid, value)

def run():
    while True:
        # 1.获取任务
        task_dict = get_task()
        print(task_dict)
        if not task_dict:
            continue

        # 2. 执行任务处理
        # {'tid':'...', 'data':'......'}
        ordered_string = task_dict['data']
        encrypt_string = ordered_string + "123456789"
        obj = hashlib.md5(encrypt_string.encode('utf-8'))
        sign = obj.hexdigest()

        # 3.写入到结果队列(redis_result)
        tid = task_dict['tid']
        set_result(tid, sign)

if __name__ == '__main__':
    run()

// 运行效果:

3 调用者获取结果

在app.py添加视图函数 result() 用于处理:用户拿着tid查询指定任务的处理结果。

获取参数 tid,链连接 redis-结果队列,从Hash中获取结果后,在原来字典中删除相应记录。

python 复制代码
@app.route("/result", methods=["GET"])
def result():
    '''
    请求的url格式:/result?tid=......
    '''
    tid = request.args.get('tid')
    if not tid:
        return jsonify({'status': False, 'error': "参数错误"})

    # 查询结果队列
    conn = redis.Redis(host="127.0.0.1", password="123456", port=6379)
    # 从 Hash 中获取结果
    sign = conn.hget('spider_task_result', tid)

    if not sign:
        return jsonify({"status": True, 'data':"", "message":"未完成,请继续等待"})

    sign_str = sign.decode('utf-8')         # 获取的字节结果转化为字符串
    conn.hdel('spider_task_result', tid)    # 取出即焚

    return jsonify({"status": True, "data": sign})

// 运行效果:

优化

可以看redis的连接代码是有重复的,为了更加简洁与高效,我们考虑将redis的连接以及队列的名称 写成全局,另外redis的官方也提供了相关的连接池。

下面是**++使用redis连接池实现内置键值对数据库连接++**的优化代码:

Flask API部分代码如下:(Worker对应更改即可)

python 复制代码
import json
import uuid
import redis
from flask import Flask, request, jsonify

app = Flask(__name__)

REDIS_POOL = redis.ConnectoinPool(host='127.0.0.1', port=6379, password='123456', encoding='utf-8')
TASK_QUEUE = "spider_task_list"
RESULT_QUEUE = "spider_result_dict"

@app.route("/task", methods=["POST"])
def task():
    '''
    请求的数据格式要求: {"ordered_string": "......"}
    '''
    ordered_string = request.json.get('ordered_string')
    if not ordered_string:
        return jsonify({"status":False, "error":"参数错误"})

    # 创建任务 ID
    tid = str(uuid.uuid4())

    # 1.加入redis任务队列
    task_dict = {'tid': tid, 'data': ordered_string}
    conn = redis.Redis(connection_pool=REDIS_POOL)
    conn.lpush(TASK_QUEUE, json.dumps(task_dict))

    # 2. 返回给调用者
    return jsonify({'status':True, 'data':tid, 'message':'正在处理中,预计1分钟完成'})

@app.route("/result", methods=["GET"])
def result():
    '''
    请求的url格式:/result?tid=......
    '''
    tid = request.args.get('tid')
    if not tid:
        return jsonify({'status': False, 'error': "参数错误"})

    # 查询结果队列
    conn = redis.Redis(connection_pool=REDIS_POOL)
    # 从 Hash 中获取结果
    sign = conn.hget(RESULT_QUEUE, tid)

    if not sign:
        return jsonify({"status": True, 'data':"", "message":"未完成,请继续等待"})

    sign_str = sign.decode('utf-8')                     # 获取的字节结果转化为字符串
    print(sign)
    conn.hdel(RESULT_QUEUE, tid)    # 取出即焚

    return jsonify({"status": True, "data": sign_str})

if __name__ == '__main__':
    app.run()

笔记便利贴

🎤问题1:代码过程中混淆了 json.dump() 和 json.dumps()而导致报错,区分如下:

  • json.dump(obj, fp):用于将对象序列化并写入文件,它需要第二个参数 fp(文件指针);
  • json.dumps(obj) :用于将对象序列化为字符串(S代表String)

🎤问题2:Redis(Remote Dictionary Server)

Redis是一个开源的、高性能的内存键值对数据库。具有如下核心特征:

📝注意事项:

① 队列的存放与取出要相对应,比如 hget()与hset(),lpush()-lpop()-rpush-rpop()等

② 若要清除 redis 中清除掉旧的错误类型的 Key:

python 复制代码
DEL spider_task_result    # DEL Key的名字

总结

本文介绍了基于Redis实现的异步任务队列架构,用于解决Flask API在处理长耗时任务和高并发请求时的性能瓶颈。通过将任务拆分为API接收、Worker处理、API返回 三个模块,实现了请求的异步处理。

API接收请求后将任务参数存入Redis队列并立即返回任务ID,Worker进程持续监听队列并处理任务,最终将结果存入Redis。用户可通过任务ID查询处理结果。

文章详细展示了Flask API、Worker处理程序的具体实现代码,并提供了Redis连接池优化方案,同时指出了JSON序列化方法区别和Redis数据结构操作的注意事项。该架构有效解决了同步阻塞问题,提高了系统吞吐量和响应能力。

相关推荐
pchaoda2 小时前
基本面因子计算入门
python·matplotlib·量化
Wpa.wk2 小时前
接口自动化测试 - 请求构造和响应断言 -Rest-assure
开发语言·python·测试工具·接口自动化
岱宗夫up2 小时前
机器学习:标准化流模型(NF)
人工智能·python·机器学习·生成对抗网络
狂奔蜗牛飙车2 小时前
Python学习之路-循环语句学习详解
python·学习·python学习·#python学习笔记·循环语句详解
花月mmc2 小时前
CanMV K230 波形识别——整体部署(4)
人工智能·python·嵌入式硬件·深度学习·信号处理
lang201509282 小时前
Java WebSocket API:JSR-356详解
java·python·websocket
这周也會开心2 小时前
Redis与MySQL回写中的数据类型存储设计
数据库·redis·mysql
jiang_changsheng2 小时前
环境管理工具全景图与深度对比
java·c语言·开发语言·c++·python·r语言
linjoe993 小时前
【Medical AI\pathology】WSI 的 JPEG 压缩质量与存储效率权衡分析
python·图像压缩·计算病理学·wsi