Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >基于redis实现分布式服务限流器

基于redis实现分布式服务限流器

作者头像
用户5326575
发布于 2022-06-28 01:12:20
发布于 2022-06-28 01:12:20
1.9K00
代码可运行
举报
文章被收录于专栏:茶饭跑先生茶饭跑先生
运行总次数:0
代码可运行

限流器概述

在后台开发中,服务端的限流器是一个很常见并且十分有用的组件,利用好限流器可以限制请求速率,保护后台服务。 比较常见的限流器分为两种,漏桶算法和令牌桶算法。

漏桶算法

漏桶算法原理很简单,用一个漏斗来控制请求的速率。在漏斗上方是收到的所有请求,请求就像水一样会进入漏斗中,同时漏斗也会以恒定的速度将水(请求)从下方进行排出,被排出的水(请求)才能访问服务。当请求量不大时候,如进水速率 < 出水速率那么其实漏斗并没有起到作用;当请求量很大的时候,超过漏斗容量的请求将被溢出,并且出水口可以一直保证恒定的速率。

令牌桶算法

令牌桶算法原理也很简单,假设我们的服务允许请求速度上限为5000次/分,那么这就意味着桶内的令牌数为5000,并且每隔一分钟桶内的令牌数就会被重置为5000。每一个请求过来都需要从桶内拿一块令牌,如果能取得令牌则允许访问服务,否则将会拒绝请求。

基于redis的分布式服务限流器

本文将基于redis来设计一个在分布式场景下的令牌桶算法,旨在重点解决以下问题:

  • 并发请求如何处理?
  • 何时进行加锁?何时不需要加锁?
  • 如何提高准确性和稳定性?

流程图

详细设计

在实际场景中,服务的限流往往会和一些参数绑定在一起,比如:限制同一个ip地址的请求速率为5000次/分,限制某一个业务id的请求速率为5000次/分,根据这些绑定的变量数值,我们可以在redis中设置对应的key,通过不断累加该key对应的数值来实现限流器的设计。

计数器初始化

假设我们服务请求速率的最大值max5000次/分。 当服务器收到请求时,首先判断redis中对应键k的数值v是否超过5000,如果是则拒绝请求,如果为否则继续判断v是否为0,当v0的时候,我们需要进行初始化。初始化需要将v的值置为1,并且设置过期时间为60s。考虑以下几个问题:

  • 初始化是否需要加锁?
  • 为什么不能直接用incr命令?

对于第一个问题,答案肯定是必然的,我们需要保证只有一个请求能进行初始化,否则在并发情况下会出现多个请求线程都对v进行置1操作,从而导致计数器不准确。 那么如何进行加锁操作呢?在分布式场景下是用本地锁是不正确的,因此我们同样可以利用redis的SET .. NX命令来实现分布式锁,来保证只有一个线程能进行初始化。 有一个需要注意的细节是:线程在获得锁之后,还需要在读取一次v的值,如果此时读取到数值不为0则说明在此之前已经被其他线程捷足先登了,此时就应该放弃初始化。 对于第二个问题,虽然redis的incr命令也可以保证只有一个请求线程能进行置1操作(因为redis是单线程的,天然满足锁),但是incr没有办法设置过期时间,因此不能直接使用incr命令。 代码如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func initCount(key string) (int, error) {
   lockKey := key + "_" + initLockKeySuffix
   getLock := false
   var err error
   // 一个循环去抢占锁
   for !getLock {
      // 再读取一次count数值,保证只有一个线程进行初始化
      // 这一步很重要,可能在抢锁的过程中已经有其他线程完成了初始化,那么此线程就不需要初始化了
      count, err := redis.GetInt(key)
      if err != nil {
         // 记录错误继续抢占初始化
         log.Errorf("get redis failed, err: %v", err)
         time.Sleep(time.Millisecond * 10)
         continue
      }
      if count > 0 {
         // 不是第一个线程,放弃初始化
         return -1, nil
      }
      // 设置一个3s过期的nx锁
      getLock, err = redis.SetNxWithExpire(lockKey, "ok", 3)
      if err != nil {
         // 记录错误继续抢占初始化
         log.Errorf("set nx failed, err: %v", err)
         time.Sleep(time.Millisecond * 10)
         continue
      }
      if getLock {
         // 抢到,退出循环
         break
      } else {
         // 没抢到锁,等下一次
         time.Sleep(time.Millisecond * 100)
      }
   }

   // 获得锁之后开始进行初始化
   ok, err := redis.SetIntWithExpire(key, 1, comm.Interval)
   if err != nil || !ok {
      // 初始化失败
      return 0, myError.WithMessage(err, "redis init failed")
   }
   log.Info("init success")
   // 删除锁
   e := redis.DelKey(lockKey)
   if e != nil {
      // 只能被动等待锁过期
      log.Errorf("redis del key failed, key: %s, err: %s", key, e)
   }
   // 初始化成功
   return 1, nil
}
请求次数累加

