在Django中使用缓存

快速开始

缓存是什么(What?)

缓存就是数据交换的缓冲区(称作Cache),是存储数据的临时地方。当用户查询数据,首先在缓存中寻找,如果找到了则直接执行。如果找不到,则去数据库查找。

缓存的本质就是用空间换时间,牺牲数据的实时性,以服务器内存中的数据暂时代替从数据库读取的数据,减少数据库IO,减轻服务器压力,减少网络延迟,加快页面打开速度。

存储介质访问速度比较 来自Google工程师Jeff Dean的分享,仅供参考:

存储介质 速度
L1 cache reference 读取CPU的一级缓存 0.5 ns
Branch mispredict(转移、分支预测) 5 ns
L2 cache reference 读取CPU的二级缓存 7 ns
Mutex lock/unlock 互斥锁\解锁 100 ns
Main memory reference 读取内存数据 100 ns
Compress 1K bytes with Zippy 1k字节压缩 10,000 ns
Send 2K bytes over 1 Gbps network 在1Gbps的网络上发送2k字节 20,000 ns
Read 1 MB sequentially from memory 从内存顺序读取1MB 250,000 ns
Round trip within same datacenter 从一个数据中心往返一次,ping一下 500,000 ns
Disk seek 磁盘搜索 10,000,000 ns
Read 1 MB sequentially from network从网络上顺序读取1兆的数据 10,000,000 ns
Read 1 MB sequentially from disk 从磁盘里面读出1MB 30,000,000 ns
Send packet CA->Netherlands->CA 一个包的一次远程访问 150,000,000 ns

访问流程:

1
2
3
4
5
6
7
graph TB
A(读操作_) --> B{查询缓存_}
B --> |有缓存_| C[返回_]

B --> |无缓存_| D[查询数据库_]

D --> E[放入缓存_]

缓存的优点:

  1. 减少了磁盘和网络IO来提高吞吐量,减少计算量(CPU计算)释放CPU,提高系统的响应速度。
  2. 面向切面的处理发出,可以在各层进行插拔,是所有性能优化的最简单有效的解决方案。

缓存应用场景(Where?)

  • 对于数据实时性要求不高
    对于一些经常访问但是很少改变的数据,读明显多于写,使用缓存就很有必要。比如一些网站配置项。

  • 对于性能要求高
    比如一些秒杀活动场景。

基于DRF快速开始(How?)

pip install drf-extensions

key值计算: {“view_instance”: “”, “view_method”: “”, “request”:””, “args”:””, “kwargs”: “”} –> json –> md5
\rest_framework_extensions\key_constructor\constructors.py

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# settings.py  
REST_FRAMEWORK_EXTENSIONS = {
'DEFAULT_OBJECT_CACHE_KEY_FUNC':
'rest_framework_extensions.utils.default_object_cache_key_func',
'DEFAULT_LIST_CACHE_KEY_FUNC':
'rest_framework_extensions.utils.default_list_cache_key_func',
'DEFAULT_CACHE_RESPONSE_TIMEOUT': 60 * 15,
'DEFAULT_CACHE_ERRORS': False
}


# views.py
# usage 1: don't overwirte list, retrieve method
from rest_framework_extensions.cache.mixins import CacheResponseMixin

class StudentViewSet(CacheResponseMixin, viewsets.ModelViewSet):
pass

# usage: 2
from rest_framework_extensions.cache.decorators import cache_response
from rest_framework_extensions.utils import default_object_cache_key_func

class StudentViewSet(CacheResponseMixin, viewsets.ModelViewSet):

@cache_response(key_func=default_object_cache_key_func, cache_errors=False)
def retrieve(self, request, *args, **kwargs):
pass
```

> python自带的缓存机制
```python
import timeit
from functools import lru_cache

# @lru_cache(None)
def fib(n):
if n < 2:
return n
return fib(n - 2) + fib(n - 1)


print(timeit.timeit(lambda: fib(35), number=1))

一、缓存类型

1. 数据库缓存

常用的缓存方案有memcached、redis等 。把经常要从数据库查询的数据,或经常更新的数据放入到缓存中。这样下次查询时,直接从缓存直接返回,减轻数据库压力,提升数据库性能。

2. 服务器端缓存

