Redis 实现限流的三种方式

项目中用到了限流,受限于一些实现方式上的东西,手撕了一个简单的服务端限流器。

服务端限流和客户端限流的区别,简单来说就是:

1)服务端限流

对接口请求进行限流,限制的是单位时间内请求的数量,目的是通过有损来换取高可用。

例如我们的场景是,有一个服务接收请求,处理之后,将数据bulk到Elasticsearch中进行索引存储,bulk索引是一个很耗费资源的操作,如果遭遇到请求流量激增,可能会压垮Elasticsearch(队列阻塞,内存激增),所以需要对流量的峰值做一个限制。

2)客户端限流

限制的是客户端进行访问的次数。

例如,线程池就是一个天然的限流器。限制了并发个数max_connection,多了的就放到缓冲队列里排队,排队搁不下了>queue_size就扔掉。

本文是服务端限流器。

我这个限流器的优点:

  • 1)简单
  • 2)管事

缺点:

  • 1)不能做到平滑限流
    例如大家尝尝说的令牌桶算法和漏桶算法(我感觉这两个算法本质上都是一个事情)可以实现平滑限流。
    什么是平滑限流?举个栗子,我们要限制5秒钟内访问数不超过1000,平滑限流能做到,每秒200个,5秒钟不超过1000,很平衡;非平滑限流可能,在第一秒就访问了1000次,之后的4秒钟全部限制住。
  • 2)不灵活

   只实现了秒级的限流。

支持两个场景:

1)对于单进程多线程场景(使用线程安全的Queue做全局变量)

这种场景下,只部署了一个实例,对这个实例进行限流。在生产环境中用的很少。

2)对于多进程分布式场景(使用redis做全局变量)

多实例部署,一般来说生产环境,都是这样的使用场景。

在这样的场景下,需要对流量进行整体的把控。例如,user服务部署了三个实例,对外暴露query接口,要做的是对接口级的流量限制,也就是对query这个接口整体允许多大的峰值,而不去关心到底负载到哪个实例。

题外话,这个可以通过nginx做。

下面说一下限流器的实现吧。

1、接口BaseRateLimiter

按照我的思路,先定义一个接口,也可以叫抽象类。

初始化的时候,要配置rate,限流器的限速。

提供一个抽象方法,acquire(),调用这个方法,返回是否限制流量。

class BaseRateLimiter(object):

    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def __init__(self, rate):
        self.rate = rate

    @abc.abstractmethod
    def acquire(self, count):
        return

2、单进程多线程场景的限流ThreadingRateLimiter

继承BaseRateLimiter抽象类,使用线程安全的Queue作为全局变量,来消除竞态影响。

后台有个进程每秒钟清空一次queue;

当请求来了,调用acquire函数,queue incr一次,如果大于限速了,就返回限制。否则就允许访问。

class ThreadingRateLimiter(BaseRateLimiter):

    def __init__(self, rate):
        BaseRateLimiter.__init__(self, rate)
        self.queue = Queue.Queue()
        threading.Thread(target=self._clear_queue).start()

    def acquire(self, count=1):
        self.queue.put(1, block=False)
        return self.queue.qsize() < self.rate

    def _clear_queue(self):
        while 1:
            time.sleep(1)
            self.queue.queue.clear()

2、分布式场景下的限流DistributeRateLimiter

继承BaseRateLimiter抽象类,使用外部存储作为共享变量,外部存储的访问方式为cache。

class DistributeRateLimiter(BaseRateLimiter):

    def __init__(self, rate, cache):
        BaseRateLimiter.__init__(self, rate)
        self.cache = cache

    def acquire(self, count=1, expire=3, key=None, callback=None):
        try:
            if isinstance(self.cache, Cache):
                return self.cache.fetchToken(rate=self.rate, count=count, expire=expire, key=key)
        except Exception, ex:
            return True

为了解耦和灵活性,我们实现了Cache类。提供一个抽象方法getToken()

如果你使用redis的话,你就继承Cache抽象类,实现通过redis获取令牌的方法。

如果使用mysql的话,你就继承Cache抽象类,实现通过mysql获取令牌的方法。

cache抽象类

class Cache(object):

    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def __init__(self):
        self.key = "DEFAULT"
        self.namespace = "RATELIMITER"

    @abc.abstractmethod
    def fetchToken(self, rate, key=None):
        return

给出一个redis的实现RedisTokenCache

每秒钟创建一个key,并且对请求进行计数incr,当这一秒的计数值已经超过了限速rate,就拿不到token了,也就是限制流量。

对每秒钟创建出的key,让他超时expire。保证key不会持续占用存储空间。

没有什么难点,这里使用redis事务,保证incr和expire能同时执行成功。

class RedisTokenCache(Cache):

    def __init__(self, host, port, db=0, password=None, max_connections=None):
        Cache.__init__(self)
        self.redis = redis.Redis(
            connection_pool=
                redis.ConnectionPool(
                    host=host, port=port, db=db,
                    password=password,
                    max_connections=max_connections
                ))

    def fetchToken(self, rate=100, count=1, expire=3, key=None):
        date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        key = ":".join([self.namespace, key if key else self.key, date])
        try:
            current = self.redis.get(key)
            if int(current if current else "0") > rate:
                raise Exception("to many requests in current second: %s" % date)
            else:
                with self.redis.pipeline() as p:
                    p.multi()
                    p.incr(key, count)
                    p.expire(key, int(expire if expire else "3"))
                    p.execute()
                    return True
        except Exception, ex:
            return False

