缓存
1 需求背景
- 缓存不是必须的,是为了提升性能而增加的
- 目标: 减少磁盘数据库的查询,比如mysql的查询 ,更多的从内存中读取数据
- mysql查询 通常在1s左右 (几百毫秒, 0.xxs),1s以上通常认为是慢查询
- redis 支持操作的性能 1s可以支持1w+ 操作(更高 可达10W+)
- 场景
- 前提: 读取频繁
- 数据不经常变化,基本一定会做缓存处理
- 数据可能变化频繁,如果数据是产品的核心数据(比如评论数据),可以考虑构建缓存, 缓存时间短,即时缓存5分钟,也能减少很大程度的数据库查询操作,可以提升性能
- 前提: 读取频繁
2 缓存架构
数据存在哪?
多级缓存
- 本地缓存
- 全局变量保存
- orm框架 queryset 查询集(查询结果集) 起到本地缓存的作用
- django orm
- sqlalchemy
- 外部缓存
- 可以构建多级
- 外部存储
- redis
- memcached
3 缓存数据
保存哪些数据 ? 数据以什么形式(类型)保存?
3.1 缓存的数据内容
-
一个数值
- 手机短信验证码
- 比如用户的状态数据 user:status -> 0 / 1
-
数据库记录
-
不以单一视图单独考虑,而是考虑很多视图可能都会用到一些公共数据,就把这些公共的数据缓存,哪个视图用到,哪个视图自己读取缓存取数据 ,(比如用户的个人信息,文章的信息)
-
比较通用,缓存一个数据可以被多个视图利用,节省空间
-
方式:
-
Caching at the object level 缓存数据对象级别
- 通用
mysql 中有用户的个人信息表 每条记录 是一个用户的数据 一个数据实体 user:1 -> user_id ,name mobile profile_photo intro certi user:20 -> user_id ,name mobile profile_photo intro certi
-
Caching at the database query level 缓存数据库查询级别
- 相比缓存数据对象级别 不太通用,只适用于比较复杂的查询,才考虑使用
sql = 'select * from ..inner join where ... group by order by limit' -> query_results hash(sql) -> 'wicwiugfiwuegfwiugiw238' md5(sql) 缓存 数据名称 数据内容 'wicwiugfiwuegfwiugiw238' -> query_results 使用的时候 sql -> md5(sql) -> 'wicwiugfiwuegfwiugiw238'
-
-
-
一个视图的响应结果
- 考虑单一的视图 ,只只对特定的视图结果进行缓存
@route('/articles') @cache(exipry=30*60) def get_articles(): ch = request.args.get('ch') articles = Article.query.all() for article in articles: user = User.query.filter_by(id=article.user_id).first() comment = Comment.query.filter_by(article_id=article.id).all() results = {...} # 格式化输出 return results # /articles?ch=123 视图的结果resuls 缓存 # 下一次再访问 ‘/articles?ch=123’
-
一个页面
-
只针对 h5页面 (html5) 网页
-
方式
-
如果是服务器端渲染 (前后端不分离)
@route('/articles') @cache(exipry=30*60) def get_articles(): ch = request.args.get('ch') articles = Article.query.all() for article in articles: user = User.query.filter_by(id=article.user_id).first() comment = Comment.query.all() results = {...} return render_template('article_temp', results) # redis # '/artciels?ch=1': html
-
页面静态化 算是一种页面缓存方式
-
-
3.2 缓存数据保存形式
针对的是外部缓存 redis
-
字符串形式
user:1 -> user_id ,name mobile profile_photo intro certi user1 -> User()对象 -> user1_dict key value user:1 -> json.dumps(user1_dict) pickle.dumps() json: 1. 只能接受 列表 字典 bytes类型 2. json转换成字符串 效率速度慢 pickle : 1. 基本支持python中的所有类型,(包括自定义的类的对象) 2. json转换成字符串 效率速度 快
- 优点: 保存一组数据的时候,存储占用的空间 相比其他类型可能节省空间
- 缺点:整存整取 ,如果想获取其中的单一字段 不是很方便,需要整体取出 再序列化或反序列化, 更新某个字段 类似 , 不灵活
-
非字符串形式
- list set hash zset
- 需要针对特定的数据来选型
user:1 -> user_id ,name mobile profile_photo intro certi user1 -> User()对象 -> user1_dict key value user:1 -> hash { name: xxx, moible: xxx photo: xxx }
- 优点: 可以针对特定的字段进行读写,相对灵活
- 缺点: 保存一组数据的时候,占用的空间相比字符串会稍大
4 缓存数据的有效期 TTL (time to live)
缓存数据一定要设置有效期,原因/作用:
- 即时清理可以节省空间
- 保证数据的一致性,(弱一致性) ,保证mysql中的数据与redis中的数据,在更新数据之后还能一直, 虽然在一定的时间内(缓存数据的有效期) redis与mysql中的数据不同 ,但是过了有效期后 redis会清理数据, 再次查询数据时 会形成新的缓存数据,redis与mysql又相同了
4.1 redis的有效期策略
通用的有效期策略:
-
定时过期
set a 100 有效期 10min set b 100 有效期 20min
开启一个计时器计时,当有效期到达之后 清理数据, 可以理解为每个数据都要单独维护一个计时器
缺点: 耗费性能
-
惰性过期
保存数据 设置有效期后 不主动维护这个数据的有效期,不计时,只有在再次访问这个数据(读写)的时候,判断数据是否到期,如果到期清理并返回空,如果没到期,返回数据
-
定期过期
- 周期性的检查哪些数据过期哪些数据没过期,比如每100ms判断哪些数据过期,如果有过期的数据,进行清理
Redis的有效期策略 : 惰性过期 + 定期过期
- redis实现定期过期的时候,还不是查询所有数据,而是每100ms 随机选出一些数据判断是否过期,再过100ms 再随机选出一些判断
思考:
如果在redis中保存了一条数据,设置有效期为10min,但是数据设置之后 再无操作, 请问 10min之后 这条数据是否还在redis的内存中? 答案: 还可能存在
5 缓存淘汰 (内存淘汰)
背景: redis的数据有效期策略不能保证数据真正的即时被清理,可能造成空间浪费,再有新的数据的时候,没地方可以存存储, 为了存储新数据,需要清理redis中的一批数据,腾出空间保存新数据
淘汰策略 指 删除哪些数据
通用的内存淘汰算法: LRU & LFU
-
LRU(Least recently used,最近最少使用)
思想: 认为 越是最近用过的数据,接下来使用的机会越大,应该清理那些很久以前使用过的数据
cache_data = [ cache1 时间最近 cache2 cache5 cache4 cache3 时间最远 ] 操作过cache3 cache_data = [ cache3 cache1 时间最近 cache2 cache5 cache4 ] 增加cache6 cache_data = [ cache6 cache3 cache1 时间最近 cache2 cache5 ]
-
LFU (Least Frequently Used, 最少使用) 以频率 次数来考虑
思想: 认为使用次数越多的数据,接下来使用的机会越大,应该清理那些使用次数少的数据
cache_data = { cache1 : 100 cache2: 2 cache5: 23 cache4: 89 cache3 : 10000 } 操作了cache2 cache_data = { cache1 : 100 cache2: 3 cache5: 23 cache4: 89 cache3 : 10000 } 新增 cache6 cache_data = { cache1 : 100 cache5: 23 cache4: 89 cache3 : 10000 cache6: 1 } cache_data = { cache1 : 100 -> 50 cache5: 23 -> 11 cache4: 89 -> 45 cache3 : 10000 -> 5000 cache6: 1 -> 1 }
- 效果更好
- 缺点: 性能不高,需要额外记录次数 频率, 还需要定期衰减
Redis的内存淘汰策略 (3.x版本以后)
- noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。 默认
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
redis 4.x 版本之后 增加了两种
- allkeys-lfu
- volatile-lfu
redis中的配置
maxmemory <bytes> 指明redis使用的最大内存上限
maxmemory-policy volatile-lru 指明内存淘汰策略
总结:
- 如果将redis作为持久存储 ,内存淘汰策略 采用默认配置 noeviction
- 如果将redis作为缓存,需要配置内存淘汰策略 选择合适的淘汰策略
6. 缓存模式
应用程序如何使用缓存
- 读缓存
- 场景: 需要频繁读取查询数据 的场景
- 方式 在应用程序与mysql数据库中间架设redis 作为缓存 ,读取数据的时候先从缓存中读取, 但是写入新数据的时候,直接保存到mysql中
- 写缓存
- 场景: 需要频繁的保存数据 的场景
- 方式 在应用程序与mysql数据库中间架设redis 作为缓存 ,保存数据的时候先保存到缓存redis中,并不直接保存的mysql中, 后续再从redis中同步数据到mysql中
读缓存的数据同步问题:
修改了mysq中的数据,如何处理redis缓存中的数据
-
先更新数据库,再更新缓存
-
先删除缓存,再更新数据库
-
先更新数据库,再删除缓存 发生问题的几率最小 ,负面影响最小
7 缓存使用过程中可能存在的问题
- 缓存穿透
- 问题: 访问不存在的数据, 数据库没有 缓存也没有存储,每次访问都落到数据查询
- 解决:
- 缓存中保存不存在的数据,比如将数据以-1保存,表示数据不存在,可以拦截 这种攻击,减少数据库查询
- 需要引入其他工具 ,过滤器 ,按照规则来判断 是否可能存在, 比如 布隆过滤器
- 缓存雪崩
- 问题: 同一批产生 的缓存数据 可能在同一时间失效,如果在同一时间大量的缓存失效,查询时又会落到数据库中查询,对数据库并发的有大量的查询,数据库吃不消,数据库又可能崩溃
- 解决:
- 将数据的有效期增加偏差值,让同一批产生的缓存数据不在同一时间 失效,将失效时间错开
- 架设多级缓存, 每级缓存有效期不同
- 以保护数据库出发, 为数据库的操作 添加锁 或者 放到队列中,强行的将并行的数据库操作改为串行操作,一个一个执行,防止数据库崩溃
8 项目的缓存的设计
- 服务器硬件层面的架设
- 本地缓存
- orm 查询结果集缓存
- 一级外部缓存
- redis cluster 配置了缓存淘汰策略 (无需配置 持久化策略) volatile-lru 4.0.13
- 本地缓存
- 程序编写开发上
- 缓存 的数据 Caching at the object level 数据库对象级别,可以被多个视图利用
- 缓存数据一定要设置有效期 , 为了防止缓存雪崩,有效期要设置偏差值
- 为了防止缓存穿透,缓存数据时 不存在的数据也要缓存下来
- 读缓存
- 大多数情况选择 先更新数据库 再删除缓存