找回密码
 立即注册
首页 业界区 业界 Redis实现高并发场景下的计数器设计

Redis实现高并发场景下的计数器设计

窖咎 2025-6-5 09:27:22
大部分互联网公司都需要处理计数器场景,例如风控系统的请求频控、内容平台的播放量统计、电商系统的库存扣减等。
传统方案一般会直接使用RedisUtil.incr(key),这是最简单的方式,但这种方式在生产环境中会暴露严重问题:
  1. // 隐患示例
  2. public long addOne(String key) {
  3.     Long result = RedisUtil.incr(key);
  4.     // 若未设置TTL,key将永久驻留内存
  5.     return result;
  6. }
复制代码
INCR 有自动初始化机制,即当 Redis 检测到目标 key 不存在时,会自动将其初始化为 0,再执行递增操作
高可用计数器的实现

原子操作保障计数准确性

NX+EX 原子初始化
  1. RedisUtil.set(key, "0", "nx", "ex", time);
复制代码
通过Redis的SET key value NX EX命令,实现原子化的"不存在即创建+设置过期时间",避免多个线程竞争初始化导致数据覆盖(如线程A初始化后,线程B用SET覆盖值为0)
Redis单线程模型保证命令原子性,无需额外分布式锁
使用setnx命令来设置了过期时间,防止key永不过期
INCR 原子递增
  1. long result = RedisUtil.incr(key);
复制代码
先setnx命令后,再使用INCR来执行递增操作
即:
  1. public void addOne(String key) {
  2.     RedisUtil.set(key, "0", "nx", "ex", time);
  3.     Long result = RedisUtil.incr(key);
  4.         return result;
  5. }
复制代码
双重补偿机制解决过期异常

但只是使用以上两个命令还是有可能导致并发安全问题。
例如:
当两个线程同时执行 SETNX 时,未抢到初始化的线程直接执行INCR,导致key存在但无TTL
如果有一个线程A正在执行SET key 0 NX EX 60 ,而线程B也执行方法addOne,此时线程A正在执行,线程B无法执行set操作,会直接继续执行后续命令(如 INCR),此时若线程A由于网络抖动等原因初始化key失败,那就有可能导致 key 永不过期。因此需要有补偿机制,完成redis key超时时间的设置
注意:当 SETNX 命令无法执行(即目标 key 已存在时),会直接继续执行后续命令(如 INCR),而不会阻塞等待
首次递增补偿

因此可以通过判断result == 1来识别是否是首次递增,如果是首次递增的话,则强制续期
  1. if (result == 1) {
  2.     RedisUtil.expire(key, time);
  3. }
复制代码
TTL异常检测补偿

极端场景下(Redis主从切换、命令执行异常导致TTL丢失),key 可能因未设置或过期时间丢失而长期存在
  1. if (RedisUtil.ttl(key) == -1) {
  2.     RedisUtil.expire(key, time);
  3. }
复制代码
检查 TTL 是否为 -1(-1表示无过期时间),重新设置过期时间,作为兜底保护。
经过双重补偿机制后的代码如下:
  1. public void addOne(String key) {
  2.     RedisUtil.set(key, "0", "nx", "ex", time);
  3.     Long result = RedisUtil.incr(key);
  4.     //解决并发问题,否则会导致计数器永不清空
  5.     //如果incr的结果为1,有两个结果,先进行set操作,此时有过期时间。第二种:直接执行incr操作,此时的redisKey没有过期时间。所以需要补偿处理
  6.     if (result == 1) {
  7.          RedisUtil.expire(key, time);
  8.     }
  9.     // 检查是否有过期时间, 对异常没有设置过期时间的key补偿
  10.     if (RedisUtil.ttl(key) == -1) {
  11.          RedisUtil.expire(key, time);
  12.     }
  13.     return result;
  14. }
复制代码
异常处理与降级策略

有时候可能会因网络抖动、服务短暂不可用、主备切换等暂时性故障,导致Redis操作失败,因此可以对这中异常进行处理,将需要完成的操作放入到队列中,再使用一个线程循环重试,保证最终一致性
  1. public void addOne(String key) {
  2.     Long result = 1;
  3.     try{
  4.         RedisUtil.set(key, "0", "nx", "ex", time);
  5.         result = RedisUtil.incr(key);
  6.         //解决并发问题,否则会导致计数器永不清空
  7.         //如果incr的结果为1,有两个结果,先进行set操作,此时有过期时间。第二种:直接执行incr操作,此时的redisKey没有过期时间。所以需要补偿处理
  8.         if (result == 1) {
  9.              RedisUtil.expire(key, time);
  10.         }
  11.         // 检查是否有过期时间, 对异常没有设置过期时间的key补偿
  12.         if (RedisUtil.ttl(key) == -1) {
  13.              RedisUtil.expire(key, time);
  14.         }
  15.     } catch (Exception e) {
  16.         //丢到重试队列中,一直重试
  17.             queue.offer(key);
  18.         }
  19.     return result;
  20. }
复制代码
架构设计示意图

graph TD    A[客户端请求] --> B{Key存在?}    B -->|否| C[SET NX EX初始化]    B -->|是| D[INCR原子递增]    C --> D    D --> E{result=1?}    E -->|是| F[补偿设置TTL]    E -->|否| G[检查TTL]    G -->|TTL=-1| H[二次补偿]    G -->|TTL正常| I[返回结果]    H --> I    F --> I关键机制对比

机制解决的问题Redis特性利用性能影响SET NX EX并发初始化竞争原子单命令O(1)INCR计数不准确/超卖原子递增O(1)TTL双重补偿Key永不过期EXPIRE命令幂等性额外1次查询异常队列重试网络抖动/Redis不可用最终一致性异步处理这个方案充分挖掘了Redis原子命令的潜力,通过补偿机制弥补分布式系统的不确定性,最终在简单与可靠之间找到平衡点。
往期推荐


  • 《SpringBoot》EasyExcel实现百万数据的导入导出
  • 《SpringBoot》史上最全SpringBoot相关注解介绍
  • Spring框架IoC核心详解
  • 万字长文带你窥探Spring中所有的扩展点
  • 如何实现一个通用的接口限流、防重、防抖机制
  • 万字长文带你深入Redis底层数据结构
  • volatile关键字最全原理剖析

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册