68. redis计数与限流中incr+expire的坑以及解决办法(Lua+TTL)

文章目录

  • 一、简介
  • 二、代码演进
    • 第一版代码(存在bug隐患)
    • 第二版代码(几乎无隐患)
    • 第三版代码(完美无瑕)

一、简介

在日常工作中,经常会遇到对某种操作进行频次控制或者统计次数的需求,此时常用的做法是采用redisincr来递增,记录访问次数, 以及 expire 来设置失效时间。本文将以一个实际的例子来说明incr存在的一个"坑",以及给出解决方案。

如:
26.redis实现日限流、周限流(含黑名单、白名单)
27.Go实现一月(30天)内不发送重复内容的站内信给用户

有这么一个场景,用户需要进行ocr识别,为了防止接口被刷,这里面做了一个限制(每分钟调用次数不能超过xx次)。 经过调研后,决定使用redisincrexpire来实现这个功能
在这里插入图片描述

二、代码演进

第一版代码(存在bug隐患)

// 执行ocr调用
func (o *ocrSvc)doOcr(ctx context.Context,uid int)(interface,err){// 如果调用次数超过了指定限制,就直接拒绝此次请求ok,err := o.checkMinute(uid)if err != nil {return nil,err}if !ok {return nil,errors.News("frequently called")}// 执行第三方ocr调用(伪代码:模拟一个rpc接口)ocrRes,err := doOcrByThird()if err != nil {return nil,err}// 调用成功则执行 incr操作if err := o.redis.Incr(ctx,buildUserOcrCountKey(uid));err!=nil{return nil,err}return ocrRes,nil
}// 校验每分钟调用次数是否超过限制
func (o *ocrSvc)checkMinute (ctx context.Context,uid int) (bool, error) {minuteCount, err := o.redis.Get(ctx, buildUserOcrCountKey(uid))if err != nil && !errors.Is(err, eredis.Nil) {log.Error("checkMinute: redis.Get failed", zap.Error(err))return false, constx.ErrServer}if errors.Is(err, eredis.Nil) {// 过期了,或者没有该用户的调用次数记录(设置初始值为0,过期时间为1分钟)o.redis.Set(ctx, buildUserOcrCountKey(uid),0,time.Minute)return true, nil}// 已经超过每分钟的调用次数if cast.ToInt(minuteCount) >= config.UserOcrMinuteCount() {log.Warn("checkMinute: user FrequentlyCalled", zap.Int64("uid", uid), zap.String("minuteCount", minuteCount))return false, nil}return true, nil
}

详解
这一版代码存在什么问题呢?问题出在了 开始判断没有超出限制,然后执行第三方rpc接口调用也成功了,接下来直接进行计数加一(incr)操作有问题,如下图所示

在这里插入图片描述

说明:

  1. 假设当前用户在进行ocr识别时,未超过调用次数。但是在redis中的ttl还剩1秒钟
  2. 然后调用第三方ocr进行识别,加入耗时超过了1
  3. 识别成功后,调用次数+1。这里就很有可能出问题,比如:在incr的时候刚好该key1s前过期了,那么redis是怎么做的呢,它会将该key的值设置为1ttl设置为-1ttl设置为-1ttl设置为-1重要的事情说三遍),-1表示没有过期时间
  4. 这时候bug就出现了,用户的调用次数一直在涨,并且也不会过期,达到临界值时用户的请求就会被拒掉,相当于该用户之后都不能访问这个接口了,并且这种key变多后,由于没有过期时间,还会一直占用redis的内存。

总结
以上代码说明了一个问题,也就是increxpire必须具备原子性。而我们第一版代码显然在边界条件下是不满足要求的,极有可能造成bug,影响用户体验,强烈不推荐使用,接下来一步一步引入修正后的代码

第二版代码(几乎无隐患)

从对第一版代码的分析可知,是由于查询次数还没有达到限制后,又进行了一些rpc调用,或者处理了一些其他业务逻辑,这个时间内,可能key过期了,然后我们直接使用incr进行计数加一,导致了永不过期的key产生。那么我们是不是可以在incr前先保证key还没有过期就行呢?答案是可以的,代码如下:

