redis八股文面试及命令

发布时间 2023-11-09 09:43:44作者: 小愉

由于以下内容较多(由本人自行整理详见图可参考:

https://www.yuque.com/xiaoyu-ay35z/uptv0o/kvbzgu8bq5bqa8y1

)包含八股,面试题,基本命令。♥如有错误望各位指导,感谢浏览♥

1.谈谈你对redis的了解

redis是开源的使用ANISC语言编写,支持网络,可基于内存亦可持久化的日志型、key-value数据库,并提供多语言的API

2.redis一般都有哪些使用场景

1、缓存:减轻mysql的查询压力,提升系统性能

2、排行榜:利用redis的SortSet(有序集合)实现

3、计数器/限速器:利用redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问等,这类操作如果用mysql,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力

4、好友关系:利用集合的一些命令,比如求交集、并集、差集等,可以方便解决一些共同好友、共同爱好之类的功能

5、消息队列:除了redis自身的发布/订阅模式,我们也可以利用list来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力,完全可以用list来完成异步解耦

6、Session共享:Session是保存在服务器的文件中,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登录;采用redis保存Session后,无论用户落在那台机器上都能够获取到对应的Session信息

redis不适合的场景:

数据量太大,数据访问频繁非常低的业务都不适合使用redis,数据太大会增加成本,访问频率太低,保存在内存中纯属浪费资源

3.redis有哪些常见的功能

1、数据缓存功能
2、分布式锁的功能
3、支持数据持久化
4、支持事务
5、支持消息队列

4.redis支持的数据类型有哪些

1、string(字符串)

字符串是redis最基础的数据结构,首先键是字符串类型, 而且其他几种结构都是在字符串类型基础上构建的,字符串类型实际上可以是字符串:简单的字符串、XML、JSON;数字:整数、浮点数;二进制:图片、音频、视频

使用场景:缓存、计数器、共享Session、限速

2、Hash(哈希)

在redis中哈希类型是指键本身是一种键值对结构,如value={{field1,value1},...{fieldN,valueN}}

使用场景:哈希结构相对于字符串序列化缓存信息更加直观,并且在更新操作上更加便捷,所以常常用于用户信息等管理,但是哈希类型和关系型数据库有所不同,哈希类型是稀疏的,而关系型数据库是完全结构化的,关系型数据库可以做复杂的关系查询,而redis去模拟关系型复杂查询开发困难且维护成本高

3、list(列表)

列表类型是用来储存多个有序的字符串,列表中的每个字符串称为元素,一个列表最多可以储存2^32-1个元素,在redis中,可以队列表两端插入和弹出,还可以获取指定范围的元素列表、获取指定索引下的元素等,列表是一种比较灵活的数据结构,它可以充当栈和队列的角色

适用场景:redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端是用lpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性

4、Set(集合)

集合类型也是用来保存多个字符串的元素,但和列表不同的是集合中不允许有重复的元素,并且集合中的元素是无序的,不能通过索引下标获取元素,redis除了支持集合内的增删改查,同时还支持多个集合取交集,并集,差集,合理的使用好集合类型,能在实际开发中解决很多实际问题

使用场景:如:一个用户对娱乐、体育比较感兴趣,另一个可能对新闻感兴趣,这些兴趣就是标签,有了这些数据就可以得到同一标签的人,以及用户的共同爱好的标签,这些数据对于用户体验以及增强用户粘度比较重要

5、zset(sorted set:****有序集合)

有序集合和集合有着必然的联系,它保留了集合不能有重复成员的特性,但不同的是,有序集合中的元素是可以排序的,但是他和列表的使用索引下标作为排序依据不同的是:他给每个元素设置一个分数,作为排序的依据

使用场景:排行榜是有序集合经典的使用场景。例如:视频网站需要对用户上传的文件做排行榜,榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等

5.redis为什么这么快

1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速

2、数据结构简单,对数据操作也简单

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多线程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗

4、使用多路I/O复用模型,非阻塞IO

6.什么是缓存穿透,怎么解决

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透

解决办法:

1、缓存空对象:如果一个查询返回的数据为空(不管是数据不存在,还是系统故障)我们仍然把这个空结果进行缓存,但他的过期时间会很短,最长不超过五分钟

缓存空对象带来的问题:

1、空值做了缓存,一位置缓存中存了更多的键,需要更多的内存空间,比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除

2、缓存和存储数据会有一段时间窗口的不一致,可能会对业务有一定影响,例如:过期时间设置为五分钟,如果此时存储添加了这个数据,那此段时间就会出现缓存和存储数据的不一致,此时可以利用消息系统或者其他方式消除掉缓存层中的空对象

2、布隆过滤器:将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力

7.什么是缓存雪崩,如何解决

如何让缓存集中在一段时间内失效,所有的查询都落在数据库上,造成了缓存雪崩

解决办法:

1、加锁排队:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量,比如对某个key只允许一个线程查询数据和写缓存,其他线程等待

2、数据预热:可以通过缓存reload机制,预先去更新缓存,在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的国企时间,让缓存失效的时间点尽量均匀

3、做二级缓存,或者双缓存策略:Cache1为原始缓存,Cache2为拷贝缓存,Cache1失效时,可以访问Cache2,Cache1缓存失效时间设置为短期,Cache2设置为长期

4、在缓存的时候给过期时间加上一个随机值,这样就会大幅度的减少缓存在同一时间过期

8.怎么样保证缓存和数据库数据的一致性

1、从理论上说,只要我们设置了合理的键和过期时间,我们就能保证缓存和数据库的数据最终是一致的,因为只要缓存数据过期了,就会被删除,随后读的时候,因为缓存里没有,就可以查数据库的数据,然后将数据库查出来的数据写入到缓存中,除了设置过期时间,我们还需要做更多的措施来尽量避免数据库与缓存处于不一致的情况发生 2、新增、更改、删除数据库操作时同步更新redis,可以使用事务机制来保证数据的一致性 一般有四种方案: 1、先更新数据库、后更新缓存 2、先更新缓、在更新数据库 3、先删除缓存、后更新数据库 4、先更新数据库、后删除缓存 第一种和第二种方案,没有人使用的,因为第一种方案存在问题时:并发更新数据库场景下,会将脏数据刷到缓存 第二种方案存在的问题是:如果先更新缓存成功,但是数据库更新失败,则肯定会造成数据不一致 目前主要用第三和第四种方案,**详情看** 32.双写一致性方案一:先删除缓存,后更新数据库 https://www.iamshuaidi.com/3664.html 33双写一致性方案二:先更新数据库,后删除缓存 https://www.iamshuaidi.com/3666.html

