切换日光/暗黑模式
077. Redis 测试修复与请求链路结构
学习目标
这一节先修复自动化测试问题,再梳理后端请求链路。
学完后,你应该能理解:
- Redis 客户端为什么会在 PyTest 里触发事件循环错误;
- 测试环境下如何避免复用失效连接;
- 一个请求从服务器到 FastAPI 会经过哪些层;
- 中间件的洋葱模型是什么;
- 通用模块拦截器和中间件有什么区别。
测试失败现象
通用模块测试里出现了 401。
表面看是认证失败。
但 Token 本身没问题。
真正原因出在认证中间件读取用户缓存时。
用户缓存依赖 Redis。
Redis 客户端复用了已经绑定旧事件循环的连接。
旧事件循环被 PyTest 关闭后,再访问 Redis 就会报错。
事件循环和 Redis 客户端
PyTest 跑异步测试时,可能为每个测试用例创建独立事件循环。
测试结束后,这个事件循环会关闭。
Redis 客户端创建时会绑定当前事件循环。
如果这个客户端被放回连接池,下一个测试再复用它,就可能遇到:
txt
Event loop is closed这个错误不是业务认证问题。
是测试环境和连接池复用之间的冲突。
测试环境处理方式
解决方式是区分测试环境和正常运行环境。
正常运行时继续使用 Redis 连接池。
测试运行时每次创建新的 Redis 客户端。
使用完后直接关闭。
这样客户端不会跨事件循环复用。
测试就能稳定通过。
单独运行失败测试
测试失败很多时,不一定要每次重跑全部测试。
可以只跑失败测试。
也可以单独运行某一个测试用例。
在 PyCharm 里,如果直接运行某个测试,面板可能覆盖原来的失败列表。
一种更方便的做法是用 Debug 单独跑目标测试。
这样保留原来的 Run 面板,同时多出一个 Debug 面板。
请求进入服务器的路径
一个外部请求进入后端,大致经过这些层:
- 云服务器安全组;
- Linux 防火墙;
- Nginx;
- Uvicorn;
- FastAPI
app; - FastAPI 中间件;
- 路由匹配;
- 接口函数;
- 服务函数;
- 数据库或缓存。
每一层负责的事情不同。
不要把它们混成一个概念。
云安全组
云安全组是云厂商提供的网络防火墙。
它可以控制:
- 哪些端口开放;
- 哪些 IP 可以访问;
- 哪些协议可以进来。
它的规则通常比较简单。
适合做最外层访问控制。
Linux 防火墙
服务器操作系统本身也可能有防火墙。
如果某些端口访问不了,除了看云安全组,还要检查系统防火墙。
有些镜像默认关闭。
有些系统可能默认开启。
这会影响 Nginx 或后端端口能不能被访问。
Nginx
Nginx 监听常见入口端口。
例如:
- HTTP 默认走 80;
- HTTPS 默认走 443。
它可以把请求转发到:
- 前端静态文件;
- 某个后端端口;
- 不同项目的子路径。
课程项目里使用类似 /704、/705 这种子路径转发到不同后端端口。
这是教学环境的管理方式。
企业环境更常见的是用 dev、uat、api 这类命名区分环境。
Uvicorn 和 FastAPI
Uvicorn 负责监听后端端口。
收到请求后,把请求交给 FastAPI 的 app 对象处理。
可以把 Uvicorn 理解成 Python 后端的运行服务器。
FastAPI app 则负责:
- 中间件;
- 路由;
- 参数解析;
- 接口执行;
- 响应返回。
中间件洋葱模型
中间件是洋葱模型。
请求进入时按顺序一层层进入。
响应返回时再反向一层层出来。
所以它是:
txt
先进后出例如请求进入:
txt
认证中间件 -> 错误处理中间件 -> 耗时统计中间件 -> 接口响应返回时顺序反过来。
中间件和拦截器区别
中间件包在接口外面。
它处理的是整个请求生命周期。
例如:
- 跨域;
- Token 认证;
- 错误处理;
- 请求耗时统计。
Battice 拦截器在服务函数内部。
它处理的是通用模块某个动作前后的扩展点。
例如:
- 新增前;
- 新增后;
- 查询前;
- 查询后;
- 更新缓存。
两者名字都像“拦截”,但层级完全不同。
路由匹配
FastAPI 根据路由规则找到对应接口。
通用接口通常是:
txt
/generic/{module}/{action}module 是模块标识。
action 是操作类型。
路由匹配后,接口再调用 Battice 服务函数。
这一节的重点
这一节把测试问题和请求结构串起来了。
你需要记住:
- PyTest 异步测试里 Redis 客户端不能随便跨事件循环复用;
- 测试环境可以每次创建独立 Redis 客户端;
- 请求先经过服务器网络层,再进入 Nginx、Uvicorn、FastAPI;
- 中间件是接口外层的洋葱模型;
- Battice 拦截器是服务函数内部的前后置动作;
- 通用接口由动态路由匹配模块和操作类型。