0

0

如何在异步环境中安全地实现线程与协程并发下的 HTTP 缓存类

霞舞

霞舞

发布时间:2026-01-03 13:29:02

|

347人浏览过

|

来源于php中文网

原创

如何在异步环境中安全地实现线程与协程并发下的 HTTP 缓存类

本文详解如何基于 `aiohttp` 和 `asyncio` 构建线程安全、协程安全的单例 http 缓存类,重点解决并发请求同一 url 时的重复拉取问题,并优化时间精度与资源竞争控制。

在使用 aiohttp 构建异步 HTTP 缓存服务(如配合 Tornado 或 FastAPI)时,一个常见误区是认为“只要用了 async/await 就天然线程安全”。实际上,Python 的 GIL 仅保障纯 Python 字节码层面的线程互斥,但无法阻止 asyncio 任务在单线程内并发修改共享状态(如 dict)所引发的逻辑竞态——尤其是当多个协程同时检测到缓存过期并触发 _fetch_update(url) 时,可能造成多次重复请求,浪费资源且增加服务压力。

✅ 核心问题:不是“数据损坏”,而是“逻辑冗余”

原代码中 self._cache[url] = {...} 虽为原子操作(CPython 中 dict 赋值是线程安全的),但其前置判断 url not in self._cache or ... 与后续写入之间存在时间窗口。若两个协程几乎同时执行该判断,均得出“需更新”结论,则会并发执行两次 session.get(),导致:

  • 同一 URL 被重复请求;
  • 后完成的响应覆盖先完成的(无一致性保证);
  • 错误日志混乱,难以调试。

这不是内存损坏,却是典型的 “check-then-act” 竞态(TOCTOU),必须通过同步机制消除。

墨刀AIPPT
墨刀AIPPT

排版/配图/美化一键优化,3分钟产出专业级PPT

下载

✅ 正确解法:按 URL 细粒度协同锁(Per-URL Coordinated Fetching)

我们不采用全局 asyncio.Lock()(会串行化所有 URL 请求,严重损害并发性能),而是为每个待请求的 URL 动态维护一个 asyncio.Event,实现按 URL 粒度的协同等待

import asyncio
import logging
import aiohttp
import time

DEFAULT_TIMEOUT = 20
HTTP_READ_TIMEOUT = 1

class HTTPRequestCache:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._cache = {}
            cls._instance._time_out = DEFAULT_TIMEOUT
            cls._instance._http_read_timeout = HTTP_READ_TIMEOUT
            cls._instance._fetching_now = {}  # {url: asyncio.Event()}
            cls._instance._lock = asyncio.Lock()  # 仅用于保护 _fetching_now 字典本身
        return cls._instance

    async def _fetch_update(self, url):
        # Step 1: 获取对 _fetching_now 的独占访问权,检查/注册当前 URL 的 fetch 状态
        async with self._lock:
            if url in self._fetching_now:
                # 另一协程已在处理该 URL → 等待其完成
                event = self._fetching_now[url]
                await event.wait()
                # 检查是否已成功缓存(避免重复等待后仍去请求)
                if url in self._cache and self._cache[url]["cached_at"] >= time.monotonic() - self._time_out:
                    return
            else:
                # 首次标记该 URL 正在被获取
                self._fetching_now[url] = asyncio.Event()

        # Step 2: 执行实际 HTTP 请求(此时无锁,允许多 URL 并发)
        try:
            async with aiohttp.ClientSession() as session:
                logging.info(f"Fetching {url}")
                async with session.get(url, timeout=self._http_read_timeout) as resp:
                    resp.raise_for_status()
                    data = await resp.json()
                    cached_at = time.monotonic()  # ✅ 使用 monotonic 时间,避免系统时钟跳变影响
                    self._cache[url] = {
                        "cached_at": cached_at,
                        "config": data,
                        "errors": 0
                    }
                    logging.info(f"Updated cache for {url}")
        except aiohttp.ClientError as e:
            logging.error(f"Failed to fetch {url}: {e}")
        finally:
            # Step 3: 清理状态,通知所有等待者
            async with self._lock:
                if url in self._fetching_now:
                    self._fetching_now[url].set()
                    del self._fetching_now[url]

    async def get(self, url):
        # 使用 monotonic 时间进行过期判断,更可靠
        now = time.monotonic()
        if (url not in self._cache 
            or self._cache[url]["cached_at"] < now - self._time_out):
            await self._fetch_update(url)
        return self._cache.get(url, {}).get("config")

? 关键设计说明

  • _lock 仅保护 _fetching_now 字典读写:因 dict 操作本身非原子(如 in + [] 组合),需锁保护其结构一致性。
  • asyncio.Event 实现“等待即订阅”:协程发现 URL 正在被获取时,直接 await event.wait(),无需轮询;完成时 event.set() 唤醒全部等待者。
  • time.monotonic() 替代 time.time():确保超时计算不受系统时间回拨(如 NTP 校准、DST 切换)干扰,符合高可靠性场景要求。
  • 无全局阻塞:不同 URL 的请求完全并发,仅相同 URL 的请求被智能协调,兼顾性能与正确性。

