学习目标
1.能够使用七牛对象存储服务保存文件
1.先把七牛云sdk代码拷贝
2.封装upload_image
2.能够编写上传用户头像接口
3.能够使用接口管理平台录入接口
4.能够知道CDN的作用
内容分发网络
5.能够知道多级缓存的结构
1.本地
2.redis集群
3.redis主从
6.能够知道可以缓存的数据内容与保存类型
1.string, zset
7.能够知道缓存数据有效期的作用
1.节省空间
2.保证弱一致性
8.能够说明redis的过期策略
1.惰性过期
2.定期过期
9.能够说明redis的内存淘汰策略
LRU LFU
10.能够知道缓存数据更新的方式
1.更新数据库->更新缓存
2.删除缓存->更新数据库(逻辑错误)
3.更新数据库 -> 删除缓存
11.能够说明数据更新后同步更新缓存的并发问题
12.能够说明缓存穿透的问题与解决方式
1.查询数据库不存在的数据
2.没有也添加缓存
13.能够说明缓存雪崩的问题与解决方式
1.同一时间,大量key过期
2.针对每个设置随机的过期时间
14.能够进行redis数据库设计
值,类型,过期时间,淘汰策略
15.能够封装头条项目缓存工具类
用户信息
16.能够在视图中使用封装的缓存工具类
1.上传七牛云工具函数封装
-
文件: d01_qiniu.py
-
用户头像, 文章中的图片等都需要上传, 把它单独封装成一个函数, 方便调用
-
安装:
pip install qiniu
-
封装步骤:
-
1.把官方的demo拷贝到本地.
from qiniu import Auth, put_file, etag import qiniu.config #需要填写你的 Access Key 和 Secret Key access_key = 'Access_Key' secret_key = 'Secret_Key' #构建鉴权对象 q = Auth(access_key, secret_key) #要上传的空间 bucket_name = 'Bucket_Name' #上传后保存的文件名 key = 'my-python-logo.png' #生成上传 Token,可以指定过期时间等 token = q.upload_token(bucket_name, key, 3600) #要上传文件的本地路径 localfile = './sync/bbb.jpg' ret, info = put_file(token, key, localfile) print(info) assert ret['key'] == key assert ret['hash'] == etag(localfile)
-
2.根据需求修改成我们自己的函数。
from qiniu import Auth, put_file, etag, put_data import qiniu.config #需要填写你的 Access Key 和 Secret Key access_key = '51DGWfSzbBws6szT3GVoZ8nMuqVVFAFV2P_StMbr' secret_key = 'pAo3kBotA7PQLCuIF9Y2wCc7AfRs0MEss2-qdTbb' def upload_image(file_data): # 构建鉴权对象 q = Auth(access_key, secret_key) #要上传的空间 bucket_name = 'hmwx02' key = None # 表示文件名由七牛云自己管理 #生成上传 Token,可以指定过期时间等 token = q.upload_token(bucket_name, key, 3600000) # 上传文件数据需要使用put_data函数 ret, info = put_data(token, key, file_data) print(info) if __name__ == '__main__': with open('swk.jpeg', 'rb') as f: upload_image(f.read())
-
备注
1.上传本地图片使用put_file 2.上传图片数据使用put_data 3.生成token, 注意指定过期时间. 由于虚拟机的时间不正确, 所以指定token过期时间久一点, 要么修改服务器的时间.
-
小结
1.一般我们需要用到的第三方的工具, 无论是API还是SDK. 基本上都是上面的两个步骤. 2.一定要培养看文档的习惯.
2.使用date修改时间
- 命令:
sudo date -s '2019-09-26 15:00:00'
3.scp工具
作用: 用于本地和服务器之间, 上传和下载文件
scp属于linux命令,需要在linux系统中使用。
上传:
scp 本地文件 用户名@ip:路径
下载:
scp 用户名@ip:路径/文件 本地路径
4.用户头像上传接口设计
1.PUT: 修改资源的时候, 需要传输完整的数据
2.PATCH: 修改资源的时候, 传递需要修改的字段即可.
PATCH /v1_0/user/profile
headers:
Authorization: Bearer token # 表示需要要登录
form-data
parameter:
photo file 用户头像
返回数据
{
"message": "ok",
"data":{
"photo_url": ""
}
}
5.上传头像接口编写
- 文件: d02_user_profile.py
6.项目中的修改用户信息接口说明
-
将图片转成base64字符串工具
http://imgbase64.duoshitong.com/ 备注: 把前面的图片类型去掉。 data:image/jpeg;base64, --去掉
7.CDN简介
-
CDN: Content Distribute Network, 内容分发网络
-
七牛云的对象存储服务, 集成了CDN服务.
-
作用:
1.提高用户访问资源的速度. 2.解决因分布、带宽、服务器性能带来的访问延迟问题
-
基本原理:
将源站内容分发至最接近用户的节点,使用户可就近取得所需内容,提高用户访问的响应速度和成功率。
-
适用场景:
站点加速、点播、直播等.
8.缓存
-
作用
1.加快访问速度. 2.减轻关系型数据库的压力(主要).
9.缓存架构
-
基本架构:
web应用程序->到redis中查询->如果没有数据->mysql中查询->添加redis缓存中->用户
-
多级缓存:
1.本地缓存(全局变量) 2.redis集群 3.redis主从
-
代码演示
-
文件: d03_cache.py
10.头条项目中缓存的应用
解析:在头条项目中,使用基本缓存架构,使用redis集群作为缓存层。
在python代码中的使用:
from rediscluster import StrictRedisCluster
REDIS_CLUSTER = [
{'host': '127.0.0.1', 'port': '7000'},
{'host': '127.0.0.1', 'port': '7001'},
{'host': '127.0.0.1', 'port': '7002'},
]
redis_cluster = StrictRedisCluster(startup_nodes=REDIS_CLUSTER)
redis_cluster.setex('test', 100, 'test')
print(redis_cluster.get('test'))
11.缓存的数据内容
*
在缓存中保存什么数据?
数据:
一个值:
1.用户id
2.验证码
3.用户状态,是否可用
一条表的记录:
用户信息
一次查询的结果
方案: md5(sql)当做key
以数据库查询的角度考虑,应用场景较特殊,一般仅针对较复杂的查询进行使用(统计)
多表查询后的结果集
一次视图函数的返回值
方案: 完整的路径当做key
整个视图函数的返回数据-json
一个网页
方案: 完整的路径当做key
12.数据保存类型的可选方式
解析:保存类型指的是redis中值类型的选择。
常用的两种情况的数据类型:
1.字符串
优点:节省空间
缺点:序列化有时间开销,更新不方便,一般直接删除 json.dumps() json.loads() CPU
2.其他类型: 哈希类型(hash) 集合(set) 有序集合(zset)
优点:不需要序列化转换,可以更新内部数据
缺点:相比字符串,采用复合结构存储空间占用大
13.缓存数据有效期的作用
解析:我们使用redis保存热点数据,不保存冷门数据。
设置有效期的作用:
1.节省空间
2.保持数据的弱一致性
14.项目中使用缓存的基本思路
黑马头条缓存策略:
1.基本全站都设置了缓存。
2.不以视图作为缓存的思考,主要以数据库的记录作为缓存思考点。
小结:
基本架构的缓存
头条中获取数据流程:
web应用程序-> redis集群 -> mysql
15.过期策略
-
目标:知道redis的过期策略
解析:在redis中,key到期后,在内存中并没有立刻被删除。 过期策略有以下三种: 1.定时过期: 每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。 优点: 对内存友好 缺点: 会占用大量的CPU资源 2.惰性过期: 只有当访问一个key时,才会判断该key是否已过期,过期则清除。 优点: 最大化地节省CPU资源 缺点: 对内存非常不友好 3.定期过期: 每隔一定的时间,扫描一定数量key,并清除其中已过期的key。 该策略是前两者的一个折中方案。 备注: 1.Redis中同时使用了惰性过期和定期过期两种过期策略。 2.在redis中有一个expires字典 3.expires字典保存设置了过期时间的key(指针),和过期时间。 redis中的解决方案: 1.redis定时随机检测expires里面的一定数量的key, 过期key立刻删除。(定期删除) 2.客户端访问key, 如果key已经过期,立刻删除(惰性删除)。 3.定期删除+惰性删除还是不能避免内存空间满的情况,所以还需要用到淘汰策略
16.内存淘汰策略的两种算法思想
-
目标:了解内存淘汰策略的两种算法
有两个经典的淘汰算法: LRU和LFU LRU: Least recently used,最近最少使用 基本思路 1.新数据插入到列表头部; 2.每当缓存命中(即缓存数据被访问),则将数据移到列表头部; 3.当列表满的时候,有新的key进来,就把列表尾部的数据丢弃,在把新的key添加到头部。 LFU: Least Frequently Used 最不经常使用 1.每个key被操作的时候,操作频率+1 2.当内存满的时候,有新key进来。以频率来做选择。选择累计次数最少的淘汰。 备注: LFU效果更好,但是有个隐含的问题。 会存在"大地主", 需要定期衰减,每隔一段时间,所有记录次数减半。
17.redis的内存淘汰策略
解析: Redis自身实现了缓存淘汰, Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。
在配置文件中设置:
maxmemory 最大使用内存数量
maxmemory-policy noeviction 淘汰策略
可选配置策略:
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
redis 4.x 后支持LFU策略,最少频率使用
allkeys-lfu
volatile-lfu
面试题:
mysql里面有2000w的数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据
答:1.配置redis 中的maxmemory为20w大小的内存。
2.配置maxmemory-policy 为 volatile-lru
3.所有key都需要设置过期时间
黑马头条redis淘汰策略方案:
1.黑马头条中使用redis集群作为缓存层。redis主从做持久,所以不设置淘汰策略。
2.缓存的数据都设置有效期
3.配置redis集群,使用volatile-lru
18.缓存操作下的缓存模式
缓存操作分为读缓存和写缓存:
读缓存:web -> redis -> mysql -> redis
1.Cache Aside
2.Read-through 通读 (多加了一个缓存工具)
写缓存:
1.write-through 通写 (多加了一个缓存工具)
2.Write-behind caching
头条项目中的方案:
1.Read-through + Cache Aside
2.因为头条项目,很少更新数据,所以没用到写缓存
-
代码演示通读
-
d04_read_through.py
import json import random from flask import current_app from models import User from models import Relation class UserProfileCache(object): """ 用户基本信息缓存工具 """ def __init__(self, user_id): self.user_id = user_id self.key = 'user:{}:profile'.format(user_id) self.redis_cli = current_app.redis_cluster def save(self): """ 从数据库中获取用户基本信息,并把数据缓存到redis集群中 :return: 用户基本信息 """ user = User.query.get(self.user_id) if user: # 组装需要缓存的数据 user_data = { 'user_id': user.id, 'mobile': user.mobile, 'user_name': user.name, 'photo': user.profile_photo } # 把用户基本数据,缓存到redis集群中。 # 为了演示缓存雪崩,把有效期设置为10秒 # self.redis_cli.setex(self.key, 10, json.dumps(user_data)) # self.redis_cli.setex(self.key, 5 * 60, json.dumps(user_data)) # 为了解决缓存雪崩,加上一个随机的整数 self.redis_cli.setex(self.key, 10 + random.randint(1, 30), json.dumps(user_data)) else: # 组装需要缓存的数据 user_data = { 'user_id': -1, 'mobile': '', 'user_name': '用户不存在', 'photo': '' } # 就算数据库没有数据,在缓存中缓存一条错误记录。 self.redis_cli.setex(self.key, 5 * 60, json.dumps(user_data)) return user_data def get(self): """ 对外调用获取用户基本信息的方法 :return: 用户基本信息 """ user_data = self.redis_cli.get(self.key) if user_data: # 从redis获取的数据都是bytes类型 return json.loads(user_data.decode()) else: user_data = self.save() return user_data
19.缓存更新方式
-
更新方式:
1.先更新数据库,再更新缓存。这种做法最大的问题就是两个并发的写操作导致脏数据(用户信息) 2.先删除缓存,再更新数据库。这个逻辑是错误的,因为两个并发的读和写操作导致脏数据。 3.先更新数据库,再删除缓存。正确的做法(也是有可能出现数据不一致,但是概率很低)
-
正确的方式: 第三种方式。
-
文件:d05_update_cache.py
@app.route('/update/<int:user_id>') def update(user_id): user_name = request.args.get('user_name') #1.先更新数据库,再更新缓存。 # user = User.query.get(user_id) # user.name = user_name # db.session.add(user) # db.session.commit() # # 更新缓存 # key = 'user:{}:profile'.format(user_id) # user_data = { # 'user_id': user.id, # 'mobile': user.mobile, # 'user_name': user.name, # 'photo': user.profile_photo # } # current_app.redis_cluster.setex(key, 5*60, json.dumps(user_data)) # 2.先删除缓存,再更新数据库。 # 2.1删除缓存 # key = 'user:{}:profile'.format(user_id) # current_app.redis_cluster.delete(key) # # 2.2更新数据 # user = User.query.get(user_id) # user.name = user_name # db.session.add(user) # db.session.commit() # 3.先更新数据库,再删除缓存。(正确的方式) # 3.1更新数据 user = User.query.get(user_id) user.name = user_name db.session.add(user) db.session.commit() # 3.2删除缓存 key = 'user:{}:profile'.format(user_id) current_app.redis_cluster.delete(key) return '更新成功'
20.缓存穿透
-
缓存穿透:
查询数据库不存在的数据,进而每次查询都会落到数据库中。
-
解决办法:
1.没有记录也缓存空数据。建议 2.布隆过滤器(把所有数据库中不存在的请求都过滤掉)
21.缓存雪崩
-
缓存雪崩:
某一时间设置了大量的缓存,在未来的某一段时间内,有大量缓存一起失效,称为缓存雪崩。
-
解决办法:
1.给缓存加上一定区间内的随机有效期。 (黑马头条采用的方法) 2.多级缓存 3.串行判断
-
代码演示:
-
文件:d06_cache_snow_slide.py
@app.route('/set_user_cache') def set_user_cache(): """ 把所有用户的数据,都缓存到redis中 """ users = User.query.all() for user in users: UserProfileCache(user.id).get() return '缓存成功'
22.用户缓存数据设计
-
目标:知道头条项目中的用户缓存数据是如何存储的
每个用户单独一条记录,也就是单独一个key key: user:{user_id}:profile value: 1.复合类型,内存消耗更大(不建议使用) 2.string. json.loads json.dumps。虽然会有性能上的损耗,但是相对于内存消耗,cpu性能降一点更可以接受。 头条项目中使用的方案: 每个用户都单独存放一条记录,值都是使用字符串类型。
23.头条项目中用户缓存演示说明
24.用户关注缓存数据设计
-
目标:知道头条用户关注缓存是使用zset来做的
头条中的用户关注列表使用有序集合zset来做 每个用户的关注列表,使用单独一个键 键值描述: 1.key: user:{user_id}:following 2.值使用zset有序集合 成员: target_user_id 分数: 关注时的时间戳 获取用户的关注列表步骤: 1.先获取关注列表的user_id 2.在获取用户的基本信息 备注: 数据库中的原则, 能一次查询出来就不要多次查询,这只是针对mysql. 因为redis的性能很高, 每秒中大概可以执行10000次左右查询操作,所以这样存储更省空间,效果更好。 value 不使用字符串的原因,是考虑到获取数据的时候需要分页。 value 不使用list类型的原因,需要自己提前排序。 value 使用zset, 值user_id, 分数使用时间戳。就可以做到根据时间排序。 zset命令: # 一次添加一个成员 zadd(key, score, member) -> zadd(key, update_time, user_id) # 一次添加多个成员 zadd(key, score1, member1, score2, member2, ......) # 根据score从小到大获取 zrange(key, 0, -1) # 根据score从大到小获取 zrevrange(key, 0, -1)
-
文件: d07_followings.py d07_cache_tool.py
-
关键代码
class UserFollowingCache(object): """ 获取用户关注列表的缓存层工具 """ def __init__(self, user_id): self.user_id = user_id self.key = 'user:{}:followings'.format(user_id) self.redis_cli = current_app.redis_cluster def save(self): """ 从数据中获取用户关注列表信息,保存到redis集群中。 :return: """ relations = Relation.query.filter(Relation.user_id==self.user_id, Relation.relation == Relation.RELATION.FOLLOW)\ .order_by(Relation.utime.desc()).all() # followings是需要返回的关注列表 followings = [] # zadd, 使用zadd命令把数据添加到redis中 # 使用redis事务, pl是一个事务对象 pl = self.redis_cli.pipeline() for i in relations: followings.append(i.target_user_id) # zadd(self.key, score, member) pl.zadd(self.key, i.utime.timestamp(), i.target_user_id) # 执行事务 pl.execute() return followings def get(self): """ 获取用户的关注列表,先从缓存中获取,没有再从数据库获取 :return: """ ret = self.redis_cli.zrevrange(self.key, 0, -1) followings = [] if ret: # 遍历从redis中获取到的target_user_id, 并转换成整形 for uid in ret: followings.append(int(uid)) return followings else: followings = self.save() return followings
25.头条项目用户关注缓存演示说明
1.增加的内容:
1.1缓存
# 存放一些常量,或者获取一些数值的类等
common/cache/constants.py
# 用户缓存工具模块
# UserProfileCache 用户基本信息缓存
# UserFollowingCache 用户关注缓存
# UserFollowersCache 用户粉丝缓存
# UserAdditionalProfileCache 用户额外的信息缓存. 也就是user_profile表中的数据
common/cache/user.py
1.2接口
1.2.1用户关注接口
# 接口路由:/v1_0/user/followings
# 接口所在文件:toutiao/resources/user/following
1.2.2取消关注
# 接口路由:/v1_0/user/followings/<int(min=1):target>
# 接口所在文件:toutiao/resources/user/following
1.2.3粉丝列表
# 接口路由:/v1_0/user/followers
# 接口所在文件:toutiao/resources/user/following
2.修改的内容。
2.1 在获取用户基本信息的接口中,不再直接操作数据库,而是使用缓存工具
难点:
1.使用zset一条命令保存关注列表。
cache = []
for relation in ret:
# 组装zadd命令要用到的数据. [score1, member1, socre2, member2]
cache.append(relation.utime.timestamp())
cache.append(relation.target_user_id)
# zadd(self.key, score1, member1, score2, member2)
pl.zadd(self.key, *cache)
2.获取随机缓存有效期
小结:
一定要认证阅读项目中的代码,有很多小细节在课上是没有讲的。课上讲的都是方案