【9.0】Redis之缓存优化

发布时间 2023-09-10 13:45:16作者: Chimengmeng

【一】缓存的收益与成本

【1】受益

  • 1 加速读写
  • 2 降低后端负载:后端服务器通过前端缓存降低负载,业务端使用redis降低后端mysql负载

【2】成本

  • 1 数据不一致:缓存层和数据层有时间窗口不一致,和更新策略有关
  • 2 代码维护成本:多了一层缓存逻辑
  • 3 运维成本:比如使用了Redis Cluster

【3】使用场景

  • 1 降低后端负载:对高消耗的sql,join结果集/分组统计的结果做缓存
  • 2 加速请求响应:利用redis优化io响应时间
  • 3 大量写合并为批量写:如计数器先redis累加再批量写入db

【二】缓存更新策略

【1】LRU/LFU/FIFO算法剔除

  • maxmemory-policy(到了最大内存,对应的应对策略)

    • LRU -Least Recently Used,没有被使用时间最长的

    • LFU -Least Frequenty User,一定时间段内使用次数最少的

    • FIFO -First In First Out,先进先出,最早放的线删除

    • LIRS (Low Inter-reference Recency Set)是一个页替换算法,相比于LRU(Least Recently Used)和很多其他的替换算法,LIRS具有较高的性能。

      • 这是通过使用两次访问同一页之间的距离(本距离指中间被访问了多少非重复块)作为一种尺度去动态地将访问页排序,从而去做一个替换的选择
  • 配置文件中设置:

(1)LRU配置

maxmemory-policy:volatile-lru
  • (1)noeviction: 如果内存使用达到了maxmemory,client还要继续写入数据,那么就直接报错给客户端 - (2)allkeys-lru: 就是我们常说的LRU算法,移除掉最近最少使用的那些keys对应的数据,ps最长用的策略
  • (3)volatile-lru: 也是采取LRU算法,但是仅仅针对那些设置了指定存活时间(TTL)的key才会清理掉
  • (4)allkeys-random: 随机选择一些key来删除掉
  • (5)volatile-random: 随机选择一些设置了TTL的key来删除掉
  • (6)volatile-ttl: 移除掉部分keys,选择那些TTL时间比较短的keys

(2)LFU配置

  • Redis4.0之后为maxmemory_policy淘汰策略添加了两个LFU模式:
    • volatile-lfu:对有过期时间的key采用LFU淘汰算法
    • allkeys-lfu:对全部key采用LFU淘汰算法
  • 还有2个配置可以调整LFU算法:
    • lfu-log-factor 10
      • lfu-log-factor可以调整计数器counter的增长速度
      • lfu-log-factor越大,counter增长的越慢。
    • lfu-decay-time 1
      • lfu-decay-time是一个以分钟为单位的数值,可以调整counter的减少速度

【2】超时剔除

  • 例如expire,设置过期时间

【3】主动更新

  • 开发控制生命周期

【4】小结

策略 一致性 维护成本
LRU/LIRS算法剔除 最差
超时剔除 较差
主动更新
  • 1 低一致性:最大内存和淘汰策略

  • 2 高一致性:超时剔除和主动更新结合,最大内存和淘汰策略兜底

【三】缓存粒度控制

【1】从mysql获取用户信息

select * from user where id=100

【2】设置用户信息缓存

set user:100 select * from user where id=100

【3】缓存粒度

  • 缓存全部属性

  • 缓存部分重要属性

【4】小结

  • 1 通用性:全量属性更好
  • 2 占用空间:部分属性更好
  • 3 代码维护:表面上全量属性更好

【四】缓存穿透

【1】描述

  • 缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求
  • 如发起为id为“-1”的数据或id为特别大不存在的数据。
    • 这时的用户很可能是攻击者,攻击会导致数据库压力过大。

【2】解决方案

  • 1 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  • 2 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。
    • 这样可以防止攻击用户反复用同一个id暴力攻击
  • 3 通过布隆过滤器实现
  • 在接口层增加校验
    • 可以对用户进行鉴权校验,并在接口层对ID进行基础校验,例如拦截ID小于等于0的请求,以减少无效请求的访问量。
  • 设置缓存空值
    • 对于从数据库中获取不到的数据,将对应的key-value对设置为key-null,并将缓存有效时间设置较短,比如30秒。这样做可以防止攻击者不断用相同的ID进行攻击。
  • 使用布隆过滤器
    • 布隆过滤器是一种高效的数据结构,可以判断某个元素是否存在于集合中。
    • 可以利用布隆过滤器来过滤掉那些确定不存在于数据库中的请求,减轻数据库的压力。

