一篇就够了!!!——Redis缓存设计

一、缓存介绍

1.1 缓存架构

基本架构:
在这里插入图片描述

1.1 缓存粒度
  • 缓存某个值(string)
    场景:验证码

  • 缓存数据对象(哈希hash)
    一条数据库记录
    优点: 可以多次复用
    场景: 用户/文章数据

  • 缓存数据集合(list、set、zset)
    如set可以去重,zset按照分数/权重排序
    场景: 文章/关注列表

list:有遍历的需要
zset:有排序的需要
set:有判断是否存在的需要

  • 缓存视图响应(string)
    视图返回的响应数据
    缺点: 复用性比较差

请求URL
响应结果对应的字符串, 前端渲染json字符串 或 后端渲染html字符串

二、缓存过期和淘汰

2.1 缓存过期

1、定时过期(节约空间、但耗性能)

每个设置过期时间的key都创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源进行计时和处理过期数据,从而影响缓存的响应时间和吞吐量。

2、惰性过期(提高性能,但占用内存空间)

只有当访问一个key时,才会判断该key是否已过期,过期则清除(返回nil)。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。

3、定期过期(以上方案的折中)

每隔一定的时间,扫描数据库中一部分设置了有效期的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果

  • Redis的过期策略
    Redis中同时使用了惰性过期(上面的方案2)定期过期(上面的方案3)两种过期策略。

定期过期: 默认是每100ms检测一次,遇到过期的key则进行删除,这里的检测并不是顺序检测,而是随机检测。
惰性过期: 当我们去读/写一个key时,会触发Redis的惰性过期策略,直接删除过期的key

2.2 缓存淘汰

假定某个key逃过了定期过期, 且长期没有使用(即逃过惰性过期), 那么redis的内存会越来越高。当redis占用的内存达到系统上限时, 就会触发 内存淘汰机制

  • 所谓内存淘汰机制, 是指 在Redis允许使用的内存达到上限时,如何淘汰已有数据及处理新的写入需求。

  • Redis自身提供了多种缓存淘汰策略, 最常用的是 LRULFU

2.2.1 LRU (Least recently used 最后使用时间策略)

LRU算法根据数据的历史访问记录来进行淘汰数据,优先淘汰最近没有使用过的数据。

基本思路:
新数据插入到列表头部;
每当缓存命中(即缓存数据被访问),则将数据移到列表头部;
当列表满的时候,将列表尾部的数据丢弃。

2.2.2 LFU (Least Frequently Used 最少使用次数策略)

0、redis 4.x 后支持LFU策略
1、它是基于“如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小”的思路, 优先淘汰使用率最低的数据。
2、考虑到新添加的数据往往使用次数要低于旧数据, LFU还实现了 定期衰减机制

  • LFU的缺点:
    需要每条数据维护一个使用计数
    还需要定期衰减

  • redis的淘汰策略
    建议使用volatile开头的

noeviction(redis默认的策略):当内存不足以容纳新写入数据时,新写入操作会报错。
allkeys-lfu: 当内存不足以容纳新写入数据时,在键空间中,优先移除使用次数最少的key。
volatile-lfu: 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,优先移除使用次数最少的key。
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,优先移除最近没有使用过的key。
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,优先移除最近没有使用过的key。
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

经典问题: mySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据?

答:
配置redis配置文件,设置redis内存大小为只存20w数据大小的空间,并开启淘汰策略
注意:(redis的内存默认是贪婪地,没有配置内存大小,会一直扩展内存,直到电脑内存满了)

$ sudo vi redis.conf
maxmemory 1048576  # 最大使用内存数量, 以字节为单位  如服务器内存10G, 最多给redis分配9G
maxmemory-policy volatile-lfu  # 淘汰策略

问题延伸:我们如何知道存储20w数据会占用多少内存空间呢?
答:
方案一:通过内存信息粗略计算

# 方案1: 根据内置信息查询

$ redis-cli 
127.0.0.1:6379> dbsize  # 查询当前库中记录了多少个键
(integer) 150

127.0.0.1:6379> info Memory
# Memory
used_memory:1045456  # Redis分配的内存总量,包含了redis进程内部的开销和数据占用的内存,以字节(byte)为单位
used_memory_human:1020.95K  # 展示优化的 Redis内存总量

方案二:通过第三方分析工具计算

# 方案2: 使用第三方分析工具 redis-rdb-tools

# 安装工具
git clone https://github.com/sripathikrishnan/redis-rdb-tools
cd redis-rdb-tools
sudo python3 setup.py install

# 将Redis持久化的数据导出,  其中 /path/dump.rdb 为Redis持久化的数据文件路径
$ rdb -c memory /path/dump.rdb > ~/redis_memory_report.csv
$ cat ~/Desktop/redis_memory_report.csv

# 库号, 类型, 键名, 占用空间, 编码方式, 元素个数, 最大元素占用的空间, 过期时间
database,type,key,size_in_bytes,encoding,num_elements,len_largest_element,expiry  
0,sortedset,user:all:art_count,89,ziplist,3,8,
0,sortedset,list2,63,ziplist,1,8,
...

