Redis 基础

一、概述

  1. Redis为什么快?
    1. 基于内存操作:Redis的绝大部分操作在内存里就可以实现,数据也存在内存中,与传统的磁盘文件操作相比减少了IO,提高了操作的速度。高效的数据结构:Redis有专门设计了string、list、hash等高效的数据结构,依赖各种数据结构提升了读写的效率。采用单线程:单线程操作省去了上下文切换带来的开销和CPU的消耗,同时不存在资源竞争,避免了死锁现象的发生。I/O多路复用:采用I/O多路复用机制同时监听多个Socket,根据Socket上的事件来选择对应的事件处理器进行处理。
  2. 为什么Redis是单线程?
    1. 单线程指的是:网络请求模块使用单线程进行处理,其他模块仍用多个线程。
    2. 官方答案是:因为CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。
    3. redis采用多线程的模块和原因:Redis 在启动的时候,会启动后台线程(BIO):
      • Redis的早期版本会启动2个后台线程,来处理关闭文件、AOF 刷盘这两个任务;
      • Redis的后续版本,新增了一个新的后台线程,用来异步释放 Redis 内存。执行 unlink key / flushdb async / flushall async 等命令,会把这些删除操作交给后台线程来执行。
      之所以 Redis 为关闭文件、AOF 刷盘、释放内存这些任务创建单独的线程来处理,是因为这些任务的操作都很耗时,把这些任务都放在主线程来处理会导致主线程阻塞,导致无法处理后续的请求。后台线程相当于一个消费者,生产者把耗时任务丢到任务队列中,消费者不停轮询这个队列,拿出任务去执行对应的方法即可。
  3. Redis为什么要引入多线程?
    1. 因为Redis的瓶颈不在内存,而是在网络I/O模块带来CPU的耗时,所以Redis6.0的多线程是用来处理网络I/O这部分,充分利用CPU资源,减少网络I/O阻塞带来的性能损耗。Redis引入的多线程 I/O 特性对性能提升至少是一倍以上。
  4. 为什么用Redis作为MySQL的缓存?
    1. MySQL是数据库系统,对于数据的操作需要访问磁盘,而将数据放在Redis中,需要访问就可以直接从内存获取,避免磁盘I/O,提高操作的速度。使用Redis+MySQL结合的方式可以有效提高系统QPS。
  5. 如何理解Redis原子性操作原理?
    1. API:Redis提供的API都是单线程串行处理的。网络模型:采用单线程的epoll的网络模型,用来处理多个Socket请求。请求处理:Redis会fork子进程来出来类似于RDB和AOF的操作,不影响主进程工作。