⚠️ 注意事项与扩展建议

  • Tornado 兼容性:Tornado 6+ 原生支持 async/await,可直接将 HTTPRequestCache 实例挂载为应用级单例(如 app.settings['cache']),无需额外线程适配。
  • 错误重试策略:当前示例未集成指数退避或错误计数(如 MAX_ERRORS)。如需增强鲁棒性,可在 except 块中增加 self._cache[url]["errors"] += 1,并在 get() 中根据错误次数决定是否跳过缓存或降级返回。
  • 内存清理:长期运行需添加 LRU 或 TTL 驱逐逻辑(如定期扫描 cached_at 过期项),防止内存泄漏。
  • 多进程场景:若部署于多进程(如 Gunicorn + --workers),单例失效,需改用 Redis 等外部缓存。

此方案在保持异步高性能的同时,彻底消除了缓存更新的逻辑竞态,是构建生产级异步 HTTP 客户端缓存的推荐实践。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
Python FastAPI异步API开发_Python怎么用FastAPI构建异步API
Python FastAPI异步API开发_Python怎么用FastAPI构建异步API

Python FastAPI 异步开发利用 async/await 关键字,通过定义异步视图函数、使用异步数据库库 (如 databases)、异步 HTTP 客户端 (如 httpx),并结合后台任务队列(如 Celery)和异步依赖项,实现高效的 I/O 密集型 API,显著提升吞吐量和响应速度,尤其适用于处理数据库查询、网络请求等耗时操作,无需阻塞主线程。

28

2025.12.22

Python 微服务架构与 FastAPI 框架
Python 微服务架构与 FastAPI 框架

本专题系统讲解 Python 微服务架构设计与 FastAPI 框架应用,涵盖 FastAPI 的快速开发、路由与依赖注入、数据模型验证、API 文档自动生成、OAuth2 与 JWT 身份验证、异步支持、部署与扩展等。通过实际案例,帮助学习者掌握 使用 FastAPI 构建高效、可扩展的微服务应用,提高服务响应速度与系统可维护性。

251

2026.02.06

session失效的原因
session失效的原因

session失效的原因有会话超时、会话数量限制、会话完整性检查、服务器重启、浏览器或设备问题等等。详细介绍:1、会话超时:服务器为Session设置了一个默认的超时时间,当用户在一段时间内没有与服务器交互时,Session将自动失效;2、会话数量限制:服务器为每个用户的Session数量设置了一个限制,当用户创建的Session数量超过这个限制时,最新的会覆盖最早的等等。

334

2023.10.17

session失效解决方法
session失效解决方法

session失效通常是由于 session 的生存时间过期或者服务器关闭导致的。其解决办法:1、延长session的生存时间;2、使用持久化存储;3、使用cookie;4、异步更新session;5、使用会话管理中间件。

775

2023.10.18

cookie与session的区别
cookie与session的区别

本专题整合了cookie与session的区别和使用方法等相关内容,阅读专题下面的文章了解更详细的内容。

97

2025.08.19

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

743

2023.08.10

常用的数据库软件
常用的数据库软件

常用的数据库软件有MySQL、Oracle、SQL Server、PostgreSQL、MongoDB、Redis、Cassandra、Hadoop、Spark和Amazon DynamoDB。更多关于数据库软件的内容详情请看本专题下面的文章。php中文网欢迎大家前来学习。

1003

2023.11.02

内存数据库有哪些
内存数据库有哪些

内存数据库有Redis、Memcached、Apache Ignite、VoltDB、TimesTen、H2 Database、Aerospike、Oracle TimesTen In-Memory Database、SAP HANA和ache Cassandra。更多关于内存数据库相关问题,详情请看本专题下面的文章。php中文网欢迎大家前来学习。

669

2023.11.14

JavaScript浏览器渲染机制与前端性能优化实践
JavaScript浏览器渲染机制与前端性能优化实践

本专题围绕 JavaScript 在浏览器中的执行与渲染机制展开,系统讲解 DOM 构建、CSSOM 解析、重排与重绘原理,以及关键渲染路径优化方法。内容涵盖事件循环机制、异步任务调度、资源加载优化、代码拆分与懒加载等性能优化策略。通过真实前端项目案例,帮助开发者理解浏览器底层工作原理,并掌握提升网页加载速度与交互体验的实用技巧。

23

2026.03.06

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
最新Python教程 从入门到精通
最新Python教程 从入门到精通

共4课时 | 22.5万人学习

Django 教程
Django 教程

共28课时 | 4.8万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.8万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号