大部分互联网公司都需要处理计数器场景,例如风控系统的请求频控、内容平台的播放量统计、电商系统的库存扣减等。

传统方案一般会直接使用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;
}

架构设计示意图

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原子命令的潜力,通过补偿机制弥补分布式系统的不确定性,最终在简单与可靠之间找到平衡点。

往期推荐

Redis实现高并发场景下的计数器设计的更多相关文章

  1. C++高并发场景下读多写少的解决方案

    C++高并发场景下读多写少的解决方案 概述 一谈到高并发的解决方案,往往能想到模块水平拆分.数据库读写分离.分库分表,加缓存.加mq等,这些都是从系统架构上解决.单模块作为系统的组成单元,其性能好坏也 ...

  2. C++高并发场景下读多写少的优化方案

    概述 一谈到高并发的优化方案,往往能想到模块水平拆分.数据库读写分离.分库分表,加缓存.加mq等,这些都是从系统架构上解决.单模块作为系统的组成单元,其性能好坏也能很大的影响整体性能,本文从单模块下读 ...

  3. Qunar机票技术部就有一个全年很关键的一个指标:搜索缓存命中率,当时已经做到了>99.7%。再往后,每提高0.1%,优化难度成指数级增长了。哪怕是千分之一,也直接影响用户体验,影响每天上万张机票的销售额。 在高并发场景下,提供了保证线程安全的对象、方法。比如经典的ConcurrentHashMap,它比起HashMap,有更小粒度的锁,并发读写性能更好。线程安全的StringBuilder取代S

    Qunar机票技术部就有一个全年很关键的一个指标:搜索缓存命中率,当时已经做到了>99.7%.再往后,每提高0.1%,优化难度成指数级增长了.哪怕是千分之一,也直接影响用户体验,影响每天上万张机 ...

  4. 【转】记录PHP、MySQL在高并发场景下产生的一次事故

    看了一篇网友日志,感觉工作中值得借鉴,原文如下: 事故描述 在一次项目中,上线了一新功能之后,陆陆续续的有客服向我们反应,有用户的个别道具数量高达42亿,但是当时一直没有到证据表示这是,确实存在,并且 ...

  5. 高并发场景下System.currentTimeMillis()的性能问题的优化 以及SnowFlakeIdWorker高性能ID生成器

    package xxx; import java.sql.Timestamp; import java.util.concurrent.*; import java.util.concurrent.a ...

  6. HttpClient在高并发场景下的优化实战

    在项目中使用HttpClient可能是很普遍,尤其在当下微服务大火形势下,如果服务之间是http调用就少不了跟http客户端找交道.由于项目用户规模不同以及应用场景不同,很多时候可能不需要特别处理也. ...

  7. 高并发场景下System.currentTimeMillis()的性能问题的优化

    高并发场景下System.currentTimeMillis()的性能问题的优化 package cn.ucaner.alpaca.common.util.key; import java.sql.T ...

  8. MySQL在大数据、高并发场景下的SQL语句优化和"最佳实践"

    本文主要针对中小型应用或网站,重点探讨日常程序开发中SQL语句的优化问题,所谓“大数据”.“高并发”仅针对中小型应用而言,专业的数据库运维大神请无视.以下实践为个人在实际开发工作中,针对相对“大数据” ...

  9. 高并发场景下System.currentTimeMillis()的性能优化

    一.前言 System.currentTimeMillis()的调用比new一个普通对象要耗时的多(具体耗时高出多少我也不知道,不过听说在100倍左右),然而该方法又是一个常用方法, 有时不得不使用, ...

  10. 高并发场景下的httpClient优化使用

    1.背景 我们有个业务,会调用其他部门提供的一个基于http的服务,日调用量在千万级别.使用了httpclient来完成业务.之前因为qps上不去,就看了一下业务代码,并做了一些优化,记录在这里. 先 ...

随机推荐

  1. python3 使用中的问题

    gmpy2 GMP(GNU Multiple Precision Arithmetic Library,即GNU高精度算术运算库),它是一个开源的高精度运算库,其中不但有普通的整数.实数.浮点数的高精 ...

  2. MongoDB:文档基本CRUD

  3. CV高手是怎么炼成的?

    你平时都怎么复制粘贴的?是否每次都是复制一段粘贴一段?是否厌倦了每次只能复制粘贴一次的限制?那这篇文章就是为你量身订做的. CopyQ简介 CopyQ is clipboard manager – a ...

  4. SQL Server 2022新功能:将数据库备份到S3兼容的对象存储

    SQL Server 2022新功能:将数据库备份到S3兼容的对象存储 本文介绍将S3兼容的对象存储用作数据库备份目标所需的概念.要求和组件. 数据库备份和恢复功能在概念上类似于使用SQL Serve ...

  5. Mac安装Charles抓包工具

    一.下载安装 地址1:官网 下载地址: https://www.charlesproxy.com/download/ 激活信息 Registered Name: https://zhile.io Li ...

  6. .Net 6 配置日志

    前言   .Net 6 与之前的配置有点不一样了记录下日志配置方式. 当前日志以Serilog为例,.Net 6 的日志由内置的Logger获取,然后可以交给Serilog|NLog等框架处理, 框架 ...

  7. WPF程序性能优化总结

    原文链接: https://blog.csdn.net/u010265681/article/details/77571947 WPF程序性能由很多因素造成,以下是简单地总结: 元素: 1. 减少需要 ...

  8. 腾讯ima接入deepseek-r1,借用别人脑子用用成真了~

    大家好,我是汤师爷! 最近,腾讯发布了一款ima产品,主打用AI来帮你搜索信息.管理知识库. 说实话,一开始用混元大模型,感觉不咋地,很鸡肋的. 不过最近腾讯把DeepSeek R1接进来了,一下子解 ...

  9. Python 脚本编写指南:从框架到实践

    一.引言 Python 作为一种强大且易于学习的编程语言,在各个领域都有着广泛的应用.编写 Python 脚本是实现各种功能和任务的常见方式. 二.Python 脚本框架的基本组成部分 导入必要的模块 ...

  10. 【论文随笔】推荐系统综述_推荐模型、推荐技术与应用领域(A Survey of Recommendation Systems_ Recommendation Models, Techniques, and Application Fields)

    前言 今天读的论文为一篇于2022年1月3日发表的论文,这篇文章是关于推荐系统的综述,主要研究了推荐系统在不同服务领域的应用趋势,包括推荐模型.技术和应用领域.通过分析2010年至2021年间发表的顶 ...