背景


用户需要进行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下


说明:

  1. 假设当前用户在进行ocr识别时,未超过调用次数。但是在redis中的ttl还剩1秒钟
  2. 然后调用第三方ocr进行识别
  3. 识别成功后,调用次数+1。这里就很有可能出问题,比如:在incr的时候刚好该key过期了,那么redis是怎么做的呢,它会将该key的值设置为1,ttl设置为-1,ttl设置为-1,ttl设置为-1(重要的事情说三遍)
  4. 这时候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的坑相关推荐

  1. php redis incr过期时间,Redis 利用 incr 和 expire 来限流, 并发导致过期时间失效问题...

    当某一个接口需要限流时,可以采用redis的incr来递增,记录访问次数, 以及 expire 来设置失效时间. 大概的代码如下: r = redis.Redis.connect() key = &q ...

  2. redis 的incr 高并发 原子性计数器

    前言:6月底 公司录单的人比较多,由于先前的系统用的同步锁 ,我们是多服务实例,导致出现重复单号的问题,我想到的解决办法有两种 ,第一种是 Redis锁 第二种是自增key,下面实现的是用第二种方法 ...

  3. Redis的INCR方法

    INCR key 将 key 中储存的数字值增一. 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作. 如果值包含错误的类型,或字符串类型的值不能表示为数字,那 ...

  4. laravel用redis保存session遇到的坑,没报错,但redis-cli就是查不到

    laravel用redis保存session遇到的坑, 配置redis存储session流程是这样的 在.evn文件中把session驱动和连接改为了redis的 如下: SESSION_DRIVER ...

  5. 如果redis没有设置expire,他是否默认永不过期?

    版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/soulmate_P/article/details/81136054 如果没有设置有效期,即使内存用 ...

  6. 如果redis没有设置expire,是否默认永不过期?

    最近在对项目中redis缓存的过期时间进行设置的时候,忽然想到如果没有设置expire,缓存是否永不过期. 如果没有设置有效期,即使内存用完,redis 自动回收机制也是看设置了有效期的,不会动没有设 ...

  7. Redis 从入门到弃坑

    Redis 从入门到弃坑 简介 摘自:http://www.redis.cn/ Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库.缓存和消息中间件. 它支持多种类型的 ...

  8. redis分布式锁的这些坑,我怀疑你是假的开发

    摘要:用锁遇到过哪些问题? 一.白话分布式 什么是分布式,用最简单的话来说,就是为了较低单个服务器的压力,将功能分布在不同的机器上面:就比如: 本来一个程序员可以完成一个项目:需求->设计-&g ...

  9. Redis学习之expire命令

    目录 expire命令 语法 返回值 例子 expire命令 Redis expire 命令用于设置 key 的过期时间. key 过期后将不再可用. 语法 expire key seconds EX ...

最新文章

  1. 虚拟机VMware安装Kali Linux
  2. 数据库修改,删除的操作必须有保险操作。
  3. PHP之composer切换国内源
  4. Hive的两种操作模式
  5. D3.js系列——布局:打包图和地图
  6. Yocto Project - basic - 01 - Quick Start
  7. java集合转labelpoint_java – 向Spark ML LabeldPoint添加自定义字段
  8. SpringCloud工作笔记048---RESTful API 中 HTTP 状态码的定义_以及把RESTFul版本号_放到http协议header中_以及RestFul设计时的两个误区
  9. 7-30 字符串的冒泡排序 (20 分) or 7-27 冒泡法排序 (20 分)
  10. C#数据库事务机制及实践(下)
  11. Axis生成wsdl的三种方法以及注意事项
  12. 【C++】(三) MFC入门教程 (VS 2005)
  13. 【常见网页排版布局】
  14. Xmanager7 解决图形显示问题
  15. java基础知识粗略整理
  16. ISCSI的target和initiator的部署
  17. UVA10635--Prince and Princess
  18. matlab solve 警告,当发出警告时令测试失败的插件
  19. 太平人寿黄金十年 保险理财要买么?
  20. 文档大小超出上传限制怎么办_压缩PDF大小该怎么操作?压缩PDF的软件有哪些?...

热门文章

  1. 【ROS】[rosrun] Couldn't find executable named
  2. ATM机程序Linux,c语言模拟银行ATM机程序
  3. 为了梦想而奋斗的人值得敬佩
  4. 网易云课堂python怎样_有木有人上过网易云课堂的 Python Web 微专业,怎么样?
  5. 运维常说的 5个9、4个9、3个9 的可靠性,到底是什么鬼?
  6. 网站结构优化要做好哪些
  7. 深度学习之DCN-v2
  8. Java回炉学习(一)
  9. 【Linux回炉 目录配置】
  10. One-Error多标签分类_多分类及多标签分类算法