官网访问次数统计方案:数据库原子自增与Redis缓存计数实战 “统计首页访问次数”这类需求做后端的人应该都不陌生。很多官方网站的首页上线后第一个被提上来的小需求往往就是这个页面底部放一个数字或者后台看板要一条每日访问曲线。需求本身看着就一句话但真要动手会发现口径怎么定、总数怎么存、并发怎么扛、爬虫怎么滤每一环都有坑。这篇文章把这几年里我给各类官网、活动页做访问统计时踩过的坑和沉淀下来的方案整理出来希望能帮你快速落地一套靠谱的计数模块。我会从方案选型讲起然后是数据库计数器、Redis 缓存计数再到 PV/UV 口径和防刷。如果你刚接到这种需求或者正在重构老旧的统计逻辑这篇可以直接当参考手册用。1. 需求先拆透访问次数统计到底要统计什么1.1 一句话需求背后的四个隐藏问题很多人拿到“给首页加访问次数统计”这个需求第一反应就是写个计数器页面打开就加一。但实际调研时会发现这个需求至少包含四个必须当场确认的点。第一口径问题。要统计的是页面浏览量PV、独立访客数UV还是单纯的服务端接口被调用的次数同一个网站三种数字可能差出一个量级。第二持久化问题。数字是存数据库还是放缓存如果服务重启历史数据能不能接受丢失第三展示问题。这个数字是给访客看还是给运营团队看给机器看的走接口给运营看的可能还要按天拆曲线。第四抗压问题。官网上线活动时流量可能瞬间放大几十倍计数器扛不扛得住这四个问题没有标准答案只有基于具体场景的选择。很多项目做砸不是因为技术不行而是因为一开始没把口径聊明白。1.2 四种可实现方案快速对比我经手的官网统计项目里使用过的方案大类不过四种各有各的适用场景。把它们的差异摆出来后面选型就不纠结了。方案实现方式优点缺点适合场景数据库计数器固定行自增UPDATE cntcnt1简单直接数据可靠可回溯高并发下数据库行锁竞争明显日访问量几万以下的中小官网缓存加异步落库Redis INCR 实时计数定时同步到 MySQL抗高并发性能好实时性强架构稍复杂需处理缓存和库的一致性问题有活动、可能被流量冲击的公开页面第三方统计 JS嵌入一段前端脚本由外部统计平台采集接入最快无需开发后端自带可视化数据在第三方手里定制能力弱有合规隐忧临时项目、纯静态站点访问日志离线分析分析 Nginx 或网关访问日志离线聚合不改业务代码能回溯历史数据非实时日志量大了以后清洗成本高已有日志系统、对实时性要求不高的场景表格列完你就看出来了本质上不是技术选型是成本和实时性的取舍。第三方 JS 最快但数据不可控数据库最简单但扛不了大流量。我个人的建议是除了一次性展示用的临时页面官网正式上线后自建计数链路更稳妥至少数据是握在自己手里的。1.3 选型判断先给这个需求定个级别具体怎么选我一般按访问量和数据价值判断。问三个问题就行这个网站一天会有多少真实用户这个数字要不要作为运营考核依据服务重启丢几分钟数据可不可以接受。如果是内部系统或日访问量几百的小官网直接上数据库计数器一天几十万次 UPDATE 对 MySQL 来说毫无压力。如果是会公开宣传、可能上活动页的官网那务必在数据库前面加一层 Redis因为活动当天流量峰值不可预判你不想在大半夜被线上告警吵醒。如果网站本身已经有比较完整的 Nginx 日志采集链路那日志分析也可以但前提是业务方不要求实时数据。我常看到有人一上来就搭一套 Hadoop 离线分析结果官网日均流量三百纯属杀鸡用牛刀。先定级别再选技术这个顺序不能反。2. 数据库计数器小流量官网最简实现2.1 表结构设计用单行计数而不是流水表先说说最容易被想歪的地方。很多人会设计一张“访问日志表”每次访问插入一条记录统计的时候执行SELECT COUNT(*) FROM visit_log。小流量时还看不出问题但一旦访问量上来表会无限膨胀COUNT 全表扫描越来越慢最后不得不做分区、归档、定期清理运维成本很高。更优雅的做法是维护一个单行的计数器表。它的结构非常简单核心字段就三个CREATE TABLE home_page_counter ( id TINYINT PRIMARY KEY COMMENT 固定主键永远为1, pv_count BIGINT NOT NULL DEFAULT 0 COMMENT 累计访问次数, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 最后更新时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;初始化时插入一行id1的数据。之后所有更新都走主键定位这一行。InnoDB 的行锁机制会保证同一时刻只有一个事务能改这一行更新天然就是串行化的不会出现两条线程同时写导致计数覆盖的情况。为什么主键固定为 1因为我们要的只是一个全局标量一行就够了。用固定主键还能让 SQL 走主键索引更新性能很高配合 BIGINT 类型19 位数以内都能存正常官网累死也写不满。2.2 并发安全的核心一条 UPDATE 搞定原子自增数据表定了就该写更新逻辑了。很多刚入行的同学会自然写成三步先查当前值再加一再更新。我见过太多线上事故就是这么来的。先看一段错误示例以 Spring Boot 加 MyBatis 为例Mapper public interface SiteCounterMapper { Select(SELECT pv_count FROM home_page_counter WHERE id 1) long getCount(); Update(UPDATE home_page_counter SET pv_count #{count} WHERE id 1) int updateCount(long count); }Service 里这样调用long current siteCounterMapper.getCount(); siteCounterMapper.updateCount(current 1);这段代码在低并发下没问题但一旦两个请求同时读到current100就会同时把 100 改成 101实际两次访问只加了一。这不是 MySQL 的问题是应用层做的“读改写”三步本身不原子中间被并发插队了。正确写法就应该用一条 SQL让数据库自己完成自增Mapper public interface SiteCounterMapper { Update(UPDATE home_page_counter SET pv_count pv_count 1 WHERE id 1) int increaseOnce(); Select(SELECT pv_count FROM home_page_counter WHERE id 1) long getCount(); }这里的关键是pv_count pv_count 1。UPDATE 语句会对命中的行加行锁执行时其他会话的更新请求只能在锁外面等。同一时间只有一个会话能执行这一行的自增自增完成后再轮到下一个。所以哪怕并发来了两万次请求数据库队列里的自增次数也一次都不会少。2.3 更新后想拿到最新值怎么办有的业务会希望“访问一次接口直接把最新计数返回给前端展示”。这个需求不复杂但有一个细节要注意。用 PostgreSQL 可以写UPDATE ... RETURNING pv_count一条语句直接拿到更新后的值。MySQL 没有 RETURNING常见做法是先执行 UPDATE再在同事务内 SELECT 一次。MySQL 默认的 REPEATABLE READ 隔离级别下事务内第一次读取会建立快照如果这个 SELECT 发生在 UPDATE 之后就能看到刚才自增的结果拿到的是更新后的值。不过更推荐的做法是把“计数接口”和“展示接口”拆开。统计接口只有一句 UPDATE不返回任何数据展示页需要数字时再走单独的查询接口这样职责清晰也不容易踩到隔离级别的各种边界问题。计数延迟个几十毫秒展示出来用户根本感知不到。2.4 什么时候该放弃纯数据库方案数据库计数器也不是万能。它的瓶颈很明显就是那行的行锁。一次 UPDATE 本身很快可能零点几毫秒就完成但高并发下大量会话堆积在行锁等待上数据库连接池会被占满最终拖垮的是整个网站的主库连接。我做过一个对比测试单台 MySQL 上这种单行自增大概能承受每秒几千次请求再往上连接数和行锁就开始出现明显排队。对大多数官网来说日常流量根本到不了这个水位所以它依然是首选。真正需要考虑放弃的场景是网站在未来三个月内会做大流量活动、被新闻媒体报道或者业务方明确要求扛住高并发峰值。那就进入下一节讲的 Redis 方案。3. Redis 计数链路高并发访问下怎么设计3.1 为什么要把计数放到缓存层官网页面的特点是读多写少而且大多数页面会被 CDN 缓存掉真正打到源站的请求并不多。但计数接口和普通页面不同它不能被缓存每次用户访问都要真实上报一次否则数字就停了。这意味着统计接口是首页所有接口里最“穿透”的那一个。如果官网访问量一上来统计接口的数据库行锁会成为最明显的瓶颈。这时候用 Redis 就很合适INCR 命令本身就是原子操作执行效率高单机也能轻松扛住每秒十万级自增。把热计数的压力从数据库解耦到 Redis数据库只需要周期性接收快照就行。3.2 Redis 原子计数与 Key 设计Redis 的 INCR 是在内存里执行的单条命令服务端单线程处理命令队列根本不存在并发覆盖的问题。这也是它比“应用层读改写”方案稳得多的根本原因。假设今天日期是 2025-06-22我通常同时维护两个 Key127.0.0.1:6379 INCR site:home:total (integer) 12345 127.0.0.1:6379 INCR site:home:daily:2025-06-22 (integer) 789第一个 Key 是累计总数第二个 Key 是当日计数。日期格式化建议统一用站点所在时区别用 UTC否则运营看跨天报表时会发现凌晨的数据“跑到前一天去了”。如果你的统计要按小时拆那就把日期后缀扩到小时维度比如site:home:2025-06-22:14灵活程度完全够用。调用逻辑用伪代码表示大概是这样import redis r redis.Redis(host127.0.0.1, port6379, db0) today 2025-06-22 # 首页每次访问两个维度各加一 r.incr(site:home:total) r.incr(fsite:home:daily:{today})这套逻辑可以放在后端接口里也可以放在一个独立的计数服务里。官网后端接口在业务逻辑里顺手调用即可不必单独建服务除非你同时统计几十个站点。3.3 持久化设计Redis 重启不能全丢用 Redis 做计数最大风险就在持久化。如果 Redis 只开着默认的 RDB 快照崩溃时可能丢失最近几分钟甚至更久的数据如果完全没配置持久化重启直接清零。我实际项目里常用的方案是“AOF 每秒刷盘加定时落库”双保险。Redis 配置里把 AOF 打开并设置成每秒写盘appendonly yes appendfsync everysec这样极端情况下最多丢一秒的数据。但光靠 AOF 还不够Redis 本身是内存态数据一直在里面膨胀也不是办法所以还要加一个定时任务每隔五到十分钟把 Redis 里的计数快照同步到 MySQL。同步逻辑长这样def sync_to_mysql(): today 2025-06-22 total int(r.get(site:home:total) or 0) daily int(r.get(fsite:home:daily:{today}) or 0) db.execute( REPLACE INTO counter_daily(date, total, daily) VALUES (CURDATE(), %s, %s) , (total, daily), )注意这里用的是 REPLACE INTO每天只保留一条记录覆盖当天快照。历史数据的口径以 MySQL 表为准Redis 只负责热数据的实时累加。这样即使 Redis 某天彻底崩了我们从 MySQL 里也能恢复最近一个快照再补上 AOF 里剩的一秒数据损失基本可以忽略。在设计上还有一个容易绕晕的点总数要不要也单独存在 MySQL 表里我的建议是不需要。历史总数由counter_daily表 SUM 出来Redis 里的total只作为实时展示的快速缓存。这样只有一份权威数据源不会出现“MySQL 总数和 Redis 总数对不上”的尴尬。3.4 用 Set 或 HyperLogLog 做独立访客去重上面那套 Key 设计只能统计 PV不能统计 UV。如果运营要“独立访客数”就需要引入另外一个结构用 Set 记录当天访问过的用户标识用 SCARD 取集合大小就是当天的 UV。用户标识怎么来官网没有登录态常见做法是前端生成一个匿名 ID 存在 localStorage 里每次加载时通过请求头或者参数带上。后端拿到这个 ID执行SADD site:home:uv:2025-06-22 {userId}。当 UV 规模不大时Set 很准。一旦单日 UV 达到几十万级别Set 的内存消耗会非常大这时候可以换成 Redis 的 HyperLogLog用PFADD记录标识PFCOUNT估算基数误差在 0.81% 左右。做运营趋势分析这点误差完全够用但注意它只能算近似值不能用来做精确对账。4. PV、UV 与防刷让数字尽可能接近真实4.1 PV、UV、IP 三个口径别混着用官网运营里最常见的争论就是“我们网站到底多少访问量”。这句话在不同人嘴里含义完全不同。开发者说的访问量通常指 PV运营说的可能指 UV领导可能只想知道有多少不同 IP 来过。PV 每刷新一次加一UV 按人去重IP 按来源 IP 去重。同一个办公室的人共用一个出口 IPUV 会明显大于 IP 数但 PV 可能远大于两者。如果需求没讲清楚技术照自己的理解做了后面数据对不上必然吵架。我在每个统计项目开始前都会要求业务方明确两件事第一页面上展示的那个“数字”是累计访问次数还是独立访客数第二后台日报要不要按天拆。前者决定用什么数据结构后者决定要不要建每日明细表。通常最简单的方案是同时维护 PV 和 UV 两条链路后台展示两个指标让业务方自己选省得后期返工。4.2 爬虫和刷量流量怎么过滤官网数字虚高绝大多数情况不是代码 bug而是爬虫在刷。有一次我发现某单位官网统计数字一夜之间从 2 万跳到 800 万查下来是一个搜索引擎爬虫对首页做了全量抓取单线程几十个并发跑了一整晚。爬虫可不管你的统计口子你加一它就贡献一次。基础的过滤手段有三层。第一层是 User-Agent 过滤把空的 UA、带有 bot/spider/crawler 关键词的请求直接排除在计数之外。注意别过滤太狠现在很多正常用户浏览器也带着奇怪的 UA 尾巴。第二层是 IP 频率限制同一个 IP 在一分钟内的访问只计一次用 Redis 的 INCR 加 EXPIRE 实现几行代码就能挡住大部分脚本刷量。第三层是设置统计接口的调用协议要求带特定 Header 或者签名参数让脚本不能直接无脑请求。防刷不是要做到天衣无缝只要把明显不合理的流量挡掉让数据趋势线看起来合理就够了。完全的防刷在互联网上不存在别在这上面钻牛角尖。4.3 纯静态官网和 CDN 缓存场景的处理很多官网其实是纯静态托管后端接口一个都没有这时候怎么统计答案很简单页面静态但浏览器是活的。在静态页的 HTML 里嵌入一段统计脚本脚本在页面加载完成后向后端统计接口发一个异步请求照样能计数。这里有一个特别坑的点统计接口本身不能被 CDN 缓存。如果你的统计接口和静态资源一样被 CDN 缓存了那么第一个用户访问后计数加一后面一万个用户拿到的都是缓存结果计数直接停摆。解决方法是统计接口的响应头显式设置Cache-Control: no-store并且在请求 URL 后面带上随机参数比如?t当前时间戳彻底绕开 CDN。跨域问题同样要留意。统计脚本的域名与官网主域名不一致时接口要支持 CORS 或者用 JSONP 方式请求。我在一个项目里就是没配 CORS结果统计脚本在浏览器控制台里报了一堆跨域错误数字几乎不动查了半天才定位到是浏览器拦截不是后端逻辑问题。5. 真实案例复盘遇到过的问题和排查过程5.1 并发丢数活动当天统计少了四成某企业官网上线了一个限时活动首页引流效果很好但活动结束后发现 PV 统计数字比实际预估少了百分之四十。代码走查后发现统计逻辑是标准的“读取-加一-写回”三步。平时流量低并发窗口小问题不明显活动当天并发一上来两个请求同时读同一数值、同时写回后写覆盖先写丢得悄无声息。修复方式就是我前面说的改成一句原子 UPDATE。上线后重新对拍了两天数据计数恢复正常。这个案例给到你的经验是线上流量没有“低到不可能并发”的时候。任何涉及共享计数、库存、余额之类的写操作都要默认并发会发生直接用原子操作。5.2 Redis 重启后计数归零另一个项目初期图省事统计直接放 Redis觉得反正有 RDB 快照兜底。结果某次机器资源紧张运维把 Redis 容器重启了一次发现统计数字从几万直接归零。查下来原因有两个RDB 快照默认配置的触发条件在低写入量场景下很难触发上一次快照可能是几小时前甚至更早加上容器重启后数据目录没挂持久化卷快照文件也跟着没了。这个事故之后的改造方案就是上文提到的组合开启 AOF 每秒刷盘定时任务每五分钟把 Redis 快照同步到 MySQL。同时给 Redis 容器挂了独立的数据卷重启不再丢文件。此后又经历了几次升级重启统计数字再没归零过。5.3 数字虚高一夜涨了八百倍某个单位官网的首页统计数字突然异常前一天两万第二天早上八百万。第一反应以为是代码死循环或者有人不停刷新。查了访问日志发现某个知名爬虫以数十个并发线程连续请求首页每次请求都触发了一次统计自增。处理办法是三步并进在计数处理逻辑里加上 User-Agent 关键词过滤把空 UA 和常见的爬虫标识排除加了一层 IP 频率限制同一 IP 每分钟最多贡献一次计数最后在服务网关层把这个爬虫的 UA 段整体拦截避免它继续消耗服务器资源。很多团队会忽略统计接口的防刷总觉得“数字大一点也没关系”。实际上数据虚高会直接影响运营对渠道投放的判断这个后果比技术故障更麻烦。5.4 技术做的是 PV运营要的是 UV有一次运营反馈官网“访问人数”和后台统计对不上。技术给的日报是 5 万运营手里的第三方流量分析工具显示 2 万。两边都不服气最后对了一下口径原来第三方工具按独立用户去重技术这边按每次页面加载累加本质上是两种不同的指标。这不算技术故障但暴露了需求沟通的问题。后来我们把后台统计模块改成同时展示 PV 和 UV 两张卡片并按天分表存储运营再也没来问过“数字为什么对不上”。给所有做统计需求的人一个建议在开工前把指标定义白纸黑字确认下来比事后解释省力一百倍。5.5 问题速查表现象可能原因处理方式计数少于实际访问应用层读改写非原子操作高并发丢更新改为 SQL 原子自增cnt cnt 1重启后计数归零Redis 无持久化或持久化配置未覆盖重启开启 AOF配置 appendfsync everysec定时落库数字短时间暴涨爬虫、刷量脚本触发统计过滤 User-Agent配置 IP 频率限制数字完全不动统计接口被 CDN 缓存或浏览器跨域拦截接口禁用缓存加时间戳参数配置 CORS技术与运营数据对不上PV 与 UV 口径不一致需求阶段明确指标后台同时展示两套口径顺带提一个容易被忽略的小问题统计逻辑如果要自己写过滤器一定要把过滤动作放在计数之前并且过滤不要影响正常业务响应。别因为过滤爬虫把正常用户的请求也拦了那种线上事故更难看。我个人现在接到这类需求都会习惯性先问三个问题这个数字是给人看还是给系统看PV 还是 UV服务重启丢数据能不能接受。三个问题问完方案基本就定了。至于技术栈真的不复杂小官网就用数据库原子自增有大流量预期就上 Redis把持久化和防刷做好这套东西能稳稳跑好几年。希望这篇整理对正在做官网统计的朋友有帮助。