本文 的 原文 地址
原始的内容,请参考 本文 的 原文 地址
10Wqps 高并发计数器组件,来一个 最系统最透彻的计数器方案
尼恩说在前面
在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:
如果要你设计一个支持10万QPS的分布式计数器组件,你会如何设计它的整体架构?
如果要你设计一个支持10万QPS的 滑动窗口 分布式计数器组件,如何 设计?
最近又有小伙伴在面试阿里、网易,都遇到了相关的面试题。
很多小伙伴回答了一些边边角角,但是回答不全面不体系,面试官不满意,面试挂了。
借着此文,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,展示一下雄厚的 “技术肌肉、技术实力”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提,offer自由”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V140版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到文末公号【技术自由圈】获取
高并发计数器组件 10Wqps深度实践
在各种应用中,计数器 无处不在。
为啥说无处不在 ?
从文章阅读数、视频点赞量,到API接口限流、在线用户统计,都离不开它的身影。
然而, 在高并发的冲击下, 计数器这个组件, 可能成为系统的 的 致命硬上。
错误的计数器设计不仅会导致数据失真,更可能引发库存超卖、恶意刷单等灾难性业务事故。
本章将深入剖析一个专为解决高并发计数难题而设计的企业级组件——redis-counter-starter。
我们将从业务痛点出发,详细阐述其声明式的架构设计、核心实现原理,并提供详尽的实践指南与高级优化策略,助你彻底掌握高并发计数器的设计与应用。
第一章:计数器:从简单到复杂的业务需求
1.1 业务之殇:一个小计数器引发的“血案”
让我们从一个真实的案例开始。
某电商平台在一次大促活动中,因商品库存计数器未能正确处理瞬时的高并发请求,导致锁机制失效,库存数据被并发“击穿”。
最终,系统错误地多卖出了数千件商品,造成了严重的经济损失和客户投诉。
这个“血案”生动地揭示了高并发下计数器的脆弱性。我们可以用下面的图表来复盘整个过程:

这个案例警示我们:在复杂的业务场景中,对计数器的要求远不止“加一”那么简单。
1.2 典型应用场景分析
不同的业务场景对计数器的要求千差万别。
我们需要在一致性、性能和可靠性之间做出权衡。
下表详细分析了几个典型场景:
| 业务场景 | 核心需求 | 一致性要求 | 性能要求 | 可靠性要求 | 推荐方案 |
|---|---|---|---|---|---|
| 文章阅读数 | 统计内容被阅读的次数,允许少量延迟和误差。 | 最终一致性 | 极高 (读写频繁) | 中 | SIMPLE 计数 |
| API 接口限流 | 在特定时间窗口内,精确限制某个IP或用户的访问次数。 | 强一致性 | 极高 (请求前判断) | 高 | SLIDING_WINDOW 计数 |
| 在线用户统计 | 实时统计当前活跃的用户数量。 | 近似一致性 | 高 (实时更新) | 中 | SLIDING_WINDOW 计数 |
| 电商商品库存 | 精确控制商品库存,防止超卖。 | 严格强一致性 | 高 (事务内操作) | 极高 | 分布式锁 + 数据库 |
| 视频点赞数 | 记录用户点赞行为,允许最终一致。 | 最终一致性 | 极高 (写入密集) | 高 | SIMPLE 计数 + 分片 |
通过上表,我们可以清晰地看到, 没有一种“银弹”方案能解决所有问题。
因此,设计一个能够灵活适应不同场景、可配置的计数器组件至关重要。
这正是 尼恩团队 redis-counter-starter 组件的设计初衷。
第二章:架构设计:打造声明式的计数器组件
一个理想的高并发计数器组件,应该具备高性能、可配置、易于使用和可扩展等核心特质。
它必须能够轻松应对高并发的写入请求,允许根据不同场景选择不同的计数策略,同时对业务代码无侵入,通过简单的注解即可使用,并且能够方便地增加新的计数策略。
为了将开发人员从复杂的计数器逻辑中解放出来,redis-counter-starter 采用了“约定优于配置”和“声明式”的设计哲学。
2.1 设计目标:高内聚、低耦合
我们为什么选择 “注解 + AOP” 的方案,而不是让业务代码去手动调用 Redis 命令?
1、关注点分离 (Separation of Concerns):
业务代码的核心职责是实现业务逻辑,如“发布一篇文章”或“处理一个API请求”。计数、限流等非功能性需求应作为横切关注点,与主业务流程解耦。
2、降低认知负担:
开发人员无需关心 Redis 的具体命令、数据结构或网络延迟,只需在需要计数的方法上添加一个 @Count 注解,即可“声明”其计数意图。
3、提升可维护性:
当计数策略需要升级(例如,从简单计数升级到滑动窗口),或底层实现需要更换(例如,引入新的分片算法)时,我们只需修改切面 CountAspect 的逻辑,而无需改动任何业务代码。这极大地提高了代码的可维护性和可扩展性。
2.2 核心架构与组件类图
redis-counter-starter 的核心由四个部分组成:
@Count注解CounterType枚举CountAspect切面CounterConfiguration配置类。
它们之间的关系如下图所示:

