# Redis面试问题

# 谈谈你对Redis的理解?

  • Redis是一个基于Key-Value存储结构的Nosql开源内存数据库。
  • 它提供了5种常用的数据类型,String、Map、Set、ZSet、List。针对不同的结构,可以解决不同场景的问题。因此它可以覆盖应用开发中大部分的业务场景,比如top10问题、好友关注列表、热点话题等。
  • 由于Redis是基于内存存储,并且在数据结构上做了大量的优化所以IO性能比较好,在实际开发中,会把它作为应用与数据库之间的一个分布式缓存组件。
  • 并且它又是一个非关系型数据的存储,不存在表之间的关联查询问题,所以它可以很好的提升应用程序的数据IO效率。
  • 作为企业级开发来说,它又提供了主从复制+哨兵、以及集群方式实现高可用,在Redis集群里面,通过hash槽的方式实现了数据分片,进一步提升了性能。

# Redis的为什么快?

  1. Redis是基于内存操作,需要的时候需要我们手动持久化到硬盘中
  2. Redis高效数据结构,对数据的操作也比较简单,支持字符串(strings)、列表(lists)、散列(hash)、集合(sets)、有序集合(sorted sets)等。。
  3. Redis是单线程模型,从而避开了多线程中上下文频繁切换的操作
  4. 使用多路I/O复用模型,非阻塞I/O

# Redis的基础数据结构

五大基本数据结构:

  1. String:是采用的结构体+链表的结构 SDS 这样的一个结构。
  2. List:采用的是结构体+双链表
  3. Hash:数组+链表的的这样的一种的
  4. Set: inset 的结构+hashtable 的这样的这样的一种结构
  5. Zset:采用的是跳跃表(skiplist)/ziplist 两种数据结构

当存储的信息是不需要修改的情况采用的是的String结构。当对象的某个属性需要频繁修改时,采用的hash结构,如果使用hash类型,则可以针对某个属性单独修改, 没有序列化,也不需要修改整个对象。比如,商品的价格、销量、关注数、评价数等可能经常发生变化的属性,就适合存储在 hash 类型里。

img.png

# Redis string类型原理

String 类型,也就是字符串类型,是Redis中最简单的存储类型。它可以存储字符串、整数或浮点数。

在 Redis 中,String 类型的数据结构并不是采用 C 语言中自带的字符串类型,C 语言中的数据结构存在很多问题,比如:获取字符串长度的需要通过运算、非二进制安全、不可修改 因此,String 在 Redis 中有其他三种编码方式: int、embstr、raw 。其中, raw 和 embstr 类型,都是基于动态字符串(SDS)实现的,下面我们先来看看动态字符串的结构是怎样的。

img.png

struct __attribute__ ((__packed__)) hisdshdr8 {
    uint8_t len;          // 已经保存的字符串字节数,不包含结束标示
    uint8_t alloc;        // 申请的总的字节数,不包含结束标示
    unsigned char flags;  // 不同的 SDS 的头类型,用来控制 SDS 的头大小
    char buf[];           // 真正存储数据 
};

flags 这个成员变量。在 redis 中其实定义了5 个SDS结构体(其中 hisdshdr5 已经弃用)如图所示。他们之间的主要区别在于len和alloc 的长度不同。 在 redis 中,为了尽可能地节省内存空间,当字符串长度在不同的区间时,会选择不同的结构体, 当字符串长度在 0~255 个字节之间时,会选择 hisdshdr8 ,这样一来,用于表示字符串字节数和申请的总字节数的空间就会被大大节省,以此类推。

img_1.png

SDS之所以叫做动态字符串,是因为它具备动态扩容的能力,例如一个内容为 “hello” 的 SDS,假如我们要给这个 SDS 追加一段字符串 ”world” ,这里首先会申请新内存空间:

  1. 如果新字符串小于1M,则新空间为扩展后字符串长度的两倍+1
  2. 如果新字符串大于1M,则新空间为扩展后字符串长度+1M+1。

这种机制称为内存预分配。内存预分配可以减少进行内存重新分配的开销,减少内存碎片,使得 redis 的性能得到提高,空间利用率也得到提高。

RAWraw 是 string 的基本编码方式,基于简单动态字符串(SDS)实现,存储上限为512mb。当一个字符串采用raw的编码方式的时候,它的结构如图所示。

img.png

EMBSTR如果存储在 SDS 中的数据小于等于 44 字节,则会采用 EMBSTR 编码,此时 **RedisObject 与 SDS 是一段连续空间。 而不是像 RAW 的编码方式一样,由 ptr 指向另外一片空间,申请内存时只需要调用一次内存分配函数,效率更高。结构如下,

img.png

为什么是 44 字节?Redis 默认的内存分配器 jemalloc 分配内存大小的单位是 $2^n$ ,因此,如果分配的空间大小为 2、4 、8 … 字节等 $2^n$ 字节,就不会产生内存碎片。 而 redisObject 和 hisdshdr8 中 len alloc flags三个成员变量加起来刚刚好是 16 + 4 = 20 字节,如果 char[] (数据大小)的大小为 44 字节时, 加起来刚刚好是 64 字节,也即 262^626 不会产生内存碎片。

INT如果存储的字符串是整数值,并且大小在 LONG MAX 范围内,则会采用 INT 编码,直接将数据保存在 RedisObject 的 ptr 指针位置(刚好8字节),不再需要SDS了。

img.png

String类型使用

  1. 几乎所有场景都是用String类型来存储数据。
  2. 使用String类型的incr命令,实现原子递增
  3. 使用计数器实现手机验证码频率限流。
  4. 基于登录场景中,保存token信息。

# Redis-list底层原理

list底层有两种数据结构:**链表linkedlist和压缩列表ziplist。当list元素个数少且元素内容长度不大时,使用ziplist实现,否则使用linkedlist。

Redis使用的链表是双向链表。为了方便操作,使用了一个list结构来持有这个链表。如图所示:

链表: Redis使用的链表是双向链表。为了方便操作,使用了一个list结构来持有这个链表。如图所示:

img.png

typedef struct list{
    //表头节点
    listNode *head;
    //表尾节点
    listNode *tail;
    //链表所包含的节点数量
    unsigned long len;
    //节点值复制函数
    void *(*dup)(void *ptr);
    //节点值释放函数
    void *(*free)(void *ptr);
    //节点值对比函数
    int (*match)(void *ptr,void *key);
}list;

data存的其实也是一个指针。链表里面的元素是上面介绍的string。因为是双向链表,所以可以很方便地把它当成一个栈或者队列来使用。

压缩列表: 与上面的链表相对应,压缩列表有点儿类似数组,通过一片连续的内存空间,来存储数据。不过,它跟数组不同的一点是, 它允许存储的数据大小不同。每个节点上增加一个length属性来记录这个节点的长度,这样比较方便地得到下一个节点的位置。

img.png

  1. zlbytes:列表的总长度
  2. zltail:指向最末元素
  3. zllen:元素的个数
  4. entry:元素的内容,里面记录了前一个Entry的长度,用于方便双向遍历
  5. zlend:恒为0xFF,作为ziplist的定界符

压缩列表不只是list的底层实现,也是hash的底层实现之一。当hash的元素个数少且内容长度不大时,使用压缩列表来实现。

list实际使用场景

  1. 消息队列:列表类型可以使用 rpush 实现先进先出的功能,同时又可以使用 lpop 轻松的弹出(查询并删除)第一个元素,所以列表类型可以用来实现消息队列,
  2. 在发红包的场景中,假设发一个10元,10个红包,需要保证抢红包的人不会多抢到,也不会少抢到。

# redis hash底层原理

hash底层有两种实现:压缩列表和字典(dict)。压缩列表刚刚上面已经介绍过了,下面主要介绍一下字典的数据结构。

字典: 字典其实就类似于Java语言中的Map,Python语言中的dict。与Java中的HashMap类似, Redis底层也是使用的散列表作为字典的实现,解决hash冲突使用的是链表法。Redis同样使用了一个数据结构来持有这个散列表:

img.png

