栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Redis深入学习——缓存

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Redis深入学习——缓存

Redis的缓存击穿、缓存穿透、缓存雪崩问题

缓存击穿

缓存击穿指的是有一个热点数据的key在缓存中失效时,大量的请求将会直接访问到数据库,会使数据库层面的压力大大增加。

解决方案:

       Key在缓存中最常见的失效问题就是key设置了过期时间,过期时间到了,key失效了,针对这一层面,我们可以将这个热点数据的key的过期时间设置为永不过期。但是这样会导致redis占用的内存逐渐增加,降低内存利用率。

       所以还可以利用互斥锁,保证同一时间内只有一个客户端的一个请求才能访问数据库,查询到数据之后就立马缓存到redis中,然后解开锁,而其他的请求则访问redis缓存中的该数据。

       但是这样又会有一个问题,就是当第一个请求抢到了锁,在访问数据库之前,因为各种原因比如断电挂掉了,这个时候没有人去解锁,所有的请求就会继续等待锁。这就导致了加锁进程死亡,锁无法释放,出现了死锁的问题。

       处于这个问题,我们可以给redis的分布式锁加上过期时间,当加锁进程死亡,没有人去解锁,锁长时间存在,超过了过期时间则锁自动解锁。

       如果先设置锁在加过期时间,中间可能会出现一些异常,所以使用redis本身支持的set命令同时设置锁和过期时间。

缓存雪崩

缓存雪崩相当与缓存击穿的大面积升级版,缓存击穿时大量并发请求访问同一个失效的热点数据key,而缓存雪崩指的是大面积的,大量的key早同一时间内过期,导致大量并发请求直接访问数据库,导致数据库层面压力倍增。

在现实应用中,缓存雪崩发生的概率是远远大于缓存击穿的,因为正常使用中,大部分的公司大量并发请求访问同一个热点数据的概率没有那么高。

解决方案:

       导致出现这种情况出现的原因就是大量的key在同一时间失效,最简单直接的解决方法就是和缓存击穿一样,直接将key的过期时间延长或者设置为永不过期,这样做的缺点也很明显,就是和缓存击穿中的一样,占用更多的内存,导致内存利用率降低。

       除此之外,就是将这些key的过期时间分散开来,这个思路有两种解决方案:

  1. 从源头解决问题,将这些key的过期时间从设置的时候就分散开,在设置过期时间的时候,在可接受的时间范围里,随机设置key的过期时间。
  2. 从最后解决问题,针对于必须要在固定时间内让key失效的场景,比如固定时间更新排行榜之类的,则可以在这些key在这个固定失效时间失效时设置延时随机时间,让每个时间分散开来,先让一部分请求访问数据库,将查询出的数据存储起来。

缓存穿透

缓存穿透和缓存击穿在名称上是有些想象的,但是他们最大的差别就是,缓存击穿是redis中查询不到数据,去数据库查数据能查到数据,而缓存穿透是redis中和数据库中都没有数据。

发生缓存穿透时,由于redis中没有数据,对于数据的保护作用没有起到作用,会导致数据库的压力增加,同时又因为数据库中同样没有数据,所以这些请求在数据库中访问不到数据,不能在redis中另设缓存,同样这些请求也不会存在返回值,这就造成了网络与计算机资源的浪费。

解决方案:

       在接口访问对用户做校验,比如接口传参,登录状态,若干时间内访问接口的次数。

       再来就是两种过滤器了:布隆过滤器和布谷鸟过滤器。

       实际上,这两种过滤器都时在请求访问redis前判断redis中是否存在这个key,如果没有就直接返回。

布隆过滤器:

       布隆过滤器实际上就是一个比较长的二进制数组和一系列随机hash算法映射函数,类似与bit数组,主要用来判断一个元素是否存在。

       优点:高效的插入和查询效率,占用的空间少。

       缺点:不能删除元素、存在误判,因为不同的数据也有可能有相同的hash值。

自定义的布隆过滤器 

public class MyBloomFilter {

    
    private static final int DEFAULT_SIZE = 2 << 28;

    
    private static BitSet bitSet = new BitSet(DEFAULT_SIZE);

    
    private static final int[] ints = {1, 6, 16, 38, 68, 98};

    
    public void add(Object key) {
        Arrays.stream(ints).forEach(i -> bitSet.set(hash(key, i)));
    }

    
    public boolean isContain(Object key) {
        boolean flag = true;
        for (int i : ints) {
            //短路与,只要有一个bit位为false,则返回false
            flag = flag && bitSet.get(hash(key, i));
        }
        return flag;
    }

    
    private int hash(Object key, int i) {
        int h;
        return key == null ? 0 : (i * (DEFAULT_SIZE - 1) & ((h = key.hashCode()) ^ (h >>> 16)));
    }
}

