一、缓存
Django为我们提供了5种缓存机制:
1)开发调试用的缓存(什么都不干)
2)内存缓存
3)文件缓存
4)数据库缓存
5)memcache缓存
1.通用配置
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': 'func_name' # 生成key的函数(默认函数会生成为:【前缀:版本:key】,默认函数名为 default_key_func) } }
2.开发调试缓存
CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', # 开发调试缓存 引擎 #通用配置放这里 } }
3.内存缓存
CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', # 内存缓存引擎 'LOCATION': 'unique-snowflake', # 保证命名唯一,因为内存缓存就是一个全局变量 #通用配置放这里 } }
4.文件缓存
CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', # 文件缓存引擎 'LOCATION': os.path.join(BASE_DIR,'cache'), # 文件存放位置 #通用配置放这里 } }
5.数据库缓存
CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.db.DatabaseCache', #数据库缓存引擎 'LOCATION': 'my_cache_table', # 使用的数据库表 #通用配置放这里 } }
使用数据库缓存之前,要执行命令先创建一张缓存表:
python manage.py createcachetable
6.Memcache缓存
# 使用单台memcache CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 'LOCATION': '127.0.0.1:11211', } } # 使用本地memcache文件 CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 'LOCATION': 'unix:/tmp/memcached.sock', } } # 使用简单集群 CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', 'LOCATION': [ '172.19.26.240:11211', # 如果带权重,则加上权重 ('172.19.26.240:11211',5), '172.19.26.242:11211', # ('172.19.26.242:11211',15), ] } }
memcache的引擎除了有MemcachedCache(这个使用的是python-memcached模块),还可以使用PyLibMCCache引擎(这个使用的是pylibmc模块)。
二、缓存的三种应用
Django提供了以上所述的几种缓存,但是缓存到底缓存什么东西?
1.缓存视图函数(页面缓存)
对视图函数的结果进行一定时间的缓存,当在这个时间段内,用户再次请求这个页面,Django会直接从缓存中获取数据并返回。
例如我们写一个简单页面,显示当前时间:
html页面:
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CTimetitle>
head>
<body>
<h1>{{ ctime }}h1>
body>
html>
视图函数
def cache(request): import time ctime = time.time() return render(request, 'cache.html', {'ctime': ctime})
urls.py
from django.contrib import admin from django.urls import path, re_path, include from mgmt import views urlpatterns = [ path('admin/', admin.site.urls), re_path('login', views.login), re_path('index', views.index), re_path('logout', views.logout), re_path('cache', views.cache), ]
未使用缓存时的效果:
我们每次请求该页面,时间都会发生变化。
当我们为视图函数加上装饰器后:
from django.views.decorators.cache import cache_page @cache_page(10) def cache(request): import time ctime = time.time() return render(request, 'cache.html', {'ctime': ctime})
在加上这个装饰器后,表示对该视图函数的返回结果进行缓存,缓存时间为10s。
使用缓存的效果:
首次请求后,返回的结果会缓存10秒,10秒内再次访问页面的话,时间不会变化。10秒过去后,再请求,时间会重新生成,然后又放入缓存。如此反复。
如果视图函数有参数(例如分页),则Django会分别做缓存:
from django.views.decorators.cache import cache_page @cache_page(10) def cache(request,page_num): import time ctime = time.time() return render(request, 'cache.html', {'page_num':page_num,'ctime': ctime})
效果:
这两页面的数据分别缓存10s,互不影响。
2.局部缓存(细粒度缓存)
局部缓存就是指在一个页面中,某些部分的数据使用缓存中的数据,而其他数据由视图函数生成。
主要原理是在render的时候,对模板中模板语言标记的替换部分进行数据替换时,对指定使用缓存的部分用缓存数据进行替换,而不是用视图函数获取的新数据。
例如。使用视图函数动态生成的当前时间替换模板中的多个位置:
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CTimetitle>
head>
<body>
<h1>{{ page_num }}h1>
<h2>{{ ctime }}h2>
<h2>{{ ctime }}h2>
<h2>{{ ctime }}h2>
body>
html>
从上述HTML代码中可以看出,我们需要用ctime(当前时间)替换三个部分。
如果使用页面缓存(即@cache_page装饰器),则页面上的三个时间会同步缓存和同步更新。
使用局部缓存:
{% load cache %} DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>CTimetitle> head> <body> <h1>{{ page_num }}h1> <h2>{{ ctime }}h2> <h2>{{ ctime }}h2> {% cache 10 c1 %} <h2>{{ ctime }}h2> {% endcache %} body> html>
{% calue 10 c1 %}中,10表示缓存10秒,c1表示cache_id,会和 KEY_PREFIX以及VERSION拼接生成真正的key。
注意,同时使用局部缓存的时候,要注意和页面缓存之间的影响。
3.全站缓存(缓存所有数据)
全站缓存是对用户所请求的所有数据进行缓存,既然是对全部的response数据进行缓存,则直接使用中间件处理就可以了。
在settings.py中添加两个中间件配置:
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', ]
这两个中间件分别对response进行缓存存储和获取,如下图所示:
查看UpdateCacheMiddleware的源码:
class UpdateCacheMiddleware(MiddlewareMixin): """ Response-phase cache middleware that updates the cache if the response is cacheable. Must be used as part of the two-part update/fetch cache middleware. UpdateCacheMiddleware must be the first piece of middleware in MIDDLEWARE so that it'll get called last during the response phase. """ def __init__(self, get_response=None): self.cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX self.cache_alias = settings.CACHE_MIDDLEWARE_ALIAS self.cache = caches[self.cache_alias] self.get_response = get_response def _should_update_cache(self, request, response): return hasattr(request, '_cache_update_cache') and request._cache_update_cache def process_response(self, request, response): """Set the cache, if needed.""" if not self._should_update_cache(request, response): # We don't need to update the cache, just return. return response if response.streaming or response.status_code not in (200, 304): return response # Don't cache responses that set a user-specific (and maybe security # sensitive) cookie in response to a cookie-less request. if not request.COOKIES and response.cookies and has_vary_header(response, 'Cookie'): return response # Don't cache a response with 'Cache-Control: private' if 'private' in response.get('Cache-Control', ()): return response # Try to get the timeout from the "max-age" section of the "Cache- # Control" header before reverting to using the default cache_timeout # length. timeout = get_max_age(response) if timeout is None: timeout = self.cache_timeout elif timeout == 0: # max-age was set to 0, don't bother caching. return response patch_response_headers(response, timeout) if timeout and response.status_code == 200: cache_key = learn_cache_key(request, response, timeout, self.key_prefix, cache=self.cache) if hasattr(response, 'render') and callable(response.render): response.add_post_render_callback( lambda r: self.cache.set(cache_key, r, timeout) ) else: self.cache.set(cache_key, response, timeout) return response
可以看到,UpdateCacheMiddleware的源码中只有process_response函数,该函数判断response是否有缓存,没有则缓存,有的话就直接返回给用户。
查看FetchFromCacheMiddleware的源码:
class FetchFromCacheMiddleware(MiddlewareMixin): """ Request-phase cache middleware that fetches a page from the cache. Must be used as part of the two-part update/fetch cache middleware. FetchFromCacheMiddleware must be the last piece of middleware in MIDDLEWARE so that it'll get called last during the request phase. """ def __init__(self, get_response=None): self.key_prefix = settings.CACHE_MIDDLEWARE_KEY_PREFIX self.cache_alias = settings.CACHE_MIDDLEWARE_ALIAS self.cache = caches[self.cache_alias] self.get_response = get_response def process_request(self, request): """ Check whether the page is already cached and return the cached version if available. """ if request.method not in ('GET', 'HEAD'): request._cache_update_cache = False return None # Don't bother checking the cache. # try and get the cached GET response cache_key = get_cache_key(request, self.key_prefix, 'GET', cache=self.cache) if cache_key is None: request._cache_update_cache = True return None # No cache information available, need to rebuild. response = self.cache.get(cache_key) # if it wasn't found and we are looking for a HEAD, try looking just for that if response is None and request.method == 'HEAD': cache_key = get_cache_key(request, self.key_prefix, 'HEAD', cache=self.cache) response = self.cache.get(cache_key) if response is None: request._cache_update_cache = True return None # No cache information available, need to rebuild. # hit, return cached response request._cache_update_cache = False return response
可以看到,FetchFromCacheMiddleware的源码中只有process_request函数,该函数检查缓存中是否有对应的response数据,如果有则无需调用视图函数处理,直接拿缓存中的数据返回。如果没有,则将请求交给视图函数处理。
4.三个级别缓存的优先级
实际上Django提供的三个级别的缓存:全站缓存、页面缓存、局部缓存,下面讨论一下他们的优先级关系。
实验现象:
当全站缓存、页面缓存共存时,页面缓存生效。
当全站缓存、局部缓存共存时,全站缓存生效,局部缓存不生效(因为局部缓存生效的条件是视图函数被执行)。
当全站缓存、页面缓存、局部缓存三者共存时,页面缓存和局部缓存生效。
当页面缓存、局部缓存共存时,局部缓存时间大于页面缓存时间时,可以看出效果,否则看不出效果。
总结:我们只需要关心全站缓存和页面缓存是否同时启用。页面缓存优先级更高。而局部缓存是在页面缓存生效的基础上才生效。
三、信号
在Django框架中,如果我们想在某些操作的前后做一些自定义操作(例如在插入数据库的前后写日志),则可以使用Django为我们提供的信号。
如果不使用信号,我们可能会使用装饰器来进行处理,但是不是特别好,因为可能有很多地方需要添加装饰器,导致代码很凌乱。
Django为我们提供了很多种信号,所谓信号就是Django在执行某些操作的前后给我们预留的可以自定义操作的钩子。
1.Django提供的信号种类
Django为我们提供了以下信号:
Model signals pre_init # django的modal执行其构造方法前,自动触发 post_init # django的modal执行其构造方法后,自动触发 pre_save # django的modal对象保存前,自动触发 post_save # django的modal对象保存后,自动触发 pre_delete # django的modal对象删除前,自动触发 post_delete # django的modal对象删除后,自动触发 m2m_changed # django的modal中使用m2m字段操作第三张表(add,remove,clear)前后,自动触发 class_prepared # 程序启动时,检测已注册的app中modal类,对于每一个类,自动触发 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 # 创建数据库连接时,自动触发
我们以pre_init信号为例,这个信号是在model执行构造函数之前触发的,也就是在我们生成一条数据库记录之前(还未save),即obj = models.TB(username='leo') 之前。
触发这个信号可以执行我们为其绑定的自定义函数(所以称之为钩子)。
2.为信号绑定自定义函数
我们用以下方式为信号绑定自定义函数:(创建一个.py文件,例如sg.py)
#sg.py 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 def callback(sender, **kwargs): print("pre_init_callback") print(sender, kwargs) pre_init.connect(callback)
首先导入信号,然后使用信号名.connect(call_back_func)来绑定自定义函数(可以绑定多个函数,触发时会按顺序执行)。
最后,需要在Django中导入该py文件:(可以选择在工程目录的__init__.py中,因为Django程序开始运行时就会运行__init__.py)
# Django程序目录/__init__.py import sg
这样,Django程序一开始运行就会导入sg,从而为信号绑定自定义函数。
至此以后,我们每次在插入数据库之前都会执行一次callback函数。
callback函数中的参数:
def callback(sender, **kwargs): print("pre_init_callback") print(sender, kwargs)
当Django触发信号时,调用我们的自定义函数callback,并且会为我们传入一系列参数,这些参数包含我们所需的所有数据(例如model构造函数相关的数据),我们拿到这些数据就可以写到日志中(或者做其他操作)。
3.信号是怎么触发的
Django中信号是怎么被触发的?
我们看一段Django-ORM的源码:(from django.db.models import Model)
在Model类的源码中,有一个save方法,还有一个save_base方法。我们主要看save_base方法(省略了多余部分):
def save_base(self, raw=False, force_insert=False, force_update=False, using=None, update_fields=None): #...... if not meta.auto_created: pre_save.send( sender=origin, instance=self, raw=raw, using=using, update_fields=update_fields, ) #......
我们可以看到,在save_base方法中,触发了一个pre_save信号,触发的方法是 信号名.send(),而send方法的一系列参数,就是传递给我们的callback(自定义函数)的,在callback函数中用sender和**kwargs来接收。
所以,信号的触发方式其实很简单,就是 信号.send()。那么在后面我们自定义信号的时候,也可以使用这样的方法来触发。
四、自定义信号
Django虽然为我们提供了十多个常用的信号(钩子),但也可能不能满足我们多样的业务需求,所以我们可能需要自定义信号。
Django提供的预定义信号,我们只需要为其绑定自定义函数即可,使用很方便。自定义信号也差不多。
1.自定义信号
import django.dispatch my_signal = django.dispatch.Signal(providing_args=['arg1', 'arg2'])
自定义信号很简单,就是创建一个Signal对象。provideing_args这个参数是一个列表,里面的元素是发送该信号需要传入的参数名(不包含sender参数)。
2.为自定义信号绑定函数
def my_callback(sender, **kwargs): request = kwargs['arg1'] print(request) page_num = kwargs['arg2'] print("用户请求的页数是:%s" % page_num) my_signal.connect(my_callback)
3.触发自定义信号
我们以cache视图函数为例,用户请求/cache/1页面,触发信号,并将request和page_num作为参数传递给自定义函数。
def cache(request, page_num): # 导入自定义信号 from sg import my_signal # 触发自定义信号 my_signal.send(sender=None, arg1=request, arg2=page_num) import time ctime = time.time() return render(request, 'cache.html', {'page_num': page_num, 'ctime': ctime})
关于参数sender,理论上应该传递信号发送方的信息,例如对象、类、方法,但这个参数我们有可以随意传递任何信息,例如传递一个发送方函数名,方便日志记录。
自定义信号方便简单,我们可以灵活的应用在某些我们想做固定处理的地方。感觉很好用。。。
五、Django的Form组件
在前面的章节中,我们如果要使用Form,都是在html上写上