在键增加或减少时,会扩容或缩容,并且进行rehash,根据hash值重新计算索引值。那如果这个字典太大了怎么办呢?

为了解决一次性扩容耗时过多的情况,可以将扩容操作穿插在插入操作的过程中,分批完成。 当负载因子触达阈值之后,只申请新空间,但并不将老的数据搬移到新散列表中。当有新数据要插入时,将新数据插入新散列表中, 并且从老的散列表中拿出一个数据放入到新散列表。每次插入一个数据到散列表,都重复上面的过程。经过多次插入操作之后, 老的散列表中的数据就一点一点全部搬移到新散列表中了。这样没有了集中的一次一次性数据搬移,插入操作就都变得很快了。这个过程也被称为渐进式rehash。

Hash实际应用场景

Hash表使用用来存储对象数据,比如用户信息,相对于通过将对象转化为json存储到String类型中,Hash结构的灵活性更大,它可以任何添加和删除对象中的某些字段。

购物车功能

  1. 以用户ID作为key
  2. 以商品id作为field
  3. 以商品的数量作为value

对象类型数据

比如优化之后的用户信息存储,减少数据库的关联查询导致的性能慢的问题。

  1. 用户信息
  2. 商品信息
  3. 计数器

# redis-set底层实现

set里面没有重复的集合。set的实现比较简单。如果是整数类型,就直接使用整数集合intset 使用二分查找来辅助,速度还是挺快的。 不过在插入的时候,由于要移动元素,时间复杂度是O(N)。如果不是整数类型,就使用上面在hash字典。key为set的值,value为空。

set类型的实际应用场景

标签管理功能:这种标签系统在电商系统、社交系统、视频网站,图书网站,旅游网站等都有着广泛的应用。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,

相关商品信息展示:比如在电商系统中,当用户查看某个商品时,可以推荐和这个商品标签有关的商品信息。

# redis-zset底层实现

zset是可排序的set。与hash的实现方式类似,如果元素个数不多且不大,就使用压缩列表ziplist来存储。 如果元素个数多采用redis-skiplist来实现。 不过由于zset包含了score的排序信息,所以在ziplist内部,是按照score排序递增来存储的。意味着每次插入数据都要移动之后的数据。

跳表

跳表(skiplist)是另一种实现dict的数据结构。跳表是对链表的一个增强。我们在使用链表的时候,即使元素的有序排列的, 但如果要查找一个元素,也需要从头一个个查找下去,时间复杂度是O(N)。而跳表顾名思义,就是跳跃了一些元素,可以抽象多层。

如下图所示,比如我们要查找8,先在最上层L2查找,发现在1和9之间;然后去L1层查找,发现在5和9之间;然后去L0查找,发现在7和9之间,然后找到8。 当元素比较多时,使用跳表可以显著减少查找的次数。

img.png

同list类似,Redis内部也不是直接使用的跳表,而是使用了一个自定义的数据结构来持有跳表。下图左边蓝色部分是skiplist, 右边是4个zskiplistNode。zskiplistNode内部有很多层L1、L2等,指针指向这一层的下一个结点。BW是回退指针(backward), 用于查找的时候回退。然后下面是score和对象本身object。

img.png

ZSet的使用场景

  1. 排行榜系统
  2. 热点话题排名

# 为什么要设计sds?

# 为什么会设计Stream,Stream用在什么样场景

# 消息ID的设计是否考虑了时间回拨的问题

# redis-HyperLogLog原理

HyperLogLog是Redis2.8.9提供的一种数据结构,他提供了一种基数统计方法,简单来说就是一个集合中不重复元素的个数,比如有一个集合{1,2,3,1,2},那么它的基数就是3。

Redis 提供了 HyperLogLog 数据结构就是用来解决这种统计问题的。HyperLogLog 提供不精确的去重计数方案,虽然不精确但是也不是非常不精确, 标准误差是 0.81%,这样的精确度已经可以满足上面的 UV 统计需求了。HyperLogLog 数据结构是 Redis 的高级数据结构,它非常有用。

# redis-GeoHash原理

基于数据库实现LBS服务

计算附近的人,通过一个坐标计算这个坐标附近的其他数据,按照距离排序,如何下手呢?

以用户为中心,给定一个 1000 米作为半径画圆,那么圆形区域内的用户就是我们想要邂逅的附近的人。将经纬度存储到 MySQL。 我们可以通过区域来过滤出有限「女神」坐标数据,再对矩形区域内的数据进行全量距离计算再排序,这样计算量明显降低。在圆形外套上一个正方形, 根据用户经、纬度的最大最小值(经、纬度 + 距离),作为筛选条件过滤数据,就很容易将正方形内的人信息搜索出来。

多出来的这部分区域内的用户,到圆点的距离一定比圆的半径要大,那么我们就计算用户中心点与正方形内所有用户的距离,筛选出所有距离小于等于半径的用户, 圆形区域内的所用户即符合要求的附近的人。为了满足高性能的矩形区域算法,数据表需要在经纬度坐标加上复合索引 (longitude, latitude),这样可以最大优化查询性能。

GeoHash算法 :将二维的经纬度数据映射到一维的整数,这样所有的元素都将在挂载到一条线上,距离靠近的二维坐标映射到一维后的点之间距离也会很接近。 当我们想要计算附近的人时,首先将目标位置映射到这条线上,然后在这个一维的线上获取附近的点就行了。

# redis的渐进式rehash原理

redis的渐进式rehas背景:在键增加或减少时,会扩容或缩容,并且进行rehash,根据hash值重新计算索引值。

为了解决一次性扩容耗时过多的情况,可以将扩容操作穿插在插入操作的过程中,分批完成。 当负载因子触达阈值之后,只申请新空间,但并不将老的数据搬移到新散列表中。当有新数据要插入时,将新数据插入新散列表中, 并且从老的散列表中拿出一个数据放入到新散列表。每次插入一个数据到散列表,都重复上面的过程。经过多次插入操作之后, 老的散列表中的数据就一点一点全部搬移到新散列表中了。这样没有了集中的一次一次性数据搬移,插入操作就都变得很快了。这个过程也被称为渐进式rehash。

# SkipList的索引过程,能否越两级搜索?

在一般情况下,跳表的查找过程,是从最高层开始逐层向下查找的,每次查找都是在当前层中找到小于目标元素的最大元素,然后跳转到下一层,继续进行查找。 如果最后找到了目标元素,就返回这个元素所在的节点,否则返回空。所以,对于跳跃表中的索引过程,并没有直接跳跃两层检索的情况,也是逐层进行判断最终获得查找的结果。

# reids的应用场景

实际上,所谓的应用场景,其实就是合理的利用Redis本身的数据结构的特性来完成相关业务功能,就像mysql,它可以用来做服务注册, 也可以用来做分布式锁,但是mysql它本质是一个关系型数据库,只是用到了其他特性而已。

  1. 缓存——提升热点数据的访问速度
  2. 共享数据——数据的存储和共享的问题
  3. 全局ID —— 分布式全局ID的生成方案(分库分表)
  4. 分布式锁——进程间共享数据的原子操作保证
  5. 在线用户统计和计数
  6. 队列、栈——跨进程的队列/栈
  7. 消息队列——异步解耦的消息机制
  8. 服务注册与发现 —— RPC通信机制的服务协调中心(Dubbo支持Redis)
  9. 购物车
  10. 新浪/Twitter 用户消息时间线
  11. 抽奖逻辑(礼物、转发)
  12. 点赞、签到、打卡
  13. 商品标签
  14. 用户(商品)关注(推荐)模型
  15. 电商产品筛选
  16. 排行榜

# 一致性Hash算法与Hash环数据倾斜问题

采用一致性哈希算法(consistent hashing)

将key和节点name同时hashing,然后进行映射匹配,采用的算法是MURMUR_HASH。采用一致性哈希而不是采用简单类似哈希求模映射的主要原因是当增加或减少节点时, 不会产生由于重新匹配造成的rehashing。一致性哈希只影响相邻节点key分配,影响量小。

