如何设计一个高并发计数器服务,考虑数据一致性,和性能优化?

设计一个 高并发计数器服务 需要考虑 数据一致性、读写性能、扩展性、持久化存储 等问题。我们可以采用 缓存+分布式架构+分批更新 的方式来提升性能,同时结合 分布式锁、原子操作、日志补偿 等手段确保一致性。


1. 需求分析

功能需求

  • 支持高并发 increment()decrement()getCount()
  • 允许持久化存储,防止数据丢失。

非功能需求

  • 高可用:支持水平扩展,避免单点故障。
  • 高性能:能够承受 QPS 10W+ 级别的请求。
  • 强一致性(强一致 / 最终一致性)

2. 设计方案

(1)单机方案(适用于低并发)

  • 使用 Redis 作为缓存,INCR/DECR 操作计数。
  • 采用 MySQL 进行持久化存储(定期落盘)。
  • 问题:单机容易成为瓶颈,不能支持高并发。

(2)分布式架构(适用于高并发场景)

采用 Redis+MySQL+分布式架构,结合 缓存+分批更新+异步落盘,提升性能。

📌 核心组件
组件作用
Redis(缓存)高速计数,减少数据库压力
MySQL / ClickHouse持久化计数,保证数据安全
分布式锁(可选)避免并发更新冲突
消息队列(Kafka/RabbitMQ)异步批量落地 MySQL
一致性方案(乐观锁 / 事务)确保计数准确

3. 关键优化点

(1)高并发优化:Redis 计数 + 批量落盘

  1. Redis 作为缓存,使用 INCR / DECR 操作:

    jedis.incr("counter:page_views");
    jedis.decr("counter:page_views");
    

    优点:Redis 计数在内存中操作,O(1) 时间复杂度,比数据库快几个数量级。

  2. 定期异步批量写入数据库

    • 采用 后台定时任务消息队列(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);
      }
      

(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 INCRO(1) 操作,低延迟适用于所有高并发计数
批量落盘 MySQL降低数据库压力,最终一致需要持久化存储的计数
分布式锁确保数据一致性多实例写入数据库
本地计数器 + Redis 合并极致性能优化超高 QPS

🔥 终极优化

  • 使用 Redis + 本地计数器 进行极限吞吐优化。
  • 批量落盘,保证最终一致性。
  • 结合 KafkaMySQL 分库分表 进行水平扩展。

👉 结论:通过分层设计 + 缓存优化,可以实现一个高并发、高性能的计数器!🚀

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值