【Python使用】嘿马头条项目从到完整开发教程第8篇:缓存,多级缓存【附代码文档】
本教程的知识点为:简介 1. 内容 2. 目标 产品效果 ToutiaoWeb虚拟机使用说明 数据库 理解ORM 作用 思考: 使用ORM的方式选择 数据库 SQLAlchemy操作 1 新增 2 查询 all() 数据库 分布式ID 1 方案选择 2 头条 使用雪花算法 (代码 toutiao-backend/common/utils/snowflake) 数据库 Redis 1 Redis事务 基本事务指令 Python客户端操作 Git工用流 调试方法 JWT认证方案 JWT & JWS & JWE Json Web Token(JWT) OSS对象存储 存储 需求 方案 使用 缓存 缓存架构 多级缓存 头条项目的方案 缓存数据 缓存 缓存问题 1 缓存 2 缓存 头条项目缓存与存储设计 APScheduler定时任务 定时修正统计数据 RPC RPC简介 1. 什么是RPC RPC 编写客户端 头条首页新闻推荐接口编写 即时通讯 即时通讯简介 即时通讯 Socket.IO 1 简介 优点: 缺点: Elasticsearch 简介与原理 1 简介 属于面向文档的数据库 2 搜索的原理——倒排索引(反向索引)、分析、相关性排序 Elasticsearch 文档 索引文档(保存文档数据) 获取指定文档 判断文档是否存在 单元测试 为什么要测试 测试的分类 什么是单元测试 断言方法的使用:
完整笔记资料代码:https://gitee.com/yinuo112/Backend/tree/master/Python/嘿马头条项目从到完整开发教程/note.md
感兴趣的小伙伴可以自取哦~
全套教程部分目录:
部分文件图片:
缓存
缓存架构
脑中的直观反应
计算机体系结构中的缓存
多级缓存
头条项目的方案
-
SQLAlchemy起到一定的本地缓存作用
-
在同一请求中多次相同的查询只查询数据库一次,SQLAlchemy做了本地缓存(类似Django中的Queryset查询结果集)
-
使用Redis构建一层缓存
缓存数据
缓存数据的类型
在设计缓存的数据时,可以缓存以下类型的数据
- 一个数值
例如
- 验证码
- 用户状态
如:user:{user_id}: enable
-
数据库记录,
-
Caching at the object level
以数据库对象的角度考虑, 应用更普遍
例如, 用户的基本信息
user = User.query.filter_by(id=1).first()
user -> User对象
{
'user_id':1,
'user_name': 'python',
'age': 28,
'introduction': ''
}
- Caching at the database query level
以数据库查询的角度考虑,应用场景较特殊,一般仅针对较复杂的查询进行使用
query_result = User.query.join(User.profile).filter_by(id=1).first()
-> sql = "select a.user_id, a.user_name, b.gender, b.birthday from tbl_user as a inner join tbl_profile as b on a.user_id=b.user_id where a.user_id=1;"
# hash算法 md5
query = md5(sql) # 'fwoifhwoiehfiowy23982f92h929y3209hf209fh2'
# redis
setex(query, expiry, json.dumps(query_result))
- 一个视图的响应结果
@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
# redis
# '/artciels?ch=1': json.dumps(results)
- 一个页面
@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
缓存数据的保存方式
- 序列化字符串
如
# 序列化 json字符
# setex('user:{user_id}:info')
setex('user:1:info', expiry, json.dumps(user_dict))
-
优点
- 存储字符串节省空间
-
缺点
- 序列化有时间开销
- 更新不方便(一般直接删除)
-
Redis的其他数据类型,如hash、set、zset
如
hmset('user:1:info', user_dict)
-
优点
- 读写时不需要序列化转换
- 可以更新内部数据
-
缺点
- 相比字符串,采用复合结构存储空间占用大
缓存有效期与淘汰策略
有效期 TTL (Time to live)
设置有效期的作用:
- 节省空间
- 做到数据弱一致性,有效期失效后,可以保证数据的一致性
Redis的过期策略
过期策略通常有以下三种:
- 定时过期
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
setex('a', 300, 'aval')
setex('b', 600, 'bval')
- 惰性过期
只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
- 定期过期
每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。
Redis中同时使用了惰性过期和定期过期两种过期策略。
Redis过期删除采用的是定期删除,默认是每100ms检测一次,遇到过期的key则进行删除,这里的检测并不是顺序检测,而是随机检测。那这样会不会有漏网之鱼?显然Redis也考虑到了这一点,当我们去读/写一个已经过期的key时,会触发Redis的惰性删除策略,直接回干掉过期的key
为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
定期删除+惰性删除是如何工作的呢?
定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
采用定期删除+惰性删除就没其他问题了么?
不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。
缓存淘汰 eviction
Redis自身实现了缓存淘汰
Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。
- noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
redis 4.x 后支持LFU策略,最少频率使用
-
allkeys-lfu
-
volatile-lfu
LRU
LRU(Least recently used,最近最少使用)
LRU算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
基本思路
-
新数据插入到列表头部;
-
每当缓存命中(即缓存数据被访问),则将数据移到列表头部;
-
当列表满的时候,将列表尾部的数据丢弃。
LFU
LFU(Least Frequently Used 最近最少使用算法)
它是基于“如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小”的思路。
LFU需要定期衰减。
Redis淘汰策略的配置
-
maxmemory最大使用内存数量
-
maxmemory-policy noeviction 淘汰策略
思考题
mySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据
头条项目方案
-
缓存数据都设置有效期
-
配置redis,使用volatile-lru
缓存模式
1) Cache Aside
更新方式
- 先更新数据库,再更新缓存。这种做法最大的问题就是两个并发的写操作导致脏数据。如下图(以Redis和Mysql为例),两个并发更新操作,数据库先更新的反而后更新缓存,数据库后更新的反而先更新缓存。这样就会造成数据库和缓存中的数据不一致,应用程序中读取的都是脏数据。
- 先删除缓存,再更新数据库。这个逻辑是错误的,因为两个并发的读和写操作导致脏数据。如下图(以Redis和Mysql为例)。假设更新操作先删除了缓存,此时正好有一个并发的读操作,没有命中缓存后从数据库中取出老数据并且更新回缓存,这个时候更新操作也完成了数据库更新。此时,数据库和缓存中的数据不一致,应用程序中读取的都是原来的数据(脏数据)。
- 先更新数据库,再删除缓存。这种做法其实不能算是坑,在实际的系统中也推荐使用这种方式。但是这种方式理论上还是可能存在问题。如下图(以Redis和Mysql为例),查询操作没有命中缓存,然后查询出数据库的老数据。此时有一个并发的更新操作,更新操作在读操作之后更新了数据库中的数据并且删除了缓存中的数据。然而读操作将从数据库中读取出的老数据更新回了缓存。这样就会造成数据库和缓存中的数据不一致,应用程序中读取的都是原来的数据(脏数据)。
但是,仔细想一想,这种并发的概率极低。因为这个条件需要发生在读缓存时缓存失效,而且有一个并发的写操作。实际上数据库的写操作会比读操作慢得多,而且还要加锁,而读操作必需在写操作前进入数据库操作,又要晚于写操作更新缓存,所有这些条件都具备的概率并不大。但是为了避免这种极端情况造成脏数据所产生的影响,我们还是要为缓存设置过期时间。
2) Read-through 通读
3) Write-through 通写
4) Write-behind caching
头条项目方案
-
使用Read-throught + Cache aside
-
构建一层抽象出来的缓存操作层,负责数据库查询和Redis缓存存取,在Flask的视图逻辑中直接操作缓存层工具。
-
更新采用先更新数据库,再删除缓存
- 点赞
- 收藏
- 关注作者
评论(0)