为了避免一致性哈希只影响相邻节点造成节点分配压力,ShardedJedis会对每个Redis节点根据名字(没有,Jedis会赋予缺省名字)会虚拟化出160个虚拟节点进行散列。 根据权重weight,也可虚拟化出160倍数的虚拟节点。用虚拟节点做映射匹配,可以在增加或减少Redis节点时,key在各Redis节点移动再分配更均匀,而不是只有相邻节点受影响。

Hash环的数据倾斜问题:

一致性Hash算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题, 例如系统中只有两台服务器,其环分布如图所示,此时必然造成大量数据集中到Node A上,而只有极少量会定位到Node B上。 为了解决这种数据倾斜问题,一致性Hash算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。 具体做法可以在服务器IP或主机名的后面增加编号来实现。例如上面的情况,可以为每台服务器计算三个虚拟节点, 于是可以分别计算 “Node A#1”、“Node A#2”、“Node A#3”、“Node B#1”、“Node B#2”、“Node B#3”的哈希值,于是形成六个虚拟节点:

# redis的哨兵机制的原理

Redis里面的Master-Slave集群,是不具备故障恢复能力的。也就是Master节点挂了以后,需要从集群中的其他Slave节点选举新的Master继续提供服务。 因此在Redis里面引入了Sentinel哨兵机制,通过哨兵来监控集群的状态,实现Master选举。哨兵是一个单独的进程,所以为了保证哨兵的可靠性,我们也会对哨兵做部署集群。

假设哨兵节点有3个,那这个时候这三个节点分别去监听Redis的三个主从节点,这里就存在一个问题:一旦Redis主从集群的某个节点出现故障, 而故障节点被其中一个Sentinel哨兵节点检测到,但是另外两个节点还没检测到,那三个哨兵节点如何在意见上达成意见上的一致呢? 同时,哨兵节点怎么判断哪一个Slave节点应该成为Master呢?

当Redis集群中的Master节点出现故障,哨兵节点检测到以后,会从Redis集群中的其他Slave节点选举出一个作为新的Master。

筛选

在筛选阶段,会过滤掉不健康的节点,比如(下线或者断线),或者没有回复Sentinel哨兵心跳响应的Slave节点。同时,还会评估实例过往的网络连接情况, 如果在一定时间内,Slave和Master经常性断链,而且超出了一定的阈值,也不会考虑。经过筛选后,留下的都是健康的节点了。

选举机制

接下来就对健康节点进行综合评估,具体有三个维度,按照顺序来判断。

  1. 根据Slave优先级来判断,通过 slave-priority 配置项(redis.conf),可以给不同的从库设置不同优先级,优先级高的优先成为master。
  2. 选择数据偏移量差距最小的,即slave_repl_offset与 master_repl_offset进度差距,其实就是比较 slave 与 原master 复制进度差距,避免丢失过多数据的问题。
  3. slave runID,在优先级和复制进度都相同的情况下,选用runID最好的,runID越小说明创建时间越早,优先选为master。

经过以上步骤,就可以选举出新的Master节点了。另外,如果哨兵存在集群的情况下,如果其中一个哨兵节点认为Redis集群主线故障,另外两个哨兵还没感知到的情况下。 在进行Master选举之前,Sentinel哨兵集群需要通过共识算法来达成一致,这里用到了Raft协议。

# Redis中分布式集群的数据倾斜问题

一致性hash,是一种比较特殊的hash算法,它的核心思想是解决在分布式环境下,hash表中可能存在的动态扩容和缩容的问题。 一般情况下,我们会使用hash表的方式以key-value的方式来存储数据,但是当数据量比较大的时候,我们就会把数据存储到多个节点上,然后通过hash取模的方法来决定当前key存储到哪个节点上。

img.png

这种方式有一个非常明显的问题,**就是当存储节点增加或者减少的时候,原本的映射关系就会发生变化。**也就是需要对所有数据按照新的节点数量重新映射一遍,这个涉及到大量的数据迁移和重新映射,迁移代价很大。

而一致性hash就是用来优化这种动态变化场景的算法,它的具体工作原理也很简单。首先,一致性Hash是通过一个Hash环的数据结构来实现的,这个环的起点是0,终点是2^32-1。也就是这个环的数据分布范围是[0,2^32-1]。

然后我们把存储节点的ip地址作为key进行hash之后,会在Hash环上确定一个位置。接下来就是把需要存储的目标key使用hash算法计算后得到一个hash值,同样也会落到hash环的某个位置上。

然后这个目标key会按照顺时针的方向找到离自己最近的一个节点进行数据存储。假设现在需要新增一个节点node4,那数据的映射关系的影响范围只限于node3和node1,只有少部分的数据需要重新映射迁移就行了。

img.png

如果是已经存在的节点node1因为故障下线了,只那只需要把原本分配在node1上的数据重新分配到node2上就行了。同样对数据影响的范围非常小。

img.png

所以,在我看来,一致性hash算法的好处是扩展性很强,在增加或者减少服务器的时候,数据迁移范围比较小。 另外,在一致性Hash算范里面,为了避免hash倾斜导致数据分配不均匀的情况,我们可以使用虚拟节点的方式来解决。

# redis的缓存失效与解决方案

  1. 缓存雪崩缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决方案

  • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  • 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
  • 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。
  1. 缓存穿透指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

解决方案:

  • 接口层增加校验(限流操作),如用户鉴权校验(token方法),id做基础校验,直接拦截。
  • 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击。
  • 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
  1. 缓存击穿指缓存没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大, 造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案:

  • 设置热点数据永远不过期。
  • 加互斥锁,互斥锁。

实际的工作中,采用服务降级或者扩容的方式,系统监控与预警的方式来实现缓存的失效的问题。

不过,在我看来,您提出来的这个问题,有点过于放大了它带来的影响。

  1. 首先,在一个成熟的系统里面,对于比较重要的热点数据,必然会有一个专门缓存系统来维护,同时它的过期时间的维护必然和其他业务的key会有一定的差别。而且非常重要的场景,我们还会设计多级缓存系统
  2. 即便是触发了缓存雪崩,数据库本身的容灾能力也并没有那么脆弱,数据库的主从、双主、读写分离这些策略都能够很好的缓解并发流量。
  3. 最后,数据库本身也有最大连接数的限制,超过限制的请求会被拒绝,再结合熔断机制,也能够很好的保护数据库系统,最多就是造成部分用户体验不好。
  4. 另外,在程序设计上,为了避免缓存未命中导致大量请求穿透到数据库的问题,还可以在访问数据库这个环节加锁。虽然影响了性能,但是对系统是安全的

# 布隆过滤器实现原理

布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。 它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

img.png

当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行如下操作:

  1. 对给定元素再次进行相同的哈希计算;
  2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。例如下面的图例种:

img.png

布隆过滤器应用场景

  1. 数据库防止穿库. Google Bigtable,HBase 和 Cassandra 以及 Postgresql 使用BloomFilter来减少不存在的行或列的磁盘查找。避免代价高昂的磁盘查找会大大提高数据库查询操作的性能。
  2. 判断用户是否访问过,判断用户是否阅读过某视频或文章,比如抖音或头条,当然会导致一定的误判,但不会让用户看到重复的内容。
  3. 解决缓存穿透: 一般判断用户是否在缓存中,如果在则直接返回结果,不在则查询db,如果来一波冷数据,会导致缓存大量击穿,造成雪崩效应,这时候可以用布隆过滤器当缓存的索引,只有在布隆过滤器中,才去查询缓存,如果没查询到,则穿透到db。如果不在布隆器中,则直接返回。
  4. Web拦截器,黑名单校验: 如果相同请求则拦截,防止重复被攻击。用户第一次请求,将请求参数放入布隆过滤器中,当第二次请求时,先判断请求参数是否被布隆过滤器命中。可以提高缓存命中率。

# redis的脑裂

脑裂其实就是网络分区导致的现象,比如,我们的master机器网络突然不正常了发生了网络分区,和其他的slave机器不能正常通信了, 其实master并没有挂还活着好好的呢,但是哨兵可不是吃闲饭的啊,它会认为master挂掉了啊,那么问题来了,client可能还在继续写master的呀, 还没来得及更新到新的master呢,那这部分数据就会丢失。

