Python自动化运维 - Django(五)中间件 - 缓存 - 信号
1 中间件
由一个需求引入中间件:我要记录所有url的访问日志,该如何操作?鉴于我们前面所学的知识,那么最好的方法就是使用装饰器了,那么如果我有1000个函数,就需要写1000遍装饰器... ... 。 这该咋办?利用django中间件完成!
django 中的中间件(middleware,在其他语言中会称为管道,或者 HTTP handler),在django中,中间件其实就是一个类,在请求到来和结束后,django会根据自己的规则在合适的时机执行中间件中相应的方法。
引入了中间件之后,一个请求的生命周期就有了如下的改变
包含中间件的请求周期
一创建Python项目就会存在的中间件有:(存放在settings.py文件中)
MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ]
包含了中间件的请求周期如图所示:
注意:中间件是有顺序的,即从上倒下依次执行。从图上看,我们知道,请求的入方向和出方向都需要经过中间件,所以一个中间件应该有两个函数来对应不同的方向,这两个函数就是process_request(入)和process_response(出)。
process_request和process_response函数
这里以其他中间件为例子:
1 class SecurityMiddleware(MiddlewareMixin): 2 def __init__(self, get_response=None): 3 self.sts_seconds = settings.SECURE_HSTS_SECONDS 4 self.sts_include_subdomains = settings.SECURE_HSTS_INCLUDE_SUBDOMAINS 5 self.sts_preload = settings.SECURE_HSTS_PRELOAD 6 self.content_type_nosniff = settings.SECURE_CONTENT_TYPE_NOSNIFF 7 self.xss_filter = settings.SECURE_BROWSER_XSS_FILTER 8 self.redirect = settings.SECURE_SSL_REDIRECT 9 self.redirect_host = settings.SECURE_SSL_HOST 10 self.redirect_exempt = [re.compile(r) for r in settings.SECURE_REDIRECT_EXEMPT] 11 self.get_response = get_response 12 13 def process_request(self, request): 14 path = request.path.lstrip("/") 15 if (self.redirect and not request.is_secure() and 16 not any(pattern.search(path) 17 for pattern in self.redirect_exempt)): 18 host = self.redirect_host or request.get_host() 19 return HttpResponsePermanentRedirect( 20 "https://%s%s" % (host, request.get_full_path()) 21 ) 22 23 def process_response(self, request, response): 24 if (self.sts_seconds and request.is_secure() and 25 'strict-transport-security' not in response): 26 sts_header = "max-age=%s" % self.sts_seconds 27 if self.sts_include_subdomains: 28 sts_header = sts_header + "; includeSubDomains" 29 if self.sts_preload: 30 sts_header = sts_header + "; preload" 31 response["strict-transport-security"] = sts_header 32 33 if self.content_type_nosniff and 'x-content-type-options' not in response: 34 response["x-content-type-options"] = "nosniff" 35 36 if self.xss_filter and 'x-xss-protection' not in response: 37 response["x-xss-protection"] = "1; mode=block" 38 39 return response
可以看到该中间件定义了process_request和process_response函数,其中process_request处理处理入方向的请求,process_response处理出方向的请求。
观察这两个函数可以发现:
- process_request函数有一个参数request,用来表示用户请求的信息,并且在正常请求情况下函数并没有指定返回的内容,因为如果函数返回了非空或者非None的数据,请求将不会继续执行,而会直接交给process_response返回给用户。
- process_response函数存在两个参数,request和response,用来表示用户的请求信息,和应答信息(因为是位置参数,所以不要搞反了),函数最后返回了response,因为response存放的就是答复给用户的信息,你必须把信息返回给后续的中间件处理,并返回给用户才行,否则会直接报错。
注意:django 1.7-1.9 版本时,如果在某个一个中间件的process_request中返回了非空或者非None数据时,那么该请求将会从最后一个中间件的process_response函数开始进行返回,而django 1.10+ 版本时,修改了逻辑,会直接从当前中间件的process_response函数开始进行返回。
自定义中间件:(根据初始化的中间件代码可知,需要继承MiddlewareMixin类,所以需要先导入)
#1.这里在app01下创建middleware.py文件用于存放自定义中间件 from django.utils.deprecation import MiddlewareMixin # 引入MiddlewareMixin类 from django.shortcuts import render,HttpResponse,redirect class hello(MiddlewareMixin): def process_request(self,request): # 定义入方向的检查规则 return HttpResponse('滚') # 入方向直接返回信息,那么请求将不会到达urls.py,会直接进行返回 #2. 在settings.py中添加我们配置的中间件 MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'app01.middleware.hello', # 因为我们没有建立规范化目录,所以这里从站点目录开始标示中间件位置 ]
1 from django.utils.deprecation import MiddlewareMixin 2 from django.shortcuts import render,HttpResponse,redirect 3 4 class hello(MiddlewareMixin): 5 6 def process_request(self,request): 7 8 pass 9 10 def process_response(self,request,response): 11 12 13 print(response) # <HttpResponse status_code=200, "text/html; charset=utf-8"> ,HttpResponse类,封装了应答信息 14 15 return response
注意:
- 由于是从上倒下执行的,所以最后会执行我们的hello中间件
- 由于中间件的process_request返回了信息,那么所有请求将会永远返回('滚'),不信可以自己试试滚的感觉。
- 可以同时定义process_request和process_response函数,也可以只选择一个方向进行定义。
应用:
- 给全站增加记录日志的功能(定义中间件,在process_request的时候调用log模块进行记录)
- 当所有页面都要登录后才能查看的时候,就可以在中间件验证session或cookies。
1 class Auth(MiddlewareMixin): #添加认证中间件 2 3 def process_request(self,request): 4 5 print('m2.process_request') 6 7 if request.path_info == '/test/': # 对例外的目录进行放通,否则会进行死循环(如果不添加例外,那么所有请求都会被 重定向到/test/页面,而test页面也会检查session,所以会陷入死循环。 8 9 return None 10 11 if not request.session.get('user',None): # 所有的请求没有携带session,将会被返回 12 13 return redirect('/test/') # 跳转到test目录 14 15 def process_response(self,request,response): 16 17 print('m2.process_response') 18 19 return response
PS:很多人可能导入的时候报错,没有MiddlewareMixin这个类,那是因为由于MiddlewareMixin处于快要被淘汰的边缘,可能哪个版本就被取消了,具体原因就不知道了,所以为了不改变编写的方式,我们可以手动把如下源码放在文件的头部即可。
1 class MiddlewareMixin(object): 2 def __init__(self, get_response=None): 3 self.get_response = get_response 4 super(MiddlewareMixin, self).__init__() 5 6 def __call__(self, request): 7 response = None 8 if hasattr(self, 'process_request'): 9 response = self.process_request(request) 10 if not response: 11 response = self.get_response(request) 12 if hasattr(self, 'process_response'): 13 response = self.process_response(request, response) 14 return response
PS:针对中间件的存放位置,建议有一个规范化的存放位置,比如在站点目录下创建middleware目录,然后在目录下创建我们的中间件文件,在文件中编写我们的中间件,然后在settings.py文件中进行导入。
MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'middleware.中间件文件名.中间件类名', ]
process_view和process_exception函数
引入process_view和process_exception函数之后,请求的顺序发生了变化
从图上可以看到:
- 用户的请求经过所有中间件的process_request函数处理完毕后,由urls路由系统进行匹配,匹配完毕后会回到最初的中间件,继续按顺序执行它们的process_view函数,执行完毕后,会直接执行对应的views视图函数。
- 答复给用户的信息会有urls路由系统返回给最后一个中间件的process_exception函数,顺序执行后,再返回给最后一个中间件,继续执行process_reponse函数
- 需要注意的是,process_exception函数只要被任意一个中间件执行后,将不会执行后面中间件的process_exception函数。
针对process_view函数:如果任意一个中间件的process_view函数返回了非空或者非None,那么后续将不会被执行,会直接倒序执行所有中间件的process_response函数
1 from django.utils.deprecation import MiddlewareMixin 2 from django.shortcuts import render,HttpResponse,redirect 3 4 class hello(MiddlewareMixin): 5 6 def process_view(self,request,callback,callback_args,callback_kwargs): 7 ''' 8 :param request: 用户的请求 9 :param callback: 匹配到的视图函数名 10 :param callback_args: 视图函数的位置参数 11 :param callback_kwargs: 视图函数的关键字参数 12 :return: 如果return 非空非None会直接倒序执行所有middleware的process_response 13 ''' 14 15 pass
1 from django.utils.deprecation import MiddlewareMixin 2 from django.shortcuts import render,HttpResponse,redirect 3 4 class hello(MiddlewareMixin): 5 6 def process_exception(self,request,exceptions): 7 ''' 8 :param request: 用户的请求 9 :param exceptions: 异常信息 10 :return: 捕捉到异常后返回给用户的信息,也可以返回跳转连接 11 ''' 12 13 pass
小结:
如果在 view函数中 返回非空 非None。后续process_views不会执行,会倒序执行所有中间件的process_response,然后返回。
如果视图函数报错,那么在 views函数执行完毕后,执行process_exception函数,最后执行process_response 函数,如果没错,就直接执行process_response进行返回。
只要有中间件捕获异常并返回,那么就不会继续执行process_exceptions,会执行process_response进行返回。
应用:
可以针对后台错误,返回维护页啊等等的。
process_template_response函数(知道就好)
当我们views中的函数,返回的对象包含render方法时,这里就会执行。(HttpResoonse,redirect,render方法不会触发)
process_template_response(self,request,response) # 需要返回response对象
执行的顺序为:views函数执行完毕后,紧接着就会执行它。
不同版本django配置小差异
1、Django 1.9 和以前的版本:
MIDDLEWARE_CLASSES = ( 'zqxt.middleware.BlockedIpMiddleware', ...其它的中间件 )
2、Django 1.10 版本 更名为 MIDDLEWARE(单复同形),写法也有变化。
MIDDLEWARE = ( 'zqxt.middleware.BlockedIpMiddleware', ...其它的中间件 )
如果用 Django 1.10版本开发,部署时用 Django 1.9版本或更低版本,要特别小心此处。
2 Django缓存系统
Django 是动态网站,一般来说需要实时地生成访问的网页,展示给访问者,这样,内容可以随时变化,但是从数据库读多次把所需要的数据取出来,要比从内存或者硬盘等一次读出来 付出的成本大很多。目前只有django框架自带缓存支持,只需要进行基本的配置就可以使用。
缓存系统工作原理
对于给定的网址,尝试从缓存中找到网址,如果页面在缓存中,直接返回缓存的页面,如果缓存中没有,一系列操作(比如查数据库)后,保存生成的页面内容到缓存系统以供下一次使用,然后返回生成的页面内容。
配置django缓存
django配置缓存非常简单,只需要在settings.py中添加如下配置即可完成:
CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', # 缓存的引擎 'TIMEOUT': 300, # 缓存超时时间(默认300,None表示永不过期,0表示立即过期) 'OPTIONS': { 'MAX_ENTRIES': 300, # 最大缓存个数(默认300) 'CULL_FREQUENCY': 3, # 缓存到达最大个数之后,剔除缓存个数的比例,即:1/CULL_FREQUENCY(默认3) }, 'KEY_PREFIX': '', # 缓存key的前缀(默认空) 'VERSION': 1, # 缓存key的版本(默认1) #'KEY_FUNCTION' # 生成key的函数(默认函数会生成为:【前缀:版本:key】) } }
django支持的缓存方式有:
- 开发调试
- 内存
- 文件
- 数据库
- Memcache缓存(python-memcached模块)
- Memcache缓存(pylibmc模块)
对应的引擎如下:
'django.core.cache.backends.db.DatabaseCache' # 缓存到数据库中 'django.core.cache.backends.dummy.DummyCache' # 开发测试模式(假缓存) 哪都不放,相当于没有 'django.core.cache.backends.filebased.FileBasedCache' # 缓存到本地文件中 'django.core.cache.backends.locmem.LocMemCache' # 缓存到本地内存中 'django.core.cache.backends.memcached.MemcachedCache' # 缓存到memcached中去(利用python-memcached模块)* 'django.core.cache.backends.memcached.PyLibMCCache' # 缓存到memcached中去(利用pylibmc模块)
注意:django默认不支持redis,需要使用第三方插件
PS:如果使用数据库引擎,那么配置完毕需要执行如下命令生成缓存信息表: python manage.py createcachetable
其他缓存参数
1)TIMEOUT # cache 默认过期时间(seconds),未设置则为300s(5mins) 2)OPTIONS # 可选项配置,不同的后端,可选项配置不同 MAX_ENTRIES # 最大缓存个数(默认300) CULL_FREQUENCY # 缓存到达最大个数之后,剔除缓存个数的比例,即:1/CULL_FREQUENCY(默认3) 3)KEY_PREFIX # 默认会被自动加到所有缓存 keys 的前端 4)VERSION # 默认缓存 keys 的 version 5)KEY_FUNCTION # 生成最终缓存 keys 的函数路径 6)LOCATION # 不同的引擎含义不同 # 数据库时,这里填写表名 # 存放到文件时,这里填写文件的路径及名称 # 存放到内存,这里填写的是全局变量的名称 # 放到memcached,这里填写的就是memcached的地址和端口
配置示例
1、放在文件中
1 CACHES = { 2 'default': { 3 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',# 引擎 4 'LOCATION': '/Users/DahlHin/Downloads/cache', # 缓存的路径 5 'TIMEOUT': 300, # 缓存超时时间(默认300,None表示永不过期,0表示立即过期) 6 'OPTIONS':{ 7 'MAX_ENTRIES': 300, # 最大缓存个数(默认300) 8 'CULL_FREQUENCY': 3, # 缓存到达最大个数之后,剔除缓存个数的比例,即:1/CULL_FREQUENCY(默认3) 9 }, 10 # 'KEY_PREFIX': '', # 缓存key的前缀(默认空) 11 # 'VERSION': 1, # 缓存key的版本(默认1) 12 # 'KEY_FUNCTION' 函数名 # 生成key的函数(默认函数会生成为:【前缀:版本:key】) 13 } 14 }
2、放在memcache中
1 CACHES = { 2 'default': { 3 'BACKEND':'django.core.cache.backends.memcached.MemcachedCache', 4 'LOCATION': '192.168.10.3:11211', 5 'OPTIONS': { 6 'MAX_ENTRIES': '400', # 使用memcached这会报错,提示没这个key,后续研究好了 7 }, 8 'KEY_PREFIX': 'Django_cache', 9 'VERSION': 1, 10 } 11 } 12 # 多台memcached可以设置权重,只需要修改location中的memcache list是元组即可 13 # ('172,16,0,1:11211',20), 14 # ('172,16,0,1:11211',30), 15 # 这些权重不是django来做的,是python-memcached模块来做的 16 17 18 19 # memcached中的key value 如下: 20 stats cachedump 11 0 21 ITEM Django_cache:1:views.decorators.cache.cache_page..GET.e76ff9eca7dbf29254e8d11875f7f807.d41d8cd98f00b204e9800998ecf8427e.en-us.UTC [579 b; 1521963803 s] 22 END
应用缓存
使用django缓存,包含三个级别:全局,视图函数,局部模板。
1、对单独视图函数的缓存
from django.views.decorators.cache import cache_page # 引入装饰器 @cache_page(3) # 对test1视图函数进行缓存,括号里的表示时间单位是秒,这里的失效时间要优先于settings文件中配置的失效时间 def test1(request): import time ctime = time.time() return render(request,'test1.html',{'ctime':ctime}) def test2(request): import time ctime = time.time() return render(request,'test2.html',{'ctime':ctime})
2、局部模板
顾名思义,即在模板中的局部进行缓存
{% load cache %} # 首先要加载缓存 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>TEST1 -> {{ ctime }}</h1> {% cache 10 'helloworld' %} # 缓存开始,10表示时间,helloworld表示名称 <h1>TEST1 -> {{ ctime }}</h1> # 缓存的内容 {% endcache %} # 结束缓存 </body> </html>
3、全局缓存
当针对全局来做某些的操作的时候,很容易的就想到了中间件,那么如何利用中间件来完成全局缓存呢?又应该在哪里进行缓存呢?
什么时候查找缓存的?我们知道一个请求要经过各个中间件进行过滤才会到达urls路由系统,那么只有经过了层层的过滤才能访问数据(查找缓存),所以查找缓存的中间件,应该放在所有中间件的最后面。
什么时候更新缓存呢?一个请求要经过所有中间件的response函数处理完毕后最后才会返回给用户,任何一个中间件的response都有可能更改数据,所以应该放在第一个中间件,让它最后返回给用户时,同时更新缓存系统。
如何使用全局缓存呢?django已经为我们写好了:
- django.middleware.cache.UpdateCacheMiddleware 用于更新缓存,所以应该放在所有中间件的第一个位置
- django.middleware.cache.FetchFromCacheMiddleware 用于查找缓存,所以应该放在所有中间件的最后一个位置
配置全局缓存:
MIDDLEWARE = [ 'django.middleware.cache.UpdateCacheMiddleware', # 出方向 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.cache.FetchFromCacheMiddleware', # 入方向 ]
根据业务的缓存方向,可以知道:由于入向的 django.middleware.cache.FetchFromCacheMiddleware 只查找缓存,那么它应该只有process_request方法,而 出向的 django.middleware.cache.UpdateCacheMiddleware 只更新缓存,那么它应该只有process_response方法,这里可以自行查看源码验证。
配置全局缓存时,可选的其他参数(settings文件中):
CACHE_MIDDLEWARE_ALIAS = "" CACHE_MIDDLEWARE_SECONDS = 10 # 全局缓存超时时间 CACHE_MIDDLEWARE_KEY_PREFIX = "" # key的前缀
注意:这些参数仅仅用于控制全局缓存,如果没有设置,则按照cache定义的TIMOUT等参数定义的属性进行缓存。
PS:当三种类型同时使用时,那么优先级为:全局 优先于 视图 优先于 局部模版。
3 信号
还是由一个需求开始:如果我要把所有数据库的操作记录日志,那么应该该怎么办?其实我们都知道,对数据库的操作,归根结底都是数据库的save操作,如果我能把所有的save操作捕获,那么我就能记录所有的save操作,那么信号就油然而生了。
Django内部包含了一位“信号调度员”:当某事件在框架内发生时,它可以通知到我们的应用程序。 简而言之,当event(事件)发生时,signals(信号)允许若干 senders(寄件人)通知一组 receivers(接收者)。这在我们多个独立的应用代码对同一事件的发生都感兴趣时,特别有用。
个人理解,django的signal可理解为django内部的钩子,当一个事件发生时,其他程序可对其作出相关反应,可通过signal来回调定义好的处理函数(receivers),从而更大程度的解耦我们的系统。
django提供的信号
django内部帮我们定义了多组信号:
Model signals pre_init # django的model执行其构造方法前,自动触发 post_init # django的model执行其构造方法后,自动触发 pre_save # django的model对象保存前,自动触发 post_save # django的model对象保存后,自动触发 pre_delete # django的model对象删除前,自动触发 post_delete # django的model对象删除后,自动触发 m2m_changed # django的model中使用m2m字段操作第三张表(add,remove,clear)前后,自动触发 class_prepared # 程序启动时,检测已注册的app中model类,对于每一个类,自动触发 Management signals pre_migrate # 执行migrate命令前,自动触发 post_migrate # 执行migrate命令后,自动触发 Request/response signals request_started # 请求到来前,自动触发 request_finished # 请求结束后,自动触发 got_request_exception # 请求异常后,自动触发 Test signals setting_changed # 使用test测试修改配置文件时,自动触发 template_rendered # 使用test测试渲染模板时,自动触发 Database Wrappers connection_created # 创建数据库连接时,自动触发
使用信号
要使用信号,就需要在我们的程序中预先注册信号,注册信号是要在程序一开始运行就需要执行的,通过什么办法来让程序已启动就加载呢?没错,我们知道在程序启动的时候,__init__文件就会被执行,所以可以在项目同名的目录的__init__文件中导入(写好py文件,在__init__中 import)。
# 1、内置信号,使用时需要先行导入 from django.core.signals import request_finished from django.core.signals import request_started from django.core.signals import got_request_exception from django.db.models.signals import class_prepared from django.db.models.signals import pre_init, post_init from django.db.models.signals import pre_save, post_save from django.db.models.signals import pre_delete, post_delete from django.db.models.signals import m2m_changed from django.db.models.signals import pre_migrate, post_migrate from django.test.signals import setting_changed from django.test.signals import template_rendered from django.db.backends.signals import connection_created # 2、定义触发的函数 def callback(sender, **kwargs): print("xxoo_callback") print(sender,kwargs) # 参数: # sender:针对哪个类进行操作,触发信号的类 # kwargs:传递的其他参数,信号不同传递的参数不同。 # 3、注册我们自定义的函数,使得信号产生时,执行 XXX.connect(callback) # 这里的XXX,表示上面的各种信号
当某个信号被触发的时候,那么就会自动执行callback函数,我们可以利用该函数来记录日志,又或者在留言通知的场景下,当用户保存留言后,发送信号,对博主进行提示等。
注意:django的信号是同步的,如果信号是post_save,那么每次保存都会去执行对应的函数,所以在大批量任务时,斟酌使用。
自定义信号
虽然django给我们提供了很多信号,但是有时候我们需要对我们的业务进行定制化,举个例子:在监控的场景下,某些数据到达特定的阈值,我们就需要进行报警,那么我们就可以自定义一个报警的信号,用于处理报警后的动作,比如发短信、发微信等等。
使用自定义信号的三步:
- 创建信号
- 注册信号
- 触发信号
创建信号
通过导入django提供的类来创建自定义信号:
import django.dispatch pizza_done = django.dispatch.Signal(providing_args=["toppings", "size"]) # pizza_done 为信号的名称 # providing_args 表示信号在出发时需要传递的两个参数,可自定义。
注册信号
自定义信号使用,同样需要预先注册
# 定义触发函数 def callback(sender, **kwargs): print("callback") print(sender,kwargs) # 注册信号 pizza_done.connect(callback)
触发信号
由于内置信号的触发者已经集成到Django中,所以其会自动调用,而对于自定义信号则需要开发者在任意位置触发。
# 在views中引入我们的信号 from 路径 import pizza_done # 利用信号的send方法进行触发 pizza_done.send(sender='seven',toppings=123, size=456) # 为什么要利用send,可以查看内置信号的源码
记录操作数据库的SQL
# utils/signales.py 创建并注册信号 import django.dispatch mysingal = django.dispatch.Signal(providing_args=['name','action']) def loggin(sender,**kwargs): print(sender) # 这里可以调用logging模块记录日志。 print(kwargs) mysingal.connect(loggin) # views中触发信号 from utils import signales from django.db import connection # 用于查看数据库的操作语句 def add(request): abc=models.Test.objects.create(title='hello') sql = connection.queries signales.mysingal.send(sender=abc,name='daxin',action=sql) # 主动触发信号,并传递参数 return HttpResponse('ok')