缓存方案
一缓存介绍
- 作用
减小数据库的访问压力, 提高并发能力
- 多级缓存
二.本地缓存/一级缓存
-
flask-cache
- 安装
pip install flask-cache
- 选择
simple
类型, 可以实现对响应的内存缓存 - https://www.cnblogs.com/cwp-bg/p/9687005.html
- 安装
-
项目中的应用
- 一般不设置一级缓存
- 内存缓存会影响web应用的运行效率
- 对响应做缓存的性价比低
- 一般不设置一级缓存
from flask import Flask
from flask_cache import Cache
import random
app = Flask(__name__)
# 设置配置
app.config['CACHE_TYPE'] = 'simple' # 使用本地python字典进行存储, 一级缓存
app.config['CACHE_DEFAULT_TIMEOUT'] = 5 * 60 # 默认过期时间 5分钟
# 创建缓存对象
cache = Cache(app)
@app.route('/')
@cache.cached(timeout=60) # 对无参数的路由响应进行缓存
def index():
num = random.randint(0, 9)
print(num)
return str(num)
@app.route('/demo1/<user_id>') # demo1/11 demo1/12 的缓存结果会做区分
@cache.memoize(timeout=30)
def demo1(user_id):
num = random.randint(0, 9)
print(num)
return str(num)
# 对自定义函数设置缓存
@cache.cached(timeout=20)
def func1():
num = random.randint(0, 9)
print(num)
return str(num)
@app.route('/demo2')
def demo2():
print('demo2')
# return func1()
# 可以对指定的数据进行缓存
cache.set('name', 'zs', timeout=30)
return 'demo2'
@app.route('/demo3')
def demo3():
# 取出缓存
name = cache.get('name')
print(name)
return 'demo3'
if __name__ == '__main__':
app.run(debug=True)
三.缓存粒度
- 缓存某个数值
- 一个键只保存一个值, 性价比较低, 使用率低
- 场景 验证码
- 缓存数据对象
- 数据库记录对应的具体数据
- 优点
可以多次复用
- 场景 用户/文章数据
-
缓存数据集合
-
数据库查询对应的结果集
-
场景 文章/关注列表
-
可以和数据对象配合使用
, 方便数据对象的重用
-
- 缓存视图响应
- 视图返回的响应数据
- 缺点
复用性比较差
- 项目中主要对
数据集合+数据对象
进行缓存, 优点复用性强, 节省内存
四.缓存格式
- 数值
- 类型 string
- 数据对象
- hash
- 不需要格式转换
- 可以单独更新某一个字段
- 占用的空间比较多
- 结构化string (json字符串)
- 需要进行转换 使用pickle模块提高性能
- 无法单独更新某个字段
占用的空间小
- 头条项目中优先使用
结构化string
- hash
- 数据集合
- 键
article:top:30
/md5("select * from t_arcitle order by(create_time) desc limit 30;")
- 值 list/zset/set/hash/结构化string
- list
- 有遍历的需要
- 增加效率 追加数据速度很快 插入速度和插入的位置有关
- 查询效率 和查询的区间有关
- 如果添加顺序和不满足排序要求, 则无法排序
- zset
- 有排序的需要
- 增加效率 和存储的数据量负相关, 数据量越大, 添加时间越长
- 查询效率 和存储的数据量负相关, 并且和查询的结果集数据量有关
- set
- 有判断是否存在的需要
- hash
- 只能根据键取值, 无法遍历
- 空间占用多
- json字符串
- 节省空间
- 头条项目中主要使用了
json字符串
和zset
的形式
- 键
- 视图响应
- string
- 键 请求URL
- 值 响应结果对应的字符串 前端渲染json字符串/后端渲染html字符串
五.缓存机制
1.过期策略
-
作用
- 节省空间
- 实现数据的弱一致性
-
过期策略
- 定时过期
- 效率太低, 每个数据都需要设置定时器进行计数
- 惰性过期
- 查询时, 才去检查数据的有效期, 如果过期, 则返回nil, 并删除过期数据
- 定期过期
- 每隔100ms, 随机取出一部分数据进行过期校验, 如果过期, 删除数据
- 定期过期
- 定时过期
-
redis的选择
惰性过期+定期过期
(每100ms对设置了过期时间的数据随机查询并删除过期数据)
2.缓存淘汰
-
LRU
- least recently use 优先淘汰不是最近使用的数据
-
LFU
- least frequently use 优先淘汰不是频繁使用的数据
- 采用了
定期衰减
的机制, 防止旧数据始终无法删除
-
缺点
- 需要每条数据维护一个使用计数
- 还需要定期衰减- 面试问题
- mysql中有100万数据, 要求redis中保留20万热点数据
- 设置内存最大存储空间, 并且设置缓存策略为LFU
- 面试问题
-
淘汰配置
maxmemory 最大使用内存数量 如服务器内存10G, 最多给redis分配9G
maxmemory-policy volatile-lru 淘汰策略
3. 缓存的模式
-
读缓存
-
先读取缓存中的数据, 没有才会读取数据库中的数据
-
解决数据库读取压力
-
方式
- cache aside 具体读写操作交给应用完成
read through
具体读写操作交给缓存层完成
, 即使后期修改存储方案, 业务代码不需要修改, 有利于项目的重构和架构升级
-
-
写缓存
- 先写入缓存, 再写入数据库
- 解决数据库的写入压力
- 方式
- write through 具体写操作交给缓存层完成, 立即更新数据库
- write behind caching 具体读操作交给缓存层完成, 定时异步更新数据库
六.缓存问题
1 缓存更新
- 问题
- mysql和redis是两个独立的系统, 在并发环境下, 无法保证更新的一致性
- 解决办法
更新数据时, 先写入mysql, 再删除缓存
facebook- 设计分布式锁/使用消息队列串行处理
2 缓存穿透
- 问题
- 黑客会主动访问数据库不存在的数据, 缓存会被穿透, 直接访问数据库, 导致数据库的的访问压力变大
- 解决办法
对于数据库中不存在的数据, 也对其在缓存中设置默认值
一般过期时间会比较短- 可以设置一些过滤规则, 如布隆过滤器(算法, 用于判断数据是否包含在集合中), 将所有可能的值录入过滤器, 如果不包含直接返回None, 有误杀概率
3 缓存雪崩
-
问题
- 如果大量缓存数据都在同一个时间过期, 那么很可能出现缓存集体失效, 会导致所有的请求都直接访问数据库, 导致数据库压力过大
-
解决办法
设置过期时间时, 添加随机值, 让过期时间进行一定程度分散
- 多级缓存的方式来处理
- 利用锁/队列的形式
七.缓存层实现
1 缓存类设计
class UserProfileCache(object):
"""用户基本信息缓存类 每个缓存对象 对应 一条用户缓存数据"""
def __init__(self, user_id):
self.user_id = user_id # 用户id
self.key = 'user:{}:profile'.format(self.user_id) # redis键
def get(self):
"""获取缓存数据"""
pass
def clear(self):
"""清空缓存"""
pass
2 获取缓存
class UserProfileCache(object):
"""用户基本信息缓存类 每个缓存对象 对应 一条用户缓存数据
user:<用户id>:profile string '{"name": "zs", "age": 20}'
"""
def __init__(self, user_id