多线程场景下测试代码

limiter = ThreadingRateLimiter(rate=10000)

def job():
    while 1:
        if not limiter.acquire():
            print ‘限流‘
        else:
            print ‘正常‘

threads = [threading.Thread(target=job) for i in range(10)]
for thread in threads:
    thread.start()

分布式场景下测试代码

token_cache = RedisTokenCache(host=‘10.93.84.53‘, port=6379, password=‘bigdata123‘)
limiter = DistributeRateLimiter(rate=10000, cache=token_cache)
r = redis.Redis(connection_pool=redis.ConnectionPool(host=‘10.93.84.53‘, port=6379, password=‘bigdata123‘))

def job():
    while 1:
        if not limiter.acquire():
            print ‘限流‘
        else:
            print ‘正常‘

threads = [multiprocessing.Process(target=job) for i in range(10)]
for thread in threads:
    thread.start()

可以自行跑一下。

说明:

我这里的限速都是秒级别的,例如限制每秒400次请求。有可能出现这一秒的前100ms,就来了400次请求,后900ms就全部限制住了。也就是不能平滑限流。

不过如果你后台的逻辑有队列,或者线程池这样的缓冲,这个不平滑的影响其实不大。

原文地址:https://www.cnblogs.com/ExMan/p/12088402.html

时间: 2024-10-07 00:30:27

Redis 实现限流的三种方式的相关文章

Java基础知识强化之IO流笔记62:三种方式实现键盘录入

1. 三种方式实现键盘录入     System.in 标准输入流.是从键盘获取数据的 键盘录入数据三种方式:  A:main方法的args接收参数.  java HelloWorld hello world java  B:Scanner(JDK5以后的)  Scanner sc = new Scanner(System.in);  String s = sc.nextLine();  int x = sc.nextInt()  C:通过字符缓冲流包装标准输入流实现  BufferedRead

spring配置datasource三种方式

1.使用org.springframework.jdbc.datasource.DriverManagerDataSource 说明:DriverManagerDataSource建立连接是只要有连接就新建一个connection,根本没有连接池的作用. <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">           

android解析XML总结-SAX、Pull、Dom三种方式

在android开发中,经常用到去解析xml文件,常见的解析xml的方式有一下三种:SAX.Pull.Dom解析方式.最近做了一个android版的CSDN阅读器,用到了其中的两种(sax,pull),今天对android解析xml的这三种方式进行一次总结. xml示例(channels.xml)如下: <?xml version="1.0" encoding="utf-8"?> <channel> <item id="0&q

[转]javascript指定事件处理程序包括三种方式:

javascript指定事件处理程序包括三种方式: (1):DOM0级事件处理程序 如: 代码如下: var btn=document.getElementById("mybtn"); //取得该按钮的引用 btn.onclick=function(){ alert('clicked'); alert(this.id); // mybtn 以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理. 删除DOM0级方法指定的事件处理程序: btn.onclick=null; // 删除事件

android客户端与服务端交互的三种方式

android客户端向服务器通信一般有以下选择: 1.传统的java.net.HttpURLConnection类 2.apache的httpClient框架(已纳入android.jar中,可直接使用) 3.github上的开源框架async-http(基于httpClient) ---------------------------------------------------------------------------------- 下面分别记录这三种方式的使用, 传统方式: /**

解析Xml文件的三种方式及其特点

解析Xml文件的三种方式 1.Sax解析(simple api  for xml) 使用流式处理的方式,它并不记录所读内容的相关信息.它是一种以事件为驱动的XML API,解析速度快,占用内存少.使用回调函数来实现. 1 class MyDefaultHander extends DefaultHandler{ 2 private List<Student> list; 3 private Student student; 4 5 @Override 6 public void startDo

Velocity中加载vm文件的三种方式

Velocity中加载vm文件的三种方式: a.  加载classpath目录下的vm文件 Properties p = new Properties(); // 加载classpath目录下的vm文件 // 这里是加载模板VM文件,比如:/META-INF/template/Web_B2CPayment.vm(请参考mas_spring_integration.xml) p.setProperty("file.resource.loader.class", "org.apa

(五十七)android解析XML总结(SAX、Pull、Dom三种方式)

一.SAX.DOM.PULL的比较 SAX.DOM.PULL各有自己的特点,具体操作XML的时候该如何选择呢? 1.内存占用 这是一个根本性问题.由于Android手机性能相对于现在的应用操作还是有限的,程序对内存的占用直接影响到了解析XML的速度.在这点上,SAX.Pull以它们比DOM占用更少的内存的解析方式,更适合于Android手机开发. 2.编程方式 SAX采用事件驱动,在相应事件触发的时候,会调用用户编写好的方法.也就是说,每解析一类XML,就要编写一个新的适合该类XML的处理类.这

文件上传的三种方式-Java

前言:因自己负责的项目(jetty内嵌启动的SpringMvc)中需要实现文件上传,而自己对java文件上传这一块未接触过,且对 Http 协议较模糊,故这次采用渐进的方式来学习文件上传的原理与实践.该博客重在实践. 一.Http协议原理简介 HTTP是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统.它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展.目前在WWW中使用的是HTTP/1.0的第六版,HTTP/1.1的规范化工作正在进行之中,而且HT