2.1 代理服务器缓存

代理服务器是浏览器和源服务器之间的中间服务器,浏览器先向这个中间服务器发起Web请求,经过处理后(比如权限验证,缓存匹配等),再将请求转发到源服务器。

代理服务器缓存的运作原理跟浏览器的运作原理差不多,只是规模更大。可以把它理解为一个共享缓存,不只为一个用户服务,一般为大量用户提供服务,因此在减少响应时间和带宽使用方面很有效,同一个副本会被重用多次。

2.2 CDN缓存

也叫网关缓存、反向代理缓存。CDN缓存一般是由网站管理员自己部署,为了让他们的网站更容易扩展并获得更好的性能。

浏览器先向CDN网关发起Web请求,网关服务器后面对应着一台或多台负载均衡源服务器,会根据它们的负载请求,动态将请求转发到合适的源服务器上。

虽然这种架构负载均衡源服务器之间的缓存没法共享,但却拥有更好的处扩展性。从浏览器角度来看,整个CDN就是一个源服务器。

2.3 DNS缓存

万维网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。DNS协议运行在UDP协议之上,使用端口号53。

有dns的地方,就有缓存。浏览器、操作系统、Local DNS、根域名服务器,它们都会对DNS结果做一定程度的缓存。

DNS查询过程如下:

  1. 首先搜索浏览器自身的DNS缓存,如果存在,则域名解析到此完成。
  2. 如果浏览器自身的缓存里面没有找到对应的条目,那么会尝试读取操作系统的hosts文件看是否存在对应的映射关系,如果存在,则域名解析到此完成。
  3. 如果本地hosts文件不存在映射关系,则查找本地DNS服务器(ISP服务器,或者自己手动设置的DNS服务器),如果存在,域名到此解析完成。
  4. 如果本地DNS服务器还没找到的话,它就会向根服务器发出请求,进行递归查询。

3. 浏览器缓存

浏览器缓存根据一套与服务器约定的规则进行工作,在同一个会话过程中会检查一次并确定缓存的副本足够新。如果在浏览过程中前进或后退时访问到同一个图片,这些图片可以从浏览器缓存中调出而即时显示。

4. web应用层缓存

应用层缓存指的是从代码层面上,通过代码逻辑和缓存策略,实现对数据、页面、图片等资源的缓存,可以根据实际情况选择将数据存在文件系统或者内存中,减少数据库查询或者读写瓶颈,提高响应效率。

二、缓存淘汰策略

1. FIFO

FIFO (First in First out), 先进先出。核心原则就是: 如果一个数据最先进入缓存中,则应最早淘汰掉。

2. LFU

LFU (Least Frequently Used),最不频繁使用,以使用次数作为参考。

核心思想:如果数据过去被访问多次,那么将来被访问的几率也更高

3. LRU

LRU(Least Recently Used),最近最少使用,以时间作为参考。

核心思想:如果数据最近被访问过,那么将来被访问的频率也更高

三、Django缓存系统

伪代码解释动态网站生成页面时,缓存是怎么工作的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
given a URL, try finding that page in the cache  
if the page is in the cache:
return the cached page
else:
generate the page
save the generated page in the cache (for next time)
return the generated page
```
## 设置缓存
### django-redis
- 更多详细配置参阅[官方文档](https://github.com/jazzband/django-redis)
- `pip install django-redis`

```python
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
# "LOCATION": "redis://username:password@localhost:6379/0"
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}

