Flask项目(5)

该博客介绍了Flask项目中关于用户头像上传的实现,包括上传七牛云的工具函数封装、接口设计与编写。同时,文章详细探讨了缓存的概念、架构、在头条项目中的应用,以及缓存更新、淘汰策略和问题解决方案,如缓存穿透、雪崩等。还展示了用户信息和关注缓存的数据设计与实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

学习目标

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.获取随机缓存有效期

小结:
	一定要认证阅读项目中的代码,有很多小细节在课上是没有讲的。课上讲的都是方案
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值