img.png

减少脑裂数据的丢失(不能保证完全保证数据不丢失,竟可能的少丢失)

  1. 如果master出现了脑裂,和其他的slave失去了通信,不能继续给指定数量的slave发送数据。
  2. slave超过10秒没有给自己返回ack消息。
  3. master就会拒绝客户端的写请求

# 主从异步复制导致的数据丢失

img.png

redis master 和slave 数据复制是异步的,像前面说的MySQL差不多,这样就有可能会出现部分数据还没有复制到slave中,master就挂掉了,那么这部分的数据就会丢失了。

img.png

解决方案:现在当我们的slave在数据复制的时候,发现返回的ACK时延太长达到了 min-slaves-max-lag 配置, 这个时候就会认为如果master宕机就会导致大量数据丢失,所以就提前进行了预测,就不再去接收客户端的任何请求了,来将丢失的数据降低在可控范围内。

# 哨兵集群如何实现自动发现

  1. 通过redis的pub/sub系统实现的,每个哨兵都会往__sentinel__:hello这个channel里发送一个消息。
  2. 其他哨兵可以消费到这个消息,且可以感知到其他哨兵的存在。
  3. 每隔两秒钟,每个哨兵都会向自己监控的某个master、slaves对应的__sentinel__:hello channel里发送一个消息(包括自己的host、ip和runid还有对这个master的监控配置)。
  4. 每个哨兵也会去监听自己监控的每个master+slaves对应的__sentinel__:hello channel,然后去感知到同样在监听这个master+slaves的其他哨兵的存在。
  5. 每个哨兵还会跟其他哨兵交换对master的监控配置,互相进行监控配置的同步。

# redis持久化的方式和原理

首先,Redis本身是一个基于Key-Value结构的内存数据库,为了避免Redis故障导致数据丢失的问题,所以提供了RDB和AOF两种持久化机制。

RDB是通过快照的方式来实现持久化的,也就是说会根据快照的触发条件,把内存里面的数据快照写入到磁盘,以二进制的压缩文件进行存储。

img.png

RDB快照的触发方式有很多,比如:

  • 执行bgsave命令触发异步快照,执行save命令触发同步快照,同步快照会阻塞客户端的执行指令。
  • 根据redis.conf文件里面的配置,自动触发bgsave
  • 主从复制的时候触发

AOF是Redis里面的一种数据持久化方式,它采用了指令追加的方式。近乎实时的去实现数据指令的持久化,因为AOF,会把每个数据更改的操作指令,追加存储到aof文件里面。 所以很容易导致AOF文件出现过大,造成IO性能问题。

img.png

Redis为了解决这个问题,设计了AOF重写机制,也就是说把AOF文件里面相同的指令进行压缩,只保留最新的数据指令。 简单来说,如果aof文件里面存储了某个key的多次变更记录,但是实际上,最终在做数据恢复的时候,只需要执行最新的指令操作就行了,历史的数据就没必要存在这个文件里面占空间。

img.png

AOF文件重写的具体过程分为几步:

  • 首先,根据当前Redis内存里面的数据,重新构建一个新的AOF文件
  • 然后,读取当前Redis里面的数据,写入到新的AOF文件里面
  • 最后,重写完成以后,用新的AOF文件覆盖现有的AOF文件

另外,因为AOF在重写的过程中需要读取当前内存里面所有的键值数据,再生成对应的一条指令进行保存。而这个过程是比较耗时的,对业务会产生影响。 所以Redis把重写的过程放在一个后台子进程里面来完成,这样一来,子进程在做重写的时候,主进程依然可以继续处理客户端请求。 最后,为了避免子进程在重写过程中,主进程的数据发生变化导致AOF文件和Redis内存中的数据不一致的问题,Redis还做了一层优化。 就是子进程在重写的过程中,主进程的数据变更需要追加到AOF重写缓冲区里面。等到AOF文件重写完成以后,再把AOF重写缓冲区里面的内容追加到新的AOF文件里面。

首先,Redis本身是一个基于Key-Value结构的内存数据库,为了避免Redis故障导致数据丢失的问题,所以提供了RDB和AOF两种持久化机制。 RDB是通过快照的方式来实现持久化的,也就是说会根据快照的触发条件,把内存里面的数据快照写入到磁盘,以二进制的压缩文件进行存储。

img.png

RDB快照的触发方式有很多,比如

  • 执行bgsave命令触发异步快照,执行save命令触发同步快照,同步快照会阻塞客户端的执行指令。
  • 根据redis.conf文件里面的配置,自动触发bgsave
  • 主从复制的时候触发

AOF持久化,它是一种近乎实时的方式,把Redis Server执行的事务命令进行追加存储。简单来说,就是客户端执行一个数据变更的操作, Redis Server就会把这个命令追加到aof缓冲区的末尾,然后再把缓冲区的数据写入到磁盘的AOF文件里面,至于最终什么时候真正持久化到磁盘,是根据刷盘的策略来决定的。

# redis是单线程还是多线程?

  • 首先,Redis在6.0支持的多线程,并不是说指令操作的多线程,而是针对网络IO的多线程支持。也就是Redis的命令操作,仍然是线程安全的。
  • Redis本身的性能瓶颈,取决于三个纬度,网络、CPU、内存。而真正影响内存的关键问题是像内存和网络。而Redis6.0的多线程,本质上解决网络IO的处理效率问题。 在Redis6.0之前。Redis Server端处理接受到客户端请求的时候,Socket连接建立到指令的读取、解析、执行、写回都是由一个线程来处理,这种方式,在客户端请求比较多的情况下,单个线程的网络处理效率太慢,导致客户端的请求处理效率较低。 于是在Redis6.0里面,针对网络IO的处理方式改成了多线程,通过多线程并行的方式提升了网络IO的处理效率。但是对于客户端指令的执行过程,还是使用单线程方式来执行。
  • 最后,Redis6.0里面多线程默认是关闭的,需要在redis.conf文件里面修改io-threads-do-reads配置才能开启。另外,之所以指令执行不使用多线程,我认为有两个方面的原因。
    • 内存的IO操作,本身不存在性能瓶颈,Redis在数据结构上已经做了非常多的优化。
    • 如果指令的执行使用多线程,那Redis为了解决线程安全问题,需要对数据操作增加锁的同步,不仅仅增加了复杂度,还会影响性能,代价太大不合算。

img.png

# redis事务原理

大家应该对事务比较了解,简单地说,事务表示一组动作,要么全部执行,要么全部不执行。 Redis提供了简单的事务功能,将一组需要一起执行的命令放到multi和exec两个命令之间。multi命令代表事务开始,exec命令代表事务结衷。另外discard命令是回滚但是要注意Redis的事务功能很弱。在事务回滚机制上,Redis只能对基本的语法错误进行判断

redis运行时错误: 例如:事务内第一个命令简单的设置一个string类型,第二个对这个key进行sadd命令,这种就是运行时命令错误,因为语法是正确的。

事务是Redis实现在服务器端的行为,用户执行MULTI命令时,服务器会将对应这个用户的客户端对象设置为一个特殊的状态, 在这个状态下后续用户执行的查询命令不会被真的执行,而是被服务器缓存起来,直到用户执行EXEC命令为止, 服务器会将这个用户对应的客户端对象中缓存的命令按照提交的顺序依次执行。

# redis的主从同步策略?

主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。 redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

全量同步

Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:

  1. 从服务器连接主服务器,发送SYNC命令;
  2. 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令
  3. 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
  4. 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
  5. 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
  6. 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;

img.png

增量同步

Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。 增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。

# redis-cluster数据分片原理

集群的整个数据库被分为 16384 个槽(slot),数据库中的每个键都属于这 16384 个槽的其中一个,集群中的每个节点可以处理 0 个或最多 16384 个槽。 Key 与哈希槽映射过程可以分为两大步骤:

  1. 根据键值对的 key,使用 CRC16 算法,计算出一个 16 bit 的值;
  2. 将 16 bit 的值对 16384 执行取模,得到 0 ~ 16383 的数表示 key 对应的哈希槽。