memcached

  • 完全基于内存的缓存服务器:

    是Django支持的最快,最高效的缓存类型。—> Facebook,Wikipedia都有使用其来减少数据库访问并显著提高网站性能
    缓存的数据存储在内存中,如果服务器崩溃,那么数据将会丢失。

  • pip install python-memached
  • pip install pylibmc
    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
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    # use python-memcached  
    CACHES = {
    'default': {
    'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
    'LOCATION': '127.0.0.1:11211',
    # 'LOCATION': 'unix:/tmp/memcached.sock',

    # 能在多个服务器上共享缓存,即无需再每台机器上复制缓存值
    # 'LOCATION': [
    # '172.19.26.240:11211',
    # '172.19.26.242:11211',
    # ]
    }
    }

    # use pylibmc
    CACHES = {
    'default': {
    'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
    'LOCATION': '/tmp/memcached.sock',
    }
    }
    ```
    ### 数据库缓存
    - 适用于有一个快速,索引正常的数据库服务器。
    - `python manage.py createcachetable`
    ```python
    CACHES = {
    'default': {
    'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
    'LOCATION': 'my_cache_table',
    }
    }
    ```

    ### 文件系统缓存
    - 一个缓存值为一个单独的文件
    - 注意指定目的写权限问题。
    ```python
    CACHES = {
    'default': {
    'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
    'LOCATION': '/var/tmp/django_cache',
    # 'LOCATION': 'c:/foo/bar',
    }
    }
    ```

    ### 本地内存缓存
    - 是默认的缓存方式
    - 使用LRU淘汰策略
    > 每个进程都有其自己的私有缓存实例,意味着不存在跨进程的缓存。
    > 也意味着本地缓存不是特别节省内存,不是生产环境的好选择,但在开发环境表现很好。
    ```python
    CACHES = {
    'default': {
    'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
    'LOCATION': 'unique-snowflake',
    }
    }
    ```

    ### 虚拟缓存(用于开发模式)
    - 只是实现了缓存接口,并不做其他操作
    - 如果你有一个正式网站在不同地方使用了重型缓存,但你不想在开发环境使用缓存时非常有用。
    ```python
    CACHES = {
    'default': {
    'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
    }
    }
    ```

    ### 缓存参数
    - **`TIMEOUT`**: 超时时间,默认为300秒。设置为`None`表示永不过时。
    - **`OPTIONS`**: 实现自有的淘汰策略的缓存后端(比如 `locmem`, `filesystem` 和 `database` 后端)将遵循以下选项
    -- **`MAX_ENTRIES`**: 允许缓存的最大条目, 默认为300
    -- **`CULL_FREQUENCY`**: 当达到最大条目时淘汰的条目数量,默认为3。比率为1 / CULL_FREQUENCY,为0是清空整个缓存。
    - **`KEY_PREFIX`**: Django 服务器使用的所有缓存键的字符串。
    - **`VERSION`**: 通过 Django 服务器生成的缓存键的默认版本号。
    - **`KEY_FUNCTION`**: 一个包含指向函数的路径的字符串,该函数定义将如何前缀、版本和键组成最终的缓存键。

    ## 站点缓存
    ```python
    MIDDLEWARE = [
    'django.middleware.cache.UpdateCacheMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.cache.FetchFromCacheMiddleware',
    ]
    ```

    ## 视图缓存
    > **cache_page**设置的缓存超时优先于Cache-Control头中的"max_age"
    > 和缓存站点一样,对试图缓存以URL为键。如果多个URL指向相同的试图,每个URL将被单独缓存。
    ```python
    from django.views.decorators.cache import cache_page

    @cache_page(60 * 15, cache="default", key_frefix="site1")
    def my_view(request):
    pass
    ```

    ## 底层缓存API
    > 以任意级别粒度在缓存中存储对象,如:模型对象的字符串、字典、列表,或者其他(pickle)。

    ### 访问缓存
    > 可通过`django.core.cache.caches`对象访问在CACHES配置的缓存。
    > 重复请求同一个线程里的同一个别名将返回同一个对象。

    ```python
    from django.core.cache import caches

    cache1 = caches['myalias']
    cache2 = caches['myalias']
    cache1 is cache2 # True

    作为快捷方式,默认缓存可以通过django.core.cache.cache引用。
    等价于caches['default']

基本用法

  • cache.set(key, value, timeout=DEFAULT_TIMEOUT, version=None)
  • cache.get(key, default=None, version=None)
  • cache.add(key, value, timeout=DEFAULT_TIMEOUT, version=None)
  • cache.get_or_set(key, default, timeout=DEFAULT_TIMEOUT, version=None)
  • cache.get_many(keys, version=None)
  • cache.set_many(dict, timeout)
  • cache.delete(key, version=None)
  • cache.delete_many(keys, version=None)
  • cache.clear()
  • cache.touch(key, timeout=DEFAULT_TIMEOUT, version=None)
1
2
3
4
5
6
7
8
# delete cache
from django.core.cache import cache
from django.utils.cache import get_cache_key