9.redis持久化有几种方式

持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。 redis提供了两种持久化方式:RDB(默认)和AOF RDB:RDB是redis Database的缩写,按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件,即Snapshot快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期,核心函数:rdbSave(生成RDB文件)和rdbLoad(从文件加载内存)两个函数

 

AOF: AOF是Append-only file的缩写,redis会将每一个收到的写命令都通过Write函数追加到文件最后,类似于mysql的binlog。当redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。每当执行服务器(定时)任务或者函数时,flushAppendOnlyFile函数都会被调用,这个函数执行以下两个工作: Write:根据条件,将aof_buf中的缓存写入到AOF文件; save:根据条件,调用fsync或fdatasync函数,将AOF文件保存到磁盘中

 

RDB和AOF的区别:

1、AOF文件比RDB更新频率高,优先使用AOF还原数据

2、AOF比RDB更安全也更大

3、RDB性能比AOF好

4、如果两个都赔了优先加载AOF

10.redis内存淘汰策略有哪些

1、volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰; 2、volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰; 3、colatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意挑选数据淘汰; 4、allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰; 5、allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰; 6、no-enviction(驱逐):禁止驱逐数据

11.redis常见性能问题和解决方案

1、Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件,如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次 2、未来主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内 3、主从复制不要用图状结构,用单向链表结构更加稳定,即:Master《-Slave1《-Slave2《-Slave3...

12.redis的过期键的删除策略

redis是key-value数据库,可以设置redis中缓存的key的过期时间,redis的古琦欧i策略就是指当redis中缓存的key过期了,redis如何处理 过期策略通常有以下三种: 定时过期: 每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除,该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量 惰性过期: 只有当访问一个key时,才会判断该key是否已过期,过期则清除,该策略可以最大化的节省CPU资源,却对内存非常不友好,极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存 定期清除: 每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key,该策略是前两者的一个折中方案,通过调整定时扫瞄的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果 (expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间,键空间是指该redis集群中保存的所有键) redis中同时使用了惰性过期和定期过期两种过期策略

13.我们知道通过expire还设置key的过期时间,那么对过期的数据怎么处理呢

