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

最低0.47元/天 解锁文章
726

被折叠的 条评论
为什么被折叠?



