前言:
现时小伙伴们对“django功能模块”大约比较讲究,小伙伴们都需要了解一些“django功能模块”的相关知识。那么小编在网上汇集了一些对于“django功能模块””的相关知识,希望姐妹们能喜欢,大家快快来学习一下吧!此篇将基于后端服务进行实践,并针对Django RestFramework框架的相关源码进行语义剖析和流程解读。
软件包版本
本次测试了两个新旧两个后端版本,均可兼容使用:
软件
旧版
新版
python
3.8.16
3.11.2
django
3.0.5
4.1.5
djangorestframework
3.11.0
3.14.0
附源码阅读顺序:
1.python3.11/site-packages/rest_framework/throttling.py
2.python3.11/site-packages/rest_framework/views.py
3.python3.11/site-packages/rest_framework/exceptions.py
一、简易实例代码
假设有个安全需求,要求在某敏感接口设置限流,每个认证用户访问不得超过每分钟30次,否则输出超限等待提示,并执行钉钉告警。先画一个简易的请求步骤流程图:
配置方面很简单,先在settings.py的REST_FRAMEWORK定义全局默认限流触发条件(此处意为服务端收到认证用户请求达到 30次/分钟 即触发):
接着自定义UserDisabledRateThrottle类用于限流处置,继承自UserRateThrottle类:
接着在接口对应的视图类重写关键字属性throttle_classes,指向刚刚自定义的UserDisabledRateThrottle类:
至此限流代码就已完成,下面跟踪一下Django RestFramework底层是如何实现限流功能的。
二、源码剖析(rest_framework/throttling.py)
前面自定义的UserDisabledRateThrottle类继承了UserRateThrottle类,故从这里入手 打开UserRateThrottle源码,看到其继承自SimpleRateThrottle类,其中get_cache_key()是在子类必须要实现的方法,使用scope+ident(即应用范围+UID)将cache_format拼接(生成访问缓存cache_key):
查看SimpleRateThrottle,里面定义了cache_format和需要子类实现的方法get_cache_key(),这里可以看到之所以必须要子类实现,是因为scope和ident都是字符串变量(%代入):
继续向下看,核心方法allow_request()中调用了这个get_cache_key()生成缓存key,并算出是否要触发限流(返回布尔值):
有几个参数很重要,key(缓存的键),history(访问历史),duration(持续时间),num_requests(请求数量),下面一一说明:key是通过self.get_cache_key()获取的,这就是为什么继承SimpleRateThrottle类就必须要实现重写get_cache_key()的原因;history就是上面图中的cache.get返回的这个数据列表,因为符合FIFO原则,可以把它当成队列结构;duration和num_requests是在parse_rate设置的,是从settings.py的DEFAULT_THROTTLE_RATES中split分割出来的固定数值。本文中DEFAULT_THROTTLE_RATES为“30/minute”,所以num_requests为30(单位:次),duration为60(单位:秒)。
了解完参数,接下来看执行赋值的两个重要判断逻辑:第一,128~129行,请求过期时(self.history[-1] <= self.now - self.duration)则删除(self.history.pop())历史记录的对应数据。这里用pop()是因为最新请求在cache中是插入队首的(见138~140行的self.history.insert(0, self.now)),所以pop会删除最老的数据(见上面的cache.get结果)。第二,130~132行,当history的length大于num_requests时,访问频次到达限流阈值,触发throttle_failure限流逻辑(本次重写的正是此部分)返回False;否则插入缓存队列,返回True,继续APIView的后续操作。
可以测试下cache_key的生成过程,当throttle对应的接口未触发限流时,每次请求会在数据库cachetable表中更新cache_key(在settings.py中设置的是CACHE_BACKEND = 'db://cachetable')
通过SQL语句查看库中的cache_key值:(py3) mbp:src $ ./manage.py dbshellmysql> desc cachetable;+-----------+--------------+------+-----+---------+-------+| Field | Type | Null | Key | Default | Extra |+-----------+--------------+------+-----+---------+-------+| cache_key | varchar(255) | NO | PRI | NULL | || value | longtext | NO | | NULL | || expires | datetime(6) | NO | MUL | NULL | |+-----------+--------------+------+-----+---------+-------+3 rows in set (0.00 sec)访问一下接口,缓存里出现一条数据:mysql> select * from cachetable where cache_key like '%throttle_%';+----------------------+----------------------------------+----------------------------+| cache_key | value | expires |+----------------------+----------------------------------+----------------------------+| :1:throttle_user_274 | gAWVDQAAAAAAAABdlEdB2PZuE43BZWEu | 2023-02-01 08:55:38.000000 |+----------------------+----------------------------------+----------------------------+1 row in set (0.00 sec)通过代码获取value值:(py3) mbp:src $ ./manage.py shell_plus# 设置datatable为cache>>> from django.core.cache import cache as default_cache>>> cache = default_cache# 设置key值为上面的cache_key值:>>> key = "throttle_user_274"# 查看用来测试的用户user_id:>>> User.objects.filter(username='admin').first().id274# 注意:我在访问接口时使用的用户是admin,此时查到的admin的id是274,与上面cache_key中的274吻合# scope的值:UserRateThrottle()中已将其设置为"user"# ident的值:用户admin的user_id,即为274# 将上述2个值作为变量代入 cache_format = 'throttle_%(scope)s_%(ident)s',则get_cache_key()结果值为"throttle_user_274",与上面的cache_key值结果相符获取访问的1次接口的对应value:>>> cache.get(key, [])[1675212878.2149289]再访问3次接口,列表数据内容变成4条(从队头插入):>>> cache.get(key, [])[1675212918.3631868, 1675212917.43252, 1675212915.0148559, 1675212878.2149289]
综上所述,throttling.py的核心方法是allow_request(),它实现了用于限流的cache_key和触发逻辑。
三、源码剖析(rest_framework/views.py)
接着看下APIviews里是在哪里、如何调用及捕获allow_request的。再次回到视图类,这里继承了ReadOnlyModelViewSet方法:
打开 rest_framework/viewsets.py 文件,发现如下继承关系:ReadOnlyModelViewSet -> GenericViewSet -> generics.GenericAPIView:
这里generics.GenericAPIView(位置rest_framework/generics.py)又继承自API视图基类views.APIView:
来到views.APIView(位置rest_framework/views.py),它封装了throttle的常用关键字属性(还包含了authentication_classes、permission_classes等常用模块),这里相当于一个钩子:
APIView类在初始化方法中会调用check_throttles()函数:
关键点来了,check_throttles()函数用到了前面提到的限流核心方法allow_request(),当它返回布尔值为False(到达限流阈值)时,throttle_durations列表非空,继而调用self.throttle()模块:
之后self.throttle()会主动抛出一个限流异常:
限流异常捕获类(位置rest_framework/exceptions.py)定义了拼接文案和返回码:
到此,限流的整体核心代码就都关联起来了。
四、测试限流
代码上线后开始测试,频繁访问接口会触发限流,直到下一个统计周期前,页面都将返回如下(返回文案与上图拼接的一致):
验证下日志里的请求返回码(对应 Throttled类中 status_code = status.HTTP_429_TOO_MANY_REQUESTS):
触发后的自定义逻辑(发送钉钉机器人告警)正常执行:
限流功能测试完成,符合预期处置结果。
总结
回顾下刚刚的限流代码,整体流程见下图:
所以实现一个限流功能只需三步:1.设置限流阈值 2.编写处置逻辑 3.应用到视图类
源码方面,有以下几个重点文件:
1.throttling.py文件:实现了访问频次统计的cache_key结构及限流判断逻辑;
2.views.py文件:实现了用户视图基类的限流钩子;
3.exceptions.py文件:捕获限流异常并生成文案。
作者:钟仕骏
来源:微信公众号:新东方技术
出处:
标签: #django功能模块