组件协作流程:
(1) 开发者在业务方法上(如 BusinessService.someMethod())添加 @Count 注解。
(2) 当该方法被调用并成功返回后,Spring AOP 会激活 CountAspect 切面。
(3) CountAspect 从 @Count 注解中获取 key、value 和 type 等配置。
(4) 根据 CounterType 的类型,CountAspect 选择不同的策略,并调用 RedisService 执行相应的 Redis 操作。
(5) CounterConfiguration 作为一个自动配置类,确保了 CountAspect 能够被 Spring 容器扫描并激活。
2.3 两种核心计数策略剖析
redis-counter-starter 内置了两种最核心的计数策略:简单计数和滑动窗口计数。
2.3.1 简单计数 (SIMPLE)
核心原理: 基于 Redis 的 INCRBY 原子命令。每次调用时,Redis 会对指定的 key 执行原子性的增加操作。
优点:
- 性能极高:
INCRBY是 Redis 中性能最高的命令之一,时间复杂度为 O(1)。 - 实现简单: 无需复杂的逻辑,直接调用即可。
- 原子性: Redis 的单线程模型保证了操作的原子性,无需担心并发问题。
缺点: 功能单一
-
只能进行累加计数(一直雷增),无法按 时间单元 进行统计。
-
例如,无法知道“过去一分钟内(时间单元) 发生了多少次”。
适用场景:
文章阅读数、视频播放量、用户总积分等场景, 这些场景 无需时间窗口的累计统计。
2.3.2 滑动窗口 (SLIDING_WINDOW)
第一个版本: 按 时间单元 进行统计 ,统计 事件数, 每一次发生,相当于一个event事件 。
核心原理:
基于 Redis 的 ZSET (有序集合) 数据结构。
它巧妙地利用 ZSET 的 score 来存储时间戳,从而实现一个“滑动”的时间窗口。
ZADD 添加 / 更新单个 / 多个成员的 基础语法
# 单个成员:ZADD 有序集合键 成员1分数 成员1值
ZADD key score1 member1
# 多个成员:ZADD 有序集合键 成员1分数 成员1值 成员2分数 成员2值 ...
ZADD key score1 member1 score2 member2 ...
工作流程图解:
key 为 zset 的key , member 为 事件id ,score 为事件发生的时间。

详细步骤:
1、记录事件:
当一个事件发生时,生成一个唯一的成员(如 eventid),并以当前时间戳作为 score,使用 ZADD 命令将其添加到 ZSET 中。
2、清理窗口:
在添加新成员后,立即使用 ZREMRANGEBYSCORE 命令,移除所有 score (即时间戳) 小于 (当前时间 - 窗口大小) 的成员。这一步是“滑动”的关键,它确保了 ZSET 中只保留最近一个时间窗口内的数据。
3、获取总数:
使用 ZCARD 命令获取 ZSET 中的成员数量,这个数量就是当前时间窗口内的事件总数。
优点:
- 精确时间统计: 能够精确地统计任意时间周期内的事件发生次数。
- 内存高效: 相比于为每个事件都创建一个带 TTL 的 key,
ZSET更加节省内存。
缺点:
- 性能开销: 相比
INCRBY,ZSET的操作更复杂,性能开销稍高。 - big问题: 如果 1分钟内有 1 亿万次计数,那么 set 就有1 亿 个成员,这就是big key 。
适用场景:
API 接口限流、防止恶意刷单、统计单位时间内的在线用户数等。
第二个版本: 每一个redis subkey 单元,记录一个 时间槽位的 事件数,解决big问题 。
可以把要统计的时间窗口(例如 1 分钟)想象成一个由 60 个格子组成的传送带,每个格子代表 1 秒。
每一个格子就是一个时间槽位。
每一个格子, 用一个 redis key 来表达,或者用 zset 的一个 subkey 来表达。
每一个 subkey 单元,记录一个 时间槽位的 事件数。
当一个请求进来时,首先计算 当前时间对应的那个格子,然后进行计数 加 1,并给这个格子设置一个略大于 1 分钟的过期时间。
当要获取总数时,只需把传送带上所有格子的值加起来即可。
由于过期的格子会自动被 Redis 的过期机制清理,这个传送带就实现了“滑动”的效果。
工作流程图解:

