本文目录导读:

- 核心痛点
- 方案一:基于 Redis 的计数器(最常用,秒杀/点赞/访问量)
- 方案二:基于数据库的计数器(强一致,不易丢)
- 方案三:分布式锁 + 原子类(不推荐用于高频)
- 方案四:消息队列 + 异步消费(削峰填谷)
- 方案五:专业分布式计数器中间件(高难度)
- 方案对比表
- 推荐实践路线
- 特别提醒
Java分布式数据计数器的实现方案很多,选择哪种取决于你的性能要求、一致性要求、数据量级以及是否需要持久化。
下面按从简单到复杂、从中间件专用到自研的顺序,梳理几种主流方案及适用场景。
核心痛点
在分布式系统中,传统 int count++ 无法使用,因为:
- 原子性问题:多个JVM实例同时修改同一数据,需要锁或原子操作。
- 数据一致性问题:如何保证最终读到的总数是正确的。
- 性能瓶颈:所有计数请求都打到一个单点,系统容易崩溃。
基于 Redis 的计数器(最常用,秒杀/点赞/访问量)
这是最经典、最高效的纯内存计数方案。
-
原理:利用 Redis 单线程模型加
INCR命令的原子性。 -
实现:
// 使用 Spring Data Redis @Autowired private StringRedisTemplate redisTemplate; public Long increment(String key) { // INCR 是原子操作,返回增长后的值 return redisTemplate.opsForValue().increment(key); } -
高并发优化:
-
Pipeline/批处理:合并多个INCR请求。
-
Lua脚本:实现复杂的计数逻辑(如先检查再自增)。
-
本地缓存+定时批量同步:每个JVM先本地计数,每1秒或每100次批量sync到Redis,大幅减少网络IO。
// 伪代码:本地缓存批量写入 private AtomicLong localCount = new AtomicLong(0); private ScheduledExecutorService scheduler = ...; public void incr() { long val = localCount.incrementAndGet(); if (val % 100 == 0) { // 批量提交到Redis redisTemplate.opsForValue().increment(key, val); localCount.set(0); } }
-
-
缺点:Redis 宕机可能丢数据(若未开启AOF/RDB),纯内存成本高。
-
适用场景:实时性要求高的高频计数,如秒杀库存扣减、PV/UV统计、点赞数。
基于数据库的计数器(强一致,不易丢)
适用于需要严格事务保障的场景。
-
单行乐观锁(最经典)
-
数据库表设计:
counter(name, value, version) -
SQL:
-- 原子增加,利用数据库行锁 UPDATE counter SET value = value + 1 WHERE name = ?; -- 带版本号,防止ABA问题 UPDATE counter SET value = value + 1, version = version + 1 WHERE name = ? AND version = ?;
-
-
批量合并写入
- 不适合每点击一次就写一次DB,通常做法:内存聚合+定时刷盘。
- 累积10个或每1秒将多个增量合并为一条
UPDATE counter SET value = value + 10 WHERE name = ?,极大降低数据库压力。
-
适用场景:最终一致性要求高,且数据不能丢失,如账户余额变动、订单总数统计。
分布式锁 + 原子类(不推荐用于高频)
利用 Zookeeper 或 Redis 分布式锁来保证同一时间只有一个节点可以操作计数器。
- 实现:
public long increment() { DistributedLock lock = zkLock.acquire("counter_lock"); try { // 从集中存储读取当前值 long current = getFromDb(); long newVal = current + 1; saveToDb(newVal); return newVal; } finally { lock.release(); } } - 缺点:性能极低,所有请求串行化,完全不符合高并发场景。
- 适用场景:低频、少量、但必须严格互斥的后台任务。
消息队列 + 异步消费(削峰填谷)
将“计数请求”当成事件发送到 MQ(如 Kafka、RocketMQ),然后由后端消费者批量写入。
- 流程:
- 用户点击 -> 发送一条
CountEvent到 Kafka Topic。 - Kafka 消费者拉取消息。
- 消费者在内存中聚合(如累加100条后),一次性写入 Redis 或 DB。
- 用户点击 -> 发送一条
- 优点:解耦、削峰、保证最终一致。
- 缺点:存在秒级延迟,不适合实时显示。
- 适用场景:高并发但允许秒级延迟的统计,如非核心的业务指标、日志计数。
专业分布式计数器中间件(高难度)
如果不想重复造轮子,可以封装自己的计数器服务。
- 核心技术:TCC(Try-Confirm-Cancel)模式 + 分段计数。
- 实现思路:
- 将整个大计数拆成 N 个分段(Segment),分散到不同机器或数据库分片。
- 查询时,汇总所有分段的值。
- 更新时,选择某一台机器的一个分段进行原子更新。
- 若某节点宕机,由协调器(如Zookeeper)将它的分段漂移到其他节点。
- 适用场景:1000万+ QPS 级别的极高频场景(如微博热搜、抖音点赞等)。
方案对比表
| 方案 | 性能 | 一致性 | 持久化 | 复杂度 | 典型实现工具 |
|---|---|---|---|---|---|
| Redis INCR | 极高 | 最终一致(主从可能丢) | 弱 | 低 | Redis + Lua |
| 本地缓存 + 批量写Redis | 极高 | 最终一致(小窗口丢失) | 弱 | 中 | Guava Cache + Redis |
| 数据库乐观锁 | 低 | 强一致 | 强 | 低 | MySQL/PostgreSQL |
| 消息队列 + 批量写入DB | 中 | 最终一致(秒级延迟) | 强 | 中 | Kafka + Flink/定时任务 |
| 分布式锁 | 极低 | 强一致 | 强 | 低 | ZK/Redisson |
| 分段计数器 | 极高 | 强一致 | 可配置 | 极高 | 自研 + ZK + Sharding |
推荐实践路线
- 80%场景:Redis INCR + 本地缓存批量同步,这是性能和稳定性的最佳平衡点,扛住万级QPS通常没问题。
- 需要强一致且不能丢:MySQL 乐观锁 + 异步批量刷盘。
- 超大规模(千万级QPS):分段计数器(Segment Counter)或基于 Ring Buffer 的无锁计数器。
特别提醒
- 避免使用
synchronized/ReentrantLock加static int:在分布式环境下,这只能保证单个JVM内线程安全,无法跨进程。 - 精度损失确认:如果业务允许(如点赞数),最终一致性通常就够了,不需要强一致。
- 考虑反作弊:计数器经常会被刷,要考虑风控逻辑。