一、Redis的内存淘汰策略有哪些?

Redis将数据存储在内存中,但是内存有限,当存储的数据超过内存容量时,需要对缓存的数据进行剔除。

1.1 淘汰算法一般有以下几种(Redis有使用LRU):

  • FIFO:淘汰最早数据
  • LRU:剔除最近最少使用
  • LFU:剔除最近使用频率最低的数据

1.2 Redis内存淘汰策略,有以下几种:

noeviction: 返回错误。当内存达到限制,客户端尝试执行的命令(大部分的写入指令,但DEL和几个例外)

allkeys-lru: 尝试回收最少使用的键(LRU)(适用所有缓存数据,不管是否设置过期时间)

volatile-lru: 尝试回收最少使用的键(LRU),(仅限于在过期集合的键)。

allkeys-random: 随机回收数据(适用所有缓存数据,不管是否设置过期时间)

volatile-random: 随机回收数据(仅限于在过期集合的键)。

volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键。

二、自己如何实现一个LRU算法?

要不要一上来就这么猛!!!(这个问题在面试阿里的时候被问过)!

首先,我们需要知道什么是LRU算法。

LRU算法: 剔除最近最少使用的数据(具体的实现方式有很多,有兴趣的可以了解一下)。

有时候可能会让你手写LRU算法,如果从零手写,那简直........。

在JDK中LinkedHashMap可以作为LRU算法以及插入顺序的实现,LinkedHashMap继承自HashMap,底层结合hash表和双向链表,元素的插入和查询等操作通过计算hash值找到其数组位置,在做插入或则查询操作是,将元素插入到链表的表头(当然得先删除链表中的老元素),如果容量满了,则删除在链表表尾的元素即是。

所以我们可以通过继承LinkedHashMap实现简易版的LRU算法:(我们只需要知道原理即可)

public class LRUTest<K, V> extends LinkedHashMap<K, V> { 
     private static final long serialVersionUID = 1L;  
      /** 
      *设置容量大小:不能扩容,一旦数据放满,就只能淘汰最近最少使用的数据(LRU算法)     
      */ 
     private final int size; 
     public LRUTest(int size) {   
         /** 
          * true:告诉LinkedHashMap,按访问循序进行排序,最近访问的放在头部,最老访问的放末尾。     
         */    
         super((int) Math.ceil(size / 0.8) + 1, 0.8f, true);    
         this.size = size;  
     }  
     /**
      *LinkedHashMap 默认返回false:永不移除数据   
      *这里覆写removeEldestEntry方法,当添加数据数量>size(最大容量时,淘汰最近最少使用的数据)   
      */  
      @Override  
      protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {    
           return size()>size;  
      }
}
复制代码

三、Redis是怎么进行持久化的?Redis数据都在内存中,断电或者重启,数据会不会丢失?

