REDIS × ASYNC PYTHON

连接池与超时预算实验台

把连接想成有限的水管:请求可以一起涌来,但同一时刻真正流向 Redis 的命令,不能超过管道数量。调一调阀门,看看排队从什么时候开始变成超时。

0.0× 每轮请求 / 连接

请求流量观察窗

虚拟时间:0 ms
最终完成0
峰值排队0
借连接超时0
命令超时0

每个圆点就是一个请求

准备模拟

先分清:到底是谁超时

请求到达借到连接拿到响应
  • 借连接等待:池满时,请求先排队。实验里的“连接超时”就是这段等待预算,可映射到 BlockingConnectionPool(timeout=...)
  • 命令执行:借到连接以后,socket_timeout 约束读写 Redis 响应的等待时间;它不是总请求时限。
  • TCP 建连:socket_connect_timeout 约束新连接建立过程,和“池里暂时没有空闲连接”不是一回事。
  • 相对结果:本实验把所有请求同时到达、命令耗时固定,并忽略网络抖动、连接重建与事件循环调度,适合比较参数方向,不等于生产压测数字。
为什么连接数不是越多越好?

连接过少会把压力留在应用队列里;连接过多则可能把压力一股脑推给 Redis,增加文件描述符、服务端连接和并发工作的负担。更可靠的做法,是结合实例容量、命令延迟分布和应用并发上限一起测量。

异步 Web 服务里的落地形态

from contextlib import asynccontextmanager
from fastapi import FastAPI
import redis.asyncio as redis

pool = redis.BlockingConnectionPool.from_url(
    "redis://127.0.0.1:6379/0",
    max_connections=12,
    timeout=0.160,           # 等待借连接
    socket_connect_timeout=0.5, # TCP 建连
    socket_timeout=0.090,    # 命令读写
)
client = redis.Redis(connection_pool=pool)

@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.redis = client      # 全应用共享一个客户端
    yield
    await client.aclose()         # 关停时释放连接池

app = FastAPI(lifespan=lifespan)

@app.get("/profile/{user_id}")
async def profile(user_id: str):
    # 每次命令自动借连接,用完归还;不要每个请求新建客户端
    return await app.state.redis.hgetall(f"user:{user_id}")

关于重试:只有读取、幂等写入,或已经设计了幂等键的操作,才适合自动重试。自增、扣库存、弹出队列等操作若无法确认是否已在服务端执行,盲目重放可能造成二次副作用。