redis面试题

最后更新:2020-04-16

一个字符串类型的值能存储最大容量是多少

https://redis.io/topics/data-types

String类型:一个String类型的value最大可以存储512M

Lists类型:list的元素个数最多为2^32-1个,也就是4294967295个。

Sets类型:元素个数最多为2^32-1个,也就是4294967295个。

Hashes类型:键值对个数最多为2^32-1个,也就是4294967295个。

Sorted sets类型:跟Sets类型相似。

Redis 的持久化机制是什么?各自的优缺点?

https://edgar615.github.io/redis-rdb.html

https://edgar615.github.io/redis-aof.html

RDB快照、AOF日志、RDB和AOF混合持久化,

RDB是全量快照,可以通过配置定时执行,也可以手动通过命令执行。它的文件很小,适合备份,启动时加载RDB文件比加载AOF文件要更快;但是它不能做到实时持久化,数据有丢失风险

AOF将对redis的写入命令追加到日志文件,先写缓冲池,在写磁盘。它将日志最终刷到新磁盘有3种方式:

  • appendfsync always #命令写入aof_buf后调用系统fsync同步到AOF文件,fsync完成后线程返回
  • appendfsync everysec #命令写入aof_buf后调用系统write操作,write完成后线程返回,fsync同步文件操作由专门的线程每秒调用一次
  • appendfsync no #令写入aof_buf后调用系统write操作,不对AOF文件做fsyunc同步,同步磁盘操作由操作系统负责,通常同步周期最长30秒

根据刷盘的设置不同,也会有丢失数据的风险。

AOF也支持重写,用了压缩日志文件

当redis做RDB或AOF重写时,一个必不可少的操作就是执行fork操作创建应子进程。对于大多数操作系统来说fork是一个重量级操作。虽然fork创建的子进程不需要拷贝父进程的物理内存空间,但是会复制父进程的空间内存表。例如对于10GB的redis进程,需要复制大约20MB的内存页表,因此fork操作耗时跟进程总内存量息息相关 fork耗时跟内存量成正比,建议控制每个redis实例的内存(每个页表条目消耗 8 个字节)

Redis 过期键的删除策略

https://edgar615.github.io/redis-expire-policy.html

Redis支持两种方式:

  1. 定期删除:redis 会把所有过期的键值对加入到 expires字典。redis内部有一个定时任务,100毫秒执行一次,会在定时任务的回调函数中执行清理任务,每次只随机清理部分过期key,每次清理时间不能超过CPU时间的25%,如果消耗过大,会自动退出清理过程。因此定期删除不能保证过期key都被清理。del key 也是利用的expire机制
  2. 惰性删除 当访问一个 Key 时,才判断该 Key 是否过期,过期则删除。但是这种方式对内存不友好

但是redis的删除策略存在一个问题:如果过期 Key 较多,定期删除漏掉了一部分,而且也没有及时去查,即没有走惰性删除,那么就会有大量的过期 Key 堆积在内存中,导致 Redis 内存耗尽。

Redis 的回收策略(淘汰策略)

https://edgar615.github.io/redis-maxmemory-policy.html

Redis 会在每一次处理命令的时候(processCommand 函数调用 freeMemoryIfNeeded)判断当前 Redis 是否达到了内存的最大限制,如果达到限制,则使用对应的算法去处理需要删除的 Key

  • noeviction 默认值,不会删除任何数据,拒绝所有写入操作并返回错误信息。此时redis只响应读操作
  • volatile-lru 首先通过LRU算法从设置了过期时间的键集合中驱逐最久没有使用的键,直到腾出足够的空间为止。如果没有可删除的键对象,回退到noeviction策略
  • allkeys-lru 首先通过LRU算法驱逐最久没有使用的键根据,不管数据有没有设置超时属性,直到腾出足够空间为止
  • allkeys-random 随机删除所有键,直到腾出足够空间为止
  • volatile-random 随机删除过期键,直到腾出足够空间为止
  • volatile-ttl 根据键值对象的TTL过期时间,删除最近将要过期的数据,如果没有,回退到noeviction策略
  • volatile-lfu 从所有配置了过期时间的键中驱逐使用频率最少的键,如果没有,回退到noeviction策略
  • allkeys-lfu 从所有键中驱逐使用频率最少的键

简单归纳一下淘汰策略:

  1. 默认不做任何处理,拒绝写入操作,只响应读操作
  2. 按照LRU算法淘汰最久没有使用的key,key的选择可以是所有key或者设置了过期时间的key
  3. 按照LFU算法淘汰使用频率最少的key,key的选择可以是所有key或者设置了过期时间的key
  4. 按照随机算法淘汰key,key的选择可以是所有key或者设置了过期时间的key
  5. 根据key的过期时间淘汰最近要过期的key

