网页访问计数器
作者:焦点 来源:娱乐 浏览: 【大 中 小】 发布时间:2026-10-17 06:11:26 评论数:
去年帮朋友搭了个小博客,网页他提了个需求:想知道每天有多少人来看。访问我第一反应是计数装个Google Analytics,他摆摆手说太重,网页就要个数字,访问页脚显示“您是计数第几位访客”那种。行,网页那就自己写一个。访问
网页访问计数器这东西,计数原理不复杂。网页核心就一句话:每次有人打开页面,访问让服务器把某个数字加一,计数再把这个数字返回给浏览器渲染出来。网页听起来像是访问十行代码能搞定的事,但真动手写,计数坑一个接一个。
最朴素的做法
假设你用PHP,最直觉的写法是这样:
<?php $file = 'counter.txt'; $count = (int)file_get_contents($file); $count++; file_put_contents($file, $count); echo "您是第 { $count} 位访客"; ?> 这代码能跑,但有个致命问题——并发。两个人同时打开页面,A读到100,B也读到100,各自加一写回去,结果都是101。本该是102。访问量一上来,丢计数是必然的。文件锁能解决一部分问题,flock()加个排他锁,但性能立刻掉下来。每次请求都要排队等文件IO,页面加载速度肉眼可见地变慢。
我朋友那个博客一天也就几十个人看,用文件锁其实够了。但我想试试别的方案。
Redis的原子操作
换成Redis就舒服多了。Redis的INCR命令本身是原子性的,天然适合计数场景:
$redis = new Redis(); $redis->connect('127.0.0.1', 6379); $count = $redis->incr('page_views'); echo "您是第 { $count} 位访客"; 没有锁,没有文件读写,单线程模型保证了原子性。速度快到可以忽略不计。但问题也来了——Redis重启,数据就没了。得配持久化,RDB或者AOF。我选了AOF,每秒同步一次,最多丢一秒的数据。对于一个博客计数器来说,丢一秒的访问量,完全可以接受。
不过这里还有个细节:INCR是全局计数,所有页面共用一个数字。如果你想知道每篇文章各自的访问量,得用HINCRBY,把文章ID作为field:
$redis->hIncrBy('article_views', $articleId, 1); 这样每篇文章的计数独立,互不干扰。我朋友后来改口说想要每篇文章的阅读数,幸好一开始就用了Hash结构。
前端刷新的问题
计数器写好了,但每次刷新页面数字都加一。我自己调试的时候刷了二十几次,计数器直接飙到三十多。真实用户可能也会反复刷新,或者浏览器预加载、爬虫抓取,都会让数字虚高。
常见的做法是加一层去重。简单点的用Cookie,同一个浏览器一天内只计一次。但Cookie可以被清除,也可以被禁用。再复杂点用IP加User-Agent做指纹,存到Redis里设个过期时间。我选了后者,用SETNX加EXPIRE,同一个指纹24小时内只计一次。代码大概长这样:
$fingerprint = md5($ip . $userAgent); $key = "viewed:{ $fingerprint}"; if ($redis->setnx($key, 1)) { $redis->expire($key, 86400); $redis->incr('page_views'); } 这样刷新的问题基本解决了。但爬虫还是会漏进来一些,除非专门做User-Agent黑名单。我朋友说算了,爬虫那点量无所谓。
显示上的小把戏
数字直接显示出来有点单调。很多网站会把它做成图片,或者用等宽字体加个背景框,看起来像老式计数器。我朋友想要那种LED数字的效果,我找了段CSS,用text-shadow和monospace字体模拟了一下,效果还行。
还有个细节:数字太大不好看。十万以上的访问量,显示成“100,234”比“100234”舒服得多。PHP里用number_format()一行搞定。
最后上线那天,我朋友盯着页脚那个数字看了半天,说:“怎么才1?”我说你刷新一下。他刷新,变成2。他笑了,说这玩意还挺有意思。
其实网页访问计数器就是这么个东西——技术含量不高,但真要做得好用,得考虑并发、持久化、去重、显示效果。每一个环节都有取舍。选什么方案,取决于你对“准确”和“性能”的容忍度。像我朋友那种小博客,Redis加个去重就够了。换成日PV百万的站点,可能得上Kafka加Flink做实时流处理。工具没有好坏,只有合不合适。
