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

传统方案一般会直接使用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. 微信小程序安全开发、测试的一些记录

    目录 开发原则 漏洞类型--越权 漏洞类型--信息泄露 漏洞类型--爬虫遍历 漏洞类型--授权用户信息变更不跟进 漏洞类型--注入类 漏洞类型--上传.下载类 Reference 本文基于微信开发者平 ...

  2. Django和FastAPI的比较

    在 Python 的 Web 开发领域,Django 和 FastAPI 是两款备受瞩目的框架. 通过对二者的实践与比较,本文总结了它们的特点与适用场景,希望能给开发者在选择时提供参考. 1. 设计理 ...

  3. 个人数据保全计划:部署joplin server笔记同步服务

    前言 在这个数据爆炸的时代,个人数据的价值愈发凸显,成为我们生活与工作中无可替代的重要资产.上一篇文章里,我介绍了从印象笔记迁移至 Joplin 的过程,这是我寻求数据自主掌控的关键一步.在探索同步方 ...

  4. java中线程的创建方式-休眠-生命周期-工作方式

    进程 进程的定义:进程是操作系统分配资源的基本单位.每个进程都有自己独立的内存空间和系统资源. 进程的独立性:进程之间是相互独立的,一个进程的崩溃不会影响到其他进程. java中的体现:在Java中, ...

  5. C盘扩展卷碰到的那些事-->不是同一块物理磁盘操作扩展卷是有坑的

    自己电脑上面用过win10系统资源管理器扩展卷的功能,用过几次都成功扩容了磁盘空间,简单说一下原理: 就是将剩余未分配的磁盘空间划给要扩展的磁盘. 这天公司的电脑C盘老是红色提示空间不足,那就扩充容量 ...

  6. 安全稳定地远程访问飞牛NAS

    春节前从一个网友那里了解到一个新的NAS--飞牛. 起因是我们一个用户用我们的SD-WAN来远程访问飞牛NAS,市面上做NAS的很多,之所以单独体验这个产品主要是: 不需要购买硬件,这个是非常重要的, ...

  7. 『Python底层原理』--CPython的变量实现机制

    在Python中,变量的使用看起来非常简单,例如 a = 10,s = "hello"等等. 然而,这种简单的赋值操作背后,CPython其实做了很多复杂的工作. 本文将通过一些简 ...

  8. 初探PApplet窗口打开方式(Processing程序)

    使用Processing快6年了,是时候回过头来看看它的"main"方法了,也就是它从哪出生的?~~~ 源码学习 ///////////////////////////////// ...

  9. C# 委托与 Lambda 表达式转换机制及弱事件模式下的生命周期分析

    1. 委托内部结构 委托类型包含三个重要的非公共字段: _target 字段 静态方法包装:当委托包装一个静态方法时,该字段为 null. 实例方法包装:当委托包装实例方法时,该字段引用回调方法所操作 ...

  10. 解决bootstrapvalidator配合select2插件不能正常校验的问题

    我在使用bootstrapvalidator对select2插件进行校验时,出现了不能校验的问题,于是求助度娘,发现大多的解决方法是这样的: 1.添加一个隐藏域,将bootstrapvalidator ...