Pipeline 有什么好处

批量执行指令、节省多次IO的时间

Redis key 永久有效怎么设置

persist key

Redis 的同步机制

https://edgar615.github.io/redis-slave.html

  1. 从节点执行 slaveof 命令。
  2. 从节点只是保存了 slaveof 命令中主节点的信息,并没有立即发起复制。
  3. 从节点内部的定时任务发现有主节点的信息,开始使用 socket 连接主节点。
  4. 连接建立成功后,发送 ping 命令,希望得到 pong 命令响应,否则会进行重连。
  5. 如果主节点设置了权限,那么就需要进行权限验证,如果验证失败,复制终止。
  6. 权限验证通过后,进行数据同步,这是耗时最长的操作,主节点将把所有的数据全部发送给从节点。
  7. 当主节点把当前的数据同步给从节点后,便完成了复制的建立流程。接下来,主节点就会持续的把写命令发送给从节点,保证主从数据一致性

redis内部通过psync完成数据同步,当启动slave节点时,它会发送psync命令给主节点,从向主传递主节点的runid以及自己的偏移量。

在数据同步阶段主节点新写入的命令会单独记录在buffer,然后当RDB文件加载完毕之后,会通过偏移量对比将这个期间产生的写入值同步给slave

redis的复制分为全量复制和部分复制两种

全量复制的开销很大,假如master和slave网络发生了抖动,那一段时间内这些数据就会丢失,对于slave来说这段时间master更新的数据是不知道的。最简单的方式就是再做一次全量复制,从而获取到最新的数据。redis会了优化这个问题,实现了部分复制的方案

部分复制:当从节点再次连上主节点后,如果条件允许,主节点会补发丢失数据给从节点。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销,需要注意的是,如果网络中断时间过长,造成主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制

复制积压缓冲区:由主节点维护的、固定长度的、先进先出(FIFO)队列,默认大小 1MB。当主节点开始有从节点时创建,其作用是备份主节点最近发送给从节点的数据。注意,无论主节点有一个还是多个从节点,都只需要一个复制积压缓冲区。由于该缓冲区长度固定且有限,因此可以备份的写命令也有限,当主从节点 offset 的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。

从节点在心跳中回复主节点偏移量

Redis 常见性能问题和解决方案

通过redis-cli --bigkeys发现大对象

使用info stats检查lastest_fork_usec指标,获取redis最近一次fork操作耗时

检查日志是否有AOF的阻塞日志,然后通过info persistence检查aof_delay_fsync指标

检查当前连接数、拒绝连接数

监控内存碎片率,避免swap

哪些办法可以降低 Redis 的内存使用情况

尽量减少key的长度

尽量使用合适的编码方式

  • 如果一个字符串对象保存的是一个字符串值,并且长度大于32字节,那么该字符串对象将使用 SDS 进行保存,并将对象的编码设置为 raw。如果字符串的长度小于32字节,那么字符串对象将使用embstr 编码方式来保存。
  • 列表对象保存的所有元素的长度都小于 64 字节且元素数量小于512个使用 ziplist 编码,否则使用linkedlist。
  • 集合对象保存的元素都是整数且元素数量小于512个使用 intset编码,否则使用dict。
  • 有序集合对象保存的所有元素的长度都小于 64 字节且元素数量小于128个使用 ziplist 编码,否则使用linkedlist。
  • 哈希对象保存的所有字符串元素的长度都小于 64 字节且键值对数量小于512个使用 ziplist 编码,否则使用dict。

使用过 Redis 分布式锁么

https://edgar615.github.io/distributed-lock.html

加锁

SET resource_name my_random_value NX PX 30000
  • my_random_value 在所有的客户端和请求锁的请求中必须唯一,因为加锁和解锁必须是同一个客户端
  • NX 在指定的 key 不存在时,为 key 设置指定的值。保证了只有第一个请求的客户端能持有锁,而其它客户端在锁被释放之前都无法获得锁,满足互斥性。
  • PX 指定key的过期时间,单位毫秒。即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。

释放锁

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

因为释放锁涉及到两个命令,不是原子性,所以需要使用lua来保证原子性

缺陷:如果客户端拿到锁之后设置了超时时长,但是业务执行的时长超过了超时时长,导致客户端还在执行业务但是锁已经被释放,此时其他进程就会拿到锁从而执行相同的业务,此时因为并发导致分布式锁失去了意义

Redis如何解决key冲突 拉链法

一般来说解决key值冲突的方法有俩种,一种是开放地址法,另一种就是拉链法。

Edgar

Edgar
一个略懂Java的小菜比