该策略的优点:
首先,它通过将压力分散到多个小 key 上,完美地避免了 Big Key 风险;
其次,其核心操作是INCR,性能远高于传统ZSET方案;
最后,通过注解的windowSize和timeUnit参数,定义任意大小的时间窗口变得轻而易举。
这些特性使它成为API 接口限流、用户行为频率限制(如每分钟发帖数)、防恶意刷单等场景的理想选择。
第三章:redis-counter-starter 组件深度实践
理论结合实践是最好的学习方式。
3.1 核心代码剖析
3.1.1 @Count 注解 类
@Count 是与开发者直接交互的入口。
package org.dromara.common.counter.annotation;
import org.dromara.common.counter.enums.CounterType;
import java.lang.annotation.*;
/**
* 分布式计数器注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Count {
/**
* Redis Key, 支持 Spring EL 表达式。
* SpEL 表达式必须用单引号包裹,例如: "'user:login:fail:' + #username"
*/
String key();
/**
* 每次计数的增量,默认为 1。
* 对于 SLIDING_WINDOW 类型,此参数无效。
*/
long value() default 1L;
/**
* 计数器类型,默认为 SLIDING_WINDOW。
* 可选值: SIMPLE, SLIDING_WINDOW
*/
CounterType type() default CounterType.SLIDING_WINDOW;
}
key: 最核心的参数。它支持 Spring EL (SpEL) 表达式,这使得我们可以动态地根据方法的参数来生成 key。
例如,"'article:read:count:' + #id" 会将方法参数 id 的值拼接到 key 中。注意:SpEL 表达式本身必须用单引号包裹。
value: 仅在 SIMPLE 类型下有效,表示每次增加的计数值。
type: 用于切换计数策略,是组件灵活性的关键。
3.1.2 CountAspect 切面类
CountAspect 是所有魔法发生的地方。
它是一个后置切面 (@After),意味着它会在被注解的方法成功执行**之后(而不是之前)**才进行计数。
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class CountAspect {
private final RedisService redisService;
@After("@annotation(count)")
public void doAfter(JoinPoint joinPoint, Count count) {
// 1. 获取方法签名和参数,为解析SpEL做准备
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 2. 使用工具类解析SpEL表达式,生成最终的Redis Key
String key = SpringExpressionUtil.parseExpression(method, joinPoint.getArgs(), count.key());
// 3. 根据注解中定义的类型,选择不同的计数策略
if (count.type() == CounterType.SIMPLE) {
// 3.1. SIMPLE策略:直接调用INCRBY
redisService.incrBy(key, count.value());
} else if (count.type() == CounterType.SLIDING_WINDOW) {
// 3.2. SLIDING_WINDOW策略:使用ZSET
long currentTimeMillis = System.currentTimeMillis();
// 创建唯一成员,防止重复
String member = UUID.randomUUID().toString() + ":" + currentTimeMillis;
// 使用当前时间戳作为score,添加到ZSET
redisService.getZSetCache().add(key, member, currentTimeMillis);
// 移除1分钟前的旧数据,实现窗口“滑动”
// 注意:这里的1分钟是硬编码的
redisService.getZSetCache().removeRangeByScore(key, 0, currentTimeMillis - TimeUnit.MINUTES.toMillis(1));
}
}
}
核心逻辑拆解:
(1) 解析动态 Key: 通过 SpringExpressionUtil.parseExpression 方法,切面能够解析 @Count 注解中 key 属性的 SpEL 表达式,从方法参数中提取值,并动态构建出最终的 Redis key。这是实现精细化计数的关键。
(2) 策略路由: 一个简单的 if-else 判断,根据 count.type() 的值,将执行流程引导至不同的 Redis 操作。
(3) 执行 Redis 命令:
- 对于 `SIMPLE`,直接执行 `INCRBY`。
- 对于 `SLIDING_WINDOW`,则执行 `ZADD` 和 `ZREMRANGEBYSCORE` 的组合操作,以维护时间窗口。
3.2 封装起步依赖
把上面的代码,封装一个起步依赖 ,供业务使用。
基于已有核心代码(@Count注解、CountAspect切面),按 Spring Boot 起步依赖规范封装,实现 “业务方引入依赖即能用”,无需额外配置切面或 Bean,核心包含目录结构设计、依赖配置、自动配置、可配置扩展四部分,具体如下:
3.2.1、起步依赖目录结构(Maven 规范)
遵循 Spring Boot Starter 标准目录结构,确保业务方引入后 Spring 能自动扫描加载组件:
redis-counter-spring-boot-starter/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── org/
│ │ │ └── dromara/
│ │ │ └── common/
│ │ │ └── counter/
│ │ │ ├── annotation/ // 注解定义
│ │ │ │ └── Count.java // 已有@Count注解
│ │ │ ├── aspect/ // 切面逻辑
│ │ │ │ └── CountAspect.java // 已有切面类
│ │ │ ├── config/ // 自动配置类
│ │ │ │ ├── RedisCounterAutoConfiguration.java // 核心自动配置
│ │ │ │ └── RedisCounterProperties.java // 可配置属性(扩展滑动窗口时间)
│ │ │ ├── enums/ // 枚举定义
│ │ │ │ └── CounterType.java // 已有计数器类型枚举
│ │ │ └── util/ // 工具类
│ │ │ └── SpringExpressionUtil.java // 已有SpEL解析工具
│ │ └── resources/
│ │ └── META-INF/
│ │ ├── spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports // 自动配置入口(Spring Boot 2.7+)
│ │ └── MANIFEST.MF // 可选,Maven自动生成
│ └── test/ // 测试代码(略,可加单元测试验证切面逻辑)
│ └── java/
└── pom.xml // 依赖配置
3.2.2、核心配置文件编写
1、 pom.xml 依赖配置(关键)
引入 Spring Boot 核心依赖、AOP 依赖(切面必需)、Redis 依赖(操作 Redis 必需),并设置依赖 scope 确保不传递冗余依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.10</version> <!-- 适配主流Spring Boot版本,可按需调整 -->
<relativePath/>
</parent>
<!-- 起步依赖坐标:遵循Spring Boot Starter命名规范(xxx-spring-boot-starter) -->
<groupId>org.dromara.common</groupId>
<artifactId>redis-counter-spring-boot-starter</artifactId>
<version>1.0.0</version>
<name>redis-counter-spring-boot-starter</name>
<description>高并发Redis计数器起步依赖,支持SIMPLE/滑动窗口计数</description>
<dependencies>
<!-- 1. Spring Boot核心依赖:确保起步依赖能融入Spring生态 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<!-- 排除logback,避免与业务方日志框架冲突 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 2. AOP依赖:切面功能必需 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 3. Redis依赖:操作Redis必需,兼容业务方RedisTemplate -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 4. Lombok:简化代码(CountAspect用@Slf4j) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional> <!-- 可选依赖,业务方若不用可排除 -->
</dependency>
<!-- 5. Spring表达式依赖:解析SpEL表达式必需 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
</dependency>
</dependencies>
<!-- 打包配置:确保生成的jar包包含所有必要资源 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip> <!-- 起步依赖无需打包成可执行jar,跳过此步骤 -->
</configuration>
</plugin>
</plugins>
</build>
</project>
2、自动配置入口文件(关键)
在resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports中,指定自动配置类全路径,让 Spring Boot 启动时自动加载:
org.dromara.common.counter.config.RedisCounterAutoConfiguration
3.3 使用 redis-counter-starter 组件
非常简单。
在你的业务模块中使用 redis-counter-starter 组件非常简单。
第一步:引入依赖
在你的业务模块(例如 ruoyi-demo)的 pom.xml 文件中,添加对 redis-counter-starter 的依赖。
<dependency>
<groupId>org.dromara</groupId>
<artifactId>redis-counter-starter</artifactId>
</dependency>
第二步:添加注解
在需要计数的方法上,添加 @Count 注解并配置相应的参数。
示例一:文章阅读数统计 (简单计数)
假设我们有一个根据文章ID查询文章详情的接口,我们希望每次调用时都为该文章的阅读数加一。
import org.dromara.common.counter.annotation.Count;
import org.dromara.common.counter.enums.CounterType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
// ...
/**
* 获取文章详情,并增加阅读数
* @param id 文章ID
*/
@Count(key = "'article:read:count:' + #id", type = CounterType.SIMPLE)
@GetMapping("/article/{id}")
public R<ArticleVo> getArticle(@PathVariable Long id) {
// ...查询文章的业务逻辑...
return R.ok(articleVo);
}
key = "'article:read:count:' + #id": SpEL 表达式从方法参数中获取id,如果id的值为123,则最终的 Redis key 为article:read:count:123。type = CounterType.SIMPLE: 指定使用简单计数策略,每次调用后,对应 key 的值会原子性地加 1。
示例二:IP 访问限流 (滑动窗口)
假设我们希望限制同一个 IP 在 1 分钟内对某个重要接口的访问不能超过 10 次。
import org.dromara.common.counter.annotation.Count;
import org.dromara.common.counter.enums.CounterType;
import org.dromara.common.redis.utils.RedisService;
import org.dromara.common.core.utils.ServletUtils;
// ...
@Autowired
private RedisService redisService;
/**
* 执行一个敏感操作,限制每个IP每分钟的访问次数
*/
@GetMapping("/sensitive-op")
public R<Void> sensitiveOperation() {
String ip = ServletUtils.getClientIP();
String key = "'limit:ip:' + ip";
// 在执行业务逻辑前,先检查计数
long count = redisService.getZSetCache().size(key);
if (count > 10) {
return R.fail("操作频繁,请稍后再试");
}
// 执行计数(注解会自动完成)
doSensitiveOperation(ip);
return R.ok("操作成功");
}
@Count(key = "'limit:ip:' + #ip", type = CounterType.SLIDING_WINDOW)
public void doSensitiveOperation(String ip) {
// ...真正的敏感操作业务逻辑...
}
key = "'limit:ip:' + #ip": SpEL 表达式获取传递进来的 ip 地址,为每个 IP 创建独立的计数器。
type = CounterType.SLIDING_WINDOW: 指定使用滑动窗口策略。
限流逻辑:
在业务方法执行前,我们手动使用 redisService.getZSetCache().size(key) 来获取当前窗口的计数值,并进行判断。@Count 注解则在方法执行后完成计数的增加。
注意:
redis-counter-starter的当前实现中,滑动窗口的大小固定为 1 分钟。
第三步:性能压测
为了直观地展示不同策略的性能差异,在相同的硬件环境下(单机,4 核 8G,本地 Redis),使用wrk工具对四种策略的写入接口进行了压测。
压测命令模板:
# -t: 线程数 -c: 连接数 -d: 持续时间
wrk -t8 -c200 -d30s http://localhost:8080/demo/counter/simple
压测结果汇总:
| 策略类型 | 并发连接数 | 压测后 QPS (每秒请求数) | 性能分析 |
|---|---|---|---|
| SIMPLE | 200 | ~ 11,000 | 作为基准,纯Redis INCR操作,QPS受限于应用与Redis之间的网络I/O。 |
| SLIDING_WINDOW | 200 | ~ 9,500 | 增加了时间戳计算和key拼接,逻辑稍复杂,性能略低于SIMPLE,但依然很高。 |
| SHARD (100 分片) | 200 | ~ 32,000 | 效果显著!通过将压力分散到 100 个 key,QPS 提升了近 3 倍,证明了其应对热点 key 的有效性。 |
| LOCAL_BATCH | 200 | ~ 105,000+ | 性能王者!请求在本地内存中完成,几乎无网络瓶颈,QPS表现非常惊人。 |
第四章:超高并发 分片计算器 ( 第一次升级)
掌握了基础用法后,我们来探讨一些更高级的优化和扩展方案。
当单个 key 的 QPS 达到数万甚至更高时(例如,一个爆款视频的点赞),单个 Redis key 会成为瓶颈。
此时,我们需要引入更高级的优化策略。
当SIMPLE策略遇到热点 Key 瓶颈时,就轮到的第一次架构升级了——引入分片计数器。
这一策略的核心目标只有一个:突破单 Key 的写入极限。
4.1 核心思想:化整为零,分散火力
分片 (Sharding) 策略的思想非常经典:将一个热点 key 的写入压力,均匀地分散到 N 个小的 key 上。
'就像一个收费站,原来只有一个窗口,现在开 100 个窗口,通行效率自然大大提升。
每次计数请求来临时,不再是固定地INCR同一个 key,而是随机选择一个分片 key(如 key:0 到 key:99)进行INCR。
分片 (Sharding) 策略的思想非常经典:将一个热点 key 的写入压力,均匀地分散到 N 个小的 key 上。就像一个收费站,原来只有一个窗口,现在开 100 个窗口,通行效率自然大大提升。
每次计数请求来临时,不再是固定地INCR同一个 key,而是随机选择一个分片 key(如 key:0 到 key:99)进行INCR。
工作流程

