Redis(Remote Dictionary Server)的特点
- 纯内存操作
- 采用单线程,避免了不必要的上下文切换和竞争条件,不用考虑各种锁的问题
- 使用多路 I/O 复用模型,非阻塞型IO
- 高效优化的数据结构(如 SDS,Zset 中的跳表等等)
Redis Object
Redis 中的每个键值对的键和值都由redisObject结构体表示。
1 | typedef struct redisObject { |
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 | # no: don't fsync, just let the OS flush the data when it wants. Faster. |
优势:
- 最大限度地避免数据丢失,默认的 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 | # volatile-lru -> remove the key with an expire set using an LRU algorithm |
Redis 共有以下 6 种策略:
- volatile-lru:在过期的 key 中删除最近最久未使用的
- volatile-ttl:从已设置过期的 key ,删除最接近过期
- volatile-random:在过期的 key 中随机删除
- allkeys-lru:在所有 key 中删除最近最久未使用的
- allkeys-random:在所有的 key 中随机删除
- noeviction:不删任何数据,如果内存不够,会对写操作直接返回 error
无疑 volatile-lru
是最佳选择。
缓存穿透
访问一个不存在的 key 时,缓存不起作用,请求会穿透到 DB ,流量大时 DB 自然就会瘫痪。
解决方案:
利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。没得到锁,则休眠一段时间重试。
即使查询到的数据为空,仍把该空值缓存,还需要设置过期时间。
布隆过滤器,内部维护一系列合法有效的 key 就可以迅速判断出请求所携带的key是否合法有效。如果不合法,则直接返回。
缓存击穿
一个存在的 key,在缓存过期的一刻,恰好在这个时间点对这个 key 有大量的并发请求过来,这个时候大量的请求可能会瞬间瘫痪掉 DB 。
解决方案:
- 分级缓存,采用 L1 (一级缓存)和 L2(二级缓存) 缓存方式,L1 缓存失效时间短,L2 缓存失效时间长。请求优先从 L1 缓存获取数据,如果 L1 缓存未命中则加锁,只有1个线程能获取到锁,这个线程再从数据库中读取数据并将数据再更新到到 L1 缓存和 L2 缓存中,而其他线程依旧从 L2 缓存获取数据并返回。L2 缓存中可能会存在脏数据,需要业务能够容忍这种短时间的不一致
- 互斥锁
缓存雪崩
大量的 key 设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时 DB 请求量大、压力骤增,引起雪崩。
解决方案:
- 给缓存设置过期时间时加上一个随机值时间,使得每个key的过期时间分布开来,不会集中在同一时刻失效。
- 分级缓存