请求统计表应建在独立库(如stats_db),字段精简为id、controller、action、status_code、created_at,用myisam或archive引擎,按天分表(如request_logs_20240615),controller与action建联合索引;埋点统一用response_send钩子确保响应发出后再写库,高频查询须预聚合至daily_api_stats等汇总表,避免实时group by扫全表。

请求统计表结构怎么建才不拖慢主业务
直接加个 request_logs 表记录所有请求,99% 的项目会卡死——尤其当并发上来、日志写入和主业务共用同一个数据库连接池时。必须拆开:统计归统计,业务归业务。
推荐用独立库(比如 stats_db)+ 分表策略。核心字段只要 5 个:id、controller、action、status_code、created_at。去掉 ip、user_agent 这类非必要字段;真要分析来源,走 Nginx 日志更稳。
-
controller和action建联合索引,查某个接口的调用量才快 -
created_at必须是DATETIME(不是TIMESTAMP),避免时区转换翻车 - 表引擎用
MyISAM或Archive(只写不读场景下比 InnoDB 轻 3 倍) - 每天按
created_at自动分表,比如request_logs_20240615,删旧表也方便
ThinkPHP 中哪里埋点最干净
别在每个控制器里手动写 Db::name('request_logs')->insert(...) ——漏一个就断层。应该用中间件 + app_init 钩子兜底。
新建 app/middleware/RequestLogger.php,在 handle() 里取当前路由信息,但注意:不能在中间件里直接写 DB,因为事务或异常可能让请求没走到终点,却已记了一条“成功”日志。
- 改用
Log::write()写本地文件(格式为 JSON 行式),再用 Logstash 或定时脚本异步入库 - 如果坚持 DB 写入,必须放在
response_send钩子之后,确保响应已发出 -
$request->controller()和$request->action()可能为空(比如路由闭包),得先isset()判空
示例片段:
// 在 response_send 钩子中
\think\facade\Event::listen('response_send', function ($response) {
$route = \think\Route::getRule();
if (is_array($route) && isset($route['controller'])) {
\think\Db::connect('stats_db')->name('request_logs')->insert([
'controller' => $route['controller'],
'action' => $route['action'] ?? '',
'status_code'=> $response->getCode(),
'created_at' => date('Y-m-d H:i:s')
]);
}
});
高频查询怎么避免 GROUP BY 扫全表
查“昨天每个接口的 500 错误数”,写 SELECT controller, COUNT(*) FROM request_logs WHERE created_at >= ? GROUP BY controller,一千万行数据基本卡住。得预聚合。
- 每天凌晨跑一次定时任务,把昨日数据按
controller + status_code汇总到daily_api_stats表 -
daily_api_stats主键设为date + controller + status_code,查某天某接口错误数就是主键等值查询 - 如果还要看小时级趋势,额外建
hourly_api_stats,但只存最近 7 天,过期自动删 - 别用视图或存储过程做实时聚合——TP 默认不支持存储过程,且视图无法走索引
为什么不能用 think-swoole 直接记请求日志
Swoole 是常驻内存模型,多个请求共享同一个 PDO 连接对象。如果在 Worker 进程里直接执行 Db::insert(),会出现连接被复用、事务未提交、甚至连接超时后报 MySQL server has gone away。
- 必须用
Db::connect(['break_reconnect' => true])强制每次新建连接 - 更稳妥的做法是发消息到 Redis 队列(如
queue:requests),另起一个php think queue:work进程消费写库 - 注意
think-queue默认序列化用serialize(),遇到 Closure 会失败,得换igbinary或改用 JSON 存原始数组
复杂点在于:Swoole 下的 $request 对象生命周期和 HTTP 请求不完全对齐,某些钩子(比如 WorkerStart)里根本拿不到请求上下文。真要统计,优先走 Nginx access_log + ELK,比 PHP 层埋点更准、更轻。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











