设计一个 高并发计数器服务 需要考虑 数据一致性、读写性能、扩展性、持久化存储 等问题。我们可以采用 缓存+分布式架构+分批更新 的方式来提升性能,同时结合 分布式锁、原子操作、日志补偿 等手段确保一致性。
1. 需求分析
功能需求:
- 支持高并发
increment()
、decrement()
和getCount()
。 - 允许持久化存储,防止数据丢失。
非功能需求:
- 高可用:支持水平扩展,避免单点故障。
- 高性能:能够承受 QPS 10W+ 级别的请求。
- 强一致性(强一致 / 最终一致性)。
2. 设计方案
(1)单机方案(适用于低并发)
- 使用 Redis 作为缓存,
INCR/DECR
操作计数。 - 采用 MySQL 进行持久化存储(定期落盘)。
- 问题:单机容易成为瓶颈,不能支持高并发。
(2)分布式架构(适用于高并发场景)
采用 Redis+MySQL+分布式架构,结合 缓存+分批更新+异步落盘,提升性能。
📌 核心组件
组件 | 作用 |
---|---|
Redis(缓存) | 高速计数,减少数据库压力 |
MySQL / ClickHouse | 持久化计数,保证数据安全 |
分布式锁(可选) | 避免并发更新冲突 |
消息队列(Kafka/RabbitMQ) | 异步批量落地 MySQL |
一致性方案(乐观锁 / 事务) | 确保计数准确 |
3. 关键优化点
(1)高并发优化:Redis 计数 + 批量落盘
-
Redis 作为缓存,使用
INCR
/DECR
操作:jedis.incr("counter:page_views"); jedis.decr("counter:page_views");
优点:Redis 计数在内存中操作,O(1) 时间复杂度,比数据库快几个数量级。
-
定期异步批量写入数据库:
- 采用 后台定时任务 或 消息队列(Kafka)。
- 避免每次更新都写数据库,减少 I/O 开销。
- 示例:
String key = "counter:page_views"; int count = jedis.get(key); updateToDB(count); // 每 10s 批量更新一次数据库
- 数据库 SQL 批量更新:
INSERT INTO counter_table (counter_name, count) VALUES ('page_views', 1000) ON DUPLICATE KEY UPDATE count = count + VALUES(count);
(2)分布式一致性方案
✅ Redis 分布式锁(适用于计数落地)
- 使用 RedLock(基于 Redis 分布式锁) 保证只有一个实例负责落地数据库:
RLock lock = redisson.getLock("counter_update_lock"); if (lock.tryLock()) { try { updateToDB(); } finally { lock.unlock(); } }
- 避免多个实例重复落盘,保证一致性。
✅ 乐观锁(适用于 MySQL 持久化)
- 使用
version
字段保证原子更新:UPDATE counter_table SET count = count + 100, version = version + 1 WHERE counter_name = 'page_views' AND version = 5;
- 如果 version 变化,说明有并发更新,重试。
(3)高吞吐优化
✅ 缓存热点优化
-
使用 Redis 的
HyperLogLog
(适用于唯一计数):jedis.pfadd("unique_users", userId); long count = jedis.pfcount("unique_users");
- 适用于 UV(独立访问数)计数,占用极小内存。
-
本地计数器 + 合并
- 让 每个应用实例维护本地计数器,定期合并到 Redis:
AtomicLong localCounter = new AtomicLong(0); localCounter.incrementAndGet(); // 每 1000 次写入 Redis if (localCounter.get() % 1000 == 0) { jedis.incrBy("counter:page_views", localCounter.get()); localCounter.set(0); }
- 让 每个应用实例维护本地计数器,定期合并到 Redis:
(4)高可用架构
✅ Redis 高可用
- 主从复制 + 哨兵(Sentinel)
- Redis Cluster 进行数据分片
✅ 数据库水平扩展
-
分库分表(Sharding)
- 例如
counter_{hash(id) % 10}
,将数据均匀分布在 10 个表。
- 例如
-
ClickHouse/TimescaleDB 进行时序存储
- 适用于大规模日志计数,查询更快。
4. 代码示例
Java 版高并发计数器
public class CounterService {
private static final String REDIS_KEY = "counter:page_views";
private static final int BATCH_SIZE = 1000;
private AtomicLong localCounter = new AtomicLong(0);
private final Jedis jedis;
public CounterService(Jedis jedis) {
this.jedis = jedis;
}
public void increment() {
long count = localCounter.incrementAndGet();
// 每 1000 次合并到 Redis
if (count % BATCH_SIZE == 0) {
jedis.incrBy(REDIS_KEY, count);
localCounter.set(0);
}
}
public long getCount() {
return localCounter.get() + Long.parseLong(jedis.get(REDIS_KEY));
}
// 定时任务:落地数据库
@Scheduled(fixedRate = 10000)
public void persistToDB() {
long count = Long.parseLong(jedis.get(REDIS_KEY));
updateToDB(count);
}
private void updateToDB(long count) {
// 执行 SQL:INSERT INTO counter_table (name, count) VALUES (...) ON DUPLICATE KEY UPDATE ...
}
}
5. 总结
方案 | 优势 | 适用场景 |
---|---|---|
Redis INCR | O(1) 操作,低延迟 | 适用于所有高并发计数 |
批量落盘 MySQL | 降低数据库压力,最终一致 | 需要持久化存储的计数 |
分布式锁 | 确保数据一致性 | 多实例写入数据库 |
本地计数器 + Redis 合并 | 极致性能优化 | 超高 QPS |
🔥 终极优化
- 使用 Redis + 本地计数器 进行极限吞吐优化。
- 批量落盘,保证最终一致性。
- 结合 Kafka 和 MySQL 分库分表 进行水平扩展。
👉 结论:通过分层设计 + 缓存优化,可以实现一个高并发、高性能的计数器!🚀