thinkphp 6.0中统计访问量的正确方式是使用全局中间件,而非事件钩子;需在中间件中通过redis原子计数(如inc)缓存日志,再由定时命令行脚本异步同步至数据库,以保障性能与完整性。

ThinkPHP 5.1/6.0 中钩子(Event)不是统计请求的合适工具
直接说结论:ThinkPHP 的 event 钩子机制本身不拦截 HTTP 请求,也不自动触发于每次访问;它只是事件分发器,需手动调用 event('name') 才会触发监听。拿它做全站请求统计,等于在每个控制器方法开头硬塞一行 event('request_log') —— 不仅漏掉静态资源、中间件异常、路由未匹配等场景,还破坏业务逻辑的纯净性。
真正可靠的方式是使用中间件(Middleware),它在请求进入和响应发出时自动执行,覆盖 100% 可达的 HTTP 请求(包括 404)。以下以 ThinkPHP 6.0 为例说明实操路径:
- 统计逻辑必须放在全局中间件中,而非控制器或钩子监听器里
- 避免在中间件中执行阻塞式数据库写入(如直接
$model->save()),否则拖慢所有请求 - 推荐先写入缓存(如 Redis 的
INCR或LPUSH),再由后台脚本异步落库
如何编写一个轻量级请求统计中间件
在 app/middleware/ 下新建 RequestStat.php:
<?php namespace app\middleware;
use think\facade\Cache;
use think\facade\Request;
class RequestStat
{
public function handle($request, \Closure $next)
{
// 仅统计 GET 请求(可按需调整)
if ($request->method() !== 'GET') {
return $next($request);
}
// 过滤掉常见静态资源路径,减少无效计数
$path = $request->path();
if (preg_match('/\.(js|css|png|jpg|gif|ico|svg|woff2?)$/i', $path)) {
return $next($request);
}
// 使用 Redis 原子计数(需已配置 redis 缓存驱动)
$key = 'stat:total:' . date('Ymd');
Cache::store('redis')->inc($key);
// 可选:记录 IP 和路径到列表,用于后续分析(注意内存控制)
// Cache::store('redis')->lPush('stat:log:' . date('Ymd'), json_encode([
// 'ip' => $request->ip(),
// 'path' => $path,
// 'time' => time()
// ], JSON_UNESCAPED_UNICODE));
return $next($request);
}
}
然后在 app/middleware.php 中注册为全局中间件:
return [
\app\middleware\RequestStat::class,
];
如何安全地把缓存数据转成数据库记录
不能在每次请求里连 MySQL 插入;应单独写一个命令行脚本,定时执行(如每 5 分钟一次):
在 app/command/ 下新建 SyncStat.php:
<?php namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\facade\Cache;
use app\model\StatLog;
class SyncStat extends Command
{
protected function configure()
{
$this->setName('stat:sync')->setDescription('Sync request stats from cache to db');
}
protected function execute(Input $input, Output $output)
{
$date = date('Ymd');
$key = 'stat:total:' . $date;
$count = (int) Cache::store('redis')->get($key, 0);
if ($count $date],
['count' => $count, 'updated_at' => date('Y-m-d H:i:s')]
)->id;
// 重置缓存计数
Cache::store('redis')->set($key, 0);
$output->writeln("Synced {$count} requests for {$date}");
}
}
添加数据库表(MySQL):
CREATE TABLE `stat_log` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `date` char(8) NOT NULL COMMENT '格式:Ymd', `count` int unsigned NOT NULL DEFAULT '0', `updated_at` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uniq_date` (`date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
为什么不用行为(Behavior)或钩子(Hook)替代中间件
ThinkPHP 5.0 的 hook 和 5.1+ 的 event 本质都是“通知机制”,不是“拦截机制”。常见误用现象:
- 在
app/common.php里写event('app_init')—— 这个事件只在应用初始化时触发一次,不是每次请求 - 监听
action_begin行为 —— 但该行为只在控制器动作执行前触发,不覆盖中间件提前退出、路由失败、跨域预检等场景 - 在模板中调用
{:event('page_view')}—— 模板渲染失败时统计就丢失,且无法获取真实客户端 IP
中间件天然具备请求生命周期可见性,而钩子/行为需要你精确知道“哪个时机点存在、是否一定触发、是否已被跳过”,实际维护成本远高于中间件。
Redis 计数 + 定时同步是最小侵入、最高可用的方案;如果连 Redis 都没有,至少用 file_put_contents(..., FILE_APPEND | LOCK_EX) 写日志文件,也比在钩子里拼 SQL 强。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