二、数据结构

  1. Redis数据类型?
    1. 主要有STRING,LIST,ZSET,SET,HASH。
    2. String:String类型的底层的数据结构实现主要是SDS(简单动态字符串)。应用场景主要有:
      • 缓存对象:例如可以用STRING缓存整个对象的JSON。
      • 计数:Redis处理命令是单线程,所以执行命令的过程是原子的,因此String数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等等。
      • 分布式锁:可以利用SETNX命令。
      • 共享Session信息:服务器都会去同一个Redis获取相关的Session信息,解决了分布式系统下Session存储的问题。
    3. LIST:List 类型的底层数据结构是由双向链表或压缩列表实现的:
      • 如果列表的元素个数小于 512 个,列表每个元素的值都小于 64 字节,Redis 会使用压缩列表作为 List 类型的底层数据结构;
      • 如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;
      在 Redis 3.2 版本之后,List 数据类型底层数据结构只由 quicklist 实现,替代了双向链表和压缩列表。在 Redis 7.0 中,压缩列表数据结构被废弃,由 listpack 来实现。应用场景主要有:- **微信朋友圈点赞**:要求按照点赞顺序显示点赞好友信息,如果取消点赞,移除对应好友信息。
      – **消息队列**:可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用BRpop命令阻塞的”抢”列表尾部的数据。
    4. HASH:Hash 类型的底层数据结构是由压缩列表或哈希表实现的
      • 如果哈希类型元素个数小于 512 个,所有值小于 64 字节的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
      • 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的底层数据结构。
      在Redis 7.0 中,压缩列表数据结构被废弃,交由 listpack 来实现。应用场景主要有:- **缓存对象**:一般对象用 String + Json 存储,对象中某些频繁变化的属性可以考虑抽出来用 Hash 类型存储。
      • 购物车:以用户 id 为 key,商品 id 为 field,商品数量为 value,恰好构成了购物车的3个要素。
    5. SET:Set 类型的底层数据结构是由哈希表或整数集合实现的
      • 如果集合中的元素都是整数且元素个数小于 512个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
      • 如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。
      应用场景主要有:
      • 点赞:key 是文章id,value 是用户id。
      • 共同关注:Set 类型支持交集运算,所以可以用来计算共同关注的好友、公众号等。key 可以是用户id,value 则是已关注的公众号的id。
      • 抽奖活动:存储某活动中中奖的用户名 ,Set 类型因为有去重功能,可以保证同一个用户不会中奖两次。
      Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。在主从集群中,为了避免主库因为 Set 做聚合计算(交集、差集、并集)时导致主库被阻塞,可以选择一个从库完成聚合统计,或者把数据返回给客户端,由客户端来完成聚合统计。
    6. Zset:Zset类型(Sorted Set,有序集合)可以根据元素的权重来排序,可以自己来决定每个元素的权重值。比如说,可以根据元素插入Sorted Set 的时间确定权重值,先插入的元素权重小,后插入的元素权重大。应用场景主要有
      • 在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,可以优先考虑使用 Zset。
      • 排行榜:有序集合比较典型的使用场景就是排行榜。例如学生成绩的排名榜、游戏积分排行榜、视频播放排名、电商系统中商品的销量排名等。
    7. BitMap:bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。可以用于签到统计、判断用户登陆态等操作。
    8. HyperLogLog:HyperLogLog用于基数统计,统计规则是基于概率完成的,不准确,标准误算率是 0.81%。优点是,在输入元素的数量或者体积非常非常大时,所需的内存空间总是固定的、并且很小。比如百万级网页 UV 计数等;
    9. GEO:主要用于存储地理位置信息,并对存储的信息进行操作。底层是由Zset实现的,使用GeoHash编码方法实现了经纬度到Zset中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为Zset元素的权重分数。
    10. Stream
    11. Redis专门为消息队列设计的数据类型。相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。之前方法缺陷:不能持久化,无法可靠的保存消息,并且对于离线重连的客户端不能读取历史消息。
  2. Redis底层数据结构?
    1. SDS
      • SDS 不仅可以保存文本数据,还可以保存二进制数据。O(1)复杂度获取字符串长度,因为有Len属性。不会发生缓冲区溢出,因为 SDS 在拼接字符串之前会检查空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。
    2. 链表节点是一个双向链表,在双向链表基础上封装了listNode这个数据结构。包括链表节点数量 len、以及可以自定义实现的 dup、free、match 函数。
      • listNode 链表节点的结构里带有 prev 和 next 指针,获取某个节点的前置节点或后置节点的时间复杂度只需O(1),而且这两个指针都可以指向 NULL,所以链表是无环链表;
      • list 结构因为提供了表头指针 head 和表尾节点 tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1);
      缺陷:
      • 链表每个节点之间的内存都是不连续的,无法很好利用 CPU 缓存。能很好利用CPU缓存的数据结构是数组,因为数组的内存是连续的。
      • 保存一个链表节点的值都需要一个链表节点结构头的分配,内存开销较大。
    3. 压缩列表压缩列表是由连续内存块组成的顺序型数据结构,类似于数组。不仅可以利用 CPU 缓存,而且会针对不同长度的数据,进行相应编码,这种方法可以有效地节省内存开销。不能保存过多的元素,否则查询效率就会降低;新增或修改某个元素时,压缩列表占用的内存空间需要重新分配,甚至可能引发连锁更新的问题。缺陷:
      • 空间扩展操作也就是重新分配内存,因此连锁更新一旦发生,就会导致压缩列表占用的内存空间要多次重新分配,直接影响到压缩列表的访问性能。
      • 如果保存的元素数量增加了,或是元素变大了,会导致内存重新分配,会有连锁更新的问题。
      • 压缩列表只会用于保存的节点数量不多的场景,只要节点数量足够小,即使发生连锁更新也能接受。
    4. 哈希哈希表是一种保存键值对(key-value)的数据结构。优点在于能以O(1)的复杂度快速查询数据。Redis 采用了拉链法来解决哈希冲突,在不扩容哈希表的前提下,将具有相同哈希值的数据串起来,形成链接。渐进式哈希的过程如下:Redis定义一个dict结构体,这个结构体里定义了两个哈希表(ht[2])
      • 给ht2分配空间;
      • 在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺序将ht1中索引位置上的所有数据迁移到ht2上;
      • 随着处理客户端发起的哈希表操作请求数量越多,最终在某个时间点会把「哈希表 1 」的所有 key-value 迁移到「哈希表 2」,从而完成 rehash 操作。
      • 渐进式哈希的触发条件?
        • 触发条件:负载因子 = 哈希表已保存节点数/哈希表大小当负载因子大于等于 1 ,没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作。
    5. 跳表
      • 一种多层的有序链表,能快读定位数据。当数据量很大时,跳表的查找复杂度就是O(logN)。
      节点同时保存元素和元素的权重,每个跳表节点都有一个后向指针,指向前一个节点,目的是为了方便从跳表的尾节点开始访问节点,为了倒序查找时方便。跳表是一个带有层级关系的链表,而且每一层级可以包含多个节点,每一个节点通过指针连接起来。Zset数组中一个属性是level数组,一个level数组就代表跳表的一层,定义了指向下一个节点的指针和跨度。
      • 跳表的查找过程?查找一个跳表节点的过程时,跳表会从头节点的最高层开始,逐一遍历每一层。在遍历某一层的跳表节点时,会用跳表节点中的 SDS 类型的元素和元素的权重来进行判断:
        • 如果当前节点的权重小于要查找的权重时,跳表就会访问该层上的下一个节点。
        • 如果当前节点的权重等于要查找的权重时,并且当前节点的 SDS 类型数据小于要查找的数据时,跳表就会访问该层上的下一个节点。
        如果上面两个条件都不满足,或者下一个节点为空时,跳表就会使用目前遍历到的节点的 level 数组里的下一层指针,然后沿着下一层指针继续查找。
      • 跳表的相邻两层的节点数量的比例会影响跳表的查询性能。相邻两层的节点数量最理想的比例是 2:1,查找复杂度可以降低到 O(logN)。为了防止插入删除时间消耗,跳表在创建节点的时候,随机生成每个节点的层数。具体的做法是,跳表在创建节点时候,会生成范围为[0-1]的一个随机数,如果这个随机数小于 0.25(相当于概率 25%),那么层数就增加 1 层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数。
    6. 整数集合
      • 整数集合本质上是一块连续内存空间。整数集合有一个升级规则,就是当将一个新元素加入到整数集合里面,如果新元素的类型(int32_t)比整数集合现有所有元素的类型(int16_t)都要长时,整数集合需要先进行升级,也就是按新元素的类型(int32_t)扩展 contents 数组的空间大小,然后才能将新元素加入到整数集合里,升级的过程中也要维持整数集合的有序性。
      • quicklist其实 quicklist 就是双向链表 + 压缩列表组合,quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表。quicklist 解决办法,通过控制每个链表节点中的压缩列表的大小或者元素个数,来规避连锁更新的问题。因为压缩列表元素越少或越小,连锁更新带来的影响就越小,从而提供了更好的访问性能。
      • listpacklistpack 没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题。
  3. 为什么用跳表而不用平衡树?
    1. 从内存占用上来比较,跳表比平衡树更灵活一些:平衡树每个节点包含 2 个指针(分别指向左右子树),而跳表每个节点包含的指针数目平均为 1/(1-p),如果像 Redis里的实现一样,取 p=1/4,那么平均每个节点包含 1.33 个指针,比平衡树更有优势。
    2. 在做范围查找的时候,跳表比平衡树操作要简单:在平衡树上,找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在跳表上进行范围查找就非常简单,只需要在找到小值之后,对第 1 层链表进行若干步的遍历就可以实现。
    3. 从算法实现难度上来比较,跳表比平衡树要简单得多。平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而跳表的插入和删除只需要修改相邻节点的指针,操作简单又快速。

