redis的incr+expire的坑
背景
用户需要进行ocr识别,为了防止接口被刷,这里面做了一个限制(每分钟调用次数不能超过xxx次)。
经过调研后,决定使用redis的incr和expire来实现这个功能
说明:以下代码使用golang实现
第一版代码
// 执行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调用(伪代码)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) {elog.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() {elog.Warn("checkMinute: user FrequentlyCalled", zap.Int64("uid", uid), zap.String("minuteCount", minuteCount))return false, nil}return true, nil
}
详解
这一版代码我先不说存在哪些问题,大家可以先自行YY下
说明:
- 假设当前用户在进行ocr识别时,未超过调用次数。但是在redis中的ttl还剩1秒钟
- 然后调用第三方ocr进行识别
- 识别成功后,调用次数+1。这里就很有可能出问题,比如:在incr的时候刚好该key过期了,那么redis是怎么做的呢,它会将该key的值设置为1,ttl设置为-1,ttl设置为-1,ttl设置为-1(重要的事情说三遍)
- 这时候bug就出现了,用户的调用次数一直在涨,并且也不会过期,达到临界值时用户的请求就会被拒掉
总结
以上代码说明了一个问题,也就是incr和expire必须具备原子性。而我们第一版代码显然在边界条件下是不满足要求的,极有可能造成bug,影响用户体验,强烈不推荐使用,接下来引入修正后的代码(lua脚本)
第二版代码
吃过第一版代码的亏后,我们决定将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调用(伪代码)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 (b *baiduOcrSvc) 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
}
总结
经过一番折腾后,看样子是解决了最棘手的问题。给你们留一个问题,第二版代码你们觉得还存在哪些问题呢?欢迎在评论区留言
写作不易,烦请点个赞喽
redis的incr+expire的坑相关推荐
- php redis incr过期时间,Redis 利用 incr 和 expire 来限流, 并发导致过期时间失效问题...
当某一个接口需要限流时,可以采用redis的incr来递增,记录访问次数, 以及 expire 来设置失效时间. 大概的代码如下: r = redis.Redis.connect() key = &q ...
- redis 的incr 高并发 原子性计数器
前言:6月底 公司录单的人比较多,由于先前的系统用的同步锁 ,我们是多服务实例,导致出现重复单号的问题,我想到的解决办法有两种 ,第一种是 Redis锁 第二种是自增key,下面实现的是用第二种方法 ...
- Redis的INCR方法
INCR key 将 key 中储存的数字值增一. 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作. 如果值包含错误的类型,或字符串类型的值不能表示为数字,那 ...
- laravel用redis保存session遇到的坑,没报错,但redis-cli就是查不到
laravel用redis保存session遇到的坑, 配置redis存储session流程是这样的 在.evn文件中把session驱动和连接改为了redis的 如下: SESSION_DRIVER ...
- 如果redis没有设置expire,他是否默认永不过期?
版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/soulmate_P/article/details/81136054 如果没有设置有效期,即使内存用 ...
- 如果redis没有设置expire,是否默认永不过期?
最近在对项目中redis缓存的过期时间进行设置的时候,忽然想到如果没有设置expire,缓存是否永不过期. 如果没有设置有效期,即使内存用完,redis 自动回收机制也是看设置了有效期的,不会动没有设 ...
- Redis 从入门到弃坑
Redis 从入门到弃坑 简介 摘自:http://www.redis.cn/ Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库.缓存和消息中间件. 它支持多种类型的 ...
- redis分布式锁的这些坑,我怀疑你是假的开发
摘要:用锁遇到过哪些问题? 一.白话分布式 什么是分布式,用最简单的话来说,就是为了较低单个服务器的压力,将功能分布在不同的机器上面:就比如: 本来一个程序员可以完成一个项目:需求->设计-&g ...
- Redis学习之expire命令
目录 expire命令 语法 返回值 例子 expire命令 Redis expire 命令用于设置 key 的过期时间. key 过期后将不再可用. 语法 expire key seconds EX ...
最新文章
- 虚拟机VMware安装Kali Linux
- 数据库修改,删除的操作必须有保险操作。
- PHP之composer切换国内源
- Hive的两种操作模式
- D3.js系列——布局:打包图和地图
- Yocto Project - basic - 01 - Quick Start
- java集合转labelpoint_java – 向Spark ML LabeldPoint添加自定义字段
- SpringCloud工作笔记048---RESTful API 中 HTTP 状态码的定义_以及把RESTFul版本号_放到http协议header中_以及RestFul设计时的两个误区
- 7-30 字符串的冒泡排序 (20 分) or 7-27 冒泡法排序 (20 分)
- C#数据库事务机制及实践(下)
- Axis生成wsdl的三种方法以及注意事项
- 【C++】(三) MFC入门教程 (VS 2005)
- 【常见网页排版布局】
- Xmanager7 解决图形显示问题
- java基础知识粗略整理
- ISCSI的target和initiator的部署
- UVA10635--Prince and Princess
- matlab solve 警告,当发出警告时令测试失败的插件
- 太平人寿黄金十年 保险理财要买么?
- 文档大小超出上传限制怎么办_压缩PDF大小该怎么操作?压缩PDF的软件有哪些?...