对于上述抢占失败的线程,以及新来的请求线程就没有必要继续初始化了,而是直接对v值进行加1操作。考虑以下几个问题:

  • 1操作是否需要加锁?
  • 如何解决边界问题?
  • 如何提升程序效率?

使用redis的incr命令进行加1操作,由于redis天然是单线程的,因此加1操作是不需要进行加锁的。对于每一个请求,可以通过判断incr返回值是否大于max来决定是否拒绝请求。 在并发的情况下,假设我们的服务限制访问速率为5000次/分,在某一时刻t请求数量已经达到了4999次,此时突然并发来了10个请求,按照上面设计的流程,这10个请求首先读取redis中对应键k的数值v,同时读取到了4999这个值,那么则会都进行加1操作,于是redis中对应键k最终值则为5009,超过了5000,虽然不影响服务,但是redis中值却超过了预期值,为了解决边界问题,我采用了阈值法,根据业务的需求可以事先估计一个阈值δ,比如80%,当redis中对应键k的数值v小于max * δ时,则不加锁直接使用incr进行加1,当超过时,则进行加锁排队加1代码如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func increaseCountWithoutLock(key string) (int, error) {
   // 直接进行加1,上层对加1后的数值进行判断
   return redis.IncrOne(key)
}

func increaseCountWithLock(bid int, key string, count int) (int, error) {
   if float64(count) < comm.RateThreshold*comm.RateLimit {
      // 没有达到阈值,直接使用redis的incr来保证原子性
      return redis.IncrOne(key)
   }
   // 达到阈值后incr操作需要排队
   newCount, err := increaseSerialized(bid, key)
   if err != nil {
      return 0, myError.WithMessage(err, "increaseSerialized failed")
   }
   return newCount, nil
}

func increaseSerialized(bid int, key string) (int, error) {
   lockKey := strconv.Itoa(bid) + incrLockKeySuffix
   getLock := false
   var err error
   for !getLock {
      // 再读取一次
      oldCount, err := redis.GetInt(key)
      if err != nil {
         // 记录错误继续抢占资源
         log.Errorf("set nx failed, err: %v", err)
         time.Sleep(time.Millisecond * 10)
         continue
      }
      if oldCount >= comm.RateLimit {
         // redis不用加1,直接返回
         return oldCount + 1, nil
      }
      // 设置一个3s过期的nx锁
      getLock, err = redis.SetNxWithExpire(lockKey, "ok", 3)
      if err != nil {
         return -1, myError.WithMessage(err, "set redis lock failed")
      }
      if getLock {
         break
      } else {
         time.Sleep(time.Millisecond * 100)
      }
   }

   // 获得锁之后开始进行加1操作
   newCount, err := redis.IncrOne(key)
   // 删除锁
   e := redis.DelKey(lockKey)
   if e != nil {
      // 只能被动等待锁过期
      log.Errorf("redis del key failed, key: %s, err: %s", key, e)
   }
   return newCount, err
}

第一种方式不需要加锁,代码简单,但是没有保证redis中计数器的正确性,即没有满足解决问题(但是不影响业务);第二种方式在达到阈值后需要加锁,代码较为复杂。

重置计数器

