Redis实现高并发场景下的计数器设计
大部分互联网公司都需要处理计数器场景,例如风控系统的请求频控、内容平台的播放量统计、电商系统的库存扣减等。
传统方案一般会直接使用RedisUtil.incr(key),这是最简单的方式,但这种方式在生产环境中会暴露严重问题:
// 隐患示例
public long addOne(String key) {
Long result = RedisUtil.incr(key);
// 若未设置TTL,key将永久驻留内存
return result;
}
INCR 有自动初始化机制,即当 Redis 检测到目标 key 不存在时,会自动将其初始化为 0,再执行递增操作
高可用计数器的实现
原子操作保障计数准确性
NX+EX 原子初始化
RedisUtil.set(key, "0", "nx", "ex", time);
通过Redis的SET key value NX EX命令,实现原子化的"不存在即创建+设置过期时间",避免多个线程竞争初始化导致数据覆盖(如线程A初始化后,线程B用SET覆盖值为0)
Redis单线程模型保证命令原子性,无需额外分布式锁
使用setnx命令来设置了过期时间,防止key永不过期
INCR 原子递增
long result = RedisUtil.incr(key);
先setnx命令后,再使用INCR来执行递增操作
即:
public void addOne(String key) {
RedisUtil.set(key, "0", "nx", "ex", time);
Long result = RedisUtil.incr(key);
return result;
}
双重补偿机制解决过期异常
但只是使用以上两个命令还是有可能导致并发安全问题。
例如:
当两个线程同时执行 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来识别是否是首次递增,如果是首次递增的话,则强制续期
if (result == 1) {
RedisUtil.expire(key, time);
}
TTL异常检测补偿
在极端场景下(Redis主从切换、命令执行异常导致TTL丢失),key 可能因未设置或过期时间丢失而长期存在
if (RedisUtil.ttl(key) == -1) {
RedisUtil.expire(key, time);
}
检查 TTL 是否为 -1(-1表示无过期时间),重新设置过期时间,作为兜底保护。
经过双重补偿机制后的代码如下:
public void addOne(String key) {
RedisUtil.set(key, "0", "nx", "ex", time);
Long result = RedisUtil.incr(key);
//解决并发问题,否则会导致计数器永不清空
//如果incr的结果为1,有两个结果,先进行set操作,此时有过期时间。第二种:直接执行incr操作,此时的redisKey没有过期时间。所以需要补偿处理
if (result == 1) {
RedisUtil.expire(key, time);
}
// 检查是否有过期时间, 对异常没有设置过期时间的key补偿
if (RedisUtil.ttl(key) == -1) {
RedisUtil.expire(key, time);
}
return result;
}
异常处理与降级策略
有时候可能会因网络抖动、服务短暂不可用、主备切换等暂时性故障,导致Redis操作失败,因此可以对这中异常进行处理,将需要完成的操作放入到队列中,再使用一个线程循环重试,保证最终一致性
public void addOne(String key) {
Long result = 1;
try{
RedisUtil.set(key, "0", "nx", "ex", time);
result = RedisUtil.incr(key);
//解决并发问题,否则会导致计数器永不清空
//如果incr的结果为1,有两个结果,先进行set操作,此时有过期时间。第二种:直接执行incr操作,此时的redisKey没有过期时间。所以需要补偿处理
if (result == 1) {
RedisUtil.expire(key, time);
}
// 检查是否有过期时间, 对异常没有设置过期时间的key补偿
if (RedisUtil.ttl(key) == -1) {
RedisUtil.expire(key, time);
}
} catch (Exception e) {
//丢到重试队列中,一直重试
queue.offer(key);
}
return result;
}
架构设计示意图
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关键字最全原理剖析
Redis实现高并发场景下的计数器设计的更多相关文章
- C++高并发场景下读多写少的解决方案
C++高并发场景下读多写少的解决方案 概述 一谈到高并发的解决方案,往往能想到模块水平拆分.数据库读写分离.分库分表,加缓存.加mq等,这些都是从系统架构上解决.单模块作为系统的组成单元,其性能好坏也 ...
- C++高并发场景下读多写少的优化方案
概述 一谈到高并发的优化方案,往往能想到模块水平拆分.数据库读写分离.分库分表,加缓存.加mq等,这些都是从系统架构上解决.单模块作为系统的组成单元,其性能好坏也能很大的影响整体性能,本文从单模块下读 ...
- Qunar机票技术部就有一个全年很关键的一个指标:搜索缓存命中率,当时已经做到了>99.7%。再往后,每提高0.1%,优化难度成指数级增长了。哪怕是千分之一,也直接影响用户体验,影响每天上万张机票的销售额。 在高并发场景下,提供了保证线程安全的对象、方法。比如经典的ConcurrentHashMap,它比起HashMap,有更小粒度的锁,并发读写性能更好。线程安全的StringBuilder取代S
Qunar机票技术部就有一个全年很关键的一个指标:搜索缓存命中率,当时已经做到了>99.7%.再往后,每提高0.1%,优化难度成指数级增长了.哪怕是千分之一,也直接影响用户体验,影响每天上万张机 ...
- 【转】记录PHP、MySQL在高并发场景下产生的一次事故
看了一篇网友日志,感觉工作中值得借鉴,原文如下: 事故描述 在一次项目中,上线了一新功能之后,陆陆续续的有客服向我们反应,有用户的个别道具数量高达42亿,但是当时一直没有到证据表示这是,确实存在,并且 ...
- 高并发场景下System.currentTimeMillis()的性能问题的优化 以及SnowFlakeIdWorker高性能ID生成器
package xxx; import java.sql.Timestamp; import java.util.concurrent.*; import java.util.concurrent.a ...
- HttpClient在高并发场景下的优化实战
在项目中使用HttpClient可能是很普遍,尤其在当下微服务大火形势下,如果服务之间是http调用就少不了跟http客户端找交道.由于项目用户规模不同以及应用场景不同,很多时候可能不需要特别处理也. ...
- 高并发场景下System.currentTimeMillis()的性能问题的优化
高并发场景下System.currentTimeMillis()的性能问题的优化 package cn.ucaner.alpaca.common.util.key; import java.sql.T ...
- MySQL在大数据、高并发场景下的SQL语句优化和"最佳实践"
本文主要针对中小型应用或网站,重点探讨日常程序开发中SQL语句的优化问题,所谓“大数据”.“高并发”仅针对中小型应用而言,专业的数据库运维大神请无视.以下实践为个人在实际开发工作中,针对相对“大数据” ...
- 高并发场景下System.currentTimeMillis()的性能优化
一.前言 System.currentTimeMillis()的调用比new一个普通对象要耗时的多(具体耗时高出多少我也不知道,不过听说在100倍左右),然而该方法又是一个常用方法, 有时不得不使用, ...
- 高并发场景下的httpClient优化使用
1.背景 我们有个业务,会调用其他部门提供的一个基于http的服务,日调用量在千万级别.使用了httpclient来完成业务.之前因为qps上不去,就看了一下业务代码,并做了一些优化,记录在这里. 先 ...
随机推荐
- linux:redis
查询: 链接 redis初了解 是一个 "开源.免费" 的高性能的 key - value 的数据库 安装 yum添加epel源 yum install epel-release ...
- Linux使用堡垒机
一.linux系统使用 1. 安装 puttygen sudo apt-get install putty 2. 进入 pem 所在文件执行转换命令 puttygen mykey.pem -o myk ...
- HT-014 Div3 跳棋 题解 [ 黄 ] [ 并查集 ] [ 链表 ]
分析 依旧是一个连通块题. 观察题面不难发现两个重要性质: 一个跳棋只能以它旁边的两个跳棋为中点跳跃,且满足跳跃路线中 除中点以外没有其它跳棋阻挡. 只有我们的跳棋可以移动. 跳棋的操作具有可逆性/对 ...
- 并发编程 - 线程同步(七)之互斥锁Monitor
通过前面对锁lock的基本使用以及注意事项的学习,相信大家对锁的同步机制有了大致了解,今天我们将继续学习--互斥锁Monitor. lock是C#语言中的关键字,是语法糖,lock语句最终会由C#编译 ...
- DeepSeek本地部署
一.ollama ollama是一个管理和运行所有大模型.开源大模型的平台.在官网的Models中可以看到deepseek-r1的AI模型 1.在官网中下载对应系统的ollama,下载时需要开墙,或者 ...
- QT5笔记: 28. SplashWindow 没听懂,无内容
没有说明这个SplashWindow咋用 大概小人愚笨 this->setWindowFlag(Qt::SplashScreen);莫非是这个?
- 个人文件转移工具-来自某位大神的C盘清理神器
软件名称:个人文件转移工具 软件功能:文件转移 支持平台:Windows 软件简介:一款文件转移工具,也可用作C盘瘦身. 软件特点: ◉ "个人文件转移工具"可以把"我的 ...
- 单元测试三部曲-AAA模式
AAA 指的是 "Arrange, Act, Assert",这是一种通用的单元测试模式. 在测试方法中, 1.首先对测试对象进行准备(Arrange), 2.然后调用要测试的方法 ...
- ABC393F题解
大概评级:绿. 一看到这种题目,就知道肯定是数据结构题,我们首先用一个众所周知的二分来求出 \(pos\) 数组,\(pos_i\) 表示以 \(i\) 结尾的最长上升子序列的大小,然后将询问离线,弄 ...
- Python基础-模块和包(hashlib、random、json、time、datetime和os模块)
什么是模块和包? 模块:python中的.py文件,将一些功能按照某一种维度进行划分: 自定义.内置..第三方. 包:文件夹 里面好多个.py文件. 在讨论的时候,一般统称为:模块. 学习: 自定义模 ...