这里的hash算法主要借鉴了hashmap中的hash扰动算法,通过数组长度减一和右移16位来达到使运算出的hash值更为分散,减少hash冲突的产生。

因为hash算法运算hash值本身的原因,不同的数据可能会出现相同的hash值,也就是说,布隆过滤器判断存在的key不一定存在,但是判断不存在的key一定不存在。

由于布隆过滤存在不能删除的确定,所以一个布隆过滤器的升级版出现了,就是Counting Bloom  Filter。

这里简单描述一下为什么布隆过滤器不能进行删除操作,当过滤器中有一个Object1的k个hash函数算出的值在过滤器中数组的分布是第1位、第8位、第12位为1,这个时候又有一个Object2的k个hash函数算出的值在数组的分布为第2位、第7位、第12位为1,然后我将Object1删除,也就是将第1位、第8位、第12位置为0,但是这个时候Object2的第12位也会被置为0,这个时候就会导致判断Object2不存在了。

针对于这个问题,一个Bloom Filter的升级版Counting Bloom Filter就应运而生,Counting Bloom Filter的理念就是将原本布隆过滤器中的位数组的每一位扩展为一个计数器,在往其中插入数据时给这个计数器(Count)加一,删除元素时给这个Count减一。

虽然这样解决了删除元素的问题,但是这比之前多占用了了好几倍的存储空间。

布谷鸟过滤器

布谷鸟过滤器源于布谷鸟hash算法,布谷鸟hash算法是存在两张hash表,两个hash算法,分别算出在两张hash表中的位置。

如果这两个位置都是空的,则会随机选择一个位置存入。

如果有一个位置被占了,则会存入另一个位置。

如果两个位置都被占了,那么就会在这两个位置之中随机选择一个位置,将原本的数据踢走,然后自己占据这个位置。同时这个被踢出的数据会去另一张表找之前算出的位置,如果这个位置同样被占据,那么就会将当前位置的数据踢出,被踢出的数据就会按照之前的流程,直到所有的数据都存放了下来。

这样就不会有布隆过滤器的不支持删除的问题了,但是同样因为hash冲突,会存在误删的结果。

虽然布谷鸟访问内存次数低,hash函数计算简单,空间性能在错误率小于3%时优于布隆过滤器,但是他与布隆过滤器一样,内存空间地址不连续,并且当数据量比较大的时候可能会出现循环踢出数据的现象,导致插入失败。

Redis 的持久化

之前说到缓存雪崩是大量的key设置了相同的过期时间,在同一时间失效,导致大量请求直接打到数据库,造成数据库压力过大,甚至会导致数据库直接崩掉,导致系统瘫痪。

但是,redis直接挂掉也同样会导致大量的key同一时间失效,造成缓存雪崩。

Redis挂掉,我们第一个想到的就是赶紧恢复,那么这就需要用到redis的持久化。

除了赶紧恢复之外,我们还可以设置一个redis集群,当前redis挂了(Master),那么集群中的redis(slave)就赶紧顶上,前仆后继(升级位Master)。但是挂掉的redis我们也不能不管,我们也要将其恢复,同样也要用到redis的持久化。

Redis持久化有两种方式:RDB、AOF;

RDB方式:

RDB持久化方式就是将redis中的全部数据生成快照,保存在硬盘上。

手动触发:

命令:save 、bgsave

Save:当使用save命令来持久化redis时,会将redis阻塞住,当redis中存有大量数据时,使用save命令就会导致redis长时间阻塞,导致redis在这一段时间不可用。所以一般不推荐使用该命令。

Bgsave:bgsave与save的区别就在于bgsave会进行fork操作创建一个子线程来进行RDB文件生成,以此实现持久化。自动触发时使用的命令都是bgsave

bgsave在进行fork操作创建子线程之前会检查是否有子线程,如果有就直接结束这个命令,没有就会fork创建子线程。

在进行fork操作时依旧会阻塞redis进程,但是当fork操作创建完子线程后,bgsave命令就算是结束了。之后redis进程就不会被阻塞,也可以响应其他的命令。子线程在根据redis进程的内存生成快照文件,将原本的RDB文件替换掉之后,会通过信号量通知redis已完成。

