目录
一、缓存介绍
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自身提供了多种缓存淘汰策略, 最常用的是 LRU 和 LFU
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