除了缓存服务器自带的缓存失效策略之外(redis默认的有6种策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种: 1、定时去清理过期的缓存 2、当有用户请求过来时,在判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存 两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂,根据应用场景选择方案

14.Hash冲突怎么办

redis通过链式哈希解决冲突:也就是同一个捅里面的元素使用链表保存,但是当链表过长就会导致查找性能变差可能,所以redis为了追求快,使用了两个全局哈希表,用于rehash操作,增加现有的哈希捅数量,减少哈希冲突 开始默认使用[hash 表1]保存键值对数据,[hash 表2]此刻没有分配空间,当数据越来越多出发rehash操作,则执行以下操作: 1、给[hash 表2]分配更大的空间 2、将[hash 表1]的数据重新映射拷贝到[hash 表2]中 3、释放[hash 表1]的空间 值得注意的是,将hash表1的数据重新映射到hash表2的过程中并不是一次性的,这样会造成redis阻塞,无法提供服务 而是采用了渐进式rehash,每次处理客户端请求的时候,先从[hash 表1]中第一个索引开始,将这个位置的所有数据拷贝到[hash 表2]中,就这样将rehash分散到多次请求过程中,避免耗时阻塞

15.什么是RDB内存快照

在redis执行[写]指令过程中,内存数据会一直变化,所谓的内存快照,指定就是redis内存中的数据在某一刻的状态数据 好比时间定格在某一刻,当我们拍照的,通过照片就能吧某一刻的瞬间画面完全记录下来 redis跟这个类似,就是把某一刻的数据以文件的形式拍下来,写到磁盘上,这个快照文件叫做RDB文件

 

在做数据恢复时,直接将RDB文件读入内存完成恢复

16.在生成RDB期间,redis可以同时处理写请求吗

可以,redis使用操作系统的多进程写时复制技术 COW(copy on write)来实现快照持久化,保持数据一致性 redis在持久化时会调用glibc的函数fork产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求 当主线程执行写指令修改数据的时候,这个数据就会复制一份副本,bgsave子进程读取这个副本数据写到RDB文件 这既保证了快照的完整性,也允许主线程同时对数据进行修改,避免了对正常业务的影响

 

17.如何实现数据尽可能少丢失又能兼顾性能呢

重启redis时,我们很少使用rdb来恢复内存状态,因为会丢失大量数据,我们通常使用AOF日志重放,但是重放AOF日志性能相对rdb来说要慢很多,这样在redis实例很大的情况下,启动需要花费很长的时间 redis4.0为了解决这个问题,带来了一个新的持久化选项——混合持久化,将rdb文件的内容和增量的AOF日志文件存在一起,这里的AOF日志不在是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量AOF日志,通常这部分AOF日志很小 于是在redis重启的时候可以加载rdb内容,然后再重放增量AOF日志就可以完全替代之前的AOF全量文件重放,重启效率因此大幅得到提升

18.哈希槽又是如何映射到redis实例上呢

1、根据键值对的key,使用CRC16算法,计算出一个16bit的值 2、将16bit的值对16384执行取模,得到0~16383的数表示key对应的哈希槽 3、根据该槽信息定位到对应的实例 键值对数据、哈希槽、redis实例之间的映射关系如下:

 

19.redis如何做内存优化

1、控制key的数量: 当使用redis存储大量数据时,通常会存在大量键,过多的键同样会消耗大量内存,redis本质是一个数据结构服务器,他为我们提供多种数据结构,如hash,list,set,zset等结构,使用redis时不要进入一个误区,大量使用get/set这样的API,把redis当成Memcached使用,对于存储相同的数据内容利用redis的数据结构降低外层键的数量,也可以节省大量内存 2、缩减键值对象,降低redis内存使用最直接的方式就是所见键(key)和值(value)的长度 key长度:如在设计键时,在完整描述业务情况下,键值越短越好 value长度:值对象缩减比较复杂,常见需求是把业务对象序列化成二进制数组放入redis,首先应该在业务上精简业务对象,去掉不必要的属性避免存储无效数据,其次在序列化工具选择上,应该选择更高效的序列化工具来降低字节数组大小 3、编码优化: redis对外提供了string,list,hash,set,zet等类型,但是redis内部针对不同类型存在编码的概念,所谓编码就是具体使用那种底层数据结构来实现,编码不同将直接影响数据的内存占用和读写效率,可参考文章:https://cloud.tencent.com/developer/article/1162213

20.redis线程模型

redis的线程模型包括redis6.0之前和redis6.0 redis6.0之前: redis是基于reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器(file event handler)由于这个文件事件处理器是单线程的,所以redis才叫做单线程的模型,采用IO多路复用机制同时监听多个Socket,根据socket上的事件来选择对应的事件处理器来处理这个事件 io多路复用是io模型的一种,有时也称为异步阻塞io,是基于经典的Reactor设计模型设计的,多路指的是多个Socket连接,复用指的是复用一个线程,多路复用主要有三种技术:Select,Poll,Epoll Epoll是最新的也是目前最好的多路复用技术 模型如下图:

 

文件事件处理器的结构包括四个部分: 1、多个Socket Socket会产生AE_READABLE和AE_WRITABLE事件: 当socket变得可读时或者有新的可以应答的socket出现时,socket就会产生一个AE_READABLE事件 当socket变得可写时,socket就会产生一个AE_WRITABLE事件 2、io多路复用程序 3、文件事件分派器 4、事件处理器,事件处理器包括:连接应答处理器、命令请求处理器、命令回复处理器,每个处理器对应不同的socket事件: 如果是客户端要连接redis,那么会为socket关联连接应答处理器 如果是客户端要写数据到redis(读、写请求命令)那么会为socket关联命令请求处理器 如果是客户端要从redis读数据,那么会为socket关联命令回复处理器 多个socket会产生不同的事件,不同的事件对应着不同的操作,io多路复用程序监听着这些socket,当这些socket产生了事件,io多路复用程序会将这些事件放到一个队列中,通过这个队列,以有序、同步、每次一个事件的方式向文件时间分派器中传送,当事件处理器处理完一个事件后,io多路复用程序才会继续向文件分派器传送下一个事件 下图是客户端与redis通信的一次完整的流程:

1、redis启动初始化的时候,redis会将连接应答处理器与AE_READABLE事件关联起来

2、如果一个客户端跟redis发起连接,此时redis会产生一个AE_READABLE事件,由于开始之初AE_READABLE是与连接应答处理器关联,所以由连接应答处理器来处理该事件,这时连接应答处理器会与客户端建立连接,创建客户端响应的socket,同时将这个socket的AE_READABLE事件与命令请求处理器关联起来

3、如果这个时间客户端向redis发送一个命令(set k1 v1),这时socket会产生一个AE_READABLE事件,io多路复用程序会将该事件压入队列中,此时事件分派器从队列中取得该事件,由于该socket的AE_READABLE事件已经和命令请求处理器关联了,因此时间分派器会将该事件交给命令请求处理器处理,命令请求处理器读取事件中的命令并完成,操作完成后,redis会将该socket的AE_WRITABLE事件与命令回复处理器关联

4、如果客户端已经准备好接受数据后,redis中的该socket会产生一个AE_WRITABLE事件,同样会压入队列然后被事件分派器取出交给相对应的命令回复处理器,由该命令回复处理器将准备好的响应数据写入socket中,共客户端读取

5、命令回复处理器写完后,就会删除该socket的AE_WRITABLE事件与命令回复处理器的关联关系

21.redis事务及其相关面试题

什么是事务: 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行 redis事务的概念: redis事务的本质是通过multi、exec、watch等一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中 总结:redis事务就是一次性,顺序性,排他性的执行一个队列中的一系列命令 redis事务的三个阶段: 1、事务开始multi 2、命令入队 3、事务执行exec 事务执行过程中,如果服务端收到有exec、discard、watch、multi之外的请求,将会把请求放入队列中排 事务管理(ACID)概述 原子性(Atomicity) 原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生 一致性(Consistency) 事务前后数据的完整性必须保持一致 隔离性(lsolation) 多个事务并发执行时,一个事务的执行不影响其他事务的执行 持久性(Durability) 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响 redis的事务总是具有ACID中的一致性和隔离性,其他特性是不支持的。当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性 redis事务支持隔离性吗: redis是单进程程序,并且他保证再执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止,因此,redis事务总是带有隔离性的 redis事务保证原子性吗,支持回滚吗: redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚,事务中任意命令执行失败,其余的命令仍会被执行 redis事务其他实现 基于Lua脚本,redis可以保证脚本内的命令一次性】按顺序地执行,其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完 基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时先读取该变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐

22.redis是单线程的,如何提高多核CPU的利用率

可以在同一个服务器部署多个redis的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的,所以,如果你想使用多个CPU,你可以考虑一下分片(shard)

23.为什么要做redis分区

分区可以让redis管理更大的内存,redsi将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。分区使redis的计算能力通过简单地增加计算机得到成倍提升,redis的网络带宽也会随着计算机和网卡的增加而成倍增长

24.你知道有哪些redis分区实现方案

客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从那个redis节点读取,大多数客户端已经实现了客户端分区 代理分区意味着客户端将请求发送给代理,然后代理决定去那个节点写数据或者读数据,代理根据分区规则决定请求那些redis实例,然后根据redis响应结果返回给客户端。redis和memcached的一种代理实现就是Twemproxy 查询理由(Query routing)的意思是客户端随机地请求任意一个redis实例,然后由redis将请求转发给正确的redis节点。redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点

25.redis分区有什么缺点

1、涉及多个key的擦欧总通常不会被支持。例如你不能对两个集合求交集,因为他们可能被存储到不同的redis实例(上级上这种情况也有办法,但是不能直接使用交集命令) 2、同时操作多个key,则不能使用redis事务 3、分区使用的颗粒度是key,不能使用一个非常长的排序key存储一个数据集(The partitioning granularity is the key, so it is not possible to shard a dataset with a single huge key like a very big sorted set) 4、当使用分区的时候,数据处理会非常复杂,例如味蕾备份你必须从不同的redis实例和主机同时收集RDB/AOF文件 5、分区时动态扩容或缩容可能非常复杂。redis集群在运行时增加或者删除redis节点,能做到最大程度对用户透明地数据在平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题

26.如何解决redis的并发竞争key问题

所谓redis的并发竞争key的问题也就是多个系统同时对一个key进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同 推荐一种方案:分布式锁(zookeeper和redis都可以实现分布式锁) (如果不存在redis的并发竞争key问题,不要使用分布式锁,这样会影响性能) 基于zookeeper临时有序节点可以实现分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬间有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬间节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题,完成业务流程后,删除对应的子节点释放锁 在实践中,当然是以可靠性为主。首推zookeeper

27.分布式redis是前期做还是后期规模上来了在做好,为什么

既然redis是如此的轻量(单实例只使用1M内存),为防止以后的扩容,最好的办法就是一开始就启动较多实例。即便你只有一台服务器,你也可以一开始就让redis以分布式的方式运行,使用分区,在同一台服务器上启动多个实例 一开始就多设置几个redis实例,例如32或者64个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的 这样的话,当你的数据不断增长,需要更多的redis服务器时,你需要做的就是仅仅将redis实例从一台服务迁移到另一台服务器而已(而不用考虑重新分区的问题)。一旦你添加了另一台服务器,你需要将你一半redis实例从第一台机器迁移到第二台机器

28.redis相比Memcached有哪些优势

数据类型:Memcached所有的值均是简单的字符串,redis支持更为丰富的数据类型,支持string(字符串),list(列表),set(集合)、Sorted Set(有序集合)、hash(哈希)等 持久化:redis支持数据落地持久化存储,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。memcache不支持数据持久存储 集群模式:redis提供主从同步机制,以及Cluster集群部署能力,能够提供高可用服务。Memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据 性能对比:redis的速度比Memcached快很多 网络io模型:redis使用单线程的多路io复用模型,Memcached使用多线程的非阻塞io模式 redis支持服务器端的数据操作:redis相比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作,通常在Memcached里,你需要将数据拿到客户端来进行类似的修改在set回去 这大大增加了网络io的次数和数据体积。在redis中,这些复杂的操作通常和一般的get/set一样高效。所以,如果需要缓存能够支持更复杂的结果和操作,那么redis会是不错的选择

29.为什么要用redis而不用map/guava做缓存

缓存分为本地缓存和分布式缓存。以java为例,使用自带的map或者guava实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着jvm的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。 使用redis或memcached之类的称为分布式缓存,再多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持redis或memcached服务的高可用,整个程序架构上较为复杂 对比: 1、redis可以用几十G内存来做缓存,map不行,一般JVM也就分几个G数据就够大了 2、redis的缓存可以持久化,Map是内存对象,程序一重启数据就没了 3、redis可以实现分布式的缓存,map只能存在创建它的程序里 4、redis可以处理每秒百万级的并发,是专业的缓存服务,Map只是一个普通的对象 5、redis缓存有过期机制,Map本身无此功能;redis有丰富的API,Map就简单太多了 6、redis可单独部署,多个项目之间可以共享,本地内存无法共享 7、redis有专门的管理工具可以查看缓存数据

30.如何选择合适的持久化方式

1、如果是数据不那么敏感,且可以从其他地方重新生成补回的,那么可以关闭持久化 2、如果是数据比较重要,不想再从其他地方获取,且可以承受数分钟的数据丢失,比如缓存等,那么可以只使用RDB 3、如果使用内存数据库,要使用redis的持久化,建议是RDB和AOF都开启,或者定期执行bgsave做快照备份,RDb方式更适合做数据的备份,AOF可以保证数据不丢失 补充:redis4.0对于持久化机制的优化 redis4.0相对于3.X版本其中一个比较大的变化是4.0添加了新的混合持久化方式 简单的说:新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据

优势:

混合持久化结合RDB持久化和AOF持久化的优点,由于绝大部分都是RDB格式,加载速度快,同时结合AOF,增量的数据以AOF方式保存了,数据更少的丢失

劣式:

兼容性差,一旦开启了混合持久化,在4.0之前版本都不是别该AOD文件,同时由于前部分是RDB格式,阅读性较差

31.redis key的过期时间和永久有效分别怎么设置

通过expire或者pexpire命令,客户端可以以秒和毫秒的精度为数据库中的某个键设置生存时间 与expire和pexpire命令类似,客户端可以通过exireat和pexpireat命令,以秒或毫秒精度给数据库中的某个键设置过期时间,可以理解为:让某个键在某个时间点过期

32.什么是缓存预热

缓存预热是指系统上线后,提前将相关的缓存数据加载到缓存系统。避免在用户请求的额时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据 如果不进行预热,那么redis初始状态数据为空,系统上线初期,相对于高并发的流量,都会访问到数据库中,对数据库造成流量的压力 缓存预热解决方案: 数据量不大的时候,工程启动的时候进行加载缓存动作 数据量大的时候,设置一个定时任务脚本,进行缓存的刷新 数据量太大的时候,优先保证热点数进行提前加载到缓存

33.什么是缓存降级

缓存降级是指缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。 在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出那些必须誓死保护,那些可将级;比如可以参考日志级别设置预案: 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级; 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警; 错误:比如可用率低于90%,或者数据连接池被打爆了,或者访问量突然猛增到系统能承受的最大阈值,此时可以根据情况自动降级或者人工降级; 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级

34.redis真的是单线程吗

讨论这个问题前,先看下redis的版本中两个重要的节点: 1、redis4.0(引入多线程处理异步任务) 2、redis6.0(在网络模型中实现多线程I/O) 所以,网络上说的redis是单线程,通常是指redis6.0之前,其核心网络模型使用的是单线程 且redis6.0引入多线程I/O,只是用来处理网络数据的读写和协议的解析,而执行命令依旧是单线程 redis在4.0版本的时候就已经引入了的多线程来做一些异步操作,此举主要针对的是那些耗时的命令,通过将这些命令的执行进行异步化,避免阻塞单线程的事件循环 在redis4.0之后增加了一些的非阻塞命令unlink、flushall async、flushdb async

35.redis6.0为何引入多线程

很简单,就是redis的网络I/O平静已经越来越明显了 提升redis的性能有两个方向: 优化网络I/O模块 提高机器内存读写的速度 后者依赖硬件的发展,暂时无解,所以只能从前者下手,网络I/O的优化又可以分两个方向: 零拷贝技术或者DPDK技术 利用多核优势 零拷贝技术有其局限性,无法完全适配redis这一类复杂的网络I/O场景,更多网络I/O对CPU时间的消耗和linux零拷贝技术。而DPDK技术通过旁路网卡I/O绕过内核协议栈的方式又太过于复杂以及需要内核甚至是硬件的支持 总结起来,redis支持多线程主要就是两个原因: 可以充分利用服务器CPU资源,目前主线程只能利用一个核 多线程任务可以分摊redis同步io读写负荷

36.redis6.0多线程的实现机制

流程简述如下: 主线程负责接受建立连接请求,获取socket放入全局等待读处理队列 主线程处理完读事件之后,通过RR(round robin)将这些连接分配给这些io线程 主线程阻塞等待io线程读取socket完毕 主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行 主线程阻塞等待io线程见数据回写socket完毕

该设计的特点:

io线程要么同时再读socket,要么同时再写,不会同时读或写

io线程只负责读写socket解析命令,不负责命令处理

37.redis6.0采用多线程后,性能的提升效果如何

Redis 作者 antirez 在 RedisConf 2019 分享时曾提到:Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上。 国内也有大牛曾使用 unstable 版本在阿里云 esc 进行过测试,GET/SET 命令在 4 线程 IO 时性能相比单线程是几乎是翻倍了。

38.redis6.0开启多线程后,是否会存在线程并发安全问题

从实现机制可以看出,redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行 所以我们不需要去考虑控制key、lua、事务,lpush/lpop等等的并发及线程安全问题

39.redis6.0与Memcached多线程模型的对比

相同点: 都采用了master线程-worker线程的模型 不同点: memcached执行主逻辑也是在worker线程里,模型更加简单,实现了真正的线程隔离的常规理解 而redis把处理逻辑交还给maste线程,虽然一定程度上增加了模型复杂度,但也解决了线程并发安全等问题

40.介绍下redis单副本

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

优点:

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

缺点:

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

41.介绍下redis多副本(主从)

redis多副本,采用主从(replication)部署结构,相较于但副本而言最大的特点就是主从实例间数据实时同步,并且提供数据持久化和备份策略。主从实例部署在不同的物理服务器上,根据公司的基础环境配置,可以实现同时对外提供服务和读写分离策略

 

优点:

  • 高可靠性:一方面,采用双机主备架构,能够在主库出现故障时自动进行主备切换,从库提升为主库提供服务,保证服务平稳运行;另一方面,开启数据持久化功能和配置合理的备份策略,能有效的解决数据误操作和数据异常丢失的问题
  • 读写分离策略:从节点可以扩展主库节点的读能力,有效应对大并发量的读操作

缺点:

  • 故障恢复复杂,如果没有redisHA系统(需要开发),当主库节点出现故障时,需要手动将一个从节点晋升为主节点,同时需要通知业务方变更配置,并且需要让其他从库节点去复制新主库节点,整个过程需要人为干预,比较繁琐
  • 主库的写能力受到单机的限制,可以考虑分片
  • 主库的存储能力受到单机的限制,可以考虑Pika
  • 原生复制的弊端在早期的版本中也会比较突出,如:redis复制中断后,Slave会发起psync,此时如果同步不成功,则会进行全量同步,主库执行全量备份的同时可能会造成毫秒或秒级的卡顿,又由于COW机制,导致极端情况下的主库内存溢出,程序异常退出或宕机,主库节点生成备份文件导致服务器磁盘IO和CPU(压缩)资源消耗,发送数GB大小的备份文件导致服务器出口带宽暴增,阻塞请求,建议升级到最新版本

42.介绍下redis Sentinel(哨兵)

主从模式下,当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用,这种方式并不推荐,实际生产中我们优先考虑哨兵模式。这种模式下,master宕机,哨兵会自动选举master并将其他的slave指向新的master

redis Sentinel是社区版本推出的原生高可用解决方案,其部署架构主要包括两部分:redis Sentinel集群和redis数据集群

其中redis Sentinel集群是由若干Sentinel界定啊组成的分布式集群,可以实现故障发现、故障自动转移、配置中心和客户端通知。redis Sentinel的节点数量要满足2n+1(n>=1)的奇数个

 

优点:

  • redis Sentinel集群部署简单
  • 能够解决redis主从模式下的高可用切换问题
  • 很方便实现redis数据节点的线形扩展,轻松突破redis自身单线程瓶颈,可极大满足redis大容量或高性能的业务需求
  • 可以实现一套Sentinel监控一组redis数据节点或多组数据节点

缺点:

  • 部署相对redis主从模式要复杂一些,原理理解更繁琐
  • 资源浪费,redis数据节点中slave节点作为备份节点不提供服务
  • redis Sentinel主要针对redis 数据节点中的主节点的高可用切换,对redis的数据节点做失败判定分为主观下线和客观下线两种,对于redis的从节点有对节点做主观下线操作,并不执行故障转移
  • 不能解决读写分离问题,实现起来相对复杂

43.介绍下redis Cluster

redis的哨兵模式基本已经可以实现高可用,读写分离,但是在这种模式下每台redis服务器都存储相同的数据,很浪费内存,所以在redis3.0上加入了Cluster集群模式,实现了redis的分布式存储,对数据进行分片,也就是说每台redis节点上存储不同的内容

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

redis Cluster集群节点最小配置6个节点以上(3主3从),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用

redis Cluster采用模拟槽分区,所有的键根据哈希函数映射到0~16383个整数槽内,每个节点负责维护一部分槽以及槽所映射的键值数据

 

优点:

  • 无中心架构
  • 数据按照slot存储分布在多个节点,节点间数据分享,可动态调整数据分布
  • 可扩展性:可线性扩展到1000多个节点,节点可动态添加或删除
  • 高可用性:部分节点不可用时,集群仍可用,通过增加Slave做standby数据副本,能够实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色提升
  • 降低运维成本,提高系统的扩展性和可用性

缺点:

  • Client实现复杂,驱动要求实现Smart Client,缓存Slots mapping信息并及时更新,提高了开发难度,客户端的不成熟影响业务的稳定性,目前仅redisCluster相对成熟,异常处理部分还不完善,比如常见的max redirect exception
  • 节点会因为某些原因发生阻塞(阻塞时间大于cluster-node-timeout),被判断下线,这种failover是没有必要的
  • 数据通过异步复制,不保证数据的强一致性
  • 多个业务使用同一套集群时,无法根据统计区分冷热数据,资源隔离性较差,容易出现相互影响的情况
  • Slave在集群中充当“冷备”不能缓解读压力,当然可以通过SDK的合理设计来提高Slave资源的利用率
  • key批量操作限制,如使用mset、mget目前只支持具有相同slot值的key执行批量操作,对于映射为不同slot的值由于keys不支持跨slot查询,所以执行mset、mget、sunion等操作不支持不友好
  • key事务操作支持有限,只支持多key在同一节点上的事务操作,当多个key分布不同的节点上时无法使用事务功能
  • key作为数据分区的最小颗粒度,不能将一个很大的键值对象如hash、list等映射到不同的节点。不支持多数据库空间,单机下的redis可以支持到16个数据库,集群模式下只能使用一个数据库空间,即db0
  • 复制结构支持一层,从节点只能复制主节点,不支持嵌套树状复制结构
  • 避免产生hot-key,导致主库节点成为系统的短板
  • 避免产生big-key,导致网卡撑爆、慢查询等
  • 重试时间应该大于cluster-node-time时间
  • redis Cluster不建议使用pipeline和multi-keys操作,减少max redirect产生的场景

44.介绍一下redis 自研

redis自研是高可用解决方案,主要体现在配置中心、故障探测和failover的处理机制上,通常根据企业业务的实际线上环境来定制化

 

优点:

  • 高可靠性、高可用性
  • 自主可控性高
  • 贴切业务实际需求,可缩性好,兼容性好

缺点:

  • 实现复杂,开发成本高
  • 需要建立配套的周边设施,如监控,域名服务,存储元数据信息的数据库等
  • 维护成本高

45.redis高可用方案具体怎么实施

使用官当推荐的哨兵机制就能实现,但主节点出现故障时,由Sentinel自动完成故障发现和转移,并通知应用方,实现高可用性,他又主要四个功能: 集群监控,负责监控redis master和slave进程是否正常工作 消息通知,如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员 故障转移,如果master node挂掉了,会自动转移到slave node上 配置中心,如果故障转移发生了,通知client客户端新的master地址

46.了解主从复制的原理吗

1、主从架构的核心原理 当启动一个slave node的时候,他会发送一个PSYNC命令给master node 如果这个slave node重新连接master node,那么master node仅仅会复制给slave部分缺少的数据,否则如果是slave node第一次连接master node,那么会触发一次full resynchronization 开始full resynchronization的时候,master会启动一个后台线程,开始生成一份RDB快照文件,同时还会将从客户端收到的所有写命令缓存在内存中。RDB文件生成完毕之后,master会将这个RDB发送给slave,slave会先写入本地磁盘,然后再从本地磁盘加载到内存中,然后master会将内存中缓存的写命令发送给slave,slave也会同步这些数据 slave node如果跟master node有网络故障,断开了连接,会自动重连。master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node 2、主从复制的断点续传 从redis2.8开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份 master node会在内存中常见一个backlog,master和slave都会保存一个replica offset还有一个master id,offset就是保存在backlog中的。如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制 但是如果没有找到对应的offset,那么就会执行一次resynchronization

3、无磁盘化复制

master在内存中直接创建rdb,然后发送给slave,不会再自己本地落地磁盘了

repl-diskless-sync repl-diskless-sync-delay,等待一定时长在开始复制,因为要等更多slave重新连接过来

4、过期key处理

slave不会过期key,只会等待master过期key。如果master过期了一个key,或者通过LRU淘汰了一个key,那么会模拟一条del命令发送给slave

47.由于主从延迟导致读取到过期数据怎么处理

1、通过scan命令扫库: 当redis中的key被scan的时候,相当于访问了该key,同样也会做过期检测,充分发挥redis惰性删除的策略。这个方法能大大降低了脏读数据读取的概率,但缺点也比较明显,会造成一定的数据库压力,否则影响线上业务的效率 2、redis加入了一个新特性来解决主从不一致导致读取到过期数据问题,增加了key是否过期以及主从库的判断,如果key已过期,当前访问的master则返回null;当前访问的是从库,且执行的是只读命令也返回null

48.主从复制的过程中如果因为网络原因停止了复制了会怎么样

如果出现网络故障断开连接了,会自动重连的,从redis2.8开始,就支持主从复制的断点续传,可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份 master如果发现有多个slave node都来重新连接,仅仅会启动一个rdbsave操作,用一份数据服务所有slave node master node会在内存中创建一个backlog,master和slave都会保存一个replica offset,还有一个master id,offset就是保存在backlog中的,如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制 但是如果没有找到对应的offset,那么就会执行一次resynchronization全量复制

49.redis主从架构数据会丢失吗,为什么

有两种数据丢失的情况:

1、异步复制导致的数据丢失:

因为master->slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了

2、脑裂导致的数据丢失:

某个master所在机器突然脱离了正常的网络,跟其他slave机器不能接受,但是实际上master还运行着,此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master。这个时候,集群里就会有两个master,也就是所谓的脑裂。此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了。因此旧master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据

50.如何让解决主从架构数据丢失的问题

数据丢失的问题是不可避免的,但是我们可以尽量减少

在redis的配置文件里设置参数

min-slaves-to-write 1 min-slaves-max-lag 10 min-slaves-to-write默认情况下是0,min-slaves-max-lag默认情况下是10.

上面的配置的意思是要求至少有一个slave,数据复制和同步的延迟不能超过10秒,如果说一旦所有的slave,数据复制和同步的延迟都超过了10秒钟,那么这个时候,master就不会在接受任何请求了。

减小min-slaves-max-lag参数的值,这样就可以避免再发生故障时大量的数据丢失,一旦发现延迟超过了该值就不会往master中写入数据

那么对于client,我们可以采取降级措施,将数据暂时写入本地缓存和磁盘中,在一段时间后重新写入master来保证数据不丢失,也可以将数据写入kafka消息队列,隔一段时间去消费kafka中的数据。

51.redis哨兵是怎么工作的

1、每个Sentinel以每秒钟一次的频率向他所知的master,slave以及其他Sentinel实例发送一个PING命令

2、如果一个实例(instance)距离最后一次有效恢复PING命令的时间超过down-after-milliseconds选项所指定的值,则这个实例会被当前Sentinel标记为主观下线

3、如果一个master被标记为主观下线,则正在监视这个master的所有Sentinel要以每秒一次的频率确认master的确进入了主观下线状态

4、当有足够数量的Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认master的确进入了主观下线状态,则master会被标记为客观下线

5、当master被Sentinel标记为客观下线时,Sentinel向下线的master的所有slave发送INFO命令的频率会从10秒一次改为每秒一次(在一般情况下,每个Sentinel会以每10秒一次的频率向他已知的所有master,slave发送INFO命令)

6、若没有足够数量的Sentinel同意master已经下线,master的客观下线状态就会变成主管下线,若master重新向Sentinel的PING命令返回有效回复,master的主观下线状态就会被移除

7、sentinel节点会与其他sentinel节点进行“沟通”投票选举一个sentinel节点进行故障处理,在从节点中选取一个主节点,其他从节点挂载到新的主节点自动复制新主节点的数据

52.故障转移时会从剩下的slave选举一个新的master,被选举为master的标准是什么

如果一个master被认为down了,而且majority哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个slave来,会考虑slave的一些信息

1、跟master断开连接的时长

如果一个slave跟master断开连接已经超过了,down-after-milliseconds的10倍,外加master宕机的时长,那么slave就被认为不适合选举为master

(down-after-milliseconds * 10)+ milliseconds_since_master_is_in_SDOWN_state

2、slave优先级

按照slave优先级进行排序,slavepriority越低,优先级就越高

3、复制offset

如果slave priority相同,那么看replica offset,那个slave复制了越多的数据,offset越靠后,优先级就越高

4、run id

如果上面两个条件都相同,那么选择一个run id比较小的那个slave

53.同步配置的时候其他哨兵根据什么更新自己的配置呢

执行切换的那个哨兵,会从要切换到新master(slave->master)那里得到一个configuration epoch,这就是一个version号,每次切换的version号都必须是唯一的。

如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout时间,然后接替继续执行切换,此时会重新获取一个新的configuration epoch作为新的version号

这个version号就很重要了,因为各种消息都是通过一个channel去发布和监听的,所以一个哨兵完成一个新的切换之后,新的master配置是跟着新的version号的,其他的哨兵都是根据版本号的大小来更新自己的master配置的

54.为什么redis哨兵集群只有两个节点无法正常工作

哨兵集群必须部署2个以上节点。

如果两个哨兵实例,即两个redis实例,一主一从的模式

则redis的配置quorum=1,标识一个哨兵认为master宕机即可认为master已宕机即可认为master已宕机

但是如果是机器1宕机了,那哨兵1和master都宕机了,虽然哨兵2知道master宕机了,但是这个时候,需要majority,也就是大多数哨兵都是运行的,2个哨兵的majority就是2(2的majority=2,3的majority=2,5的majority=3,4的majority=2)2个哨兵都运行着,就可以允许执行故障转移。

但此时哨兵1没了就只有1个哨兵了,此时就没有majority来允许执行故障转移,所以故障转移不会执行。

55.redis cluster中是如何实现数据分布的,这种方式有什么优点

redis cluster有固定的16384个hash slot(哈希槽),对每个key计算CRC16值,然后对16384取模,可以获取key对应的hash slot

redis cluster中每个master都会持有部分slot(槽),比如有三个master,那么可能每个master持有5000多个hash slot

hash slot让node的增加和移除很简单,增加一个master,就将其他master的hash slot移动部分过去,减少一个master,就将他们的hash slot移动到其他master上去。每次增加或减少master节点都是对16384取模,而不是根据master数量,这样原本在老的master上的数据不会因master的新增或减少而找不到。并且增加或减少master时redis cluster移动hash slot的成本时非常低的

56.redis cluster节点间通信是什么机制

redis cluster节点间采取gossip协议进行通信,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,则改节点会把数据不断地发送给其他节点让节点进行数据变更,通过节点互相之间不断通信来确保持整个集群所有节点的数据是完整的

主要交换故障信息、节点的增加和移除、hash slot信息等

这种机制的好处在于,元数据更新比较分散,不是集中在一个地方,更新请求会陆陆续续,达到所有节点上去更新,有一定的延时,降低了压力。

缺点则是元数据更新有延时,可能导致集群的一些操作会有一些滞后

57.什么是分布式锁,为什么用

锁在程序中的作用就是同步工具,保证共享资源在同一时刻只能被一个线程访问,Java中的锁我们都很熟悉了,像synchronized 、Lock都是我们经常使用的,但是Java的锁只能保证单机的时候有效,分布式集群环境就无能为力了,这个时候我们就需要用到分布式锁。

分布式锁,顾名思义,就是分布式项目开发中用到的锁,可以用来控制分布式系统之间同步访问共享资源。

思路是:在整个系统提供一个全局、唯一的获取锁的“东西”,然后每个系统在需要加锁时,都去问这个“东西”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。至于这个“东西”,可以是Redis、Zookeeper,也可以是数据库。

一般来说,分布式锁需要满足的特性有这么几点:

1、互斥性:在任何时刻,对于同一条数据,只要一台应用剋获取到分布式锁;

2、高可用性:在分布式场景下,一小部分服务器宕机不影响正常使用,这种情况就需要将提供分布式锁的服务以集群的方式部署;

3、防止锁超时:如果客户端没有主动释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕机或者网络不可达时产生死锁;

4、独占性:加锁解锁必须由同一台服务器进行,也就是锁的持有者才可以释放锁,不能出现你加的锁,别人给你解锁了

58.常见的分布式锁有哪些解决方案

实现分布式锁目前有三种流行方案,即基本关系型数据库、redis、Zookeeper的方案

1、基于关系型数据库,如MySQL

基于关系型数据库实现分布式锁,是依赖数据库的唯一性来实现资源锁定,比如主键和唯一索引等

缺点:

  • 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用
  • 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法在获得到锁
  • 这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错,没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作
  • 这把锁是非中入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了

2、基于redis实现

优点:

redis锁实现简单,理解逻辑简单,性能好,可以支撑高并发的获取、释放锁操作

缺点:

  • redis容易单点故障,集群部署,并不是强一致性的,锁的不够健壮
  • key的过期时间设置多少不明确,只能根据实际情况调整
  • 需要自己不断去尝试获取锁,比较消耗性能

2、基于zookeeper

优点:

zookeeper天生设计定位就是分布式协调,强一致性,锁很健壮。如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小

缺点:

在高请求高并发下,系统疯狂的加锁释放锁,最后zk承受不住这么大的压力可能会存在宕机的风险

59.redis实现分布式锁

分布式锁的三个核心要素

1、加锁

使用setnx来加锁。key是锁的唯一标识,按业务来决定命名,value这里设置为test。

setx key test

当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁;当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败;

2、解锁

有加锁就得有解锁。当得到的锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式就是执行del指令。

del key

释放锁之后,其他线程就可以继续执行setnx命令来获得锁。

3、锁超时

锁超时知道的是:如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程北向进来。

所以,setnx的key必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一段时间后自动释放。setnx不支持超时参数,所以需要额外指令,

expire key 30

上述分布式锁存在的问题

通过上述setnx 、del和expire实现的分布式锁还是存在着一些问题。

1、SETNX 和 EXPIRE 非原子性

假设一个场景中,某一个线程刚执行setnx,成功得到了锁。此时setnx刚执行成功,还未来得及执行expire命令,节点就挂掉了。此时这把锁就没有设置过期时间,别的线程就再也无法获得该锁。

解决措施:

由于setnx指令本身是不支持传入超时时间的,而在Redis2.6.12版本上为set指令增加了可选参数, 用法如下:

SET key value [EX seconds][PX milliseconds] [NX|XX]

  • EX second: 设置键的过期时间为second秒;
  • PX millisecond:设置键的过期时间为millisecond毫秒;
  • NX:只在键不存在时,才对键进行设置操作;
  • XX:只在键已经存在时,才对键进行设置操作;
  • SET操作完成时,返回OK,否则返回nil。

2、锁误解除

如果线程 A 成功获取到了锁,并且设置了过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁;随后 A 执行完成,线程 A 使用 DEL 命令来释放锁,但此时线程 B 加的锁还没有执行完成,线程 A 实际释放的线程 B 加的锁。

解决办法:

在del释放锁之前加一个判断,验证当前的锁是不是自己加的锁。

具体在加锁的时候把当前线程的id当做value,可生成一个 UUID 标识当前线程,在删除之前验证key对应的value是不是自己线程的id。

还可以使用 lua 脚本做验证标识和解锁操作。

3、超时解锁导致并发

如果线程 A 成功获取锁并设置过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁,线程 A 和线程 B 并发执行。

A、B 两个线程发生并发显然是不被允许的,一般有两种方式解决该问题:

  • 将过期时间设置足够长,确保代码逻辑在锁释放之前能够执行完成。
  • 为获取锁的线程增加守护线程,为将要过期但未释放的锁增加有效时间。

4、不可重入

当线程在持有锁的情况下再次请求加锁,如果一个锁支持一个线程多次加锁,那么这个锁就是可重入的。如果一个不可重入锁被再次加锁,由于该锁已经被持有,再次加锁会失败。Redis 可通过对锁进行重入计数,加锁时加 1,解锁时减 1,当计数归 0 时释放锁。

5、无法等待锁释放

上述命令执行都是立即返回的,如果客户端可以等待锁释放就无法使用。

  • 可以通过客户端轮询的方式解决该问题,当未获取到锁时,等待一段时间重新获取锁,直到成功获取锁或等待超时。这种方式比较消耗服务器资源,当并发量比较大时,会影响服务器的效率。
  • 另一种方式是使用 Redis 的发布订阅功能,当获取锁失败时,订阅锁释放消息,获取锁成功后释放时,发送锁释放消息。具体实现参考:https://xiaomi-info.github.io/2019/12/17/Redis-distributed-lock/

60.redlock的原理

假设有5个完全独立的redis主服务器

1、获取当前时间戳

2、client尝试按照顺序使用相同的key,value获取所有redis服务的锁,在获取所得过程中的获取时间比锁过期时间短很多,这是为了不要过长时间等待已经关闭的redis服务。并且试着获取下一个redis 实例

比如:TTL为5s设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁

3、client通过获取所有能获取的锁后的时间减去第一步的时间,这个时间差小于TTL时间并且至少有3个redis实例成功获取锁,才算真正的获取锁成功

4、如果成功获取锁,则锁的真正有效时间是TTL减去第三步的时间差的时间;比如:TTL是5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该在减去时钟飘移)

5、如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例;因为可能已经获取了小于3个锁,必须释放,否则影响其他client获取锁