读取总数 的流程

4.1.1 分片 (Sharding)
思想: 将一个热点 key 的压力分散到多个 key 上。
实现: 不再使用 key 作为唯一的计数器,而是使用 key:suffix 的形式创建多个分片(Shard)。
每次计数时,随机选择一个分片进行 INCR 操作。
读取总数时,需要 MGET 所有分片并求和。
示例:
- 原始 key:
video:like:888 - 分片后的 keys:
video:like:888:0,video:like:888:1, …,video:like:888:99
SpEL 实现: 可以在 key 中引入随机数。
@Count(key = "'video:like:' + #videoId ", type = CounterType=shard,shard=100)
优点: 极大地提升了写入吞吐量,理论上可以线性扩展。
缺点: 读取总数的操作变重了,需要一次性获取所有分片。适用于写多读少的场景。
第三步:性能压测
为了直观地展示不同策略的性能差异,在相同的硬件环境下(单机,4 核 8G,本地 Redis),使用wrk工具对四种策略的写入接口进行了压测。
压测命令模板:
# -t: 线程数 -c: 连接数 -d: 持续时间
wrk -t8 -c200 -d30s http://localhost:8080/demo/counter/simple
压测结果汇总:
| 策略类型 | 并发连接数 | 压测后 QPS (每秒请求数) | 性能分析 |
|---|---|---|---|
| SIMPLE | 200 | ~ 11,000 | 作为基准,纯Redis INCR操作,QPS受限于应用与Redis之间的网络I/O。 |
| SLIDING_WINDOW | 200 | ~ 9,500 | 增加了时间戳计算和key拼接,逻辑稍复杂,性能略低于SIMPLE,但依然很高。 |
| SHARD (100 分片) | 200 | ~ 32,000 | 效果显著!通过将压力分散到 100 个 key,QPS 提升了近 3 倍,证明了其应对热点 key 的有效性。 |
第五章:巨高并发 二级计时器(第二次升级)
…由于平台篇幅限制, 剩下的内容(5000字+),请参参见原文地址
原始的内容,请参考 本文 的 原文 地址
尼恩 团队 redis 塔尖面试题, 全吃透 毒打面试官
阿里面试:Redis挂了怎么办?集群主节点挂了怎么 恢复数据?可能有多长时间 数据丢失?
哈罗面试:Redis怎么模糊查询? Redis危险的命令有哪些?
阿里面试:Redis 为啥那么快?怎么实现 100W并发?说出 这 6大架构,面试官跪 了
腾讯面试: 执行一条redis 命令时,底层干了什么? 小伙懵逼,挂了
京东面试: 亿级 数据黑名单 ,如何实现?(此文介绍了布隆过滤器、布谷鸟过滤器)
希音面试:亿级用户 日活 月活,如何统计?(史上最强 HyperLogLog 解读)
史上最全: Redis: 缓存击穿、缓存穿透、缓存雪崩 ,如何彻底解决?
史上最全: Redis锁如何续期 ?Redis锁超时,任务没完怎么办?
哈罗面试:有个 redis 大 key需要在线优化, 不能影响现有业务, 怎么优化?

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