Cluster 还允许用户强制某个 key 挂在特定槽位上,通过在 key 字符串里面嵌入 tag 标记,这就可以强制 key 所挂在的槽位等于 tag 所在的槽位。

img.png

# Redis Cluster Gossip通信协议

该集群有三个 Redis 节点组成,每个节点负责整个集群的一部分数据,每个节点负责的数据多少可能不一样。三个节点相互连接组成一个对等的集群, 它们之间通过 Gossip协议相互交互集群信息,最后每个节点都保存着其他节点的 slots 分配情况。 Gossip的作用:1.去中心化,以实现分布式和弹性扩展;2. 失败检测,以实现高可用;

Gossip 协议的主要职责就是信息交换。信息交换的载体就是节点彼此发送的Gossip 消息,常用的 Gossip 消息可分为:Ping 消息、Pong 消息、Meet 消息、Fail 消息。

  1. Meet 消息:用于通知新节点加入。消息发送者通知接收者加入到当前集群,Meet 消息通信正常完成后,接收节点会加入到集群中并进行周期性的 Ping、Pong 消息交换;
  2. Ping 消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其它节点发送 Ping 消息,用于检测节点是否在线和交换彼此状态信息。Ping 消息发送封装了自身节点和部分其它节点的状态数据;
  3. Pong 消息:当接收到 Ping、Meet 消息时,作为响应消息回复给发送方确认消息正常通信。Pong 消息内部封装了自身状态数据。节点也可以向集群内广播自身的 Pong 消息来通知整个集群对自身状态进行更新;
  4. Fail 消息:当节点判定集群内另一个节点下线时,会向集群内广播一个 Fail 消息,其他节点接收到 Fail 消息之后把对应节点更新为下线状态。

img.png

  • 集中式的优点在于元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节点读取的时候立即就可以立即感知到; 不足在于所有的元数据的更新压力全部集中在一个地方,可能导致元数据的存储压力。 很多中间件都会借助zookeeper集中式存储元数据。

  • gossip协议的优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力; 缺点在于元数据更新有延时可能导致集群的一些操作会有一些滞后。

# Redis Cluster主节点从节点选举

跟哨兵类似,两者都是基于 Raft 算法来实现的:

  1. 集群的配置纪元 +1,是一个自曾计数器,初始值 0 ,每次执行故障转移都会 +1。
  2. 检测到主节点下线的从节点向集群广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
  3. 这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,表示这个主节点支持从节点成为新的主节点。
  4. 参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,如果收集到的票 >= (N/2) + 1 支持,那么这个从节点就被选举为新主节点。
  5. 如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

img.png

# Redis Cluster请求路由方式

客户端直连 Redis 服务,进行读写操作时,Key 对应的 Slot 可能并不在当前直连的节点上,经过“重定向”才能转发到正确的节点。 和普通的查询路由相比,Redis Cluster 借助客户端实现的请求路由是一种混合形式的查询路由,它并非从一个 Redis 节点到另外一个 Redis, 而是借助客户端转发到正确的节点。实际应用中,可以在客户端缓存 Slot 与 Redis 节点的映射关系,当接收到 MOVED 响应时修改缓存中的映射关系。 如此,基于保存的映射关系,请求时会直接发送到正确的节点上,从而减少一次交互,提升效率。

哈希槽与实例之间的映射关系由于新增实例或者负载均衡重新分配导致改变了?

集群中的实例通过 Gossip 协议互相传递消息获取最新的哈希槽分配信息,但是,客户端无法感知。Redis Cluster 提供了重定向机制: 客户端将请求发送到实例上,这个实例没有相应的数据,该 Redis 实例会告诉客户端将请求发送到其他的实例上。

Redis如何告知客户端重定向访问新实例呢?

分为两种情况:

MOVED 错误: MOVED 错误(负载均衡,数据已经迁移到其他实例上):当客户端将一个键值对操作请求发送给某个实例, 而这个键所在的槽并非由自己负责的时候,该实例会返回一个 MOVED 错误指引转向正在负责该槽的节点。

ASK 错误。 如果某个 slot 的数据比较多,部分迁移到新实例,还有一部分没有迁移咋办?

如果请求的 key 在当前节点找到就直接执行命令,否则时候就需要 ASK 错误响应了,槽部分迁移未完成的情况下, 如果需要访问的 key 所在 Slot 正在从从 实例 1 迁移到 实例 2,实例 1 会返回客户端一条 ASK 报错信息:客户端请求的 key 所在的哈希槽正在迁移到实例 2 上, 你先给实例 2 发送一个 ASKING 命令,接着发发送操作命令。 比如客户端请求定位到 key的槽16330 在实例 172.17.18.1 上,节点1如果找得到就直接执行命令, 否则响应 ASK 错误信息,并指引客户端转向正在迁移的目标节点 172.17.18.2。

注意:ASK 错误指令并不会更新客户端缓存的哈希槽分配信息。所以客户端再次请求 Slot 16330 的数据,还是会先给 172.17.18.1 实例发送请求, 只不过节点会响应 ASK 命令让客户端给新实例发送一次请求。MOVED指令则更新客户端本地缓存,让后续指令都发往新实例。

# redis-cluster的添加新的节点

img.png

img.png

# Redis Cluster集群的缩容

img.png

# Redis Cluster集群不可用

**如果某个主节点没有从节点,那么当它发生故障时,集群将完全处于不可用状态。**不过 Redis 也提供了一个参数cluster-require-full-coverage可以允许部分节点故障, 其它节点还可以继续提供对外访问。比如 7000 主节点宕机,作为 slave 的 7003 成为 Master 节点继续提供服务。当下线的节点 7000 重新上线,它将成为当前70003的从节点。

# 缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库, 然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据!。

解决方案

  1. 直接写个缓存刷新页面,上线时手工操作一下。
  2. 数据量不大,可以在项目启动的时候自动进行加载。
  3. 定时刷新缓存。

# 缓存更新

