应结合会话与IP去重、队列异步更新、Redis频控及短TTL缓存来精准统计文章浏览量:先校验新会话且非爬虫UA,再通过Redis滑动窗口限制IP+UA+ID组合每小时≤5次,计数交由队列处理,缓存设5分钟并确保所有更新处同步失效。

用 increment() 更新浏览量但别直接裸调
直接在文章详情页每次请求都执行 $article->increment('views'),看似简单,实际会把爬虫、刷新、代理、前端重复请求全算进去。真实用户访问和无效流量根本分不开。
更合理的方式是结合会话或 IP 做轻量去重,比如只对新会话(session()->missing('viewed_article_'.$id))且非爬虫 UA 的请求才计数:
- 先检查
session()->has('viewed_article_'.$id),有就跳过 - UA 包含
bot、crawler、spider等关键词的请求一律忽略(可用Str::contains(request()->userAgent(), [...])) - 计数后立即写入 session:
session()->put('viewed_article_'.$id, true),有效期设为 24 小时足够 - 注意:不要用
cache()替代 session,缓存共享、无用户隔离,容易误伤
Laravel 中用队列异步写浏览量避免阻塞响应
哪怕做了会话判断,每次都要查 session、写 DB、更新缓存,对高并发文章页仍是负担。尤其当 views 字段加了索引但没做分区或归档时,写放大明显。
把计数逻辑扔进队列是最小改动、收益明显的方案:
- 定义一个
IncrementViewCountjob,构造函数接收$articleId和可选$ip - 在控制器里只 dispatch:
IncrementViewCount::dispatch($id)->onQueue('low') - job 的
handle()里再做 UA 判断 + session 检查 + 最终 DB 更新 - 别忘了配置
QUEUE_CONNECTION=database或redis,避免用sync队列假异步
防止刷票:用 Redis 记录 IP+UA+文章 ID 组合频次
单纯靠 session 不防恶意脚本——它可伪造 session ID 或干脆不用 cookie。真正要拦住批量请求,得靠服务端频控。
Laravel Cloud(cloud.laravel.com)。此技能用于处理所有 Laravel Cloud 请求,包括搜索和读取数据。只要任务涉及 Laravel Cloud,即使用此技能。
用 Redis 对每个 IP+UA+article_id 做滑动窗口计数,比数据库快一个数量级,也避免锁表:
- 键名建议用
view:rate_limit:{md5($ip . $ua . $id)},避免特殊字符问题 - 用
Redis::incr($key)+Redis::expire($key, 3600)实现小时级限制 - 如果返回值 > 5,直接 return(不 dispatch job,也不写 session)
- 注意 UA 可被伪造,所以不能单独依赖;但和 IP 组合后,对普通脚本已有足够威慑力
缓存文章浏览量时别用 Cache::rememberForever()
很多人图省事,把 views 字段缓存成永久,结果后台改了数据、定时任务修了统计、甚至手动 SQL 更新,缓存却一直不刷新,前台永远显示旧数字。
必须让缓存和数据变更联动:
- 所有修改
views的地方(包括 job、API、后台编辑),都要调用Cache::forget('article_views_'.$id) - 读取时用
Cache::remember('article_views_'.$id, 300, fn() => $article->views),TTL 设短(5 分钟足够) - 如果用了模型监听器(
Observing),在updating事件里统一 forget 缓存,比散落在各处更可靠 - 别信“缓存穿透”这种说法——浏览量不是关键业务字段,空缓存直接查 DB 完全可接受
真实线上环境里,最常被忽略的是 Redis 键命名冲突和缓存未失效。一个被多个 job、命令、API 共同更新的计数字段,只要有一处忘了 forget,前端看到的就是错的——而且很难排查。