三、持久化

  1. AOF和RDB?
    • AOF:每执行一条写操作命令,就将该命令以追加的方式写入到 AOF 文件,然后在恢复时,以逐一执行命令的方式来进行数据恢复。用 AOF 日志的方式来恢复数据很慢,因为 Redis 执行命令由单线程负责的,AOF 日志恢复数据的方式是顺序执行日志里的每一条命令,如果 AOF 日志很大,这个过程就会很慢了。
    • RDB:RDB 快照是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。RDB快照是全量快照,也就是说每次执行快照,都是把内存中的所有数据都记录到磁盘中。如果频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更多。通常可能设置至少 5 分钟才保存一次快照,这时如果 Redis 出现宕机等情况,意味着最多可能丢失 5 分钟数据。
    • AOF-RDB混用:在 AOF 重写日志时,fork出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失。缺点是AOF文件的可读性变差了。
  2. AOF的三种写回策略?
    1. Always、Everysec 和 No,这三种策略在可靠性上是从高到低,而在性能上从低到高。Always是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;Everysec每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;No就是不控制写回硬盘的时机。每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。
  3. AOF的磁盘重写机制?
    1. 随着执行的命令越多,AOF 文件的体积自然也会越来越大,为了避免日志文件过大, Redis 提供了 AOF 重写机制,它会直接扫描数据中所有的键值对数据,然后为每一个键值对生成一条写操作命令,接着将该命令写入到新的 AOF 文件,重写完成后,就替换掉现有的 AOF 日志。重写的过程是由后台子进程完成的,这样可以使得主进程可以继续正常处理命令。
  4. Redis过期机制?
    1. 三种过期删除策略:
      • 定时删除:在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器执行 key 的删除操作。
        • 优点:内存可以被尽快地释放。定时删除对内存是最友好的。
        • 缺点:定时删除策略对 CPU 不友好,删除过期 key 可能会占用相当一部分 CPU 时间,CPU 紧张的情况下将 CPU 用于删除和当前任务无关的过期键上,会对服务器的响应时间和吞吐量造成影响。
      • 惰性删除:不主动删除过期键,每次从数据库访问 key 时检测 key 是否过期,如果过期则删除该key。
        • 优点:只会使用很少的系统资源,对 CPU 最友好。
        • 缺点:如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放。惰性删除策略对内存不友好。
      • 定期删除:每隔一段时间随机从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
        • 优点:限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。
        • 缺点:内存清理方面没有定时删除效果好,同时没有惰性删除使用的系统资源少。难以确定删除操作执行的时长和频率。
    2. Redis 选择惰性删除+定期删除这两种策略配和使用,以求在合理使用 CPU 时间和避免内存浪费之间取得平衡。Redis 在访问或者修改 key 之前,都会调用 expireIfNeeded 函数对其进行检查,检查 key 是否过期:
      • 如果过期,则删除该 key,然后返回 null 客户端;
      • 如果没有过期,不做任何处理,然后返回正常的键值对给客户端;
      从过期字典中随机抽取 20 个 key;检查这 20 个 key 是否过期,并删除已过期的 key;已过期 key 的数量占比随机抽取 key 的数量大于 25%,则继续重复步骤直到比重小于25%。
  5. Redis的内存淘汰策略?
    1. 不进行数据淘汰的策略:它表示当运行内存超过最大设置内存时,不淘汰任何数据,这时如果有新的数据写入,则会触发 OOM,只是单纯的查询或者删除操作的话还是可以正常工作。
    2. 进行数据淘汰的策略:在设置了过期时间的数据中进行淘汰:
      • volatile-random:随机淘汰设置了过期时间的任意键值
      • volatile-ttl:优先淘汰更早过期的键值
      • volatile-lru:淘汰所有设置了过期时间的键值中,最久未使用的键值
      • volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值
      在所有数据范围内进行淘汰:
      • allkeys-random:随机淘汰任意键值
      • allkeys-lru:淘汰整个键值中最久未使用的键值
      • allkeys-lfu:淘汰整个键值中最少使用的键值

