直接COUNT(*)在高频场景下拖垮数据库,因InnoDB不维护精确行数,每次需全索引扫描,百万数据即达数百毫秒至秒级;并发时IO、CPU与连接池压力剧增。

为什么直接 COUNT(*) 在高频场景下会拖垮数据库
InnoDB 不维护精确行数,每次 COUNT(*) 都要扫描索引(通常是主键),数据量一过百万,单次查询就容易卡在几百毫秒甚至秒级。更麻烦的是,如果多个服务或接口并发执行这类查询,IO 和 CPU 压力会快速堆积,连接池也可能被占满。
用 Redis 缓存计数器的实操要点
这不是简单地把 COUNT(*) 结果塞进 Redis 就完事——关键在于「什么时候更新」和「怎么保证一致性」:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 缓存 key 命名建议带业务上下文,比如
user:active:count,避免和其他计数混用 - 不推荐用定时任务拉取 MySQL 的
COUNT(*)再写入 Redis,延迟高、易重复、难对齐事务边界 - 正确做法是:在业务代码中,每次 INSERT/DELETE 后同步增减 Redis 的
INCR/DECR,并确保和 DB 操作在同一个事务里(例如用 try-finally 或 Spring @Transactional + afterCommit 回调) - Redis 本身不支持原子性跨 key 操作,所以不要拆成「先读再加」两步,必须用
INCRBY或DECRBY直接操作 - 首次加载可走一次
COUNT(*)初始化,但务必加锁(如 SETNX + 过期时间),防止多个实例同时初始化导致数据错乱
用触发器维护计数表的适用边界
触发器方案适合变更频率低、但读取极频繁的场景(比如文章总浏览数、商品销量),它把计数逻辑下沉到 MySQL 内部,绕过应用层协调成本:
- 必须建一张单独的计数表,比如
counter_table,字段为counter_key VARCHAR(64) PRIMARY KEY和counter_value BIGINT UNSIGNED NOT NULL - INSERT 触发器里用
INSERT ... ON DUPLICATE KEY UPDATE counter_value = counter_value + 1,DELETE 触发器里同理减 1 - 注意:触发器无法捕获 TRUNCATE,也不能响应外部导入(如 LOAD DATA)、复制延迟等场景,这些地方要额外补漏
- 触发器会增加每条 DML 的开销,如果表每秒有上千次写入,性能损耗明显,此时不如交由应用层异步更新
- MySQL 8.0+ 支持原子性 DML + 触发器,但老版本可能因死锁或 binlog 格式问题导致从库计数不一致,上线前务必在从库验证
别忽略的三个一致性陷阱
无论选 Redis 还是触发器,这三个点最容易被跳过,但一出问题就是线上事故:
- Redis 宕机或网络分区时,计数会丢失或滞后,必须有降级策略——比如 fallback 到
SELECT COUNT(*) FROM ... WHERE ...,并加熔断(如 5 秒内失败 3 次就切回 DB 查询) - MySQL 主从延迟下,触发器在主库生效,但从库计数表可能延迟更新,如果读请求打到从库,看到的就是旧值;解决方案是强制这类计数查询走主库,或加
SELECT ... FOR UPDATE锁住计数行再查 - 批量操作(如
INSERT INTO ... SELECT或DELETE ... LIMIT)往往绕过单行触发器,需要额外写存储过程或在应用层做批量修正

