// 执行ocr调用
func (o *ocrSvc)doOcr(ctx context.Context,uid int)(interface,err){// 如果调用次数超过了指定限制,就直接拒绝此次请求ok,err := o.checkMinute(uid)if err != nil {return nil,err}if !ok {return nil,errors.News("frequently called")}// 执行第三方ocr调用(伪代码:模拟一个rpc接口)ocrRes,err := doOcrByThird()if err != nil {return nil,err}// 调用成功则执行 incr操作exists, err := o.redis.Exists(ctx, buildUserOcrCountKey(uid)).Result()if err != nil {log.Error("doOcr: redis.Exists failed", zap.Error(err))return nil, err}if exists == 1 { // key存在,计数加1if err := o.redis.Incr(ctx,buildUserOcrCountKey(uid));err!=nil{return nil,err}} else { // key不存在,设置key与过期时间if err := o.redis.Set(ctx,buildUserOcrCountKey(uid),1,expireTime);err!=nil{return nil,err}}return ocrRes,nil
}// 校验每分钟调用次数是否超过限制
func (o *ocrSvc)checkMinute (ctx context.Context,uid int) (bool, error) {minuteCount, err := o.redis.Get(ctx, buildUserOcrCountKey(uid))if err != nil && !errors.Is(err, eredis.Nil) {log.Error("checkMinute: redis.Get failed", zap.Error(err))return false, constx.ErrServer}if errors.Is(err, eredis.Nil) {// 过期了,或者没有该用户的调用次数记录(设置初始值为0,过期时间为1分钟)o.redis.Set(ctx, buildUserOcrCountKey(uid),0,time.Minute)return true, nil}// 已经超过每分钟的调用次数if cast.ToInt(minuteCount) >= config.UserOcrMinuteCount() {log.Warn("checkMinute: user FrequentlyCalled", zap.Int64("uid", uid), zap.String("minuteCount", minuteCount))return false, nil}return true, nil
}

与第一版的差异主要在于如下代码:

  • 在需要incr操作前,我们先查看key是否存在(且没有过期)
    • 确保存在后,立即incr(这两步间隔几乎可以忽略,所以几乎可以避免第一版中的问题)
    • 如果不存在则设置key并设置过期时间。

注:redis中的incr命令是不会改变key的过期时间的

// 调用成功则执行 incr操作exists, err := o.redis.Exists(ctx, buildUserOcrCountKey(uid)).Result()if err != nil {log.Error("doOcr: redis.Exists failed", zap.Error(err))return nil, err}if exists == 1 { // key存在,计数加1if err := o.redis.Incr(ctx,buildUserOcrCountKey(uid));err!=nil{return nil,err}} else { // key不存在,设置key与过期时间if err := o.redis.Set(ctx,buildUserOcrCountKey(uid),1,expireTime);err!=nil{return nil,err}}

还有一种方式是查看key的过期时间,使用ttl,这样即使在极端情况下通过incr设置出了没有过期时间的key,也会在第二次访问的时候通过Set设置过期时间了。

注:ttl命令返回值是键的剩余时间(单位是秒)。当键不存在时,ttl命令会返回-2。没有为键设置过期时间(即永久存在,这是建立一个键后的默认情况)返回-1。

// 调用成功则执行 incr操作cnt, err := o.redis.Ttl(ctx, buildUserOcrCountKey(uid)).Result()if err != nil {log.Error("doOcr: redis.Ttl failed", zap.Error(err))return nil, err}if cnt >= 1 { // key存在,且还没有过期if err := o.redis.Incr(ctx,buildUserOcrCountKey(uid));err!=nil{return nil,err}} else { // key马上过期0,key没有过期时间-1, key不存在-2if err := o.redis.Set(ctx,buildUserOcrCountKey(uid),1,expireTime);err!=nil{return nil,err}}

第三版代码(完美无瑕)

第二版代码中的两种方式其实已经可以在工作中使用了,但如果追求完美无瑕的话,ttl版本的代码在极端情况下还是有点瑕疵,比如极端情况下,key过期时间还有1s过期,然后我们用incr去累加,但是网络延迟了,导致命令到达redis服务器的时候,key已经过期了,尽管第二次访问会用set重置key并设置过期时间,但是万一该用户再也不来访问了呢?这时候这个key就会永远占据着内存了。

incr+expire放在lua脚本中执行保证原子性是最完美的。废话不多说了,直接上代码

// 执行ocr调用
func (o *ocrSvc)doOcr(ctx context.Context,uid int)(interface,err){// 如果调用次数超过了指定限制,就直接拒绝此次请求ok,err := o.checkMinute(uid)if err != nil {return nil,err}if !ok {return nil,errors.News("frequently called")}// 执行第三方ocr调用((伪代码:模拟一个rpc接口))ocrRes,err := doOcrByThird()if err != nil {return nil,err}// 调用成功则执行 incr操作if err := o.incrCount(ctx,buildUserOcrCountKey(uid));err!=nil{return nil,err}return ocrRes,nil
}func (o *ocrSvc) incrCount(ctx context.Context, uid int64) error {/*此段lua脚本的作用:第一步,先执行incr操作local current = redis.call('incr',KEYS[1])第二步,看下该key的ttllocal t = redis.call('ttl',KEYS[1]); 第三步,如果ttl为-1(永不过期)if t == -1 then则重新设置过期时间为 「一分钟」redis.call('expire',KEYS[1],ARGV[1])end;*/script := redis.NewScript(`local current = redis.call('incr',KEYS[1]);local t = redis.call('ttl',KEYS[1]); if t == -1 thenredis.call('expire',KEYS[1],ARGV[1])end;return current`)var (expireTime = 60 // 60 秒)_, err := script.Run(ctx, b.redis.Client(), []string{buildUserOcrCountKey(uid)}, expireTime).Result()if err != nil {return err}return nil
}// 校验每分钟调用次数是否超过
func (o *ocrSvc)checkMinute (ctx context.Context,uid int) (bool, error) {minuteCount, err := o.redis.Get(ctx, buildUserOcrCountKey(uid))if err != nil && !errors.Is(err, eredis.Nil) {elog.Error("checkMinute: redis.Get failed", zap.Error(err))return false, constx.ErrServer}if errors.Is(err, eredis.Nil) {// 第二版代码中在check时不进行初始化操作// 过期了,或者没有该用户的调用次数记录(设置初始值为0,过期时间为1分钟)// o.redis.Set(ctx, buildUserOcrCountKey(uid),0,time.Minute)return true, nil}// 已经超过每分钟的调用次数if cast.ToInt(minuteCount) >= config.UserOcrMinuteCount() {elog.Warn("checkMinute: user FrequentlyCalled", zap.Int64("uid", uid), zap.String("minuteCount", minuteCount))return false, nil}return true, nil
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/244630.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

快速上手的AI工具-文心一言绘画达人

前言 大家好,现在AI技术的发展,它已经渗透到我们生活的各个层面。对于普通人来说,理解并有效利用AI技术不仅能增强个人竞争力,还能在日常生活中带来便利。无论是提高工作效率,还是优化日常任务,AI工具都可…

蓝桥杯---三羊献瑞

观察下面的加法算式: 其中,相同的汉字代表相同的数字,不同的汉字代表不同的数字。 请你填写“三羊献瑞”所代表的4位数字(答案唯一),不要填写任何多余内容。 答案 代码 public class _03三羊献瑞 {public static void main(String[] args) {//c 生 b 瑞 g 献 d 辉…

Excel学习

例子可在文件-新建-模板中找到。 *快速填充 ctrl-e,需要将内容融入一个表格内,插入-表格,然后关掉数据-筛选,就可以填充了。 ctrl-D,选中单元格,然后快捷键向下填充。 *快速分析 ctrl-q,开…

c++:类和对象(5),运算符重载

目录 运算符重载概念&#xff1a; 运算符重载 1.成员函数重载号 2.全局函数重载号 打印结果&#xff1a; <<运算符重载 递增运算符重载 简单例子 输出结果为&#xff1a; 赋值运算符重载 如何重载 输出结果为&#xff1a; 什么时候重载 关系运算符重载 简单例…

【LeetCode: 36. 有效的数独 + 模拟】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

Qt 多次绘图

使用Qt 的时候发现&#xff1a; 背景&#xff1a;自己定义一个类&#xff0c;把它和某个ui文件绑定。(类似 Qt creator 默认创建的工程&#xff09;问题&#xff1a;当鼠标在窗口内单击的时候会触发2次绘图。&#xff1f;难道不应该是一次吗&#xff1f; 于是开始了如下的测试…

设备通过GB28181注册到EasyCVR,平台看不到设备信息的排查方法汇总

智慧安防平台EasyCVR能在复杂的网络环境中&#xff08;专网、局域网、广域网、VPN、公网等&#xff09;将前端海量的设备进行统一集中接入与视频汇聚管理&#xff0c;平台支持设备通过4G、5G、WIFI、有线等方式进行视频流的接入与传输&#xff0c;支持的接入协议包括&#xff1…

【立创EDA-PCB设计基础完结】7.DRC设计规则检查+优化与丝印调整+打样与PCB生产进度跟踪

前言&#xff1a;本文为PCB设计基础的最后一讲&#xff0c;在本专栏中【立创EDA-PCB设计基础】前面已经将所有网络布线铺铜好了&#xff0c;接下来进行DRC设计规则检查优化与丝印调整打样与PCB生产进度跟踪 目录 1.DRC设计规则检查 2.优化与丝印调整 1.过孔连接优化 2.泪滴…

C# .NET读取Excel文件并将数据导出到DataTable、数据库及文本

Excel文件是存储表格数据的普遍格式&#xff0c;因此能够高效地读取和提取信息对于我们来说至关重要。C#语言借助.NET Framework和各种库的广泛功能&#xff0c;能够进行高效的数据操作。利用C#读取Excel文件并将数据写入数据库和DataTable&#xff0c;或者将数据用于其他目的&…

大数据开发之SparkSQL

第 1 章&#xff1a;spark sql概述 1.1 什么是spark sql 1、spark sql是spark用于结构化数据处理的spark模块 1&#xff09;半结构化数据&#xff08;日志数据&#xff09; 2&#xff09;结构化数据&#xff08;数据库数据&#xff09; 1.2 为什么要有sparksql hive on s…

Eyes Wide Shut? Exploring the Visual Shortcomings of Multimodal LLMs

大开眼界&#xff1f;探索多模态模型种视觉编码器的缺陷。 论文中指出&#xff0c;上面这些VQA问题&#xff0c;人类可以瞬间给出正确的答案&#xff0c;但是多模态给出的结果却是错误的。是哪个环节出了问题呢&#xff1f;视觉编码器的问题&#xff1f;大语言模型出现了幻觉&…

计算机网络基础概念解释

​ 1. 什么是网络 随着时代的发展&#xff0c;越来越需要计算机之间互相通信&#xff0c;共享软件和数据&#xff0c;即以多个计算机协同⼯作来完成业务&#xff0c;于是有了网络互连。 网络互连&#xff1a;将多台计算机连接在⼀起&#xff0c;完成数据共享。 数据共享本质是…

GPT科研应用与AI绘图及论文高效写作

详情点击链接&#xff1a;GPT科研应用与AI绘图及论文高效写作 一OpenAI 1.最新大模型GPT-4 Turbo 2.最新发布的高级数据分析&#xff0c;AI画图&#xff0c;图像识别&#xff0c;文档API 3.GPT Store 4.从0到1创建自己的GPT应用 5. 模型Gemini以及大模型Claude2二定制自己…

论文阅读笔记AI篇 —— Transformer模型理论+实战 (三)

论文阅读笔记AI篇 —— Transformer模型理论实战 &#xff08;三&#xff09; 第三遍阅读&#xff08;精读&#xff09;3.1 Attention和Self-Attention的区别&#xff1f;3.2 Transformer是如何进行堆叠的&#xff1f;3.3 如何理解Positional Encoding&#xff1f;3.x 文章涉及…

常用电子器件学习——MOS管

MOS管介绍 MOS&#xff0c;是MOSFET的缩写。MOSFET 金属-氧化物半导体场效应晶体管&#xff0c;简称金氧半场效晶体管&#xff08;Metal-Oxide-Semiconductor Field-Effect Transistor, MOSFET&#xff09;。 一般是金属(metal)—氧化物(oxide)—半导体(semiconductor)场效应晶…

【Unity学习笔记】Unity TestRunner使用

转载请注明出处&#xff1a;&#x1f517;https://blog.csdn.net/weixin_44013533/article/details/135733479 作者&#xff1a;CSDN|Ringleader| 参考&#xff1a; Input testingGetting started with Unity Test FrameworkHowToRunUnityUnitTest如果对Unity的newInputSystem感…

qnx 上screen + egl + opengles 最简实例

文章目录 前言一、qnx 上的窗口系统——screen二、screen + egl + opengles 最简实例1.使用 addvariant 命令创建工程目录2. 添加源码文件3. common.mk 文件4. 编译与执行总结参考资料前言 本文主要介绍如何在QNX 系统上使用egl和opengles 控制GPU渲染一个三角形并显示到屏幕上…

【Flink-CDC】Flink CDC 介绍和原理概述

【Flink-CDC】Flink CDC 介绍和原理概述 1&#xff09;基于查询的 CDC 和基于日志的 CDC2&#xff09;Flink CDC3&#xff09;Flink CDC原理简述4&#xff09;基于 Flink SQL CDC 的数据同步方案实践4.1.案例 1 : Flink SQL CDC JDBC Connector4.2.案例 2 : CDC Streaming ETL…

threejs学习

重要概念&#xff08;场景、相机、渲染器&#xff09; 如下图所示&#xff0c;我们最终看到浏览器上生成的内容是通过虚拟场景和虚拟相机被渲染器渲染后的结果&#xff0c;下面首先介绍这三个概念&#xff0c;将贯穿所有简单复杂的threejs项目。 场景 Scene 虚拟的3D场景&a…

Linux中文件属性的获取(stat、chmod、Istat、fstat函数的使用)

修改文件权限 函数如下&#xff1a; chmod/fchmod函数用来修改文件的访问权限: #include <sys/stat.h> int chmod(const char *path, mode_t mode); int fchmod(int fd, mode_t mode); 成功时返回0&#xff1b;出错时返回EOF 注意&#xff1a;在vmware和windows共享的文…