Django Copy Flask: 实现Flask中的全局request对象

Django中的request对象是在每个线程中生成, 并通过函数参数的形式一层一层的传递给视图函数, 而Flask则采取的是全局的request对象, 并且能够保证线程安全. 那么如何在Django中实现全局的request对象呢?

0. Define: Django-version: 1.11.13

1. 为什么会有这样的需求?

为什么我们需要在Django中拥有全局的request对象呢? 写函数将对象作为参数传递不就好了. 在CacheAside缓存模式Django实现的那篇文章中, 我使用了信号量来进行解耦, 但是为了准确的获取缓存的cache_key, 手动的构造了一个request对象, 硬编码进去的. 这样的话可能会产生一些问题, 比如获取不到cache_key. 所以, 如果有了全局的request对象的话, CacheAside模式的实现将会更加的优雅.

2. 如何进行线程隔离?

对于每一个线程,都会有一个唯一的线程id, 那么我们去维护一个全局的dict, 其中dict.key即为线程id, dict.value可以是任意的一种结构, 比如字典或者列表.当不同的线程去访问自己线程的数据时, 通过该线程的线程id找到该线程中所有需要进行隔离的变量, 达到线程隔离的效果.

1
2
3
4
5
6
7
8
dict = {
"thread_id_01": {
{"attr_01": "value_01"}
},
"thread_02": {
{"attr_01": "value_02"}
}
}

通过分析werkzeug的源码最终得出了上面儿的线程隔离方法

Flask的依赖包werkzeug有一个模块local,模块下有一个Local类,其实就是在做这样的一件事情.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Local(object):
__slots__ = ('__storage__', '__ident_func__')

def __init__(self):
object.__setattr__(self, '__storage__', {})
object.__setattr__(self, '__ident_func__', get_ident)

def __iter__(self):
return iter(self.__storage__.items())

def __call__(self, proxy):
"""Create a proxy for a name."""
return LocalProxy(self, proxy)

def __getattr__(self, name):
try:
return self.__storage__[self.__ident_func__()][name]
# __getattr__方法也非常的清楚,获取当前线程id下的name属性
except KeyError:
raise AttributeError(name)

def __setattr__(self, name, value):
ident = self.__ident_func__() # 在这里获取了当前线程的线程id
storage = self.__storage__ # storage其实就是一个dict
try:
storage[ident][name] = value # 这里是针对于name的修改
except KeyError:
storage[ident] = {name: value} # 这里进行初始化的赋值

可以使用一个简单的测试文件来对Local的隔离性进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from werkzeug import Local
import threading

l = Local()
l.test = "hello"

def work():
l.test = "world"

t = threading.Thread(target=work, args=())
t.start()
t.join()

print("main thread test: {}".format(l.test))

Alt text

这里使用了一些调试技巧得到了Local最终的__storage__变量内容.可以很清晰的看到不同的线程保存着不同的属性变量

那么线程隔离的问题解决了, 现在来看一看Flask是如何运用线程隔离技术的.

3. Flask中的全局request对象

Flask的上下文管理大致就是这样的, 可能会有一些遗漏, 但是我们把握整体就好了.
总体来讲就是: 请求进入, 实例化请求对象, 若_app_ctx_stack栈为空, 则实例化app对象压入栈, 然后再将request对象压入栈. 获取时直接取栈顶元素即可. 当请求结束时弹出所有的栈元素.
对于Django来讲, 因为我们并不需要app对象, 所以我们可以直接编写一个中间件, 其中process_request方法将请求压入隔离栈, process-response将栈顶元素弹出, 在一个请求会话中, 直接使用top方法来获取当前线程的request对象. 这样一来就实现了全局request的需求.

4. Django实现全局request对象

  • global_stack.py
1
2
3
from werkzeug.local import LocalStack

request_stack = LocalStack()

在这里使用模块儿的单例模式, 一次实例化即可, 没有必要使用__new__方法或者是装饰器来实现了.

  • customize_middleware.py
1
2
3
4
5
6
7
8
9
10
11
12
from .global_stack import request_stack

class GlobalRequestMiddleware(MiddlewareMixin):
def __init__(self, get_response=None):
self.request_stack = request_stack
self.get_response = get_response

def process_request(self, request):
self.request_stack.push(request)

def process_response(self, request, response):
self.request_stack.pop()

将该中间件添加至配置文件中的MIDDLEWARE中即可.

  • 使用
1
2
3
from .global_stack import request_stack

request = request_stack.pop()

代码就这么多, 需要注意一点的是: 虽然我们能够在任何地方拿到当前线程的request对象, 但是对于我们自己开启的一个新线程来讲, 是没有办法拿到的. 只能在一个线程中使用.

5.重写CacheAside缓存失效模式

在有了当前请求线程的全局request对象之后, 我们就不必手动的构造request对象来获取cache_key了.

  • 原始版本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import os
import django
os.environ.update({"DJANGO_SETTINGS_MODULE": "Zero.settings.local"})
django.setup()

from django.core.cache import cache
from django.utils.cache import get_cache_key

from django.http import HttpRequest

http_request = HttpRequest()
http_request.META["SERVER_NAME"] = "localhost"
http_request.META["SERVER_PORT"] = "6060"
http_request.path = "/cache"
key = get_cache_key(http_request, key_prefix="test_cache")
# views.decorators.cache.cache_page.test_cache.GET.15e585e58b05970a7be785828893971e.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC 这里将key打印出来
if key:
cache.delete(key)
  • 更新后版本
1
2
3
4
5
6
7
8
9
from .global_stack import request_stack

receiver(post_save, sender=xxx)
def clear_cache(*args, **kwargs):
# 假设这是我们的一个信号量接受函数, 使得某个缓存失效
request = request_stack.pop()
key = get_cache_key(request, key_prefix="xxx")
if key:
cache.delete(key)

可以看到更新后的版本没有过多的硬编码, 整体代码也非常的简洁. 当我们因为某种原因而修改了url时, 也不必更新此处的代码, 维护成本更低.