class StudentViewSet(CacheResponseMixin, BaseModelViewSet):

def update(self, request, *args, **kwargs):
cache.delete(get_cache_key(request))

下游缓存

使用Vary标头

四、常用的缓存组件

1. Memcache

详见另一篇笔记

启动命令:memcached -d -m 10m -p 11211 -u root
缓存过期策略:当内存容量达到指定值之后,就会基于LRU算法自动删除不使用的缓存。

2. Redis

详见另一篇笔记
缓存过期策略:

# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory  
# is reached. You can select one from the following behaviors:  
  
# volatile-lru -> Evict using approximated LRU, only keys with an expire set.  
# allkeys-lru -> Evict any key using approximated LRU.  
# volatile-lfu -> Evict using approximated LFU, only keys with an expire set.  
# allkeys-lfu -> Evict any key using approximated LFU.  
# volatile-random -> Remove a random key having an expire set.  
# allkeys-random -> Remove a random key, any key.  
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)  
# noeviction -> Don't evict anything, just return an error on write operations.  
#  
# LRU means Least Recently Used  
# LFU means Least Frequently Used  
#  
# Both LRU, LFU and volatile-ttl are implemented using approximated  
# randomized algorithms.  
#  
# Note: with any of the above policies, Redis will return an error on write  
# operations, when there are no suitable keys for eviction.  
#  
# At the date of writing these commands are: set setnx setex append  
# incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd  
# sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby  
# zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby  
# getset mset msetnx exec sort  
#  
# The default is:  
#  
# maxmemory-policy noeviction  

Redis过期机制参考

五、缓存带来的问题

1. 数据一致性

  • 产生原因:

    ①先删除缓存:在写数据库之前,如果有读请求发生,可能导致旧数据入缓存, 引发数据不一致。
    ②先修改数据库:

    a.在有缓存的情况下,两个并发的读写操作。写操作在删除缓存的时候,缓存删除失败,读操作此时读到的数据是老数据,引发数据不一致。(此处考虑是删除缓存还是更新缓存)
    b.在没有缓存的情况下,两个并发的读写操作。读操作没有及时的把数据放入缓存,写操作进来修改了数据库,删除了缓存,然后读操作恢复,把老数据写进了缓存。

  • 解决方案:

    延迟双删–>改进–>内存队列删除缓存(如果删除缓存失败,可以多次尝试)
    考虑到系统复杂度,一般情况下先修改数据库,后删除缓存就行。

2. 缓存击穿

  • 产生原因:

    针对某一key,该缓存在某一时间点过期的时候,刚好有对应这个key的大量并发请求过来,此时请求会直接走到数据库,可能回导致数据库崩溃。

  • 解决方案:

    **①使用互斥锁(mutex key)**:使用zookeeper或者Redis实现互斥锁,等待第一个请求创建完缓存之后才允许后续请求继续访问。
    **②”数据永不过期”**:在value的内部设置一个超时值(timeout1),timeout1比实际的超时时间小。当从缓存读取到timeout1发现其已经过期时,马上延迟timeout1并重新设置到缓存。然后再从数据库加载数据并这是到缓存中。

3. 缓存穿透

  • 产生原因:

    由于缓存是不命中时被动写的,且出于容错考虑,如果从数据库查不到数据就不写入缓存,当数据库中本来就不存在的数据一直被请求,在流量大时,数据库可能就会崩溃。

  • 解决方案:

    ①请求校验:对请求url进行校验,有可能是恶意攻击。
    ②使用布隆过滤器:将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层数据库的查询压力。
    ③对空结果进行缓存:如果查询一个数据返回为空(不管是数据不存在还是系统故障),仍然将这个空结果进行缓存,但需要设置过期时间。

4. 缓存雪崩

  • 产生原因:

    由于设置缓存时采用了相同的过期时间(或者服务器宕机),导致缓存在某一时刻同时失效,请求全部转发到数据库,数据库瞬间压力过重而导致崩溃。

  • 解决方案:

    ①将过期时间分散:在过期时间后面加上一个随机数,让key均匀的失效。
    ②使用队列或锁控制:用队列或者锁让程序执行在压力范围之内,当然这种方案可能会影响并发量。
    ③配置redis高可用,服务降级,缓存数据持久化