Django REST framework 限流功能的使用


Posted in Python onJune 24, 2021

正文开始

先说一个限流这个概念,最早接触这个概念是在前端。真实的业务场景是在搜索框中输入文字进行搜索时,并不希望每输一个字符都去调用后端接口,而是有停顿后才真正的调用接口。这个功能很有必要,一方面减少前端请求与渲染的压力,同时减轻后端接口访问的压力。类似前端的功能的代码如下:

// 前端函数限流示例
function throttle(fn, delay) {
    var timer;
    return function () {
        var _this = this;
        var args = arguments;
        if (timer) {
            return;
        }
        timer = setTimeout(function () {
            fn.apply(_this, args);
            timer = null;
        }, delay)
    }
}

但是后端的限流从目的上来说与前端类似,但是实现上会有所不同,让我们看看 DRF 的限流。

1. DRF 中的限流

项目配置

# demo/settings.py

REST_FRAMEWORK = {
    # ...
    'DEFAULT_THROTTLE_CLASSES': (
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle',
         'rest_framework.throttling.ScopedRateThrottle',
    ),
    'DEFAULT_THROTTLE_RATES': {
        'anon': '10/day',
        'user': '2/day'
    },
}

# article/views.py

# 基于ViewSet的限流
class ArticleViewSet(viewsets.ModelViewSet, ExceptionMixin):
    """
    允许用户查看或编辑的API路径。
    """
    queryset = Article.objects.all()
    # 使用默认的用户限流
    throttle_classes = (UserRateThrottle,)
    serializer_class = ArticleSerializer

# 基于view的限流
@throttle_classes([UserRateThrottle])

因为我配置的用户每天只能请求两次,所以在请求第三次之后就会给出 429 Too Many Requests的异常,具体的异常信息为下一次可用时间为 86398 秒后。

2. 限流进阶配置

上述演示的限流配置适用于对用户的限流,比如我换个用户继续访问,依然是有两次的机会。

$ curl -H 'Accept: application/json; indent=4' -u root:root   http://127.0.0.1:8000/api/article/1/ 
{
    "id": 1,
    "creator": "admin",
    "tag": "现代诗",
    "title": "如果",
    "content": "今生今世 永不再将你想起\n除了\n除了在有些个\n因落泪而湿润的夜里 如果\n如果你愿意"
}

分别介绍一下三种限流类

  • AnonRateThrottle 适用于任何用户对接口访问的限制
  • UserRateThrottle 适用于请求认证结束后对接口访问的限制
  • ScopedRateThrottle 适用于对多个接口访问的限制

所以三种不同的类适用于不同的业务场景,具体使用根据不同的业务场景选择,通过配置相对应 scope 的频率的配置就可以达到预期的效果。

3. 限流思路分析

试想一下如果是你编码实现这个需求应该怎么实现?

其实这个功能不难,核心的参数就是 时间、次数、使用范围,下面演示对函数调用次数的限制。

from functools import wraps

TOTAL_RATE = 2

FUNC_SCOPE = ['test', 'test1']


def rate_count(func):
    func_num = {
        # 需要注意函数名不能重复
        func.__name__: 0
    }

    @wraps(func)
    def wrapper():
        if func.__name__ in FUNC_SCOPE:
            if func_num[func.__name__] >= TOTAL_RATE:
                raise Exception(f"{func.__name__}函数调用超过设定次数")
            result = func()
            func_num[func.__name__] += 1
            print(f" 函数 {func.__name__} 调用次数为: {func_num[func.__name__]}")
            return result
        else:
            # 不在计数限制的函数不受限制
            return func()

    return wrapper


@rate_count
def test1():
    pass


@rate_count
def test2():
    print("test2")
    pass


if __name__ == "__main__":
    try:
        test2()
        test2()
        test1()
        test1()
        test1()
    except Exception as e:
        print(e)
    test2()
    test2()
    
"""
test2
test2
 函数 test1 调用次数为: 1
 函数 test1 调用次数为: 2
test1函数调用超过设定次数
test2
test2
"""

这里实现了对函数调用次数的监控同时设置了能够使用该功能的函数。当函数调用次数超过设定阀值久抛出异常。只是这里没有对时间做限制。

4. 源码分析

刚才分析了如何实现对函数调用次数的限制,对于一个请求来说可能会复杂一点,下面就看看 DRF 如何实现的:

class SimpleRateThrottle(BaseThrottle):
   
    # ......
    
    def allow_request(self, request, view):
        """
        Implement the check to see if the request should be throttled.

        On success calls `throttle_success`.
        On failure calls `throttle_failure`.
        """
        if self.rate is None:
            return True

        self.key = self.get_cache_key(request, view)
        if self.key is None:
            return True

        self.history = self.cache.get(self.key, [])
        self.now = self.timer()

        # 根据设置时间的限制改变请求次数的缓存
        while self.history and self.history[-1] <= self.now - self.duration:
            self.history.pop()
        # 核心逻辑就是这里判断请求次数
        if len(self.history) >= self.num_requests:
            return self.throttle_failure()
        return self.throttle_success()
    
    # ......
    
