快速开始
缓存是什么(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 | graph TB |
缓存的优点:
- 减少了磁盘和网络IO来提高吞吐量,减少计算量(CPU计算)释放CPU,提高系统的响应速度。
- 面向切面的处理发出,可以在各层进行插拔,是所有性能优化的最简单有效的解决方案。
缓存应用场景(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 | # settings.py |
一、缓存类型
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查询过程如下:
- 首先搜索浏览器自身的DNS缓存,如果存在,则域名解析到此完成。
- 如果浏览器自身的缓存里面没有找到对应的条目,那么会尝试读取操作系统的hosts文件看是否存在对应的映射关系,如果存在,则域名解析到此完成。
- 如果本地hosts文件不存在映射关系,则查找本地DNS服务器(ISP服务器,或者自己手动设置的DNS服务器),如果存在,域名到此解析完成。
- 如果本地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 | given a URL, try finding that page in the cache |
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
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 | # delete cache |
下游缓存
略
使用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
五、缓存带来的问题
1. 数据一致性
- 产生原因:
①先删除缓存:在写数据库之前,如果有读请求发生,可能导致旧数据入缓存, 引发数据不一致。
②先修改数据库:a.在有缓存的情况下,两个并发的读写操作。写操作在删除缓存的时候,缓存删除失败,读操作此时读到的数据是老数据,引发数据不一致。(此处考虑是删除缓存还是更新缓存)
b.在没有缓存的情况下,两个并发的读写操作。读操作没有及时的把数据放入缓存,写操作进来修改了数据库,删除了缓存,然后读操作恢复,把老数据写进了缓存。 - 解决方案:
延迟双删–>改进–>内存队列删除缓存(如果删除缓存失败,可以多次尝试)
考虑到系统复杂度,一般情况下先修改数据库,后删除缓存就行。
2. 缓存击穿
- 产生原因:
针对某一key,该缓存在某一时间点过期的时候,刚好有对应这个key的大量并发请求过来,此时请求会直接走到数据库,可能回导致数据库崩溃。
- 解决方案:
**①使用互斥锁(mutex key)**:使用zookeeper或者Redis实现互斥锁,等待第一个请求创建完缓存之后才允许后续请求继续访问。
**②”数据永不过期”**:在value的内部设置一个超时值(timeout1),timeout1比实际的超时时间小。当从缓存读取到timeout1发现其已经过期时,马上延迟timeout1并重新设置到缓存。然后再从数据库加载数据并这是到缓存中。
3. 缓存穿透
- 产生原因:
由于缓存是不命中时被动写的,且出于容错考虑,如果从数据库查不到数据就不写入缓存,当数据库中本来就不存在的数据一直被请求,在流量大时,数据库可能就会崩溃。
- 解决方案:
①请求校验:对请求url进行校验,有可能是恶意攻击。
②使用布隆过滤器:将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层数据库的查询压力。
③对空结果进行缓存:如果查询一个数据返回为空(不管是数据不存在还是系统故障),仍然将这个空结果进行缓存,但需要设置过期时间。
4. 缓存雪崩
- 产生原因:
由于设置缓存时采用了相同的过期时间(或者服务器宕机),导致缓存在某一时刻同时失效,请求全部转发到数据库,数据库瞬间压力过重而导致崩溃。
- 解决方案:
①将过期时间分散:在过期时间后面加上一个随机数,让key均匀的失效。
②使用队列或锁控制:用队列或者锁让程序执行在压力范围之内,当然这种方案可能会影响并发量。
③配置redis高可用,服务降级,缓存数据持久化