Redis

60 阅读14分钟

Redis

  1. Redis有哪些数据结构,他们底层分别是如何实现的。适合哪些场景?

    1. String(字符串): 存储key-value缓存应用
      struct sdshdr {
      // 用于记录buf数组中使用的字节的数目
      // 和SDS存储的字符串的长度相等
      int len;
      // 用于记录buf数组中没有使用的字节的数目
      int free;
      // 字节数组,用于储存字符串
      char buf[]; //buf的大小等于len+free+1,其中多余的1个字节是用来存储’\0’的。
      }
      sds优势: 1.动态扩容通过内存分配策略,减少了字符串修改时候的内存分配次数
      2.存储二进制安全
      3.获取len是O(1)
      4.防止缓冲区溢出
      扩容策略:
      1.如果扩容len长度小于1M, 这时free和len相同。
      2.如果扩容大于1M, free将等于1M。

    2. List(列表): 一个链表,链表上的每个节点都包含了一个字符串,字符串可以重复;最新消息排行等功能;消息队列;关注列表,粉丝列表;
      压缩列表ziplist
      双向链表linkedlist
      当链表元素数量超过512、或单个value 长度超过64,底层就会转化成linkedlist编码

    3. Hash(字典): 含键值对的无序散列表,键不能重复;存储用户信息(能单独修改用户某一属性信息);
      redis的哈希对象的底层存储可以使用ziplist(压缩列表)和dist。
      满足两个条件使用ziplist编码。
      1.哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
      2.哈希对象保存的键值对数量小于512个
      hash扩容(渐进式rehash):
      负载因子 = 哈希表已保存节点数量 / 哈希表大小
      没有在执行BGSAVE或BGREWRITEAOF,并且负载因子大于等于1 或者 在执行BGSAVE或BGREWRITEAOF,并且负载因子大于等于5 hash缩容:
      当哈希表的负载因子小于0.1时,也就是填充率<10%

      typedef struct dict {
      dictType *type; // 字典类型
      void *privdata; // 私有数据
      dictht ht[2]; // 哈希表[两个]
      long rehashidx; // 记录rehash 进度的标志,值为-1表示rehash未进行
      int iterators; // 当前正在迭代的迭代器数
      } dict;

      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 操作已完成。

    4. Set(集合):字符串的无序收集器, 值不能重复;统计网站访问IP(利用唯一性,统计访问网站的所有独立IP);
      数据量不大时使用inset来存储,其他情况都是用hash字典来存储
      由intset或dict, 集合对象保存所有元素都是整数, 且元素数量小于512, 否则使用dict
      typedf struct inset{
      uint32_t encoding;//编码方式 有三种 默认 INSET_ENC_INT16
      uint32_t length;//集合元素个数
      int8_t contents[];//实际存储元素的数组
      }

    5. Sorted Set(有序集合): 有序集合的键被称为成员,每个成员都是各不相同的;排行版;带权重的消息队列; sortedset同时会由两种数据结构支持,ziplist和skiplist(跳表).
      满足有序集合保存的元素数量小于128个 有序集合保存的所有元素的长度小于64字节,使用的是ziplist,其他时候则是使用skiplist.
      (skiplist的复杂度和红黑树一样,而且实现起来更简单;红黑树在插入和删除的时候可能需要做一些rebalance(rotation)的操作,
      这样的操作可能会涉及到整个树的其他部分,而skiplist的操作显然更加局部性一些)
      typedef struct zskiplist {
      // 头节点,尾节点
      struct zskiplistNode *header, *tail;
      // 节点数量
      unsigned long length;
      // 目前表内节点的最大层数
      int level;
      }

  2. 单个Redis实例可以存储多少个Keys?其中list set sorted set 最多能存放多少元素?

    Redis 最多可以处理 2^32 个键,并在实践中经过测试,每个实例至少可以处理 2.5 亿个键。
    每个哈希、列表、集合和排序集合可以包含2^32个元素。
    限制可能是系统中的可用内存。
    
  3. Redis的数据淘汰机制有哪些?

    内存过期策略:
    定时策略:在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除。
    优点:保证内存被尽快释放,减少无效的缓存暂用内存。
    缺点:若过期key很多,删除这些key会占用很多的CPU时间,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重。
    惰性删除\:key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。
    优点:删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的。
    缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,此时的无效缓存是永久暂用在内存中的,那么可能发生内存泄露。
    定期删除:每隔一段时间对设置了缓存时间的key进行检测,如果可以已经失效,则从内存中删除,如果未失效,则不作任何处理。
    优点:通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用。
    缺点:在内存友好方面,不如"定时删除",因为是随机遍历一些key,因此存在部分key过期,但遍历key时,没有被遍历到,过期的key仍在内存中。在CPU时间友好方面,不如"惰性删除",定期删除也会暂用CPU性能消耗。
    

    内存淘汰策略(内存淘汰机制针对是内存不足的情况下的一种Redis处理机制):
    当内存不足以容纳新写入数据时触发以下
    volatile-lru:在设置了过期时间的键空间中,移除最近最少使用的key。
    allkeys-lru:在键空间中,移除最近最少使用的key(这个是最常用的)。
    volatile-lfu:在过期密集的键中,使用LFU算法进行删除key。
    allkeys-lfu:使用LFU算法移除所有的key。
    volatile-random:在设置了过期的键中,随机删除一个key。
    allkeys-random:随机删除一个或者多个key。
    volatile-ttl:在设置了过期时间的键空间中,有更早过期时间的key优先移除。
    noeviction:新写入操作会报错。

  4. Redis是如何做数据持久化的?

    1. RDB:对内存中数据库状态进行快照
    2. AOF:把每条写命令都写入文件,类似于mysql的binlog日志
    3. Redis 还可以同时使用 AOF 持久化和 RDB 持久化。 在这种情况下, 当 Redis 重启时,它会优先使用 AOF 文件来还原数据集, 因为 AOF 文件保存的数据集通常比 RDB 文件所保存的数据集更完整。 在写入的时候,先把当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,又能减低数据丢失的风险

    AOF写回策略:
    always 同步写回。每执行一条命令,写完AOF日志后,再返回。
    everysec 每秒写回。执行命令后,将数据写入到内核缓冲区就返回。只有会有一个线程,执行每秒刷盘的定时任务。
    no 由内核自行控制的写回。每执行一条命令,将数据写入到内核缓冲区就返回。内核会在合适的时机刷盘。
    这3种策略体现了不同的刷盘频率,因此拥有不同级别的一致性与性能。
    区别:
    always策略最大程度上保证数据不丢失,但性能最差。
    no策略性能最好,但在机器崩溃重启后会丢失比较多的数据。
    everysec是一种折中的策略,较always有不错的性能。在极端的情况下,只会丢失1秒内的数据,是比较推荐的方式。

    重写方式(AOF压缩):
    客户端向服务器发送bgrewriteaof命令 压缩aof文件,减少磁盘占用量。
    将aof的命令压缩为最小命令集,加快了数据恢复的速度。

  5. Redis的Pipeline是什么,有什么好处?

    Redis的Pipeline是一种将多个Redis命令放入管道(pipe)中一次性发送的机制。管道会一次性地把多个命令发送到服务器,并缓存服务器返回的结果,最后再一次性地返回给客户端。  
    使用Pipeline的好处:  
    减少网络开销:通过减少网络请求数量,可以提高请求效率。  
    减少延迟:通过减少请求数量,可以缩短请求时间。  
    增强并发:Pipeline可以并行处理多个请求,提高并发性。  
    Pipeline的实现并不影响Redis的单线程处理特性,它只是利用了缓存结果的优点来减少网络开销,提高性能。  
    
  6. Redis集群的一些问题。分布式,主从同步等。

    Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接,
    采用哈希槽 (hash slot类似一致性hash)的方式来分配16384slot,新增一个节点Dredis cluster的这种做法是从
    各个节点的前面各拿取一部分slotD上
    
    实现原理:
    主观下线:
    1.集群中每个节点都会定期向其他节点发送ping消息,接受节点回复ping消息作为响应。
    如果在cluster-node-timeout时间内通信一直失败,则发送节点会认为接收节点存在故障,把接受节点标记为主观下线
    客观下线:
    1.当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播.
    2.假设节点a标记节点b为主观下线,一段时间后节点a通过消息把节点b的状态发送到其他节点.
    3.此时有超过一半的槽主节点都标记了节点b为故障状态时,则标记故障节点b为客观下线
    4.向集群广播一条pfail消息, 同时通知故障节点b的从节点触发故障转移流程
    故障恢复:
    1.资格检查
    每个从节点检查与故障主节点的断线时间
    超过cluster-node-timeout\*cluster-slave-validity-factor(默认是10) 取消资格
    2.准备选举时间
    在故障节点的所有从节点中, 与主节点的数据最一致进行选举,然后是次大的节点开始选举.....剩下其余的从节点等待到它们的选举时间到达后再进行选举
    3.选举投票
    从从节点收集到N/2 + 1个持有槽的主节点投票时,从节点可以执行替换主节点操作
    4.替换主节点
    当前从节点取消复制变为主节点
    撤销故障主节点负责的槽,并把这些槽委派给自己
    向集群广播自己, 通知集群内所有的节点当前从节点变为主节点
    
    槽:英文slot;一共有多少个槽: 16384个;只有主机才有槽的分配,并且他们尽量平分;
    为什么有16384个槽:
    节点之间会定期发送ping/pong消息,交换数据信息
    消息头里面有个myslotschar数组,长度为16383/8,这其实是一个bitmap,每一个位代表一个槽,如果该位为1,表示这个槽是属于这个节点的
    16384÷8÷1024=2kb
    那在消息体中,会携带一定数量的其他节点信息用于交换。约为集群总节点数量的1/10,至少携带3个节点的信息。
    这里的重点是:节点数量越多,消息体内容越大。消息体大小是10个节点的状态信息约1kbredis的集群主节点数量基本不可能超过1000个。对于节点数在1000以内的redis cluster集群,16384个槽位够用, 节省带宽
    
    集群下命令执行流程:
    客户端向节点发送与数据库键有关的命令,接收命令的节点会计算(CRC16 & 16383(计算槽号))出命令要处理的数据库键属于
    哪个槽,并检查这个槽是否指派给了自己,如果键所在的槽并没有指派给当前节点,那么节点会向客户端返回一个MOVED错误,
    指引客户端转向(redirected) 至正确的节点,并再次发送之前想要执行的命令.
    
    主从同步:
    1.无论是初次连接还是重新连接,当建立一个从服务器时,从服务器都将给主服务器发送一个 SYNC 命令
    2.接到 SYNC 命令的主服务器将开始执行 BGSAVE,保存操作执行期间,将所有新执行的命令都保存到一个缓冲区里面,BGSAVE 执行完毕后,主服务器将.rdb 文件发送给从服务器
    3.从服务器接收这个 .rdb 文件,并将文件中的数据载入到内存中
    4.之后主服务器会以 Redis 命令协议的格式,将写命令缓冲区中的内容发送给从服务器
    
  7. 使用Redis实现:异步队列,延时队列,分布式锁。以及他们的概念。

    异步队列:异步队列是指多个任务执行的顺序不是同步的,任务的处理与执行分离。在Redis中,可以使用List类型存储任务,实现队列的功能。
    延时队列:延时队列是指在执行任务前,需要等待一段时间,任务到达时间再进行处理。在Redis中,可以使用ZSet类型存储任务,实现延时队列的功能。
    zset 数据结构维护了一个有序的集合,其中每一个元素都有一个权值。我们可以将消息的到达时间作为该元素的权值,然后利用 Redis 的排序功能来实现延时队列。
    具体操作:
    1. 创建一个 zset ,并为每个消息生成一个唯一的 id。
    2. 将消息的到达时间作为元素的权值,并将消息的 id 和到达时间作为元素存入 zset。
    3. 定时从 zset 中取出到达时间小于当前时间的元素,并执行对应的处理逻辑。
    分布式锁:分布式锁是指在分布式环境下,多个节点共享一个资源时,使用锁机制保证资源的独占使用。在Redis中,可以使用setnx(set if not exists)命令加锁,实现分布式锁的功能。