Redis的内存淘汰策略以及持久化

1.常见的缓存置换算法

缓存与数据库不同,缓存作为其他数据源的副本存在,是为了更快速地存取数据。当数据不存在于缓存中时,就需要从数据源读取数据加载到缓存中。
缓存置换: 缓存的容量是有限的,当数据快把缓存占满的时候,需要及时地把某些数据从缓存中清除掉。最理想的情况就是去置换出未来短期内不会再次访问的数据,但是我们无法预知未来,所以只能从数据在过去的访问情况中寻找规律进行置换。
常见的缓存置换算法有以下几种:
1.Random:随机去淘汰数据。
2.Size:替换掉容量占用最大的内存。
3.FIFO(First Input First Output): 先进先出的淘汰策略,使用队列实现。
4.LRU (Least Recently Used) :淘汰最近最少使用的数据(出镜率最高的内存淘汰策略)。
5.LFU (Least frequently used):最不经常使用。区别于LRU的算法,LRU只记录上次的使用时间,LFU记录一段时间内数据的使用次数,将使用次数最少的数据淘汰掉。
缓存的效率
衡量一个缓存的好坏,有两个重要的指标:缓存命中率和缓存访问效率
缓存命中率 = 访问缓存的次数 ÷ 访问总次数。 越趋近于1,说明缓存命中率越高,性能越好。
缓存访问效率 = 命中缓存的访问时间 ÷ 平均访问时间。 值越小,说明通过缓存去访问一个数据所花费的时间越短,缓存的性能越好。

2.LRU算法的实现

实现策略:
维护一个存放数据的容器,默认容器头部是最近使用的元素,每次访问容器中的元素时,都需要将元素移动到头部,新增元素时也将数据添加到头部。当数据超过容器的容量时,就去移除容器尾部的元素。
1.简单地用链表去实现
维护一个有序单链表,越靠近链表头部的数据越是最近访问的。
查找数据时,去遍历这个链表。如果数据已存在,则将数据返回,并将数据移动到链表头部。
新增缓存时,尝试去将这条数据加入到链表中:
a. 如果此时缓存未满,则将数据加入到链表的头部。
b. 如果此时缓存已满,则将链表尾部的数据直接删除,将此数据加入到链表的头部
举例: 假设缓存的大小为5
a.缓存中目前有4个元素
缓存初始大小为4
b.新增一个元素E,此时缓存未满,直接将其加入到链表的头部
新增元素E
c.获取元素D,将元素D返回并将其移动到链表的头部
获取元素D
d.此时再向缓存中增加一个元素F,发现超过缓存的大小了,此时先将尾部的元素删除,再把F添加到链表的头部。
先删除尾部元素C
再将新元素加入到链表头
2.用Hash表 + 双向链表去实现
Hash表(key/value字典)用于定位数据位置,使访问的时间复杂度为O(1)
双向链表可用于快速地移动数据位置。
缺点是浪费一个hash表的空间。
实现图:
hash表和双向链表实现LRU缓存
代码实现:
用Java中现有的HashMap和LinkedList来实现


public class LRUCache {
    /** map 用于快速查找 */
    private Map<Integer,Integer> map;
    /** 链表 存储和淘汰key */
    private LinkedList<Integer> linked;
    /** 缓存容量 超过这个容量开始淘汰key */
    private int capacity;

    public LRUCache(int capacity){
        linked = new LinkedList();
        map = new HashMap<>(capacity);
        this.capacity = capacity;
    }

    public int get(int key){
        Integer val = map.get(key);
        if (null == val) {
            return -1;
        }
        linked.remove(Integer.valueOf(key));

        linked.addFirst(key);
        return val;
    }

    public void put(int key,int value){

        // key存在的话更新值,不存在才添加值
        Integer val = map.get(key);
        if ( null != val){
            map.put(key,value);
            linked.remove(Integer.valueOf(key));
        } else {
            if (linked.size() >= capacity){
                map.remove(linked.removeLast());
            }
            map.put(key,value);

        }
        linked.addFirst(key);

    }
}

redis中LRU算法的实现方式
redis中使用的是一种近似LRU算法,和真实的LRU算法不太一样。原因是真实的LRU算法需要消耗额外内存,需要对redis现有的数据结构进行较大的改造。
具体的实现方式为:
a. 为每个key额外增加了一个24bit的字段,用于记录这个key最后一次被访问的时间戳。
b. redis执行写操作时,当发现内存超出maxmemory时,随机采样出5个(可配置数量)key,然后去淘汰最旧的key。
c. 如果淘汰后内存还是超出maxmemory,那就继续随机采样淘汰,直到内存低于maxmemory为止。
采样数据来源通过 maxmemory-policy 配置来设置,如果是allkeys 就从所有的key字典中采样,如果是volatile就从带过期时间的key字典中随机采样。

LRU和redis实现的近似LRU的差异:
LRU和redis实现的近似LRU的差异
左一图是理论上LRU算法的应用图,其余的是redis执行的近似LRU的应用图,其中浅灰色代表被淘汰的数据,灰色表示未被淘汰的数据,绿色表示后面加入的数据。
由图中可以看出以下两点:

  1. 采样的数量越大,近似LRU的效果就越接近严格的LRU算法。

  2. 同样的采样数量下,redis3.0的近似LRU效果好于redis2.8版本,原因是redis3.0加入了淘汰池,新随机出来的key列表会和淘汰池中的key列表进行融合,淘汰掉最旧的一个key之后,

