title: Redis常考的知识点
categories: 数据库
tags: Redis


一、Redis是什么,有什么功能?

Redis 是一个使用 C 语言开发的数据库,也是一种Key-Value数据库,数据存储在内存中,常用作缓存数据库,速度较快。

功能:常用来作缓存,分布式锁,消息队列,排行榜等功能

二、Redis 和 Memcached 的对比
  1. Memcached 只支持String类型,Reids支持更为丰富的数据类型
  2. Redis支持数据的持久化
  3. Redis的速度更快
  4. Memcached 是多线程,非阻塞IO复用的网络模型,Redis使用单线程的IO复用
    相同点就是都是内存型数据库,都有过期策略,性能都不错,常用来做缓存


三、Redis支持的数据类型以及底层数据结构
  1. string,底层数据结构为简单动态字符串(simple dynamic string,SDS),SDS 可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),此外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。
  2. list,底层数据结构是链表,C 语言并没有实现链表,所以 Redis 实现了自己的链表数据结构。Redis 的 list 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,获取表头表尾和链表长度都是O(1)复杂度
  3. set,是一种无序集合,集合中的元素没有先后顺序。需要存储一个列表数据,又不希望出现重复时,可以选择set,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口
  4. hash,底层是字典结构,字典在Redis中广泛被使用,包括数据库和哈希键,每个字典有两个哈希表,哈希表使用的是链地址法解决哈希冲突,扩容时采用的是渐进式哈希
  5. Zset ,有点像是 Java 中 HashMap 和 TreeSet 的结合体,底层使用跳表实现
四、Redis为什么是单线程?

Redis核心就是我所有数据都在内存里,单线程操作效率就是最高的,为什么要多线程呢?多线程会有一个代价,就是上下文切换,对于当个CPU绑定一块内存的数据,没有上下文切换就是效率最高的;相反,如果是多次磁盘IO的话,多线程更优,因为在寻道和选择的时间,线程在阻塞的等待磁盘,这个时间CPU可以去处理其他线程。

总之就是CPU不是redis的瓶颈,reids的瓶颈是机器内存和网络带宽,而单线程既不会成为瓶颈,又容易实现,那肯定单线程。

五、Redis是单线程吗?

将第五题和第四题放在一起就是为了分辨一个大部分人的误区,大家称Redis是单线程,但是Redis并不是单线程,比如持久化的时候就会fork子线程,包括网络IO也不是单线程,Redis的单线程指的是事件处理模型的单线程

Redis 基于 Reactor 模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler),通过IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。

文件事件处理器(file event handler)主要是包含 4 个部分:

  • 多个 socket(客户端连接)
  • IO 多路复用程序(支持多个客户端连接的关键)
  • 文件事件分派器(将 socket 关联到相应的事件处理器)
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
六、什么是缓存雪崩,什么是缓存穿透?

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。

解决方案就是:

  1. 设置缓存添加随机过期时间,防止大量缓存同时失效
  2. 采用Reids高可用架构比如主从或者Redis Cluster,避免Redis挂掉
  3. 及时利用本地缓存和限流,防止下游数据库崩溃
  4. 开启持久化,重启后快速恢复数据

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大

解决方案就是:

  1. 访问一个不存在的参数时,将这个结果进行缓存,下次直接返回null
  2. 使用布隆过滤器进行过滤
七、Redis的过期键的删除策略
  1. 惰性删除 :只会在取出key的时候才对数据进行过期检查。这样对CPU最友好,但是可能会造成太多过期 key 没有被删除。
  2. 定期删除 : 每隔一段时间抽取一批 key 执行删除过期key操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
八、Redis的内存淘汰机制

Redis 提供 6 种数据淘汰策略:

  1. volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  4. allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

4.0 版本后增加以下两种:

  1. volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  2. allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
九、Reids和数据库的双写一致性

a. 先更新数据再更新缓存的话是不行的,更新结束,更新缓存失败岂不是gg

b. 先删缓存再更新数据库看起来可以,实际上也有问题:

i. A删缓存,B拿旧数据,放到缓存里,A更新数据库,就出问题了

ii. 解决方案:延时双删(但是第二次删除还是会出现不一致问题),(要设置过期时间,保证最终一致性)

c. 先更新数据库再删缓存

i. 问题:缓存刚好失效,然后A拿到旧数据,然后B更新缓存删缓存,A把旧数据放到数据库,但是碰上缓存刚好失效的概率比较低

十、Redis的持久化方式

快照(snapshotting)持久化(RDB):Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。快照持久化是 Redis 默认采用的持久化方式

AOF(append-only file)持久化:与快照持久化相比,AOF 持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启

十一、Redis的渐进式扩容

每个字典有两个哈希表,一个ht[0],一个ht[1],扩展或收缩哈希表需要将 ht[0] 里面的所有键值对 rehash 到 ht[1] 里面。如果哈希表里保存的键值对数量非常大, 那么要一次性将这些键值对全部 rehash 到 ht[1] 的话, 庞大的计算量可能会导致服务器在一段时间内停止服务。为了避免此情况,所以采用渐进式哈希。

哈希表渐进式 rehash 的详细步骤:

  1. ht[1] 分配空间, 让字典同时持有 ht[0]ht[1] 两个哈希表。
  2. 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
  3. 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
  4. 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1, 表示 rehash 操作已完成。

在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找, 诸如此类。

另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作

十二、Redis分布式锁(后续会有单独文章)

方法一:SETNX key value

将 key 的值设为 value,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是SET if Not eXists的简写

方法二(Redlock算法):

起 5 个 master 节点,分布在不同的机房尽量保证可用性。为了获得锁,client 会进行如下操作:

  1. 得到当前的时间,微秒单位
  2. 尝试顺序地在 5 个实例上申请锁,当然需要使用相同的 key 和 random value,这里一个 client 需要合理设置与 master 节点沟通的 timeout 大小,避免长时间和一个 fail 了的节点浪费时间
  3. 当 client 在大于等于 3 个 master 上成功申请到锁的时候,且它会计算申请锁消耗了多少时间,这部分消耗的时间采用获得锁的当下时间减去第一步获得的时间戳得到,如果锁的持续时长(lock validity time)比流逝的时间多的话,那么锁就真正获取到了。
  4. 如果锁申请到了,那么锁真正的 lock validity time 应该是 origin(lock validity time) - 申请锁期间流逝的时间
  5. 如果 client 申请锁失败了,那么它就会在少部分申请成功锁的 master 节点上执行释放锁的操作,重置状态