四、应用

  1. 缓存雪崩、击穿、穿透和解决办法?
    1. 缓存雪崩:当大量缓存数据在同一时间过期或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力增加,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。解决办法:
      • 大量数据同时过期
        • 均匀设置过期时间:避免将大量的数据设置成同一个过期时间。
        • 互斥锁:当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存。未能获取互斥锁的请求等待锁释放后重新读取缓存,或者返回空值或者默认值。
        • 双key策略:使用两个key,一个是主key,设置过期时间,一个是备key,不会设置过期,key不一样,但是value值是一样。当业务线程访问不到主key的缓存数据时,就直接返回备key的缓存数据,然后在更新缓存的时候,同时更新主key和备key的数据。
        • 后台更新缓存:业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。
      • Redis故障宕机
        • 服务熔断或请求限流机制:启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误,所以不用再继续访问数据库,保证数据库系统的正常运行,等到 Redis 恢复正常后,再允许业务应用访问缓存服务。服务熔断机制是保护数据库的正常允许,但是暂停了业务应用访问缓存服系统,全部业务都无法正常工作。也可以启用请求限流机制,只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务
        • 构建高可靠集群:通过主从节点的方式构建 Redis 缓存高可靠集群。如果 Redis 缓存的主节点故障宕机,从节点可以切换成为主节点,继续提供缓存服务,避免了由于 Redis 故障宕机而导致的缓存雪崩问题。
    2. 缓存击穿如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮。解决方案
      • 互斥锁方案:保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
      • 不给热点数据设置过期时间:由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间。
    3. 缓存穿透当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。解决方案
      • 非法请求的限制:当有大量恶意请求访问不存在的数据的时候会发生缓存穿透,可以在 API 入口处判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
      • 缓存空值或者默认值:当线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
      • 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。
  2. 布隆过滤器是怎么工作的?布隆过滤器由初始值都为0的位图数组N个哈希函数两部分组成。在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。
    • 第一步,使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值
    • 第二步,将第一步得到的 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置
    • 第三步,将每个哈希值在位图数组的对应位置的值设置为 1
    缺陷
    • 布隆过滤器由于是基于哈希函数实现查找的,会存在哈希冲突的可能性,数据可能落在相同位置,存在误判的情况。查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据。
    • 不支持一个关键字的删除,因为一个关键字的删除会牵连其他的关键字。改进方法就是counting Bloom filter,用一个counter数组代替位数组,就可以支持删除了。
    • 对于输入的n个元素,要确定数组m大小和hash函数的个数,hash函数个数k = (ln2)(m/n)时,错误率最小。在错误率不大于E情况下,m至少要等于nlg(1/E)才能表示n个元素的集合。
  3. 如何保证数据库和缓存的一致性?
    1. Cache Aside
      • 原理:先从缓存中读取数据,如果没有就再去数据库里面读数据,然后把数据放回缓存中,如果缓存中可以找到数据就直接返回数据;更新数据的时候先把数据持久化到数据库,然后再让缓存失效。
      • 问题:假如有两个操作一个更新一个查询,第一个操作先更新数据库,还没来及删除数据库,查询操作可能拿到的就是旧的数据;更新操作马上让缓存失效了,所以后续的查询可以保证数据的一致性;还有的问题就是有一个是读操作没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,也会造成脏数据。
      • 可行性:出现上述问题的概率其实非常低,需要同时达成读缓存时缓存失效并且有并发写的操作。数据库读写要比缓存慢得多,所以读操作在写操作之前进入数据库,并且在写操作之后更新,概率比较低。
    2. Read/Write Through
      • 原理:Read/Write Through原理是把更新数据库(Repository)的操作由缓存代理,应用认为后端是一个单一的存储,而存储自己维护自己的缓存。
      • Read Through:就是在查询操作中更新缓存,也就是说,当缓存失效的时候,Cache Aside策略是由调用方负责把数据加载入缓存,而Read Through则用缓存服务自己来加载,从而对调用方是透明的。
      • Write Through:当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由缓存自己更新数据库(这是一个同步操作)。
    3. Write Behind
      • 原理:在更新数据的时候,只更新缓存,不更新数据库,而缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作非常快,带来的问题是,数据不是强一致性的,而且可能会丢。
      • 第二步失效问题:这种可能性极小,缓存删除只是标记一下无效的软删除,可以看作不耗时间。如果会出问题,一般程序在写数据库那里就没有完成:故意在写完数据库后,休眠很长时间再来删除缓存。
      2PC或是Paxos
      • 2PC或是Paxos协议保证一致性,为2PC太慢,而Paxos太复杂。
  4. 如何保证删除缓存操作一定能成功?
    1. 重试机制:引入消息队列,删除缓存的操作由消费者来做,删除失败的话重新去消息队列拉取相应的操作,超过一定次数没有删除成功就像业务层报错。
    2. 订阅BINLog:订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除。可以让删除服务模拟自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,主节点收到请求后,就会开始推送 BINLog ,删除服务解析 BINLog 字节流之后,转换为便于读取的结构化数据,再进行删除。
  5. 业务一致性要求高怎么办?
    1. 先更新数据库再更新缓存:可以先更新数据库再更新缓存,但是可能会有并发更新的缓存不一致的问题。解决办法是更新缓存前加一个分布式锁,保证同一时间只运行一个请求更新缓存,加锁后对于写入的性能就会带来影响;在更新完缓存时,给缓存加上较短的过期时间,出现缓存不一致的情况缓存的数据也会很快过期。
    2. 延迟双删:采用延迟双删,先删除缓存,然后更新数据库,等待一段时间再删除缓存。保证第一个操作再睡眠之后,第二个操作完成更新缓存操作。但是具体睡眠多久其实是个玄学,很难评估出来,这个方案也只是尽可能保证一致性而已,依然也会出现缓存不一致的现象。
  6. 如何避免缓存失效?
    1. 由后台线程频繁地检测缓存是否有效,检测到缓存失效了马上从数据库读取数据,并更新到缓存。或者在业务线程发现缓存数据失效后,通过消息队列发送一条消息通知后台线程更新缓存,后台线程收到消息后,在更新缓存前可以判断缓存是否存在,存在就不执行更新缓存操作;不存在就读取数据库数据,并将数据加载到缓存。在业务刚上线的时候,最好提前把数据缓起来,而不是等待用户访问才来触发缓存构建,这就是所谓的缓存预热,后台更新缓存的机制刚好也适合干这个事情。
  7. Redis实现分布式锁?
    1. 使用SETNX命令,只有插入的key不存在才插入,如果SETNX的key存在就插入失败,key插入成功代表加锁成功,否则加锁失败;解锁的过程就是将key删除,保证执行操作的客户端就是加锁的客户端,加锁时候要设置unique_valu,解锁的时候,要先判断锁的 unique_value 是否为加锁客户端,是才将 lock_key 键删除。此外要给锁设置一个过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,可以指定 EX/PX 参数设置过期时间。SET lock_key unique_value NX PX 10000
  8. 如何保证加锁和解锁过程的原子性?
    1. 使用Lua脚本,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。
  9. 使用Redis实现分布式锁的优点和缺点?
    • 优点:性能高效;实现方便;避免单点故障。
    • 缺点:超时时间不好设置。如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短会保护不到共享资源。Redis主从复制模式中的数据是异步复制的,导致分布式锁的不可靠性。如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。
  10. 如何为分布式锁设置合理的超时时间?
    1. 可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。实现方式就是:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可,不过这种方式实现起来相对复杂。
  11. Redis解决集群情况下分布式锁的可靠性?
    1. 分布式锁算法 Redlock(红锁)。基于多个 Redis 节点的分布式锁,即使有节点发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。官方推荐是至少部署 5 个 Redis 节点,而且都是主节点,它们之间没有任何关系,都是一个个孤立的节点。基本思路是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么就认为,客户端成功地获得分布式锁,否则加锁失败。即使有某个 Redis 节点发生故障,锁的数据在其他节点上也有保存,客户端仍然可以正常地进行锁操作,锁的数据也不会丢失。Redlock 算法加锁三个过程:
      • 第一步是,客户端获取当前时间(t1)。
      • 第二步是,客户端按顺序依次向 N 个 Redis 节点执行加锁操作:加锁操作使用 SET NX,EX/PX 选项,以及带上客户端的唯一标识。如果某个 Redis 节点发生故障了,为了保证在这种情况下,Redlock 算法能够继续运行,需要给「加锁操作」设置一个超时时间,加锁操作的超时时间需要远远地小于锁的过期时间。
      • 第三步是,一旦客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁,就再次获取当前时间(t2),然后计算计算整个加锁过程的总耗时(t2-t1)。如果 t2-t1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败。
      加锁成功要同时满足两个条件:有超过半数的 Redis 节点成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功。加锁成功后,客户端需要重新计算这把锁的有效时间,计算的结果是「锁最初设置的过期时间」减去「客户端从大多数节点获取锁的总耗时(t2-t1)」。如果计算的结果已经来不及完成共享数据的操作了,可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。加锁失败后,客户端向所有 Redis 节点发起释放锁的操作,执行释放锁的 Lua 脚本就可以。
  12. Redis如何处理大key?定义:String 类型的值大于 10 KB;Hash、List、Set、ZSet 类型的元素的个数超过5000个;影响
    • 客户端超时阻塞:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时。客户端认为很久没有响应。
    • 引发网络阻塞:每次获取大 key 产生的网络流量较大。
    • 阻塞工作线程:如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
    • 内存分布不均:集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。
    处理
    • 当vaule是string时,比较难拆分,则使用序列化、压缩算法将key的大小控制在合理范围内,但是序列化和反序列化都会带来更多时间上的消耗。
    • 当value是string,压缩之后仍然是大key,则需要进行拆分,一个大key分为不同的部分,记录每个部分的key,使用multiget等操作实现事务读取。
    • 分拆成几个key-value,存储在一个hash中,每个field代表一个具体的属性,使用hget,hmget来获取部分的value,使用hset,hmset来更新部分属性
    • 当value是list/set等集合类型时,根据预估的数据规模来进行分片,不同的元素计算后分到不同的片。