【3】应用场景

  • 假设有一个在线电商网站,用户通过输入商品ID来查询商品信息。
  • 为防止缓存穿透,可以在后端接口中先对用户进行鉴权校验,确保只有合法用户才能访问。
  • 同时,对于无效的商品ID(如小于等于0或特别大)的请求,可以直接拦截并返回提示信息,而不是继续向数据库发起查询请求。

【补充】布隆过滤器应用

  • 布隆过滤器是一种概率型数据结构,用于判断一个元素是否可能存在于一个集合中。
    • 它通过使用位数组和多个哈希函数来实现这一功能,并且具有高效的查询速度和空间效率。
  • 在使用布隆过滤器时,首先需要初始化一个位数组,所有的位都被设置为0。
    • 然后,对于要加入到集合中的每个元素,使用多个哈希函数对其进行计算,得到一系列的哈希值。
    • 根据这些哈希值,将位数组中对应的位置设置为1。
  • 判断一个元素是否存在于集合中时,同样对该元素进行多次哈希计算
    • 如果所有对应的位置都是1,则可能存在于集合中;
    • 如果存在任何一个位置是0,则一定不存在于集合中。
  • 布隆过滤器具有一定的误判率,即会存在一定的错误判断。
    • 这是因为多个元素可能哈希到了相同的位上,导致冲突。
    • 但是,布隆过滤器能够在极小的空间开销下,处理大规模的数据集,因此在某些场景下是非常适用的。
  • 下面是一个示例演示布隆过滤器的使用:
from bitarray import bitarray
import mmh3

class BloomFilter:
    def __init__(self, size, num_hash):
        self.size = size
        self.num_hash = num_hash
        self.bit_array = bitarray(size)
        self.bit_array.setall(0)

    def add(self, item):
        for seed in range(self.num_hash):
            index = mmh3.hash(item, seed) % self.size
            self.bit_array[index] = 1

    def contains(self, item):
        for seed in range(self.num_hash):
            index = mmh3.hash(item, seed) % self.size
            if self.bit_array[index] == 0:
                return False
        return True

# 创建一个布隆过滤器,设置大小为10,使用3个哈希函数
bloom_filter = BloomFilter(10, 3)

# 添加元素到布隆过滤器
bloom_filter.add("apple")
bloom_filter.add("banana")
bloom_filter.add("orange")

# 判断某个元素是否存在于布隆过滤器中
print(bloom_filter.contains("apple"))   # 输出 True
print(bloom_filter.contains("peach"))   # 输出 False
  • 在实际应用中,布隆过滤器常被用来减轻数据库的查询压力。
    • 例如,在一个网站的用户登录场景中,可以使用布隆过滤器将那些非法的登录请求快速过滤掉,从而减少对数据库的查询次数。
  • 另一个例子是在大规模数据集的搜索引擎中,当用户输入一个关键词进行搜索时,可以先通过布隆过滤器判断该关键词是否存在于索引中,如果不存在则可以直接返回搜索结果为空,从而节省了后续的查询操作。

【五】缓存击穿

【1】描述

  • 缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力

【2】解决方案

  • 设置热点数据永远不过期。
  • 设置热点数据永远不过期
    • 将热点数据,即高频访问的数据,不设置过期时间,保持其一直有效,以减少因缓存失效而引起的数据库压力。

【3】应用场景

  • 假设有一个新闻网站,热门新闻的浏览量非常大。
  • 为了避免缓存击穿,可以将热门新闻的数据存放在缓存中,并设置其不过期,这样用户访问热门新闻时,即使缓存中的数据过期了,也能保证始终能从缓存中获取最新数据,而不需要频繁访问数据库。

【六】缓存雪崩

【1】描述

  • 缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。
  • 和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

【2】解决方案

  • 1 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  • 2 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
  • 3 设置热点数据永远不过期。
  • 设置随机的缓存过期时间
    • 将缓存数据的过期时间设置为随机值,避免多个缓存同时失效导致一次性大量请求落到数据库上。
  • 分布式部署缓存数据库
    • 如果缓存数据库是分布式部署的,可以将热点数据均匀地分布在不同的缓存数据库中,从而减轻单个数据库的压力。
  • 设置热点数据永远不过期
    • 对于热点数据,可以将其设置为永远不过期,以保证即使其他数据失效,热点数据仍然能够被缓存使用。

【3】应用场景

  • 假设有一个电影推荐平台,用户请求不同类型的电影列表。
  • 为了应对缓存雪崩,可以设置每个类型的电影列表的缓存过期时间为随机值(比如在原有过期时间基础上加上一个随机值)。
  • 同时,将不同类型的电影列表分别存放在不同的缓存数据库中,这样可以避免多个缓存同时失效而导致对数据库的大量请求。
  • 对于热门的电影类型,可以设置其数据永远不过期,以保证其始终可用。