高并发计数器设计全指南:从技术选型到落地实现

在秒杀、抢购、访问量统计等高频场景中,计数器是核心组件(如 “商品剩余库存计数”“活动参与人数统计”“接口调用次数限制”)。高并发场景下,计数器面临 “瞬时请求峰值高、数据一致性要求严、避免重复计数” 三大挑战,传统单节点数据库计数极易出现性能瓶颈或数据错乱。本文将系统拆解高并发计数器的设计思路,提供可落地的技术方案与代码实现。

一、先明确高并发计数器的核心需求与挑战

在设计前,需先定义计数器的核心指标,避免 “过度设计” 或 “功能缺失”:

1. 核心需求

需求类型

具体要求

高性能

支持每秒 10 万 + 请求(如秒杀开始时的库存扣减),响应延迟 < 10ms

数据一致性

最终一致性(允许短暂延迟)或强一致性(如库存计数不允许超卖),避免重复 / 漏计数

可扩展性

支持计数器动态扩容(如新增商品维度计数),适配业务增长

防恶意攻击

拦截高频重复请求(如脚本刷计数),避免计数器被篡改

可监控性

实时监控计数器 QPS、错误率、数据偏差,便于问题排查

2. 核心挑战

  • 并发竞争:多线程 / 多节点同时更新计数器,易出现 “超卖”(如库存从 100 扣减为 98,实际显示 99)或 “计数不一致”。
  • 性能瓶颈:单节点数据库(如 MySQL)的写性能有限(每秒约 1 万 - 2 万写请求),无法支撑百万级并发。
  • 数据持久化:纯内存计数(如本地缓存)在节点宕机后数据丢失,需兼顾性能与持久化。
  • 重复计数:网络重试、前端重复提交导致同一操作被多次计数(如用户刷新页面导致重复扣减库存)。

二、高并发计数器的分层设计架构

高并发计数器需遵循 “分层拦截、异步落地、多级缓存” 原则,从接入层到数据层逐层优化,减少核心存储的压力。整体架构分为 4 层:

 

接入层(限流防刷) → 应用层(本地缓存+幂等控制) → 缓存层(Redis集群) → 数据层(数据库持久化)

各层核心职责与技术选型

架构分层

核心职责

常用技术选型

设计目标

接入层

拦截恶意请求、限制请求峰值

Nginx 限流、云 WAF、API 网关(Gateway)

过滤 60% 以上无效请求

应用层

本地缓存暂存计数、幂等校验、异步提交

Caffeine(本地缓存)、Redisson(分布式锁)

减少缓存层访问次数

缓存层

高并发计数更新、分布式锁控制

Redis Cluster(主从 + 哨兵)、Redis Pipeline

支撑百万级并发读写

数据层

计数最终持久化、数据恢复

MySQL(分库分表)、TiDB(分布式数据库)

保证数据不丢失、可追溯

三、核心技术方案:从计数更新到数据落地

1. 接入层:先拦掉无效请求(限流 + 防刷)

接入层是计数器的 “第一道防线”,核心目标是减少下游压力,避免恶意请求冲击。

(1)基于 IP / 用户 ID 的限流

通过 Nginx 或 API 网关限制单个 IP / 用户的请求频率,避免脚本高频调用。

Nginx 限流配置示例(按 IP 限流)


http {

# 定义限流策略:每秒最多10个请求,允许5个请求排队

limit_req_zone $binary_remote_addr zone=counter_limit:10m rate=10r/s;

server {

listen 80;

server_name counter.example.com;

# 计数器接口限流

location /api/counter/update {

limit_req zone=counter_limit burst=5 nodelay; # 不等待排队请求

proxy_pass http://counter-service-cluster; # 转发到应用层

proxy_set_header Host $host;

proxy_set_header X-Real-IP $remote_addr;

}

}

}
(2)防刷校验
  • 对敏感计数(如库存扣减),需先通过验证码(如滑动验证码、短信验证码)或 Token 校验,确保是真实用户操作。
  • 接入云 WAF,拦截 SQL 注入、CC 攻击等恶意请求,避免计数器接口被攻击篡改。

2. 应用层:本地缓存 + 幂等控制(减少缓存层压力)

应用层通过 “本地缓存暂存 + 幂等校验” 减少对 Redis 的访问次数,同时避免重复计数。

(1)本地缓存暂存(Caffeine)

使用 Caffeine(高性能本地缓存)暂存热点计数器(如 “商品 A 的库存”),本地缓存命中时直接返回结果,无需访问 Redis。

Caffeine 配置与使用示例

import com.github.benmanes.caffeine.cache.Caffeine;

import com.github.benmanes.caffeine.cache.LoadingCache;

import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component

public class LocalCounterCache {

    // 本地缓存:key=计数器ID(如"stock:1001"),value=计数 value

    private final LoadingCache < String, Long > counterCache;

    // 初始化Caffeine缓存:过期时间5秒,最大容量1000(存储热点计数器)

    public LocalCounterCache() {

        this.counterCache = Caffeine.newBuilder()

            .expireAfterWrite(5, TimeUnit.SECONDS) // 5秒过期,避免与Redis数据偏差过大

            .maximumSize(1000) // 最大存储1000个计数器,避免内存溢出

            .build(this::loadFromRedis); // 缓存未命中时,从Redis加载数据

    }

    /**

    * 从Redis加载计数器数据(缓存未命中时调用)

    */

    private Long loadFromRedis(String counterId) {

        // 调用Redis工具类获取计数(后续章节实现)

        return RedisCounterUtil.get(counterId);

    }

    /**

    * 获取计数器值(优先读本地缓存)

    */

    public Long getCounter(String counterId) {

        return counterCache.get(counterId);

    }

    /**

    * 更新计数器(先更新本地缓存,再异步更新Redis)

    */

    public void updateCounter(String counterId, long delta) {

        // 1. 更新本地缓存(增量更新,避免全量覆盖)

        counterCache.asMap().computeIfPresent(counterId, (k, v) -> v + delta);

        // 2. 异步提交到Redis(避免阻塞当前线程)

        CounterAsyncTask.submit(() -> RedisCounterUtil.increment(counterId, delta));

    }

}
(2)幂等控制(避免重复计数)

通过 “请求唯一 ID” 确保同一操作仅被计数一次(如用户提交订单时,生成orderId作为唯一 ID)。

幂等校验示例

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component

public class IdempotentChecker {

    private final RedisTemplate < String, Object > redisTemplate;

    private static final String IDEMPOTENT_KEY_PREFIX = "counter:idempotent:";

    private static final long EXPIRE_TIME = 24; // 幂等Key过期时间(小时)

    public IdempotentChecker(RedisTemplate < String, Object > redisTemplate) {

        this.redisTemplate = redisTemplate;

    }

    /**

    * 校验请求是否已处理(true=未处理,false=已处理)

    */

    public boolean checkAndMark(String requestId) {

        String key = IDEMPOTENT_KEY_PREFIX + requestId;

        // 用Redis的SETNX(不存在则设置)实现幂等:成功返回true(未处理),失败返回false(已处理)

        Boolean isNew = redisTemplate.opsForValue().setIfAbsent(key, "1", EXPIRE_TIME, TimeUnit.HOURS);

        return Boolean.TRUE.equals(isNew);

    }

}

// 调用示例(计数器更新接口中)

@RestController

@RequestMapping("/api/counter")

public class CounterController {

    @Autowired

    private LocalCounterCache localCounterCache;

    @Autowired

    private IdempotentChecker idempotentChecker;

    @PostMapping("/update")

    public Result updateCounter(@RequestParam String counterId,

        @RequestParam long delta,

     
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值