Redis的内存淘汰策略以及持久化
- 1.常见的缓存置换算法
- 2.LRU算法的实现
- 3.Redis的几种内存淘汰策略
- 4.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个元素
b.新增一个元素E,此时缓存未满,直接将其加入到链表的头部
c.获取元素D,将元素D返回并将其移动到链表的头部
d.此时再向缓存中增加一个元素F,发现超过缓存的大小了,此时先将尾部的元素删除,再把F添加到链表的头部。
2.用Hash表 + 双向链表去实现
Hash表(key/value字典)用于定位数据位置,使访问的时间复杂度为O(1)
双向链表可用于快速地移动数据位置。
缺点是浪费一个hash表的空间。
实现图:
代码实现:
用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的效果就越接近严格的LRU算法。
- 同样的采样数量下,redis3.0的近似LRU效果好于redis2.8版本,原因是redis3.0加入了淘汰池,新随机出来的key列表会和淘汰池中的key列表进行融合,淘汰掉最旧的一个key之后,
保留剩余较旧的key列表放入淘汰池中等待下一个循环。淘汰池的大小通过配置文件中的 maxmemory_samples来进行配置。
3.Redis的几种内存淘汰策略
当redis内存超出物理限制时,内存的数据会开始和磁盘产生频繁的交换,这样的交换会让redis的性能急剧下降。为了限制最大使用内存,redis提供了配置参数maxmemory来限制内存超出希望的大小。
当实际内存超出maxmemory时,redis提供了一些策略来淘汰数据:
序号 | 配置策略 | 策略含义 |
1 | volatile-random | 从已设置过期时间的数据集中随机选择数据进行淘汰。 |
2 | volatile-ttl | 从已设置过期时间的数据集中选择将要过期的数据淘汰。 |
3 | volatile-lru | 从已设置过期时间的数据集中选择最近最少使用的数据进行淘汰。 |
4 | volatile-lfu | 从已设置过期时间的数据集中选择最不常用的去淘汰。 |
5 | allkeys-random | 当内存中不足以容纳新写入数据时,在键空间中,随机移除key。 |
6 | allkeys-lru | 当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。 |
7 | allkeys-lfu | 当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key。 |
8 | no-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的三种策略:
命令 | 含义 | 优点 | 缺点 |
always | redis每次收到增删改的命令时,都进行一次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 100 | AOF文件的体积比上一次重写后的体积大了一倍(100%) |
auto-aof-rewrite-min-size 64mb | AOF文件的体积大于64M |
同时满足以上两个选项才进行AOF重写操作。