class UserRateThrottle(SimpleRateThrottle):
    """
    Limits the rate of API calls that may be made by a given user.

    The user id will be used as a unique cache key if the user is
    authenticated.  For anonymous requests, the IP address of the request will
    be used.
    """
    scope = 'user'

    def get_cache_key(self, request, view):
        if request.user.is_authenticated:
            ident = request.user.pk
        else:
            # 考虑到用户没有认证的情况 与 AnonRateThrottle 中 key 一致
            ident = self.get_ident(request)
        # 根据设置的范围构建缓存的 key
        return self.cache_format % {
            'scope': self.scope,
            'ident': ident
        }

综上所述:

  • 核心的判断逻辑依旧是缓存中获取每个用户调用次数,根据范围与时间判断是否超过设置定的阀值。
  • 不同类型的限流,在缓存 key 的设计上会有区别,默认的 key 为请求中REMOTE_ADDR。

5. 其它注意事项

  • 因为这里的实现用到缓存,所以需要注意在多实例部署的情况下需要配置统一的缓存服务(默认的缓存为 Django 基于内存实现的)。
  • 缓存服务的重启可能会导致已有的计数清零,如果有较强的业务逻辑需要,还请自己实现限流的逻辑。
  • 如果是自定义的用户表,需要重写缓存中 get_cache_key 的逻辑。
  • 如果需要统计分析用户被限流情况也是需要重新设计限流的逻辑。
  • 限流的逻辑在生产环境中慎用,因为会限制用户使用产品,对用户不够友好。

参考资料

DRF 限流
Django 缓存

以上就是Django REST framework 限流功能的使用的详细内容,更多关于Django REST framework 限流功能的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
python动态监控日志内容的示例
Feb 16 Python
python的描述符(descriptor)、装饰器(property)造成的一个无限递归问题分享
Jul 09 Python
Ruby元编程基础学习笔记整理
Jul 02 Python
Python爬虫实例爬取网站搞笑段子
Nov 08 Python
Python request设置HTTPS代理代码解析
Feb 12 Python
python如何将图片转换为字符图片
Aug 19 Python
解决win7操作系统Python3.7.1安装后启动提示缺少.dll文件问题
Jul 15 Python
解决pycharm同一目录下无法import其他文件
Feb 12 Python
django orm模块中的 is_delete用法
May 20 Python
基于Keras 循环训练模型跑数据时内存泄漏的解决方式
Jun 11 Python
python 实现性别识别
Nov 21 Python
pytorch锁死在dataloader(训练时卡死)
May 28 Python
Python 发送SMTP邮件的简单教程
Python开发工具Pycharm的安装以及使用步骤总结
Python语言规范之Pylint的详细用法
Python基础教程,Python入门教程(超详细)
Jun 24 #Python
Django Paginator分页器的使用示例
python随机打印成绩排名表
教你怎么用Python selenium操作浏览器对象的基础API
Jun 23 #Python
You might like
PHP MVC框架skymvc支持多文件上传
2016/05/26 PHP
PHP实现微信模拟登陆并给用户发送消息的方法【文字,图片,图文】
2017/06/29 PHP
PHP通过调用新浪API生成t.cn格式短网址链接的方法详解
2019/02/20 PHP
JS中剪贴板兼容性、判断复制成功或失败
2021/03/09 Javascript
javascript显示隐藏层比较不错的方法分析
2008/09/30 Javascript
从jQuery.camelCase()学习string.replace() 函数学习
2011/09/13 Javascript
jQuery选择器全面总结
2014/01/06 Javascript
Jquery操作js数组及对象示例代码
2014/05/11 Javascript
JavaScript实现的一个计算数字步数的算法分享
2014/12/06 Javascript
浅谈javascript中的DOM方法
2015/07/16 Javascript
AngularJS基础 ng-paste 指令简单示例
2016/08/02 Javascript
微信小程序 传值取值的几种方法总结
2017/01/16 Javascript
js实现无缝滚动图(可控制当前滚动的方向)
2017/02/22 Javascript
angularjs的select使用及默认选中设置
2017/04/08 Javascript
使用JavaScript实现一个小程序之99乘法表
2017/09/21 Javascript
JS高级技巧(简洁版)
2018/07/29 Javascript
浅谈webpack devtool里的7种SourceMap模式
2019/01/14 Javascript
详解使用webpack+electron+reactJs开发windows桌面应用
2019/02/01 Javascript
JS如何生成随机验证码
2020/03/02 Javascript
谈谈JavaScript中的函数
2020/09/08 Javascript
JavaScript本地储存:localStorage、sessionStorage、cookie的使用
2020/10/13 Javascript
[02:46]解说DC:感谢430陪伴我们的DOTA2国际邀请赛岁月
2016/06/29 DOTA
在Python中调用ggplot的三种方法
2015/04/08 Python
python查询sqlite数据表的方法
2015/05/08 Python
8种用Python实现线性回归的方法对比详解
2019/07/10 Python
使用Pyinstaller转换.py文件为.exe可执行程序过程详解
2019/08/06 Python
Python异常模块traceback用法实例分析
2019/10/22 Python
python列表切片和嵌套列表取值操作详解
2020/02/27 Python
Python基于pyjnius库实现访问java类
2020/07/31 Python
大学英语演讲稿范文
2014/04/24 职场文书
竞选村长演讲稿
2014/04/28 职场文书
优秀工会工作者事迹材料
2014/06/02 职场文书
建筑工地标语
2014/06/18 职场文书
群教个人对照检查材料
2014/08/20 职场文书
JS数组方法some、every和find的使用详情
2021/10/05 Javascript
python高温预警数据获取实例
2022/07/23 Python