自动触发:

  1. 在配置文件中设置save  m  n,表示在m秒内发生了n次数据修改,就会自动触发bgsave命令。

Redis配置文件相关默认配置如下:

###################SNAPSHOTTING#####################
#
# Save the DB on disk:
#
#   save  
#
#   Will save the DB if both the given number of seconds and the given
#   number of write operations against the DB occurred.
#
#   In the example below the behaviour will be to save:
#   after 900 sec (15 min) if at least 1 key changed
#   after 300 sec (5 min) if at least 10 keys changed
#   after 60 sec if at least 10000 keys changed
#
#   Note: you can disable saving completely by commenting out all "save" lines.
#
#   It is also possible to remove all the previously configured save
#   points by adding a save directive with a single empty string argument
#   like in the following example:
#
#   save ""

#   900秒内发生了1次数据修改
save 900 1
#   300秒内发生了10次数据修改
save 300 10
#   60秒内发生了10000次数据修改
save 60 10000
  1. 在从节点做全量复制时,主节点也会自动触发bgsave命令,将生成的RDB文件发送给从节点。
  2. 执行debug reload命令时,会触发bgsave命令。
  3. 执行shutdown命令时,如果redis没有开启AOF持久化,则会触发bgsave命令进行RDB持久化

AOF方式:

AOF方式是将redis中的写操作追加写入日志中,需要恢复数据的时候直接执行AOF日志中的这些命令,解决了实时持久化的问题。

  1. 命令追加:所有写操作的命令都会被追加到aof_buf缓冲区中。
  2. 文件同步:将aof_buf缓冲区中的数据同步写入到AOF文件中。

文件同步策略:

(1)always:每一次写入都会同步一次

(2)no:每次写入都不进行同步,由操作系统决定

(3)everysec:每秒同步一次、

3. 文件重写:

随着时间的变化,AOF持久化将所有的写入命令存入AOF文件,到这AOF文件越来越大。这时候就需要文件重写,将冗余的中间命令去掉,保存最终的写入操作。

在redis进程fork一个子进程来进行文件压缩重写的时候,同样也会有RDB持久化中存在的一个问题,就是在这个时候,又有新的写入发生,就会导致重写后的文件与当前的文件不一致。

为此,AOF重写文件时还设置了一个aof_rewrite_buf缓冲区,用来存储在文件重写期间新写入的命令,在文件重写结束后将这些命令写入到重写后的AOF文件中,最后重写的AOF文件将原来的文件替换。

我们可以通过使用bgrewiteaof命令来手动触发AOF的文件重写命令。

但在日常使用中,我们通常使用自动触发,根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage配置确定自动触发的时机。auto-aof-rewrite-min-size表示运行AOF重写时文件大小的最小值,默认为64MB;auto-aof-rewrite-percentage表示当前AOF文件大小和上一次重写后AOF文件大小的比值的最小值,默认为100。只用前两者同时超过时才会自动触发文件重写。

AOF和RDB区别:

RDB:

虽然RDB身为二进制文件结构紧凑,将全部的数据都持久化保存了起来,但是RDB持久化的save和bgsave命令所对应的redis阻塞和fork创建子进程的重量级操作是做不到实时持久化,也导致了进行RDB持久化操作的周期稍微比较长,这也导致了在这个周期内,可能会发生丢失数据的状况。

但是RDB文件大小、恢复速度是优于AOF的。而且RDB时保存的某一时间的全部数据,其实是非常适合用来做备份的。可以在不同时间点都保存一个RDB持久化文件,当redis遇上问题是或者有需要的时候,我们可以将redis数据还原到不同时间点的版本。

同时,因为RDB是一个内容非常紧凑的二进制文件,所以它也很适合灾难恢复,我们可以将其加密后传到其他的数据中心保存。

AOF:

AOF文件的大小和速度是差于RDB文件的。但是AOF的耐久性和安全性都是优于RDB的。AOF是可以通过设置fysnc策略来设置为每秒一次实现实时的持久化的。

AOF 文件是一个只进行追加操作的日志文件, 因此对 AOF 文件的写入不需要进行 seek , 即使日志因为某些原因而包含了未写入完整的命令,比如写入时磁盘已满,写入中途停机等等, redis-check-aof 工具也可以轻易地修复这种问题。

Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/490673.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号