目录
1.缓存的三种模式
1.1 Cache Aside
缓存命中模式。
读操作:试着从缓存读取数据,如果超时或未命中,则从数据库查询数据,并同步到缓存中。如果命中,直接从缓存读取数据。
写操作:直接更新数据库数据,并让缓存失效。
问题:更新频繁的场景下会导致缓存频繁的被删除,降低了缓存的作用。
1.2 Read/Write Through
读操作:与Cache Aside一致。
写操作:先看缓存是否命中,如果命中,更新缓存,再由缓存组件同步更新到数据库中。如果未命中,直接更新数据库。
特点:以缓存为操作为主,数据存先存在于缓存,缓存的数据是不会过期的
1.3 Write Behind
当应用系统对缓存中的数据进行更新时,缓存系统会在指定的时间后向底层数据源更新数据。不管缓存是否命中,所有数据都交由缓存进行持久层(数据库)的管理操作。对数据进行数据持久化存储回写时一般采用异步回写。但是异步或间隔一定时间的批量回写会导致数据延迟或数据丢失的情形出现。
2.缓存问题
2.1 缓存穿透
前面说过,有可能缓存存在未命中的情况,那么会直接访问ORM组件从数据库都要去数据。如果有恶意请求会极端情况,短时间内有大量的请求没有命中缓存,那么数据库服务器压力骤升,容易引发系统故障。
解决方法可以通过请求过滤或空值缓存替换。
(1)请求过滤
一般使用布隆过滤器(Bloom Filter),过滤器底层通过哈希算法对请求进行指纹加密并生成一个bitmap。针对服务器上的可能出现的所有查询请求,生成一个bitmap并保存。这样请求先通过布隆过滤器进行过滤,如果请求没在过滤器则直接屏蔽。
(2)空值缓存替换
对于失效请求,临时分配一个缓存key并将空值存在缓存中,这样短时间没收到的各种攻击请求就可以通过空值缓存替换的方式进行过滤。过但是如果出现大量的空值缓存,则不利于缓存组件的处理,所以一般给空值缓存设置较短的过期时间。
2.2 缓存雪崩
缓存雪崩是由于业务操作带来的:缓存设置了失效时间,如果某个时刻或时间段大量缓存同时失效,则会造成数据库压力,尤其在多线程高并发的场景中。
解决方法:
(1)多线程构建互斥环境
在业务数据缓存操作过程中,同一时间只允许一个线程更新缓存数据。其他线程处于等待。
(2)失效时间交错
明确设置不同业务模块的缓存失效时间,让不同核心模块的失效时间交错。
2.3 缓存击穿
缓存击穿是缓存雪崩的一种特殊情况。一些热点业务,其访问量非常大,一旦缓存失效,数据库压力就很大。
确定缓存击穿关键点:如何确定热点业务、如何确定缓存是否失效、如何确定缓存失效时间。
目前比较成熟的解决方案是将服务器接受到的业务请求进行分类计数存储,根据请求次数来区分什么业务的数据应该更久的缓存,比较成熟的就是LRU-K算法。
3.Django中的缓存
3.1 缓存方式
3.1.1 基于开发的虚拟缓存
Django框架内置了一个虚拟缓存,就是只实现了缓存接口,并不缓存任何数据。如果在开发环境中需要访问缓存接口实现功能流程,但是并不希望真实的缓存数据,就可以使用。
在项目中设置虚拟缓存:配置文件中添加CACHES,如下:
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.dummy.DummyCache",
}
}
3.1.2 基于本地内存的缓存
如果没有Memcached或redis这样高性能的缓存组件,可以求其次使用Django框架提供的基于本地内存的缓存。
配置如下:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'unique-snowflake'
}
}
BACKEND:指定使用基于本地内存的缓存支持;
LOCATION:缓存的空间命名,方便区分存储不同数据的缓存空间。只有一个locmem缓存的情况下,可以省略。
3.1.3 基于文件的缓存
将缓存的数据序列化到独立的文件中,通过LOCATION配置存储位置,必须是绝对位置,且要指定文件路径的读写权限。
以Unix/Linux系统为例,配置如下:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
# 指定缓存路径
'LOCATION': '/var/tmp/django_cache',
}
}
3.1.4 基于数据库的缓存
如果服务器上有提个数据索引良好,数据存取快速的数据库,则可以使用这个数据库作为缓存组件。
配置如下:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
'LOCATION': 'my_cache_table',
}
}
LOCATION配置指定在数据库中使用缓存的数据表名称。
接下来进行缓存表的迁移:
# 查看缓存表的创建语句
python manage.py createcachetable --dry-run
# 创建缓存数据表
python manage.py createcachetable
3.1.5基于Memcached的缓存
Memcached是一个高性能的分布式内存对象缓存系统,主要作为Web应用网站的缓存组件。使用Memcached作为缓存,是Django框架内置的最快最有效的缓存操作方式。
Memcached作为守护进程来运行,并分配了指定大小的内存空间。缓存模块要做的就是提供一个快速接口,用于在缓存中添加、检索和删除数据。所有的数据都被直接存储在内存中,因此不会产生数据库或文件系统的使用开销。
Django使用Memcached,首先要安装Memcached,使用python-memcached实现缓存绑定,配置如下:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
'LOCATION': '127.0.0.1:11211',
}
}
还可以使用pylibmc实现缓存绑定:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
'LOCATION': '127.0.0.1:11211',
}
}
还可以通过socket套接字文件进行配置:
CACHES = {
'default': {
# 基于python-memcached的缓存设置
'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
'LOCATION': 'unix:/tmp/memcached.sock',
# 基于pylibmc的缓存设置
#'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
# 'LOCATION': '/tmp/memcached.sock',
}
}
Memcached是一个分布式内存对象缓存系统的实现,与其他分布式部署的缓存最大的区别是,Memcached实现的分布式缓存系统不需要在每台计算机上都复制缓存数据,而是仅需要将添加了Memcached的多台主机地址包含在LOCATION配置中,可以分号或逗号分隔,也可以是包含IP:HOST主机值的列表(推荐)。配置如下:
CACHES = {
'default': {
# 基于python-memcached的缓存设置
'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
'LOCATION': [
'172.19.26.240:11211',
'172.19.26.242:11212',
'172.19.26.244:11213',
]
}
}
3.1.6基于Redis的缓存
使用redis先安装django-redis:
pip install django-redis
配置如下:
# django-redis缓存支持
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
# 无验证就是:redis://127.0.0.1:6379
# 仅密码验证就是:redis://:password@127.0.0.1:6379
# 完全验证就是:redis://username:password@127.0.0.1:6379
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
# 密码也可以写在这里
'PASSWORD': '123456',
}
}
}
3.2 数据缓存操作
3.2.1 站点缓存
以上面redis方式缓存为例。
接下来是常规数据缓存配置,Django提供了最简单的一种应用模式:缓存整个站点。需要将两个缓存中间件添加到配置文件指定位置,配置如下:
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',
]
说明:
UpdateCacheMiddleware
此中间件会在每个 HttpResponse 中自动设置一些 headers:
对于一个页面的新版本(未被缓存过)的请求,设置 Last-Modified header 为当前日期/时间;
设置 Expires header 为当前时期/时间 + 定义的CACHE_MIDDLEWARE_SECONDS
设置 Cache-Control header 的 max age 指令为 CACHE_MIDDLEWARE_SECONDS (max age是指缓存的内容将在多少秒后失效)
FetchFromCacheMiddleware
会缓存 GET 和 HEAD 200 状态的 responses,而且这些 request 和 response 的headers 是允许的;
同一个 URL request, 不同查询参数,他的 Responses 会被作为不同 page 分别缓存;
这个中间件期待用具有相同 response headers 的 GET request 的response 进行响应,这样它就可以用缓存的 GET response 响应 HEAD request。
然后还需要在配置文件中添加如下配置
# 指定存储缓存数据的缓存别名,可以不写
CACHE_MIDDLEWARE_ALIAS = 'my_cache'
# 设置缓存时间
CACHE_MIDDLEWARE_SECONDS = 300
# 指定缓存前缀,使用默认配置方式,可以不写
CACHE_MIDDLEWARE_KEY_PREFIX = 'cys'
在Django中,默认缓存响应码为200的GET和HEAD请求,并且对相同的URL请求进行单独的缓存,通常在响应头中通过Expires或Cache-Control中的max-age进行设置。
3.2.2 视图缓存
Django提供了django.views.decorators.cache.cache_page()装饰器,用于缓存单个视图的响应输出。相比站点缓存,控制粒度更细,也是开发中用的较多的缓存处理方式。
如果视图缓存和站点缓存两个都有,视图缓存会覆盖站点缓存设置。
代码如下:
@cache_page(60 * 10) # 为index函数设置缓存时间,时间为十分钟
@gzip_page
def index(request):
...
默认情况下,使用CACHES配置中的default指定缓存配置,也可以通过参数指定:
# 为index函数设置缓存时间,时间为十分钟,并将数据存储到my_cache缓存中
@cache_page(60 * 10, cache='my_cache')
@gzip_page
def index(request):
...
还有一种配置方式,就是在路由中:
urlpatterns = [
path('', cache_page(10 *60)(views.index), name='index'),
]
3.2.3 模板缓存
基于模板进行缓存,是指通过模板标签对网页视图中的数据进行缓存,提高网页视图的响应性能。
通过{% load cache %}标签色红孩子缓存。代码如下:
{% load cache %}
{% cache 500 sidebar %}
...
{% endcache %}
第一个参数设置缓存时间,如果为None则为永不过时;第二个参数为当前缓存设置名称。
另外,可以设置添加缓存条件,将缓存数据和指定变量绑定,当变量存在或不为None时缓存指定数据,如下:
{% load cache %}
{% cache 500 sidebar request.user.username %}
...
{% endcache %}
3.2.4 低级缓存
Django提供了一种低级缓存的底层操作方式,即包django.core.cache的caches和cache对象,用于针对某个细粒度的数据进行缓存操作。
(1)缓存配置管理
django.core.cache.caches提供了一个类似字典的缓存对象,可以在代码中直接获取项目的缓存配置信息。
from django.core.cache import caches
# 获取默认的default缓存设置
cache1 = caches['default']
print(cache1)
# <django_redis.cache.RedisCache object at 0x7fba40944f10>
cache1.__dict__
# {'default_timeout': 300, '_max_entries': 300, '_cull_frequency': 3, 'key_prefix': '', 'version': 1, 'key_func': <function default_key_func at 0x7fba3017e8b0>, '_server': 'redis://127.0.0.1:6379/1', '_params': {'OPTIONS': {'CLIENT_CLASS': 'django_redis.client.DefaultClient', 'PASSWORD': '123456'}}, '_client_cls': <class 'django_redis.client.default.DefaultClient'>, '_client': None, '_ignore_exceptions': False, '_log_ignored_exceptions': False, 'logger': None}
(2)缓存的存取
通过django.core.cache.cache来完成数据存取:
from django.core.cache import cache
# 存数据,第是哪个参数为超时时间,不设置默认为框架中CACHES配置的默认值,None表示永久缓存,0表示不缓存
cache.set('my_key', 'hello word', 30)
True
# 取数据
cache.get('my_key')
'hello word'
注意如果使用get获取值,没有命中返回None,所以不推荐将None作为缓存数据,否则导致缓存业务判断出问题。
(3)缓存数据添加
上面使用set犯法,如果key存在,会覆盖原来的值。如果使用add方法,则是key存在,不更新任何数据。
from django.core.cache import cache
cache.add('my_key', 'hello word')
True
# 取数据
cache.get('my_key')
'hello word'
(4)缓存数据调配
cache.get_or_set方法,获取指定key的值,如果没有,则把数据添加到缓存中。
cache.get_or_set('my_new_key', 'my new value', 100)
'my new value'
(5)多条缓存数据的存取
通过cache.set_many()和cache.get_many()完成。
cache.set_many({'a': 1, 'b': 2, 'c': 3})
# 如果某个key不存在,则改key不返回数据
cache.get_many(['a', 'b', 'c', 'd'])
# OrderedDict([('a', 1), ('b', 2), ('c', 3)])
(6)缓存超时更新
可以单独更新某个缓存的超时时间,使用cache.touch(),如下:
cache.touch('a', 10)
True
(7)缓存清理
删除缓存,可以使用以下方法:
- cache.delete(key)
- cache.delete_many([k1, k2, …]):清除多个缓存
- cache.clear():清除所有缓存
3.2.5 Vary header
在默认情况下,Django针对用户发起的URL进行缓存,用户在访问过程中不论浏览器是否存在cookie差异或者本地语言环境差异,对改页面的缓存是相同版本的,无法满足业务需求。因此,可以通过Vary注解方式告知缓存机制根据什么样的请求头进行数据缓存。
如下:
from django.views.decorators.vary import vary_on_headers
......
@vary_on_headers('User-Agent')
def my_view(request):
pass
这样,视图函数就会根据唯一的 User-Agent为每个用户生成单独的缓存数据。
另外,还可以根据Cookie对象进行缓存,如下:
from django.views.decorators.vary import vary_on_headers,vary_on_cookie
......
@vary_on_headers('Cookie')
def my_view(request):
pass
# 或者
@vary_on_cookie
def my_view(request):
pass
针对数据安全问题,数据一旦被缓存,就意味着出现访问共享,通常下游缓存会通过设置header参数为public或private控制缓存是共有还是私有,代码如下:
from django.views.decorators.cache import cache_control
@cache_control(private=True)
def my_view(request):
pass
在视图处理函数中根据某些数据状态的不同,控制返回的网页视图是公共缓存还是私有缓存,可通过
from django.views.decorators.cache import patch_cache_control
from django.views.decorators.vary import vary_on_headers,vary_on_cookie
@vary_on_cookie
def list_blog_entries_view(request):
if request.user.is_anonymous:
response = render_only_public_entries()
# 控制输出 -- 公共缓存
patch_cache_control(response, public=True)
else:
response = render_private_public_entries()
# 控制输出 -- 公共缓存
patch_cache_control(response, private=True)
return response
4.项目实战
本节选用主流的redis作为缓存中间件。首先通过docker来搭建好redis环境。
4.1 缓存配置
在配置文件在增加如下缓存支持配置:
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
# 密码
'PASSWORD': '123456',
}
}
}
上面是通过django_redis.cache.RedisCache类型的对象做缓存后端实现;通过LOCATION连接索引号为1的redis数据库;并设置了访问密码。
配置好后下面根据自己需求进行不同的缓存,而不是全站缓存。
4.2 数据缓存操作
还是以以前我们的博客项目为例,在子项目articles中,增加一个服务模块services.py,在该文件定义各种针对项目数据的服务功能支持,如缓存服务。
这个缓存服务主要实现两个目的:命中缓存时查询数据和数据出现动态变化时同步缓存数据。代码如下:
"""
数据服务模块:
主要针对数据的查询业务进行缓存等中间服务处理的模块
"""
# 引入低级缓存模块
from django.core.cache import cache
from .models import Author
def get_authors(flag=False):
"""
获取所有用户数据
:param flag 是否主动刷新缓存
默认:否 正常查询数据不需要刷新缓存
如果出现增删改数据,就需要主动刷新缓存数据以保证数据的同步
"""
# 从缓存中获取数据
print("从缓冲中提取author作者所有数据")
authors = cache.get("authors")
# 缓存中不存在数据,就从ORM中查询获取数据
if authors or flag:
print("ORM从数据库中提取数据")
authors = Author.objects.all()
# 设置同步缓存数据
cache.set("authors", authors)
# 返回查询到的数据
return authors
使用:
1.查询
在正常进行查询操作时,可以直接调用get_authors函数从缓存获取数据,如果从缓存中提取数据失败,则使用ORM操作从数据库提取数据并同步缓存。
例如加载首页时,获取所有作者的数据,代码如下:
@cache_page(60 * 30)
@gzip_page
def index(request):
# 查询所有文章,展示到页面中
articles = Article.objects.all()
# 查询所有的专题,展示到页面中
article_subjects = ArticleSubject.objects.all()
# 从缓存查询所有的用户,展示到页面中
authors = get_authors()
# 页面友情链接
link_friends = LinkFriend.objects.all()
# 快捷渲染方式
return render(request, 'index.html', {'authors': authors,
'articles': articles,
'article_subjects': article_subjects,
'link_friends': link_friends,
'f_index': 'active', 'f_main': 'none', 'f_message': 'none', 'f_article': 'none'})
2.更新
如果进行的是作者的增删改操作,那么数据处理完成后调用get_authors(flag=True)来刷新缓存。
例如用户注册时,注册完成会增加一个作者,这是就需要更新缓存,代码如下:
@require_http_methods(['GET', 'POST'])
def author_register(request):
'''作者注册'''
# 判断请求方式
if request.method == "GET":
author_register_form = AuthorRegisterForm(auto_id='reg_%s')
return render(request, 'author/register.html', {'author_register_form': author_register_form})
elif request.method == "POST":
# 获取前端页面中传递的数据
author_register_form = AuthorRegisterForm(request.POST)
if author_register_form.is_valid():
author_register_form.save()
# 创建并保存用户扩展资料对象
authorprofile = AuthorProfile(author=author_register_form.instance)
authorprofile.save()
else:
return render(request, 'author/register.html', {'author_register_form': author_register_form})
# 刷新缓存数据
get_authors(flag=True)
# 返回登录页面
return render(request,'author/login.html', {'msg_code': 0, 'msg_info': '账号注册成功'})
这样由于缓存存在,首页的加载就会更快。