保留剩余较旧的key列表放入淘汰池中等待下一个循环。淘汰池的大小通过配置文件中的 maxmemory_samples来进行配置。
在这里插入图片描述

3.Redis的几种内存淘汰策略

当redis内存超出物理限制时,内存的数据会开始和磁盘产生频繁的交换,这样的交换会让redis的性能急剧下降。为了限制最大使用内存,redis提供了配置参数maxmemory来限制内存超出希望的大小。
在这里插入图片描述
当实际内存超出maxmemory时,redis提供了一些策略来淘汰数据:

序号配置策略策略含义
1volatile-random从已设置过期时间的数据集中随机选择数据进行淘汰。
2volatile-ttl从已设置过期时间的数据集中选择将要过期的数据淘汰。
3volatile-lru从已设置过期时间的数据集中选择最近最少使用的数据进行淘汰。
4volatile-lfu从已设置过期时间的数据集中选择最不常用的去淘汰。
5allkeys-random当内存中不足以容纳新写入数据时,在键空间中,随机移除key。
6allkeys-lru当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
7allkeys-lfu当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key。
8no-eviction不淘汰数据。

其中 1~4面向的数据集是设置了过期时间的key, 5~8面向的数据集是所有key。

4.Redis的持久化机制

redis作为缓存区别于一搬缓存的特性是redis的持久化机制,持久化机制可以保证redis宕机之后,能够根据持久化文件快速地恢复数据,保证redis的数据不会因为故障而丢失。

redis的持久化机制有两种方式:
1)RDB: 快照,一次性的全量备份,内存数据的二进制序列化形式,如果存在老的RDB文件,则新的RDB文件去覆盖掉老的RDB文件。

触发RDB的方式:
a. 通过save命令触发,save命令是一个同步阻塞的命令,如果redis的数据比较多,执行save命令时间过长,对redis的其它操作会被阻塞等待。
在这里插入图片描述
b. 通过bgsave命令触发,bgsave是一个异步非阻塞的命令,执行该命令时会fork出一个linux子进程去进行RDB文件的写入,不会影响对redis的其它操作。
在这里插入图片描述
c. 自动触发,在redis的配置文件中会有以下配置
在这里插入图片描述
3600 1 表示在3600秒内有1个key发生改变时,会生成RDB文件

300 10 表示在300秒内有10个key发生改变时,会生成RDB文件

60 10000 表示在60秒被有10000个key发生改变时,会生成RDB文件
d. 其它触发rdb的操作:

redis客户端通过shutdown命令来关闭服务时,redis服务端收到请求,然后执行同步阻塞的save命令来进行RDB快照文件的生成,执行完毕之后关闭服务器。
主从同步时,当从节点执行全量复制操作时,主节点会执行bgsave命令,并将RDB文件发送给从节点,

bgsave命令的写时复制:
通过bgsave命令进行快照时,redis一边处理收到的数据请求(查找、新增、修改、删除key),一边要进行快照持久化。此时如何保证持久化数据的不出问题?
(比如一个大型的hash字典正在持久化,此时一个请求过来将它删掉了,此时还没持久化完)。
redis使用操作系统的多进程COW(Copy On Write 写时复制)机制来实现快照持久化。

redis在持久化的时候会调用Linux的fork函数fork出一个子进程。子进程和父进程共享内存里面的代码段和数据段。Linux操作系统通过这种机制来解决资源。
此时父进程专注于处理收到的数据请求,子进程去持久化数据,当有请求修改数据时,父进程会先将要修改的数据复制一份,然后再进行修改。这样子进程对应的数据是没有变化的,还是进程产生那一瞬间时的数据。(数据段是由很多操作系统的页面组合而成,父进程修改数据时,会将复制一份数据所在页面然后进行修改,子进程还是备份原页面。)
RDB持久化的缺点:
如果redis宕机了,将丢失最近一次执行快照到宕机时的数据。

2)AOF:可追加文件。连续的增量备份,记录的是内存数据修改的指令记录文本。
AOF原理:
在这里插入图片描述
通过配置中的 appendonly 选项来启用aof:
在这里插入图片描述
通过 appendfsync 来配置AOF的三种策略:
在这里插入图片描述

命令含义优点缺点
alwaysredis每次收到增删改的命令时,都进行一次AOF不丢失数据IO开销大
everysec每1秒进行一次AOF写入相比always来说IO开销较小容易丢失1秒内的数据
no不进行AOF写入,由操作系统去决定什么时候去进行AOF的写入不可靠,不可控

不去配置的话 AOF默认的策略是everysec
AOF重写:
随着redis长期的运行,AOF日志会变得十分庞大,所以需要通过AOF重写来给AOF日志进行瘦身。

AOF重写的主要原理就是合并指令和去掉不需要的指令。
在这里插入图片描述
AOF重写的主要作用就是减少硬盘占用量和提高恢复重启时的速度。

实现AOF重写的方式:
1.用户执行bgrewriteaof命令进行重写
2.通过配置文件配置选项来使redis自动进行AOF重写
在这里插入图片描述

配置项含义
auto-aof-rewrite-percentage 100AOF文件的体积比上一次重写后的体积大了一倍(100%)
auto-aof-rewrite-min-size 64mbAOF文件的体积大于64M

同时满足以上两个选项才进行AOF重写操作。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值