缓存面试题之一

Redis

持久化机制

RDB 与 AOF

  • RDB(Redis DataBase)持久化,是在指定的时间间隔内将内存中的数据集快照写入磁盘(point-in-time snapshot)。在 Redis 运行时,RDB 程序将当前内存中的数据集快照保存到磁盘文件中,在 Redis 重启动时,RDB 程序可以通过载入 RDB 文件来还原数据库的状态
  • AOF(Append Only File)持久化,以日志的形式来记录每个写操作,将 Redis 执行过的所有写指令记录下来,同时只许追加文件不能改写文件。当 Redis 重新启时,程序就可以通过重新执行 AOF 文件中的命令来达到重建数据集的目的

RDB 的优缺点

  • RDB 的优点

    • 节省磁盘空间
    • 数据恢复速度快
  • RDB 的缺点

    • 在一定的时间间隔做一次备份,如果 Redis 以为 down 掉的话,那么最后一次持久化后的数据可能会丢失
    • 虽然 Redis 在 fork 时使用了写时拷贝技术,但是如果数据量较大时,比较耗性能,可能会导致 Redis 在数毫秒或者几秒内不能响应客户端的请求

AOF 的优缺点

  • AOF 的优点

    • 可读的日志文件
    • 备份机制更稳健,丢失数据的概率更低
  • AOF 的缺点

    • 数据恢复速度较慢
    • 对于相同的数据集来说,与 RDB 比较,占用更多的磁盘空间
    • 若同步策略为每次写入都同步的话,有一定的性能压力,尤其是处理巨大的写入时

五大数据类型的应用场景

Redis 支持的键值数据类型包括:字符串类型、散列类型、列表类型、集合类型、有序集合类型。

数据类型 常用操作命令 应用场景
String get、SET、incr、decr、mget 常规的 key-value 缓存应用:IP 屏蔽;常规计数:微博数、粉丝数
Hash hget、hSET、hgetall 存储部分频繁变更的数据,如用户信息等
List LPUSH、rpush、lpop、rpop、lrange Twitter 的关注列表,微博粉丝列表,最新消息排行榜,作为栈、消息队列使用
SET sadd、spop、smembers、sunion 求差集、交集、并集,例如:微博的共同关注、共同喜好、二度好友等功能
Sorted SET zadd、zrange、zrem、zcard 商品的综合排名、价格排名、优先级队列

Redis 实现优先级队列

通常使用 List 类型来实现队列操作,这样有一个小限制,所有的任务统一都是先进先出,如果想优先处理某个任务就不太好处理了,这就需要让队列有优先级的概念,支持优先处理高级别的任务,具体实现方式有以下几种。

单一列表实现

队列正常的操作是 左进右出(LPUSH,rpop),为了先处理高优先级任务,在遇到高级别任务时,可以直接插队,即直接将任务放入队列头部(rpush),这样从队列头部(右侧)获取任务时,取到的就是高优先级的任务(rpop)。相当于普通任务按照队列结构,碰到高优先级任务,就按照栈结构(先进后出)。优点是实现简单,缺点是高级别任务总是后进先出,而且高优先级的任务之间的执行顺序是先进后出的,这样保证不了高优先级任务之间的执行顺序。适用于简单的队列需求,例如高优先级任务较少的情况。

多队列实现

使用两个队列,一个普通队列,一个高级队列,针对任务的级别放入不同的队列。获取任务时也很简单,Redis 的 BRPOP 命令可以按顺序从多个队列中取值。BRPOP 命令会按照给出的 key 顺序查看,并在找到的第一个非空 List 的尾部弹出一个元素,而且 BRPOP 命令是阻塞式的。

1
redis> BRPOP list1 list2 0

其中 list1 做为高优先级任务队列,list2 做为普通任务队列,这样就实现了先处理高优先级任务,当没有高优先级任务时,就去获取普通任务。

使用权值实现

如果优先级比较复杂,比如假设有个这样的场景,优先级不是简单的高中低或者 0-10 这些固定的级别,而是类似 0-99999 这么多级别,使用多队列的方式实现起来就不太方便了。

思路一、基于 List 类型 + 多队列 + 二分法:

虽然 Redis 有 Sorted SET 这样的可以排序的数据类型,很可惜它没有阻塞版的接口,因此只能使用 List 类型通过其他方式来完成目的。简单的做法可以只设置一个队列,并保证它是按照优先级排序的;然后通过二分查找法查找一个任务合适的位置,并通过 LSET 命令将任务插入到相应的位置。例如队列里面包含着写优先级的任务 [1, 3, 6, 8, 9, 14],当有个优先级为 7 的任务过来,通过二分算法一个个从队列里面取数据出来和目标数据比对,计算出相应的位置然后插入到指定位置即可。因为二分查找是比较快的,并且 Redis 的数据也都在内存中,理论上速度是可以保证的。但是如果说数据量确实很大的话也可以通过额外方式来调优,比如与 “多队列实现方案” 结合起来就会很大程度上减少开销。假设数据量十万的队列,它们的优先级也是随机 0-10W 万的区间。可以设置 10 个或者 100 个不同的队列,0-1W 的优先级任务投放到 1 号队列,2W-3W 的任务投放到 2 号队列。这样将一个队列按不同等级拆分后,它单个队列的数据量就减少许多,这样二分法查找匹配的效率也会高一点。但是数据所占的资源基本是不变的,十万数据该占多少内存还是多少,只是系统里面多了一些队列而已。

思路二、基于 List 类型(存疑):

假设有 3 个级别,用权值来表示为 1、2、3,此时有 4 个元素需要入队,分别是:a-1,b-2,c-3,d-3。

首先使用 LPUSH 把元素放入队列中,同时设置权值:

1
2
3
4
5
6
7
8
9
10
11
redis> LPUSH mylist a
redis> SET mylist_score_a 1

redis> LPUSH mylist b
redis> SET mylist_score_b 2

redis> LPUSH mylist c
redis> SET mylist_score_c 3

redis> LPUSH mylist d
redis> SET mylist_score_d 3

根据权值排序,并取出排名第一的元素(c):

1
redis> SORT mylist by mylist_score_* limit 0 1

元素获取完成后,要移除该元素(c):

1
redis> LREM mylist 0 c

特别注意:由于 SORT 命令与 LREM 命令是前后执行的,并不是原子操作,所以此方案并不是线程安全的。