Redis知识点总结

Redis(Remote Dictionary Server)的特点

  • 纯内存操作
  • 采用单线程,避免了不必要的上下文切换和竞争条件,不用考虑各种锁的问题
  • 使用多路 I/O 复用模型,非阻塞型IO
  • 高效优化的数据结构(如 SDS,Zset 中的跳表等等)

Redis Object

Redis 中的每个键值对的键和值都由redisObject结构体表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typedef struct redisObject {
unsigned type:4; // 对象的类型
unsigned encoding:4; // 为了节省空间,同个type的数据可以采用不同的编码方式,如Zset中的同时使用了ziplist和skipset
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
int refcount; // 引用计数
void *ptr;
} robj;

/* type类型如下 */
/* The actual Redis Object */
#define OBJ_STRING 0
#define OBJ_LIST 1
#define OBJ_SET 2
#define OBJ_ZSET 3
#define OBJ_HASH 4

/* encoding类型如下 */
/* Objects encoding. Some kind of objects like Strings and Hashes can be
* internally represented in multiple ways. The 'encoding' field of the object
* is set to one of this fields for this object.
*/
#define OBJ_ENCODING_RAW /* Raw representation */ 简单动态字符串
#define OBJ_ENCODING_INT /* Encoded as integer */ 整数
#define OBJ_ENCODING_HT /* Encoded as hash table */ 字典
#define OBJ_ENCODING_ZIPLIST /* Encoded as ziplist */ 压缩列表
#define OBJ_ENCODING_INTSET /* Encoded as intset */ 整数集合
#define OBJ_ENCODING_SKIPLIST /* Encoded as skiplist */ 跳跃表
#define OBJ_ENCODING_EMBSTR /* Embedded sds string encoding */ embstr编码的简单动态字符串
#define OBJ_ENCODING_QUICKLIST /* Encoded as linked list of ziplists */

Redis 的数据结构与应用场景

  • String:缓存、计数器、分布式锁等。
  • List:链表、队列、微博关注人时间轴列表等。
  • Hash:用户信息、Hash 表等。
  • Set:去重、赞、踩、共同好友等。
  • Zset:访问量排行榜、点击量排行榜等。

Redis 的两种持久化方式(RDB与AOF)

RDB

RDB (Redis DataBase) 持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

优势:

  • 整个 Redis 数据库将只包含一个文件,十分易于备份
  • 性能最大化,对于 Redis 的服务进程而言,在开始持久化时,它唯一需要做的只是 fork 出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大地避免服务进程执行 IO 操作
  • 相对于 AOF,基于 RDB 数据文件来重启和恢复 Redis 会更快

劣势:

  • 无法最大限度地避免数据丢失,系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失
  • 由于 RDB 是通过 fork 子进程来协助完成数据持久化工作的,因此,如果当数据较大时,可能会导致整个服务器停止服务数毫秒,甚至数秒

AOF

AOF (Append Only File) 持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,只许追加文件但不可以改写文件,可以打开文件看到详细的操作记录

AOF 提供以下三种同步策略,默认为 everysec

1
2
3
# no: don't fsync, just let the OS flush the data when it wants. Faster.
# always: fsync after every write to the append only log. Slow, Safest.
# everysec: fsync only one time every second. Compromise.

优势:

  • 最大限度地避免数据丢失,默认的 everysec 策略可以记录下每秒的修改操作,但如果一秒内宕机,有数据丢失
  • AOF 对日志文件的写入操作采用的是 append 模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。

劣势:

  • 对于同一份数据来说,AOF 文件通常要大于 RDB 文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
  • 根据同步策略的不同,AOF 在运行效率上往往会慢于 RDB。但是每秒同步策略的效率还是比较高的。

总结:AOF,存放的指令日志,做数据恢复的时候,实际上是要执行所有的历史指令日志来恢复数据,要是遇上很多 ABA 之类的操作就会很影响恢复性能。RDB,就是一份数据文件,恢复的时候,直接加载到内存中即可。

二者选择的标准,就是愿意牺牲一些性能,换取最少的数据丢失(AOF),还是牺牲一些数据来换取更高的性能(RDB)。实际中应该综合使用 AOF 和 RDB 两种持久化机制,用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备,在 AOF 文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复