三、缓存问题及解决方案

3.1 缓存更新
  • mysql和redis是两个独立的系统, 在并发环境下, 无法保证更新的一致性
  • 如下图(以Redis和Mysql为例),两个并发更新操作,数据库先更新的反而后更新缓存,数据库后更新的反而先更新缓存。这样就会造成数据库和缓存中的数据不一致,应用程序中读取的都是脏数据。
    在这里插入图片描述
  • 解决方案

方案1: 设计分布式锁(redis-setnx)/使用消息队列顺序执行
缺点: 并发能力差

方案2: 更新数据时, 先写入mysql, 再删除缓存
主要用于数据对象 (更新少)
数据集合可以考虑更新缓存 (集合的查询成本高, 频繁更新缓存效率太低)
广泛使用, 如: facebook
注意:更新数据对象, 采用先更新数据库,再删除缓存; 更新数据集合时, 采用先更新数据库, 再更新缓存

3.2 缓存穿透
3.3 缓存雪崩

见之前文章,点我跳转

四、缓存模式

4.1 Cache Aside(主要由web后端完成)(不推荐)

在这里插入图片描述

4.2 Read-through 通读(主要由缓存工具完成)(推荐使用)

在这里插入图片描述

五、缓存设计代码实现

示例:flask项目中,查询用户信息缓存设计

from sqlalchemy.orm import load_only
from flask import request, session, g, current_app
from app import redis_cluster
from models.user import User



"""
多线程隔离机制
{"thread_id_1": user1, g session}
{"thread_id_2": user2, g session}

类名称:UserCache
对象属性:当前登录用户: user_id  存储的key:  user:<user_id>:basic
对象方法:查询缓存:get()  删除缓存:delete()

缓存粒度:user模型对象  ==>  user字典
数据类型:hash
写入命令:hmset key 用户字典
写入命令:hmset user:<user_id>:basic {用户字典}
查询命令:hmget key [xxx]      hgetall()

查询缓存:UserCache(user_id).get()
删除缓存:UserCache(user_id).delete()
"""


class UserCache(object):
    """查询用户缓存"""

    def __init__(self, user_id):
        # 当前登录用户id
        self.user_id = user_id
        # 查询缓存的键
        self.key = "user:{}:basic".format(user_id)

    """
    # 查询缓存思路:
        
        # 1.查询redis缓存数据库是否存在缓存数据
        # 2.命中缓存
        # 2.1 判断数据是否是为空标志位,是为空标志位:{"null": true}  返回None
        # 2.2 非为空标志位,返回用户字典数据
        # 3.缓存未击中
        # 3.1 查询mysql中是否存在用户数据
        # 4.用户对象存在
        # 4.1 将数据转换成用户字典,并返回
        # 4.2 将用户字典回填到redis缓存中
        # TODO:注意:设置随机过期时长防止出现缓存雪崩
        # 5.用户对象不存在
        # TODO:防止缓存穿透问题
        # 5.1 即使数据不存在也往redis缓存中回填一条为空的标志位数据: key: {"null": true}
        # 5.2 设置随机过期时长防止雪崩
        # 5.3 返回空字典数据

    """

    def get(self):
        """
        查询缓存
        :return: 用户字典
        """
        # 1.查询redis缓存数据库是否存在缓存数据
        user_cache = redis_cluster.hgetall(self.key)
        # 2.命中缓存
        # 判断数据是否是为空标志位  user_cache 字典
        if user_cache:
            # 2.1 是为空标志位:{"null": true}  返回None  保护mysql数据库减少数据库压力
            if user_cache.get("null"):
                return None
            # 2.2 非为空标志位,返回用户字典数据
            else:
                print("从redis缓存中获取用户信息")
                return user_cache
        # 3.缓存未击中
        else:
            # 3.1 查询mysql中是否存在用户数据
            user = User.query.options(load_only(User.id,
                                                User.name,
                                                User.mobile,
                                                User.profile_photo,
                                                User.article_count,
                                                User.following_count,
                                                User.fans_count,
                                                User.introduction)).filter(User.id == self.user_id).first()
            # 4.用户对象存在
            if user:
                # 4.1 将数据转换成用户字典,并返回
                user_dict = user.to_dict()
                # 4.2 将用户字典回填到redis缓存中
                redis_cluster.hset(self.key, mapping=user_dict)
                # TODO:注意:设置随机过期时长防止出现缓存雪崩
                redis_cluster.expire(self.key, 2 * 60 * 60)
                # TODO:注意返回用户字典
                print("查询mysql数据返回用户字典")
                return user_dict

            # 5.用户对象不存在
            else:
                # TODO:防止缓存穿透问题
                # 5.1 即使数据不存在也往redis缓存中回填一条为空的标志位数据: key: {"null": true}
                redis_cluster.hset(self.key, mapping={"null": True})
                # 5.2 设置随机过期时长防止雪崩
                redis_cluster.expire(self.key, 60 * 60)
                # 5.3 返回空字典数据
                print("mysql数据库不存在用户对象")
                return None

    def delete(self):
        """
        删除缓存
        :return: None
        """
        pass
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值