在初始化redis计数器时,我们使用了SET...EX方式设置了过期时间,但是在实际中可能出现key过期后却没有自动删除的现象,于是这里加上了手动删除过期key的监控,采用redis的ttldel命令组合来重置计数器。 代码如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func ttlCount(key string) error {
   leftTime, err := redis.GetTtl(key)
   log.Infof("key: %s, left time: %d", key, leftTime)
   if err != nil {
      return myError.WithMessage(err, "")
   }
   if leftTime == -1 || leftTime > comm.Interval {
      // 说明此时key没有设置过期时间或者超时时间出错,则进行删除
      return redis.DelKey(key)
   }
   return nil
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-10-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
「Go工具箱」redis官网推荐的go版本的分布式锁:redsync
大家好,我是渔夫子。本号新推出「Go工具箱」系列,意在给大家分享使用go语言编写的、实用的、好玩的工具。
Go学堂
2023/01/31
7.9K0
分布式高并发系统限流原理与实践
随着业务的发展壮大,对后端服务的压力也会越来越大,为了打造高效稳定的系统, 产生了分布式,微服务等等系统设计,因为这个原因,设计复杂度也随之增加,基于此 诞生了高并发系统三大利器限流,缓存,降级/熔断。
merlinfeng
2020/12/31
8720
分布式高并发系统限流原理与实践
Redis分布式锁
在单机系统中,我们可以运用普通的锁/信号量机制来实现对公共资源的有序访问;但在分布式系统中显然就不行了
Kevinello
2022/08/19
3510
Golang+Redis分布式互斥锁
假设我们的某个业务会涉及数据更新,同时在实际场景中有较大并发量。流程:读取->修改->保存,在不考虑基于DB层的并发处理情况下,这种场景可能对部分数据造成不可预期的执行结果,此时可以考虑使用分布式锁来解决该问题
lestat
2021/05/04
3.2K0
Redis分布式锁
我比较喜欢做全套的,一个Redis分布式锁的应用示例,我准备了Redis各种环境、SpringBoot部署两个服务、用tengine做这两个服务的负载均衡、用Jmeter做压力测试,可谓是麻雀虽小,五脏俱全。
行百里er
2020/12/02
8790
Redis分布式锁
go 如何给服务做限流?
【1】限流就是限制流量进入或者从系统出去的速率,防止流量过高导致系统过载或者崩溃。
Johns
2021/07/15
3K0
go 如何给服务做限流?
「分布式」实现分布式锁的正确姿势
最近看到好多博主都在推分布式锁,实现方式很多,基于db、redis、zookeeper。zookeeper方式实现起来比较繁琐,这里我们就谈谈基于redis实现分布式锁的正确实现方式。
一个程序员的成长
2020/11/25
8710
「分布式」实现分布式锁的正确姿势
高并发系统支撑---限流算法
有些场景并不能用缓存和降级来解决,比如写服务、频繁的复杂查询,因此需有一种手段来限制这些场景的并发/请求量,即限流。
一条老狗
2019/12/26
8600
接口中的几种限流实现
这些情况都是无法预知的,不知道什么时候会有10倍甚至20倍的流量进来,如果遇到此类情况,扩容是根本来不及的,弹性扩容也是来不及的;
动力节点Java培训
2018/12/21
1.3K0
分布式系统高可用实战之限流器(Go 版本实现)
1. 问题描述2. 信号量限流2.1 阻塞方式2.2 非阻塞方式3. 限流算法3.1 漏桶算法3.2 令牌桶算法3.3 漏桶算法的实现改进4. Uber 开源实现 RateLimit 深入解析4.1 引入方式4.2 使用4.3 实现细节构造限流器限流器Take() 阻塞方法5. 小结优质图书推荐参考
aoho求索
2020/05/11
2K0
分布式系统高可用实战之限流器(Go 版本实现)
说出来你可能不信,分布式锁竟然这么简单...
作为一个后台开发,不管是工作还是面试中,分布式一直是一个让人又爱又恨的话题。它如同一座神秘的迷宫,时而让你迷失方向,时而又为你揭示出令人惊叹的宝藏。
xin猿意码
2023/10/18
3650
说出来你可能不信,分布式锁竟然这么简单...
如何使用 Redis 实现分布式锁
锁是我们在设计和实现大多数系统时绕不过的话题。一旦有竞争条件出现,在没有保护的操作的前提下,可能会出现不可预知的问题。
haifeiWu
2020/02/10
1.6K0
读书笔记:限流详解
在压测时我们可以找出每个系统的处理峰值,然后通过设定峰值阈值,来防止当系统过载时,通过拒绝处理过载的请求来保障系统可用。
看、未来
2021/12/07
3450
使用Go语言实现Redis分布式锁(附有看门狗自动续期机制)
过完年后,更新博客的热情逐渐被备战暑期实习的焦虑感没过了,今天写项目时上网搜集资料实现了一版自动续期机制的Redis分布式锁,在这里记录巩固一下
潋湄
2025/02/07
2722
使用Go语言实现Redis分布式锁(附有看门狗自动续期机制)
分布式环境下限流方案的实现redis RateLimiter Guava,Token Bucket, Leaky Bucket
对于web应用的限流,光看标题,似乎过于抽象,难以理解,那我们还是以具体的某一个应用场景来引入这个话题吧。在日常生活中,我们肯定收到过不少不少这样的短信,“双11约吗?,千款….”,“您有幸获得唱读卡,赶快戳链接…”。这种类型的短信是属于推广性质的短信。为什么我要说这个呢?听我慢慢道来。一般而言,对于推广营销类短信,它们针对某一群体(譬如注册会员)进行定点推送,有时这个群体的成员量比较大,甚至可以达到千万级别。因此相应的,发送推广短信的量也会增大。然而,要完成这些短信发送,我们是需要调用服务商的接口来完成的。倘若一次发送的量在200万条,而我们的服务商接口每秒能处理的短信发送量有限,只能达到200条每秒。那么这个时候就会产生问题了,我们如何能控制好程序发送短信时的速度昵?于是限流这个功能就得加上了
用户6182664
2020/05/11
5.9K0
redis分布式锁如果没用好,坑真多
在分布式系统中,由于redis分布式锁相对于更简单和高效,成为了分布式锁的首先,被用到了很多业务场景当中。
苏三说技术
2021/10/06
1.6K2
限流领域的黑科技:揭秘分布式限流镇山秘籍,真的那么神奇吗?
单机限流指针对单一服务器的情况,通过限制单台服务器在单位时间内处理的请求数量,防止服务器过载。常见的限流算法(固定窗口限流算法、滑动窗口算法、漏桶限流算法)上文已介绍,其优点在于实现简单,效率高,效果明显。
程序视点
2024/06/07
1250
限流领域的黑科技:揭秘分布式限流镇山秘籍,真的那么神奇吗?
redis分布式锁-被其他人解锁
实现了一个基于 Redis 的分布式锁,包括加锁和解锁功能。加锁时生成一个随机的 requestId 作为锁的标识,并设置过期时间以防止死锁。解锁时通过事务确保只有加锁的客户端才能释放锁,保证了锁的安全性。
王宝
2024/11/28
850
基于Redis实现一个简单的固定窗口限流器
限流器是在大流量中保护服务资源的一种常用手段。限流器的实现有令牌桶方式、固定窗口限流器和滑动窗口限流器。本文介绍了基于Redis如何快速的实现固定窗口限流器。
Go学堂
2023/08/28
6080
基于Redis实现一个简单的固定窗口限流器
基于Redis和Lua的分布式限流
 Java单机限流可以使用AtomicInteger,RateLimiter或Semaphore来实现,但是上述方案都不支持集群限流。集群限流的应用场景有两个,一个是网关,常用的方案有Nginx限流和Spring Cloud Gateway,另一个场景是与外部或者下游服务接口的交互,因为接口限制必须进行限流。
程序员历小冰
2019/04/07
1.9K0
基于Redis和Lua的分布式限流
推荐阅读
相关推荐
「Go工具箱」redis官网推荐的go版本的分布式锁:redsync
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验