Redis 的过期策略以及内存回收机制

过期策略

对于过期的 key,Redis 采用的是定期删除惰性删除两种策略。

  • 定期删除:每隔一段时间执行一次删除过期键的操作,并且限制删除操作执行的时长和频率。
  • 惰性删除:只有在用户获取某个 key 的时候,Redis 才会对这个 key 进行过期检查,过期则删除。

Redis 中的具体操作如下:

  • Redis 会将每个设置了 expire 的 key 存储在一个独立的字典中,以后会定时遍历这个字典来删除过期的 key。
  • Redis 默认每秒进行10次过期扫描,过期扫描不会扫描所有过期字典中的 key,而是采用了一种简单的贪心策略:从过期字典中随机选择 20 个 key;删除这 20 个 key 中已过期的 key,如果过期 key 比例超过 1/4,那就重复以上步骤。
  • 同时,为了保证在过期扫描期间不会出现过度循环,导致线程卡死,扫描算法还增加了扫描时间上限,默认不会超过 25ms。

由于 Redis 的定期删除是随机抽取检查,不可能扫描清除掉所有过期的key并删除,一些 key 也可能未被请求过,惰性删除未被触发,这样 Redis 的内存占用会越来越高。此时就需要内存回收(淘汰)机制 。

内存回收机制

在 Redis 配置文件中的 maxmemory决定了所能使用的最大内存(bytes),默认为0,表示无限制。当 Redis 中内存数据达到 maxmemory 时,触发内存回收。

1
# maxmemory <bytes>

maxmemory-policy 决定了 Redis 采用何种内存回收机制,默认为 noeviction

1
2
3
4
5
6
7
# volatile-lru -> remove the key with an expire set using an LRU algorithm
# allkeys-lru -> remove any key according to the LRU algorithm
# volatile-random -> remove a random key with an expire set
# allkeys-random -> remove a random key, any key
# volatile-ttl -> remove the key with the nearest expire time (minor TTL)
# noeviction -> don't expire at all, just return an error on write operations
# maxmemory-policy noeviction

Redis 共有以下 6 种策略:

  1. volatile-lru:在过期的 key 中删除最近最久未使用的
  2. volatile-ttl:从已设置过期的 key ,删除最接近过期
  3. volatile-random:在过期的 key 中随机删除
  4. allkeys-lru:在所有 key 中删除最近最久未使用的
  5. allkeys-random:在所有的 key 中随机删除
  6. noeviction:不删任何数据,如果内存不够,会对写操作直接返回 error

无疑 volatile-lru 是最佳选择。

缓存穿透

访问一个不存在的 key 时,缓存不起作用,请求会穿透到 DB ,流量大时 DB 自然就会瘫痪。

解决方案:

  1. 利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。

  2. 即使查询到的数据为空,仍把该空值缓存,还需要设置过期时间。

  3. 布隆过滤器,内部维护一系列合法有效的 key 就可以迅速判断出请求所携带的key是否合法有效。如果不合法,则直接返回。

缓存击穿

一个存在的 key,在缓存过期的一刻,恰好在这个时间点对这个 key 有大量的并发请求过来,这个时候大量的请求可能会瞬间瘫痪掉 DB 。

解决方案:

  1. 分级缓存,采用 L1 (一级缓存)和 L2(二级缓存) 缓存方式,L1 缓存失效时间短,L2 缓存失效时间长。请求优先从 L1 缓存获取数据,如果 L1 缓存未命中则加锁,只有1个线程能获取到锁,这个线程再从数据库中读取数据并将数据再更新到到 L1 缓存和 L2 缓存中,而其他线程依旧从 L2 缓存获取数据并返回。L2 缓存中可能会存在脏数据,需要业务能够容忍这种短时间的不一致
  2. 互斥锁

缓存雪崩

大量的 key 设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时 DB 请求量大、压力骤增,引起雪崩。

解决方案:

  1. 给缓存设置过期时间时加上一个随机值时间,使得每个key的过期时间分布开来,不会集中在同一时刻失效。
  2. 分级缓存
  • 本文作者: Marticles
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!