Redis会定时将数据持久化到磁盘上,所以redis大部分数据都不会丢失(注意:是否丢失数据, 需根据AOF持久化配置决定redis有RDB和AOF两种持久化方式:

3.1 RDB持久化

1.RDB 持久化机制,周期性的对内存中的数据生成快照,每次快照都是内存中一份完整数据的拷贝,所以会比较慢,

2.RDB适合做冷备。如杭州一份数据,广州一份数据,就算发生无法避免的自然灾害,也不会两个地方都一起挂吧。

3.1.1 优点:

1.RDB每次生成一个数据文件,每个数据文件分别都代表了某一时刻 Redis里面的数据,可以将文件上传到其他服务器做冷备,一旦出现问题,可以利用Redis恢复到前面具体时刻的数据。

2.RDB是内存中数据的拷贝,所以他在数据恢复的时候速度比AOF来的快,而AOF记录的追加的命令,回复的时候需要一条一条的执行所以比较慢。

3.1.2 缺点

1.RDB都是快照文件,都是默认五分钟甚至更久的时间才会生成一次,这意味着你这次同步到下次同步这中间五分钟的数据都很可能全部丢失掉。而AOF则最多丢一秒的数据

2.RDB需要fork了一个子进程去生成一个快照文件,虽然子进程对Redis的性能影响较小。但是如果内存中数据较多,生成的快照文件较大。客户端可能会暂停几毫秒甚至几秒。假如正在做秒杀或者高并发任务。惨了。。。。

3.1.3 RDB相关配置:


save 900 1 //900秒内,如果超过1个key被修改,则发起快照保存

save 300 10 //300秒内,如果超过10个key被修改,则发起快照保存

save 60 10000 //60秒内,如果1万个key被修改,则发起快照保存

3.2 AOF持久化

AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,因为这个模式是只追加的方式,所以没有任何磁盘寻址的开销,所以很快,有点像Mysql中的binlog。AOF适合做热备。

3.2.1 优点:

1.RDB五分钟(默认)一次生成快照,但是AOF是一秒一次(一般使用这个配置)去通过一个后台的线程fsync操作,那最多丢这一秒的数据。

2.AOF以append-only的方式追加的方式写数据,没有磁盘寻址的开销,写入性能非常快

3.2.2 缺点:

因为AOF是以命令的方式追加,所以AOF文件比RDB要大得多,如果以AOF方式恢复数据,速度会比RDB慢得多。

3.2.3 AOF文件重写的实现原理:

AOF 持久化是通过保存被执行的写命令来记录数据库状态的,所以AOF文件的大小随着时间的流逝一定会越来越大。为了解决AOF文件体积膨胀的问题,Redis提供了AOF重写功能:Redis服务器可以创建一个新的AOF文件来替代现有的AOF文件,新旧两个文件所保存的数据库状态是相同的,但是新的AOF文件不会包含任何浪费空间的冗余命令,通常体积会较旧AOF文件小很多。

AOF重写并不需要对原有AOF文件进行任何的读取,写入,分析等操作,这个功能是通过读取服务器当前的数据库状态来实现的。

127.0.0.1:6379> RPUSH list "C"

(integer) 1

127.0.0.1:6379> RPUSH list "D" "E"

(integer) 3

127.0.0.1:6379> LPOP list

"C"

127.0.0.1:6379> LPOP list

"D"

127.0.0.1:6379> RPUSH list "F" "G"

(integer) 3

127.0.0.1:6379> LRANGE list 0 -1

"E"

"F"

"G"

当前列表list在数据库中的值为[ "E", "F", "G"]。要使用尽量少的命令来记录list键的状态,最简单的方式不是去读取和分析现有AOF文件的内容,而是直接读取list键在数据库中的当前值,然后用一条RPUSH list "E" "F" "G"代替前面的6条命令。

3.2.4 使用子进程进行AOF重写的问题:

子进程在进行AOF重写期间,服务器进程还要继续处理命令请求,而新的命令可能对现有的数据进行修改,这会让当前数据库的数据和重写后的AOF文件中的数据不一致。

3.2.5 如何修正:

为了解决这种数据不一致的问题,Redis增加了一个AOF重写缓存,这个缓存在fork出子进程之后开始启用,Redis服务器主进程在执行完写命令之后,会同时将这个写命令追加到AOF缓冲区和AOF重写缓冲区。

AOF缓冲区的内容会定期被写入和同步到AOF文件中,对现有的AOF文件的处理工作会正常进行。

即子进程在执行AOF重写时,主进程需要执行以下三个工作:

  • 执行client发来的命令请求;
  • 将写命令追加到现有的AOF文件中;
  • 将写命令追加到AOF重写缓存中。

AOF相关配置:

appendonly no //appendonly修改为yes,开启AOF持久化

appendfsync everysec(默认配置) //AOF追加方式

appendfsync 可选参数如下:

  • always: 每次操作都会立即写入aof文件中,不会丢失数据
  • everysec: 每秒持久化一次(默认配置),可能会丢失1秒钟的数据
  • no: 不主动进行同步操作,默认30s一次,可能丢失30秒的数据

auto-aof-rewrite-min-size 64mb://aof文件达到64m触发AOF重写,文件太小恢复速度本来就很快,重写的意义不大

auto-aof-rewrite-percentage 100://aof文件上一次重写后文件大小增长了100%则再次触发重写

3.3 Redis 4.0混合持久化

Redis4.0以前,如果没有开启AOF,重启Redis使用RDB来恢复内存状态,会丢失大量数据。如果开启了AOF,使用AOF恢复数据。但是AOF文件可能很大,恢复会很慢,启动需要花费很长时间。Redis4.0为了解决这个问题,带来了新的持久化选项——混合持久化。

开启混合持久配置:

aof-use-rdb-preamble yes
复制代码

如果开启了混合持久化,AOF在重写时,不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将重写这一刻之前的内存做rdb快照处理,并且将RDB快照内容和增量的AOF修改内存数据的命令存在一起,都写入新的AOF文件,新的AOF文件一开始不叫appendonly.aof,等到重写完成后,新的AOF文件才会进行改名,原子的覆盖原有的AOF文件,完成新旧两个AOF文件的替换。

Redis重启的时候,先加载RDB文件,然后再重放增量的AOF日志,这样既不会出现数据丢失。而且可以完全替代之前的AOF全量文件重放,因此重启效率大幅得到提高。

四、Redis过期删除策略

Redis可以对key设置过期时间,如果key到达过期时间,Redis是如何删除的?

4.1 Redis过期时间判断

Redis会将设置了过期时间的键和过期时间存放到一个过期字典中。当我们查询一个键时,Redis便首先检查该键是否存在过期字典中,如果存在,那就获取其过期时间。然后将过期时间和当前系统时间进行比对,比系统时间大,那就没有过期;反之判定过期。

4.2 Redis过期删除策略

Redis采用惰性删除+定期删除

4.3 定期删除:

以一定的频率运行,每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键(并不是一次运行就检查所有的库,所有的键,而是随机检查一定数量的键)

在Redis2.6版本中,规定每秒运行10次,大概100ms运行一次。在Redis2.8版本后,可以通过修改配置文件redis.conf 的 hz 选项来调整这个次数

4.3.1 优点:

可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。

4.3.2 缺点:

难以确定删除操作执行的时长和频率。如果执行的太频繁对CPU不友好。如果执行频率过低,那又和惰性删除一样了,过期键占用的内存不会及时得到释放。另外最重要的是,在获取某个键时,如果某个键的过期时间已经到了,但是还没执行定期删除,那么就会返回这个键的值,这是业务不能忍受的错误。

4.4 惰性删除:

Redis的惰性删除策略由 db.c/expireIfNeeded 函数实现,所有键读写命令执行之前都会调用 expireIfNeeded 函数对其进行检查,如果过期,则删除该键,然后执行键不存在的操作;未过期则不作操作,继续执行原有的命令。

4.4.1 优点:

对 CPU友好,我们只会在使用该键时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查。

4.4.2 缺点:

如果一个键已经过期,但是一直没有使用,该键就会一直存在内存中,内存永远不会释放。如有较多这样的过期键,容易造成内存泄漏。

五、说说Redis和Memcache的区别?

现在大多数公司基本都使用Redis,很少公司使Memcache,所以这个问题面试的时候问的较少,基本属于背多分,我们只需要掌握几点主要区别就好。


Redis 支持持久化,所以 Redis 不仅仅可以用作缓存,也可以用作 NoSQL 数据库。而Memcache 把数据全部存在内存之中,断电后数据会丢失。

  1. redis在数据类型支持上要比Memcache多,Memcache只支持 K-V 结构,而Redis除了支持 K-V 之外,还支持多种数据格式,例如 list、set、sorted set、hash 等。
  2. Memcache单个key-value大小有限,一个value最大只支持1MB,而Redis最大支持512MB
  3. Redis 提供主从同步机制,以及 Cluster 集群部署能力,能够提供高可用服务。


作者:Java面试公开课
链接:https://juejin.cn/post/6991075520704167966
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。