缓存更新除了缓存服务器自带的缓存失效策略之外(Redis 默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:。

  1. 定时去清理过期的缓存。
  2. 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。

# redis中海量数据查询指定数据?

  1. 是keys命令,由于Redis单线程这一特性,keys命令是以阻塞的方式执行的,keys是以遍历的方式实现的复杂度是0(n),Redis库中的 key越多,查找实现代价越大,产生的阻塞时间越长。,
  2. 是scan命令,以非阻塞的方式实现 key值的查找,绝大多数情况下是可以替代 keys命令的,可选性更强。

# redis中 kys与scan的实现原理?

由于 Redis 是单线程在处理用户的命令,而 Keys 命令会一次性遍历所有 Key,于是在 命令执行过程中,无法执行其他命令。 这就导致如果 Redis 中的 key 比较多,那么 Keys 命令执行时间就会比较长,从而阻塞 Redis。 所以很多教程都推荐使用 Scan 命令来代替 Keys,因为 Scan 可以限制每次遍历的 key 数量。可以理解为Scan是渐进式的Keys。

Keys的缺点

  1. 没有limit,我们只能一次性获取所有符合条件的key,如果结果有上百万条,那么等待你的就是“无穷无尽”的字符串输出。
  2. keys命令是遍历算法,时间复杂度是O(N)。如我们刚才所说,这个命令非常容易导致Redis服务卡顿。因此,我们要尽量避免在生产环境使用该命令。

Scan命令有两个比较明显的优势

  1. Scan命令的时间复杂度虽然也是O(N),但它是分次进行的,不会阻塞线程。
  2. Scan命令提供了 count 参数,可以控制每次遍历的集合数。

Scan 命令注意事项:

  1. 返回的结果可能会有重复,需要客户端去重复,这点非常重要;
  2. 遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
  3. 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零;

使用时遇到一个 特殊场景,跨区域远程连接 Redis 并进行模糊查询,扫描所有指定前缀的 Key。Redis 中大概几百万 Key。 最开始也没多想,直接就是开始 Scan,然后 Count 参数指定的是 1000。 最后发现这个接口需要几十上百秒才返回。

什么原因呢?:Scan 命令中的 Count 指定一次扫描多少 Key,这里指定为 1000,几百万Key就需要几千次迭代,即和 Redis 交互几千次, 然后因为是远程连接,网络延迟比较大,所以耗时特别长。最后将 Count 参数调大后,减少了交互次数,就好多了。 Count 参数越大,Redis 阻塞时间也会越长,需要取舍。可以发现 Count 越大,总耗时就越短,不过越后面提升就越不明显了。所以推荐的 Count 大小为 1W 左右。 如果不考虑 Redis 的阻塞,其实 Keys 比 Scan 会快很多,毕竟一次性处理,省去了多余的交互。

Scan原理

Redis使用了Hash表作为底层实现,原因不外乎高效且实现简单。类似于HashMap那样数组+链表的结构。其中第一维的数组大小为2n(n>=0)。每次扩容数组长度扩大一倍。 Scan命令就是对这个一维数组进行遍历。每次返回的游标值也都是这个数组的索引。Count 参数表示遍历多少个数组的元素, 将这些元素下挂接的符合条件的结果都返回。因为每个元素下挂接的链表大小不同,所以每次返回的结果数量也就不同。

  1. 整个遍历从开始到结束期间, 一直存在于Redis数据集内的且符合匹配模式的所有Key都会被返回;
  2. 如果发生了rehash,同一个元素可能会被返回多次,遍历过程中新增或者删除的Key可能会被返回,也可能不会。

# 删除Key的命令会阻塞Redis吗?

  1. 删除单个字符串类型的key ,时间复杂度为0(1)。
  2. 删除单个列表、集合、有序集合或哈希表类型的 key ,时间复杂度为0(M),M为以上数据结构内的元素数量。

# Redis实现一个延时队列?

要回答这个问题,首先的明白什么是延时队列。其次,还得了解Redis里面哪些结构可以支持这个特性。 延时队列是一种特殊类型的消息队列,它允许把消息发送到队列中,但不立即投递给消费者,而是在一定时间后再将消息投递给消费者。 所以它通常用于需要在未来某个时间执行任务的场景,比如订单的超时处理、定时任务等。

在Redis里面可以使用Zset这个有序集合来实现延时队列。具体的实现方式可以分成几个步骤。

  1. 使用ZADD命令把消息添加到sorted set中,并将当前时间作为score(分数):ZADD delay-queue <timestamp> <message>
  2. 启动一个消费者线程,使用ZRANGEBYSCORE命令获取定时从Zset中获取当前时间之前的所有消息:ZRANGEBYSCORE delay-queue 0 <current_time> WITHSCORES LIMIT 0 <batch_size>
  3. 消费者处理完消息后,可以从有序集合中删除这些消息:ZREMRANGEBYSCORE delay-queue 0 <current_time>

这种方式实现的延迟队列,消费端需要不断的向Redis发起轮训,所以它会存在两个问题:

  1. 轮训存在时间间隔,所以延时消息的实际消费时间会大于设定的时间。
  2. 大量轮训会对Redis服务器造成压力。

# Redis主从复制原理?

Redis主从复制,是指在Redis集群里面,Master节点和Slave节点数据同步的一种机制。简单来说就是把一台Redis服务器的数据,复制到其他Redis服务器中。 其中负责复制数据的来源称为master,被动接收数据并同步的节点称为slave。

img.png

在Redis里面,提供了全量复制和增量复制两种模式。全量复制一般发生在Slave节点初始化阶段,这个时候需要把master上所有数据都复制一份。

全量复制工作原理

  1. Slave向Master发送SYNC命令,Master收到命令以后生成数据快照。
  2. 把快照数据发送给Slave节点,Salve节点收到数据后丢弃旧的数据,并重新载入新的数据。

需要注意,在主从复制过程中,Redis并没有采用实现强数据一致性,因此会存在一定时间的数据不一致问题。

img.png

增量复制工作原理

增量复制,就是指Master收到数据变更之后,把变更的数据同步给所有Slave节点。 增量复制的原理是,Master和Slave都会维护一个复制偏移量(offset),用来表示Master向Slave传递的字节数。 每次传输数据,Master和Slave维护的Offset都会增加对应的字节数量。Redis只需要根据Offset就可以实现增量数据同步了。?

# Redis哨兵机制和集群有什么区别?

Redis集群有几种实现方式,一个是主从集群、一个是Redis Cluster

主从集群

就是在Redis集中包括一个Master节点和多个Slave节点。Master负责数据的读写,Slave节点负责数据的读取。 Master上收到的数据变更,会同步到Slave节点上实现数据的同步。通过这种架构实现可以Redis的读写分离,提升数据的查询性能。

img.png

Redis主从集群不提供容错和恢复功能,一旦Master节点挂了,不会自动选出新的Master,导致后续客户端所有写请求直接失败。 所以Redis提供了哨兵机制,专门用来监听Redis主从集群提供故障的自动处理能力。哨兵会监控Redis主从节点的状态,当Master节点出现故障,会自动从剩余的Slave节点中选一个新的Master。

img.png

哨兵模式下虽然解决了Master选举的问题,但是在线扩容的问题还是没有解决。于是就有了第三种集群方式,

Redis Cluster

它实现了Redis的分布式存储,也就是每个节点存储不同的数据实现数据的分片。在Redis Cluster中,引入了Slot槽来实现数据分片, Slot的整体取值范围是0~16383,每个节点会分配一个Slot区间当我们存取Key的时候,Redis根据key计算得到一个Slot的值, 然后找到对应的节点进行数据的读写。在高可用方面,Redis Cluster引入了主从复制模式, 一个Master节点对应一个或多个Slave节点,当Master出现故障,会从Slave节点中选举一个新的Master继续提供服务。

img.png

Redis Cluster虽然解决了在线扩容以及故障转移的能力,但也同样有缺点,比如:

  1. 客户端的实现会更加复杂
  2. Slave节点只是一个冷备节点,不提供分担读操作的压力
  3. 对于Redis里面的批量操作指令会有限制

因此主从模式和Cluster模式各有优缺点,在使用的时候需要根据场景需求来选择。

因为Redis集群有两种,一种是主从复制,一种是Redis Cluster,我不清楚您问的是哪一种。按照我的理解,我认为您可能说的是Redis哨兵集群和Redis Cluster的区别。

对于这个问题,我认为可以从3个方面来回答:

  1. Redis哨兵集群是基于主从复制来实现的,所以它可以实现读写分离,分担Redis读操作的压力,而Redis Cluster 集群的Slave节点只是实现冷备机制,它只有在Master宕机之后才会工作。
  2. Redis哨兵集群无法在线扩容,所以它的并发压力受限于单个服务器的资源配置。Redis Cluster提供了基于Slot槽的数据分片机制,可以实现在线扩容提升写数据的性能
  3. 从集群架构上来说,Redis 哨兵集群是一主多从而Redis Cluster是多主多从

# Redis的缓存淘汰策略?

  • 第一个方面:当Redis使用的内存达到maxmemory参数配置的阈值的时候,Redis就会根据配置的内存淘汰策略。把访问频率不高的key从内存中移除。maxmemory默认情况是当前服务器的最大内存。
  • Redis默认提供了8种缓存淘汰策略,这8种缓存淘汰策略总的来说,我认为可以归类成五种
    1. 第一种,采用LRU策略,就是把不经常使用的key淘汰掉。
    2. 第二种,采用LFU策略,它在LRU算法上做了优化,增加了数据访问次数,从而确保淘汰的是非热点key。
    3. 第三种,随机策略,也就是是随机删除一些key。
    4. 第四种,ttl策略,从设置了过期时间的key里面,挑选出过期时间最近的key进行优先淘汰。
    5. 第五种,当内存不够的时候,直接报错,这是默认的策略。 这些策略可以在redis.conf文件中手动配置和修改,我们可以根据缓存的类型和缓存使用的场景来选择合适的淘汰策略。
  • 我们在使用缓存的时候,建议是增加这些缓存的过期时间。因为我们知道这些缓存大概的生命周期,从而更好的利用内存。

# 怎么防止缓存击穿的问题?

在实际应用中,我们会在程序和数据库之间增加一个缓存层。一方面是为了提升数据检索效率,提升程序性能,另一方面是为了缓解数据库的并发压力。

缓存击穿,表示请求因为某些原因全部打到了数据库,缓存并没有起到流量缓冲的作用

我认为有2种情况会导致缓存击穿

  • 在Redis里面保存的热点key,在缓存过期的瞬间,有大量请求进来,导致请求全部打在数据库上。
  • 客户端恶意发起大量不存在的key的请求,由于访问的key对应的数据本身就不存在,所以每次必然都会穿透到数据库,导致缓存成为了摆设。

总之,当Redis承担了流量缓冲功能的时候,就需要考虑到Redis失效导致并发压力过大对后端存储设备造成冲击的问题。因此,我认为可以通过几种方法来解决。

  • 对于热点数据,我们可以不设置过期时间,或者在访问数据的时候对数据过期时间进行续期。
  • 对于访问量较高的缓存数据,我们可以设计多级缓存,尽量减少后端存储设备的压力。
  • 使用分布式锁,当发现缓存失效的时候,不是先从数据库加载,而是先获取分布式锁,获得分布式锁的线程从数据库查询数据后写回到缓存里面。后续没有获得锁的线程就只需要等待和重试即可。这个方案牺牲了一定的性能,但是确保护了数据库避免被压垮。
  • 对于恶意攻击类的场景,可以使用布隆过滤器,应用启动的时候把存在的数据缓存到布隆过滤器里面。每一次请求进来的时候先访问布隆过滤器,如果不存在,则说明这个数据一定没有在数据库里面,就没必要再去访问数据库了。
  • 另外,我们在整个缓存架构设计中,除了尽可能避免缓存穿透的问题,还需要从全局视角做整体考虑。比如业务隔离、多级缓存、部署隔离、安全性考虑等。

# Redis存在线程安全问题吗?

第一个,从Redis 服务端层面。Redis Server本身是一个线程安全的K-V数据库,也就是说在Redis Server上执行的指令,不需要任何同步机制,不会存在线程安全问题。 虽然Redis 6.0里面,增加了多线程的模型,但是增加的多线程只是用来处理网络IO事件对于指令的执行过程,仍然是由主线程来处理,所以不会存在多个线程通知执行操作指令的情况

img.png

为什么Redis没有采用多线程来执行指令,我认为有几个方面的原因。

  • Redis Server本身可能出现的性能瓶颈点无非就是网络IO、CPU、内存。但是CPU不是Redis的瓶颈点,所以没必要使用多线程来执行指令。
  • 如果采用多线程,意味着对于redis的所有指令操作,都必须要考虑到线程安全问题,也就是说需要加锁来解决,这种方式带来的性能影响反而更大。

第二个,从Redis客户端层面。虽然Redis Server中的指令执行是原子的,但是如果有多个Redis客户端同时执行多个指令的时候,就无法保证原子性。

img.png

假设两个redis client同时获取Redis Server上的key1,同时进行修改和写入,因为多线程环境下的原子性无法被保障以及多进程情况下的共享资源访问的竞争问题,使得数据的安全性无法得到保障

当然,对于客户端层面的线程安全性问题,解决方法有很多,比如尽可能的使用Redis里面的原子指令,或者对多个客户端的资源访问加锁,或者通过Lua脚本来实现多个指令的操作等等。

# 单机模式的优缺点

Redis 单副本,采用单个 Redis 节点部署架构,没有备用节点实时同步数据,不提供数据持久化和备份策略,适用于数据可靠性要求不高的纯缓存业务场景。

优点:

  1. 架构简单,部署方便。
  2. 高性价比:缓存使用时无需备用节点(单实例可用性可以用 supervisor 或 crontab 保证),当然为了满足业务的高可用性,也可以牺牲一个备用节点,但同时刻只有一个实例对外提供服务。
  3. 高性能。

缺点:

  1. 不保证数据的可靠性。
  2. 在缓存使用,进程重启后,数据丢失,即使有备用的节点解决高可用性,但是仍然不能解决缓存预热问题,因此不适用于数据可靠性要求高的业务。
  3. 高性能受限于单核 CPU 的处理能力(Redis 是单线程机制),CPU 为主要瓶颈,所以适合操作命令简单,排序、计算较少的场景。也可以考虑用 Memcached 替代。

# 主从架构的优缺点

主(master)和 从(slave)部署在不同的服务器上,当主节点服务器写入数据时会同步到从节点的服务器上,一般主节点负责写入数据,从节点负责读取数据。 从节点设置只读属性,而主节点没有只写属性,因此,主节点可读可以写

优点:

  1. 读写分离,提高效率
  2. 主节点负责写操作,从节点负责读操作;如果写少读多场景,配置多个从节点的话,效率非常高
  3. 数据热备份,提供多个副本。
  4. 从节点宕机,影响较小

缺点:

  1. 主节点故障,集群则无法进行工作,可用性比较低,从节点升主节点需要人工手动干预。
  2. 因为只有主节点能进行写操作,一旦主节点宕机,整个服务就无法使用。当然此时从节点仍可以进行读操作,但是对于整个服务流程来说,是无法使用的。
  3. Master的写的压力难以降低。
  4. 如果写操作比较多,那么只有一个主节点的话,无法分担压力。
  5. 主节点存储能力受到单击限制。
  6. 主节点只能有一个,因此单节点内存大小不会太大,因此存储数据量受限。
  7. 主从数据同步,可能产生部分的性能影响甚至同步风暴。

# redis-sentinel哨兵的优缺点

为了解决这两个问题,在2.8版本之后redis正式提供了sentinel架构。在redis3.0以前的版本要实现集群一般是借助哨兵sentinel工具来监控master节点的状态。 如果master节点异常,则会做主从切换,将某一台slave作为master,哨兵的配置略微复杂,并且性能和高可用性等各方面表现一般。 与主从相比,哨兵仅解决了手动切换主从节点问题,至于其他的问题,基本上仍然存在。哨兵的主要问题还是由于中心架构,仅存在一个master节点引起的,写的效率太低。

优点:

  1. 对节点进行监控,来完成自动的故障发现与转移

缺点:

  1. 特别是在主从切换的瞬间存在访问瞬断的情况,等待时间比较长,至少十来秒不可用
  2. 哨兵模式只有一个主节点对外提供服务,没法支持很高的并发
  3. 单个主节点内存也不宜设置得过大,否则会导致持久化文件过大,影响数据恢复或主从同步的效率。

# Redis-Cluster模式的优缺点

Redis Cluster 是 3.0 版后推出的 Redis 分布式集群解决方案,主要解决 Redis 分布式方面的需求, 比如,当遇到单机内存,并发和流量等瓶颈的时候,Redis Cluster 能起到很好的负载均衡的目的。

Redis Cluster 集群节点最小配置 6 个节点以上(3 主 3 从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。 Redis Cluster 采用虚拟槽分区,所有的键根据哈希函数映射到 0~16383 个整数槽内,每个节点负责维护一部分槽以及槽所印映射的键值数据。

注意:集群模式下 从节点不提供读写,与主从模式不一样。 总结一下经验,分布式场景下:集群模式一般从节点不参与读写,仅作为备用节点。而主从一般都要负责读或写,都要参与具体的工作。

优点:

  1. 无中心架构。即有多个master节点,不像哨兵模式下仅有一个。这样写的压力就可以分散了;并且存储量也可以扩展了, 因为多个主节点都可以存储一部分数据,总量要远大于单主节点架构。
  2. 数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。
  3. 可扩展性:可线性扩展到 1000 多个节点,节点可动态添加或删除。
  4. 高可用性:部分节点不可用时,集群仍可用。通过增加 Slave 做 standby 数据副本,能够 实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave 到 Master 的角色提升。

缺点:

  1. 如果主节点A和它的从节点A1都宕机了,那么该集群就无法再提供服务了。

# Redis延迟队列的应用场景

  1. 订单超过30分钟未支付,自动关闭。
  2. 订单完成后, 如果用户一直未评价, 5天后自动好评。
  3. 会员到期前15天, 到期前3天分别发送短信提醒。
  4. 当订单一直处于未支付状态时,如何及时的关闭订单,并退还库存?
  5. 如何定期检查处于退款状态的订单是否已经退款成功?
  6. 新创建店铺,N天内没有上传商品,系统如何知道该信息,并发送激活短信?

# Redis性能优化方案

Redis 是基于单线程模型实现的,也就是 Redis 是使用一个线程来处理所有的客户端请求的,尽管 Redis 使用了非阻塞式 IO, 并且对各种命令都做了优化(大部分命令操作时间复杂度都是 O(1)),但由于 Redis 是单线程执行的特点,因此它对性能的要求更加苛刻, 我们将通过一些优化手段,让 Redis 更加高效的运行。

  1. 缩短键值对的存储长度;
  2. 使用 lazy free(延迟删除)特性;
  3. 设置键值的过期时间;
  4. 禁用耗时长的查询命令(比如keys);
  5. 使用 slowlog 优化耗时命令;
  6. 使用 Pipeline 批量操作数据;
  7. 避免大量数据同时失效;
  8. 客户端使用优化;
  9. 限制Redis内存大小;
  10. 使用物理机而非虚拟机安装Redis服务;
  11. 检查数据持久化策略;
  12. 使用分布式架构来增加读写速度。

# redis分布式锁的代码

@RequestMapping("/buy")
public String index() {
    RLock lock = redisson.getLock(REDIS_LOCK);
    lock.lock();
    // 每个人进来先要进行加锁,key值为"good_lock"
    String value = UUID.randomUUID().toString().replace("-", "");
    try {
        String result = template.opsForValue().get("goods");
        int total = result == null ? 0 : Integer.parseInt(result);
        if (total > 0) {
            // 如果在此处需要调用其他微服务,处理时间较长。。。
            int realTotal = total - 1;
            template.opsForValue().set("goods", String.valueOf(realTotal));
            System.out.println("购买商品成功,库存还剩:" + realTotal + "件");
            return "购买商品成功,库存还剩:" + realTotal + "件";
        } else {
            System.out.println("购买商品失败");
        }
        return "购买商品失败";
    } finally {
        // 如果锁依旧在同时还是在被当前线程持有,那就解锁。 如果是其他的线程持有 那就不能释放锁资源
        if (lock.isLocked() && lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

# 什么是bigkey,bigkey的危害是什么?

bigkey是指key对应的value所占的内存空间比较大,例如一个字符串类型的value可以最大存到512MB,一个列表类型的value最多可以存储2^(23)-1个元素。

如果按照数据结构来细分的话,一般分为字符串类型biqkey和非字符串类型bigkey。

  1. 字符串类型:体现在单个value值很大,一般认为超过10KB就是bigkey,但这个值和具体的OPS相关。
  2. 非字符串类型:哈希、列表、集合、有序集合,体现在元素个数过多。bigkey无论是空间复杂度和时间复杂度都不太友好,下面我们将介绍它的危害。

bigkey的危害体现在三个方面:

  1. 内存空间不均匀.(平衡):例如在Redis Cluster中,bigkey会造成节点的内存空间使用不均匀。
  2. 超时阻塞:由于Redis单线程的特性,操作bigkey比较耗时,也就意味着阻塞Redis可能性增大。
  3. 网络拥塞:每次获取bigkey产生的网络流量较大。

# Redis如何解决key冲突?

  1. 业务隔离:业务A SET A1, key区分出来。
  2. key的设计: 业务模块+系统名称+关键biz-pay-orderid-11,userid。
  3. 分布式锁: 多个客户端你并发写key原有值1,修改顺序,2->3->4,2客户端拿到锁才能进行操作。时间戳set key 7:00set key 6:00。

# Redis集群方案什么情况下会导致整集群不可用?

为了保证集群完整性,默认情况下当集群16384个槽任何一个没有指派到节点时整个集群不可用。执行任何键命令返回( error)CLUSTERDOWN Hash slot not served错误。 这是对集群完整性的一种保护措施,保证所有的槽都指派给在线的节点。但是当持有槽的主节点下线时,从故障发现到自动完成转移期间整个集群是不可用状态, 对于大多数业务无法容忍这种情况,因此可以将参数cluster-require-full-coverage配置为no,当主节点故障时只影响它负责槽的相关命令执行, 不会影响其他主节点的可用性。但是从集群的故障转移的原理来说,集群会出现不可用

  1. 当访问一个Master和Slave节点都挂了的时候,cluster-require-full-coverage=yes,会报槽无法获取。
  2. 集群主库半数宕机(根据failover 原理,fail掉一个主需要一半以上主都投票通过才可以)。
  3. 另外,当集群Master节点个数小于3个的时候,或者集群可用节点个数为偶数的时候,基于fail的这种选举机制的自动主从切换过程可能会不能正常工作, 一个是标记 fail 的过程,一个是选举新的 master的过程,都有可能异常。

# Redis和Memcache区别,优缺点对比

  1. Redis和Memcache都是将数据存放在内存中,都是内存数据库。不过memcache还可用于缓存其他东西,例如图片、视频等等
  2. Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,hash等数据结构的存储。
  3. 虚拟内存:Redis当物理内存用完时,可以将一些很久没用到的value交换到磁盘。
  4. 过期策略:memcache在set时就指定,例如set key1 0 0 8,即永不过期。Redis可以通过例如expire设定,例如expire name 10。
  5. 分布式设定memcache集群,利用magent做一主多从;redis可以一主一从,也可以做一主多从。
  6. 存储数据安全:memcache挂掉后,数据没了;redis可以定期保存到磁盘(持久化。
  7. 灾难恢复:memcache挂掉后,数据不可恢复; redis数据丢失后可以通过aof恢复。
  8. Redis支持数据的备份,即master-slave模式的数据备份。

# Redis如何做内存优化?

# Redis key 的过期时间和永久有效分别怎么设置?

EXPIRE 和 PERSIST 命令

# Redis 中的管道有什么用?

一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应,这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。 这就是管道(pipelining),是一种几十年来广泛使用的技术。例如许多 POP3 协议已经实现支持这个功能,大大加快了从服务器下载新邮件的过程。

# 什么是redis事务

# Redis事务相关命令

# Redis事务的三个阶段

# watch是如何监视实现的呢

# 为什么 Redis 不支持回滚

# redis 对 ACID的支持性理解

# Redis事务其他实现

基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行,其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完基于 中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐。

# Redis集群的主从复制模型是怎样的?

# 全量复制的三个阶段?

# 为什么会设计增量复制?

# 增量复制的流程?

如果在网络断开期间,repl_backlog_size环形缓冲区写满之后,从库是会丢失掉那部分被覆盖掉的数据,还是直接进行全量复制呢?

# 为什么不持久化的主服务器自动重启非常危险呢?

# 为什么主从全量复制使用RDB而不使用AOF?

# 为什么还有无磁盘复制模式?

# 为什么还会有从库的从库的设计?

# Redis哨兵机制?

# 哨兵实现了什么功能呢

# 哨兵集群是通过什么方式组建的?

# 哨兵是如何监控Redis集群的?

# 哨兵如何判断主库已经下线了呢?

# 哨兵的选举机制是什么样的?

Redis 1主4从,5个哨兵,哨兵配置quorum为2,如果3个哨兵故障,当主库宕机时,哨兵能否判断主库"客观下线?能否自动切换?

主库判定客观下线了,那么如何从剩余的从库中选择一个新的主库呢?

# 新的主库选择出来后,如何进行故障的转移?

# Redis集群会有写操作丢失吗?为什么?

# Redis并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。

# Redis如何做大量数据插入?

# Redis2.6开始redis-cli支持一种新的被称之为pipe mode的新模式用于执行大量数据插入工作。

# redis实现分布式锁实现? 什么是 RedLock?

# redis缓存有哪些问题,如何解决redis和其它数据库一致性问题如何解决

# redis性能问题有哪些,如何分析定位解决