五、集群

  1. Redis集群模式有哪些?主从:选择一台作为主服务器,将数据到多台从服务器上,构建一主多从的模式,主从之间读写分离。主服务器可读可写,发生写操作会同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令。主从服务器之间的命令复制是异步进行的,所以无法实现强一致性保证(主从数据时时刻刻保持一致)。哨兵:当 Redis 的主从服务器出现故障宕机时,需要手动进行恢复,为了解决这个问题,Redis 增加了哨兵模式,哨兵监控主从服务器,并且提供主从节点故障转移的功能。切片集群:当数据量大到一台服务器无法承载,需要使用Redis切片集群(Redis Cluster)方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,提高 Redis 服务的读写性能。
  2. Redis切片集群的工作原理?切片集群会采用哈希槽来进行数据和节点的映射,一个切片集群一共有16384个槽位,每个存储数据的key会经过运算映射到16384个槽位中,映射关系如下:
    • 由key通过CRC16算法计算出一个16bit的数字
    • 根据上面计算得到的数字对16384取模来确定对应的哈希槽
  3. 主从模式的同步过程?第一次同步主要分为建立连接协商、主从数据同步、发送新操作三个步骤:
    • 连接协商:从服务器先发送命令给主服务器表示要进行数据同步,命令内容包括主服务器的runID复制进度两个参数,主服务器收到命令之后会给从服务响应命令,响应包括主服务器的runID复制进度。从服务器收到响应之后会记录这两个值。
    • 主从数据同步:主服务器生成RDB文件并发送给从服务器,从服务器收到RDB之后先清空自己的数据,再载入RDB文件。为了主从数据的一致性,这个期间主服务器后续的写操作会记录到replication buffer缓冲区里
    • 发送新操作:主服务器发送replication buffer里面的写操作给从服务器,从服务器执行这些操作。第一次同步完成。
    命令传播第一次同步完成之后双方会维护一个TCP连接,后续主服务器的写命令通过TCP连接发送给从服务器,保证主从一致。压力分摊为了分摊服务器的压力,生成和传输RDB的工作可以分摊到经理从服务器上。增量复制如果服务器网路断开,在恢复之后,会把网络断开期间主服务器接收到的写操作命令同步给从服务器。
  4. 如何避免主从数据的不一致?
    1. 让主从节点处于同一机房,降低网络延迟;或者由外部程序监控主从复制进度:先计算得出主从服务之间的复制进度差,如果复制进度差大于程序设定的阈值,让客户端不再在此节点读取数据,减小数据不一致的情况对业务的影响。
  5. 哨兵机制是什么?
    1. 因为在主从架构中读写是分离的,如果主节点挂了,将没有主节点来响应客户端的写操作请求,也无法进行数据同步。哨兵作用是实现主从节点故障转移。哨兵会监测主节点是否存活,如果发现主节点挂了,会选举一个从节点切换为主节点,并且把新主节点的相关信息通知给从节点和客户端。
  6. 哨兵机制的工作原理?判断节点是否存活哨兵会周期性给所有主节点发送PING命令来判断其他节点是否正常运行。如果PING命令响应失败哨兵会将节点标记为主观下线,然后该哨兵会向其他节点发出投票命令,当票数达到设定的阈值之后这个主节点就被标记为客观下线。然后哨兵会从从节点中选择一个作为主节点。投票哨兵集群中会选择一个leader来负责主从切换,选举是一个投票过程:判断主节点为客观下线的是候选者,候选者向其他哨兵发送命令表示要成为leader,其他哨兵会进行投票,每个哨兵只有一票,可以投给自己或投给别人,但是只有候选者才能把票投给自己。候选者之后拿到半数以上的赞成票并且票数大于设置的阈值,就会成为候选者。选出新主节点把网络状态不好的从节点给排除:先把已经下线的从节点过滤掉,然后把以往网络连接状态不好的从节点排除掉。接下来要对所有从节点进行三轮考察:优先级、复制进度、ID号。在进行每一轮考察的时候,哪个从节点优先胜出,就选择其作为新主节点:
    • 第一轮考察:哨兵首先会根据从节点的优先级来进行排序,优先级越小排名越靠前。
    • 第二轮考察:如果优先级相同,则查看复制的下标,哪个接收的复制数据多哪个就靠前。
    • 第三轮考察:如果优先级和下标都相同,选择ID较小的那个。
    更换主节点选出新主节点之后,哨兵leader让已下线主节点属下的所有从节点指向新主节点。通知客户的主节点已更换客户端和哨兵建立连接后,客户端会订阅哨兵提供的频道。主从切换完成后,哨兵就会向 +switch-master 频道发布新主节点的 IP 地址和端口的消息,这个时候客户端就可以收到这条信息,然后用这里面的新主节点的 IP 地址和端口进行通信了。将旧主节点变为从节点继续监视旧主节点,当旧主节点重新上线时,哨兵集群就会向它发送SLAVEOF命令,让它成为新主节点的从节点。
  7. 什么是集群的脑裂?如果主节点的网络突然发生了问题与所有的从节点都失联了,但此时的主节点和客户端的网络是正常的,客户端不知道集群内部已经出现了问题,还在向这个失联的主节点写数据,此时这些数据被主节点缓存到了缓冲区里。哨兵也发现主节点失联了,就会在从节点中选举出一个leader作为主节点,会导致集群有两个主节点。网络恢复后哨兵因为之前已经选举出一个新主节点了,它就会把旧主节点降级,然后从旧主节点会向新主节点请求数据同步,因为第一次同步是全量同步的方式,旧主节点会清空掉自己本地的数据。客户端在过程之前写入的数据就会丢失了。所以脑裂会导致集群数据的丢失。
  8. 如何减少主从切换带来的数据丢失?异步复制同步丢失配置一个阈值,一旦所有的从节点数据复制和同步的延迟都超过了阈值,主节点就会拒绝接收任何请求。对于客户端发现主节点不可写后,可以采取降级措施。将数据暂时写入本地缓存和磁盘中,在一段时间后重新写入主节点来保证数据不丢失,也可以将数据写入消息队列,等主节点恢复正常,再隔一段时间去消费消息队列中的数据,让将数据重新写入主节点。集群产生脑裂数据丢失当主节点发现从节点下线或者通信超时的总数量小于阈值时,那么禁止主节点进行写数据,直接把错误返回给客户端。设置主节点连接的从节点中至少有 N 个从节点,并且主节点进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主节点就不会再接收客户端的写请求了。等到新主节点上线时,就只有新主节点能接收和处理客户端请求,此时,新写的数据会被直接写到新主节点中。而原主节点会被哨兵降为从节点,即使它的数据被清空了,也不会有新数据丢失。
文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