自研一个 redis 计数器组件(10Wqps),来一个通用的 最系统最透彻的计时器方案

本文 的 原文 地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

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 注解中获取 keyvaluetype 等配置。

(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 (有序集合) 数据结构。

它巧妙地利用 ZSETscore 来存储时间戳,从而实现一个“滑动”的时间窗口。


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 更加节省内存。

缺点:

  • 性能开销: 相比 INCRBYZSET 的操作更复杂,性能开销稍高。
  • 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方案;

最后,通过注解的windowSizetimeUnit参数,定义任意大小的时间窗口变得轻而易举。

这些特性使它成为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 (每秒请求数)性能分析
SIMPLE200~ 11,000作为基准,纯Redis INCR操作,QPS受限于应用与Redis之间的网络I/O。
SLIDING_WINDOW200~ 9,500增加了时间戳计算和key拼接,逻辑稍复杂,性能略低于SIMPLE,但依然很高。
SHARD (100 分片)200~ 32,000效果显著!通过将压力分散到 100 个 key,QPS 提升了近 3 倍,证明了其应对热点 key 的有效性。
LOCAL_BATCH200~ 105,000+性能王者!请求在本地内存中完成,几乎无网络瓶颈,QPS表现非常惊人。

第四章:超高并发 分片计算器 ( 第一次升级)

掌握了基础用法后,我们来探讨一些更高级的优化和扩展方案。

当单个 key 的 QPS 达到数万甚至更高时(例如,一个爆款视频的点赞),单个 Redis key 会成为瓶颈。

此时,我们需要引入更高级的优化策略。

SIMPLE策略遇到热点 Key 瓶颈时,就轮到的第一次架构升级了——引入分片计数器。

这一策略的核心目标只有一个:突破单 Key 的写入极限

4.1 核心思想:化整为零,分散火力

分片 (Sharding) 策略的思想非常经典:将一个热点 key 的写入压力,均匀地分散到 N 个小的 key 上

'就像一个收费站,原来只有一个窗口,现在开 100 个窗口,通行效率自然大大提升。

每次计数请求来临时,不再是固定地INCR同一个 key,而是随机选择一个分片 key(如 key:0key:99)进行INCR

分片 (Sharding) 策略的思想非常经典:将一个热点 key 的写入压力,均匀地分散到 N 个小的 key 上。就像一个收费站,原来只有一个窗口,现在开 100 个窗口,通行效率自然大大提升。

每次计数请求来临时,不再是固定地INCR同一个 key,而是随机选择一个分片 key(如 key:0key: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 (每秒请求数)性能分析
SIMPLE200~ 11,000作为基准,纯Redis INCR操作,QPS受限于应用与Redis之间的网络I/O。
SLIDING_WINDOW200~ 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锁超时,任务没完怎么办?

史上最全:Redis分布式 锁失效了,怎么办?

史上最全:Redis分段锁,如何设计?

redis 锁的5个大坑,如何规避?

史上最全:Redis热点Key,如何 彻底解决问题

哈罗面试:有个 redis 大 key需要在线优化, 不能影响现有业务, 怎么优化?

哈罗面试:有个 redis 大 key需要在线优化, 不能影响现有业务, 怎么优化?

史上最全:为啥Redis用哈希槽,不用一致性哈希?

史上最全:如何保持 Redis 数据一致性?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值