【Java面试八股文】Redis篇

引言:

本文对多个平台的面试题进行了汇总、分类、概括、整理,对重点进行了标出,更便于阅读和记忆。

【黑马Java笔记+踩坑汇总】JavaSE+JavaWeb+SSM+SpringBoot+瑞吉外卖+SpringCloud+黑马旅游+谷粒商城+学成在线+牛客面试题

目录

说说你对Redis的了解

详细的说说Redis的数据类型

请你说说Redis数据类型中的zset,它和set有什么区别?底层是怎么实现的?

说说Redis的持久化策略

说说缓存穿透、击穿、雪崩的区别

如何利用Redis实现一个分布式锁?

Redis如何与数据库保持双写一致性

说说Redis的主从同步机制

说说Redis的单线程架构

如何实现Redis高可用

说说Redis的缓存淘汰策略


说说你对Redis的了解

得分点

Redis概念(数据类型、读写性能)、Redis优点及用途

概念:Redis是一款基于键值对的NoSQL数据库,它在内存中读写性能非常高,每秒可以处理超过百万次的读写操作。

功能:键过期、事务、lua脚本、持久化机制。

数据类型: string、hash、 list、set(集合)、zset(有序集合)

应用场景:缓存热点数据、计数器、限时业务、分布式锁、队列等。

持久化机制:

  • 数据备份机制RDB:数据每隔一段时间写进磁盘rdb文件,故障后从文件读。占CPU和内存但恢复快,不能恢复完整数据。
  • 追加文件机制AOF:命令日志立刻或缓存一秒写进磁盘rdb文件,可以按条件重写rdb文件,故障后从文件读命令恢复数据。不占CPU和内存占IO,能恢复完整数据。

标准回答

Redis是一款基于键值对的NoSQL数据库,与其他键值对数据库不同的是,Redis中拥有string(字符串)、hash(哈希)、 list(列表)、set(集合)、zset(有序集合)、Bitmaps(位图)、 HyperLogLog、GEO(地理信息定位)等多种数据结构,这给Redis带来了满足多种应用场景的能力,而且,Redis将所有数据放到内存中的做法让它的读写性能十分惊人。不仅如此,Redis的持久化机制保证了在发生类似断电,机械故障等情况时,内存中的数据不会丢失。此外Redis还提供了键过期、发布订阅、事务、流水线、Lua脚本等多个附加功能。总之,在合适的情况下使用Redis会大大增强系统的性能,减少开发人员工作量。

持久化机制:RDB、AOF 

RDB全称Redis Database Backup file(Redis数据备份文件,backup译为备份,支援),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。快照文件称为RDB文件,默认是保存在当前运行目录。缺点是耗时、两次RDB间隔时间长,会丢失数据。

AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。

加分回答-适合Redis使用的场景:

- 热点数据的缓存:redis访问速度块、支持的数据类型丰富,很适合用来存储热点数据。

- 限时业务:redis中可以使用expire命令设置一个键的生存时间,到时间后redis会删除它。因此,Redis在限时业务中的表现很亮眼。

- 计数器:incrby命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动、分布式序列号的生成。

- 排行榜:关系型数据库在排行榜方面查询速度普遍偏慢,所以可以借助redis的SortedSet进行热点数据的排序

- 分布式锁:这个主要利用redis的setnx命令进行,在后面的如何用Redis实现一个分布式锁中会进行详解。

- 延时操作:redis自2.8.0之后版本提供Keyspace Notifications功能,允许客户订阅Pub/Sub频道,以便以某种方式接收影响Redis数据集的事件。

- 分页查询、模糊查询:edis的set集合中提供了一个zrangebylex方法,通过ZRANGEBYLEX zset - + LIMIT 0 10 可以进行分页数据查询,其中- +表示获取全部数据;rangebylex key min max 这个就可以返回字典区间的数据可以利用这个特性可以进行模糊查询功能。

- 点赞,好友等相互关系的存储:Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,我们可以通过这一点实现类似共同好友等功能。

- 队列:由于redis有list push和list pop这样的命令,所以能够很方便的执行队列操作。

详细的说说Redis的数据类型

得分点

Redis5种数据结构

字符串(string):存储字符串、数字和二进制数据,最大存2M。底层数据结构是二进制安全(传输时不会被篡改、破译)、可以动态分配空间和回收内存的简单动态字符串SDS(Simple Dynamic String),

  • 存字符串 时,Redis会将其视为二进制安全的字符序列。
  • 存储数字时,Redis会将其转换成64位有符号整数或双精度浮点数。
  • 存储二进制数据时,Redis会将其视为字节数组,并以原始形式进行存储,不会进行任何编码格式转换。 

哈希(hash):value是键值对类型,可用于存对象,最多村2^32-1个元素。

列表(list):元素有序可重复,底层是双向链表,可用作简单的消息队列。

集合(set):元素无序不可重复,支持多个set求交集、并集、差集。

有序集合(zset):元素有序不可重复,每个元素设置一个分数来作为排序的依据。

标准回答1

Redis主要提供了5种数据结构:字符串(string)、哈希(hash)、列表(list)、集合(set)、有序集合(zset)。

Redis还提供了Bitmap、HyperLogLog、Geo类型,但这些类型都是基于上述核心数据类型实现的。5.0版本中,Redis新增加了Streams数据类型,它是一个功能强大的、支持多播的、可持久化的消息队列。

string可以存储字符串、数字和二进制数据,除了值可以是String以外,所有的键也可以是string,string最大可以存储大小为2M的数据。

list保证数据线性有序且元素可重复,它支持lpush、blpush、rpop、brpop等操作,可以当作简单的消息队列使用,一个list最多可以存储2^32-1个元素

hash的值本身也是一个键值对结构,最多能存储2^32-1个元素。

set是无序不可重复的,它支持多个set求交集、并集、差集,适合实现共同关注之类的需求,一个set最多可以存储2^32-1个元素

zset是有序不可重复的,它通过给每个元素设置一个分数来作为排序的依据,一个zset最多可以存储2^32-1个元素。

加分回答-编码

每种类型支持多个编码,每一种编码采取一个特殊的结构来实现 各类数据结构内部的编码及结构:

string:编码分为int、raw、embstr;int底层实现为long,当数据为整数型并且可以用long类型表示时可以用long存储;embstr底层实现为占一块内存的SDS结构,当数据为长度不超过32字节的字符串时,选择以此结构连续存储元数据和值;raw底层实现为占两块内存的SDS,用于存储长度超过32字节的字符串数据,此时会在两块内存中分别存储元数据和值。

list:编码分为ziplist、linkedlist和quicklist(3.2以前版本没有quicklist)。ziplist底层实现为压缩列表,当元素数量小于2且所有元素长度都小于64字节时,使用这种结构来存储;linkedlist底层实现为双端链表,当数据不符合ziplist条件时,使用这种结构存储;3.2版本之后list一般采用quicklist的快速列表结构来代替前两种。

hash:编码分为ziplist、hashtable两种,其中ziplist底层实现为压缩列表,当键值对数量小于2,并且所有的键值长度都小于64字节时使用这种结构进行存储;hashtable底层实现为字典,当不符合压缩列表存储条件时,使用字典进行存储。

set:编码分为inset和hashtable,intset底层实现为整数集合,当所有元素都是整数值且数量不超过2个时使用该结构存储,否则使用字典结构存储。

zset:编码分为ziplist和skiplist,当元素数量小于128,并且每个元素长度都小于64字节时,使用ziplist压缩列表结构存储,否则使用skiplist的字典+跳表的结构存储。

标准回答2

Redis主要提供了5种数据结构:字符串(String)、哈希(Hash)、列表(List)、集合(set)、有序集合(zset)。

String是一组字节。在 Redis 数据库中,字符串是二进制安全的。这意味着它们具有已知长度,并且不受任何特殊终止字符的影响。可以在一个字符串中存储最多2 兆字节的内容。

Redis 列表定义为字符串列表,按插入顺序排序。可以将元素添加到 Redis 列表的头部或尾部。列表的最大长度为 232 – 1 个元素(超过 40 亿个元素)。

哈希是键值对的集合。在 Redis 中,哈希是字符串字段和字符串值之间的映射。因此,它们适合表示对象。每个哈希可以存储多达 232– 1 个字段-值对。

集合(set)是 Redis 数据库中的无序字符串集合。在 Redis 中,在redis sorted sets里面当items内容大于64的时候同时使用了和skiplist两种设计实现。这也会为了排序和查找性能做的优化。关于时间复杂度:添加和删除都需要修改skiplist,所以复杂度为O(log(n))。

但是如果仅仅是查找元素的话可以直接使用hash,其复杂度为O(1) ,其他的range操作复杂度一般为O(log(n)),当然如果是小于64的时候,因为是采用了ziplist的设计,其时间复杂度为O(n)集合中的最大成员数为 232-1 个元素(超过 40 亿个元素)。

Redis 有序集合类似于 Redis 集合,也是一组非重复的字符串集合。但是,排序集的每个成员都与一个分数相关联,该分数用于获取从最小到最高分数的有序排序集。虽然成员是独特的,但可以重复分数。

加分回答

Redis还提供了Bitmap、HyperLogLog、Geo类型,但这些类型都是基于上述核心数据类型实现的。5.0版本中,Redis新增加了Streams数据类型,它是一个功能强大的、支持多播的、可持久化的消息队列。

请你说说Redis数据类型中的zset,它和set有什么区别?底层是怎么实现的?

得分点

有序无序、底层数据结构、底层存储结构

zset(有序集合) :元素有序不可重复,每个元素关联一个可重复的double类型的分数,Redis是通过这个分数对元素排序的。

zset底层数据结构是哈希表,增删改查时间复杂度为O(1)。

zset底层存储结构是ziplist或skiplist,

ziplist存储结构:用两个紧邻的压缩列表存每个元素和分值。元素数量小于128个,且每个元素长度小于64字节时使用这种存储结构。

skiplist跳跃表存储结构:多层索引。

标准回答

Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数。Redis 正是通过分数来为集合中的成员进行从小到大的排序。有序集合的成员是唯一的,但分数 ( score ) 却可以重复。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。集合中最大的成员数为 232 – 1 ( 4294967295 ) , 每个集合可存储 40 多亿个成员。

zset底层的存储结构包括ziplist或skiplist,在同时满足有序集合保存的元素数量小于128个和有序集合保存的所有元素的长度小于64字节的时候使用ziplist,其他时候使用skiplist。

ziplist作为zset的底层存储结构时候,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个元素保存元素的分值

skiplist作为zset的底层存储结构的时候,使用skiplist按序保存元素及分值,使用dict来保存元素和分值的映射关系。

加分回答

实际上单独使用Hashmap或skiplist也可以实现有序集合,Redis使用两种数据结构组合的原因是如果我们单独使用Hashmap,虽然能以O(1) 的时间复杂度查找成员的分值,但是因为Hashmap是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序;而如果单独使用skiplist,虽然能执行范围操作,但查找操作的复杂度却由 O(1)变为了O(logN)。因此Redis使用了两种数据结构来共同实现有序集合。

说说Redis的持久化策略

得分点

RDB、AOF

数据备份机制 RDB:数据每隔一段时间写进磁盘rdb文件,故障后从文件读。占CPU和内存但恢复快,两次RDB间隔快,不能恢复完整数据。

追加文件机制AOF:命令日志立刻或缓存一秒写进磁盘rdb文件,可以按条件重写rdb文件,故障后从文件读 命令恢复数据。主要占IO,能恢复1s之前的数据。

RDB-AOF混合持久化: 基于AOF持久化,每次重写的rdb文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据。。快速恢复且恢复1s之前的数据。

标准回答 

Redis4.0之后,Redis有RDB持久化、AOF持久化、RDB-AOF混合持久化这三种持久化方式。

RDB持久化是将当前进程数据以生成快照的方式保存到硬盘的过程,也是Redis默认的持久化机制。RDB会创建一个经过压缩的二进制文件,这个文件以’.rdb‘结尾,内部存储了各个数据库的键值对等信息。RDB持久化过程有手动触发和自动触发两种方式。手动触发是指通过SAVE或BGSAVE命令触发RDB持久化操作,创建“.rdb”文件;自动触发是指通过配置选项,让服务器在满足指定条件时自动执行BGSAVE命令。RDB持久化的优点是其生成的紧凑压缩的二进制文件体积小,使用该文件恢复数据的速度非常快;缺点则是BGSAVE每次运行都要执行fork操作创建子进程,这属于重量级操作,不宜频繁执行,因此,RBD没法做到实时的持久化。

AOF以独立日志的方式记录了每次写入的命令,重启时再重新执行AOF文件中的命令来恢复数据。AOF持久化的优点是与RDB持久化可能丢失大量的数据相比,AOF持久化的安全性要高很多。通过使用everysec选项,用户可以将数据丢失的时间窗口限制在1秒之内。其缺点则是,AOF文件存储的是协议文本,它的体积要比二进制格式的”.rdb”文件大很多。AOF需要通过执行AOF文件中的命令来恢复数据库,其恢复速度比RDB慢很多。AOF在进行重写时也需要创建子进程,在数据库体积较大时将占用大量资源,会导致服务器的短暂阻塞。AOF解决了数据持久化的实时性,是目前Redis主流的持久化方式。

RDB-AOF混合持久化模式是Redis4.0开始引入的,这种模式是基于AOF持久化构建而来的。用户可以通过配置文件中的“aof-use-rdb-preamble yes”配置项开启AOF混合持久化。Redis服务器在执行AOF重写操作时,会像执行BGSAVE命令一样,根据数据库当前的状态生成相应的RDB数据,并将其写入AOF文件中;对于重写之后执行的Redis命令,则以协议文本的方式追加到AOF文件的末尾,即RDB数据之后。

通过使用RDB-AOF混合持久化,用户可以同时获得RDB持久化和AOF持久化的优点,服务器既可以通过AOF文件包含的RDB数据来实现快速的数据恢复操作,又可以通过AOF文件包含的AOF数据来将丢失数据的时间窗口限制在1s之内

加分回答-RDB的save和bgsave、AOF的文本协议格式和文件同步机制:

RDB手动触发分别对应save和bgsave命令:

- save 命令会一直阻塞当前Redis服务器到RBD过程完成为止,所以这种方式在操作内存比较大的实例时会造成长时间阻塞,因此线上环境不建议使用,该命令已经被废弃。

- bgsave命令会让Redis进程执行fork创建子进程,由子进程负责RBD持久化过程,完成后自动结束,因此只在fork阶段发生阻塞,一般阻塞的时间也不会很长。因此Redis内部所涉及的几乎所有RDB操作都采用了bgsave的方式。

除了执行命令手动触发之外,Redis内部还存在自动触发RDB的持久化机制,例如以下场景:

1. 使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改 时,自动触发bgsave。

2. 如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点。

3. 执行debug reload命令重新加载Redis时,也会自动触发save操作。

4. 默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则 自动执行bgsave。

AOF默认不开启,需要修改配置项来启用它:

appendonly yes # 启用AOF 
appendfilename “appendonly.aof“ # 设置文件名

AOF以文本协议格式写入命令,如: *3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n

文本协议格式具有如下的优点:

1. 文本协议具有很好的兼容性;

2. 直接采用文本协议格式,可以避免二次处理的开销;

3. 文本协议具有可读性,方便直接修改和处理。

AOF持久化的文件同步机制:

为了提高程序的写入性能,现代操作系统会把针对硬盘的多次写操作优化为一次写操作。

1. 当程序调用write对文件写入时,系统不会直接把书记写入硬盘,而是先将数据写入内存的缓冲区中;

2. 当达到特定的时间周期或缓冲区写满时,系统才会执行flush操作,将缓冲区中的数据冲洗至硬盘中;

这种优化机制虽然提高了性能,但也给程序的写入操作带来了不确定性。

1. 对于AOF这样的持久化功能来说,冲洗机制将直接影响AOF持久化的安全性;

2. 为了消除上述机制的不确定性,Redis向用户提供了appendfsync选项,来控制系统冲洗AOF的频率;

3. Linux的glibc提供了fsync函数,可以将指定文件强制从缓冲区刷到硬盘,上述选项正是基于此函数。

说说缓存穿透、击穿、雪崩的区别

得分点

三种问题的发生原因以及解决方式

缓存穿透: 指高并 发查询一个数据库和缓存库都不存在的数据,导致缓存库和数据库崩溃。

解决:缓存空对象,或者布隆过滤器(请求先查布隆过滤器、再查缓存库、数据库)。 

缓存击穿:一个热点数据缓存失效的瞬间,高并发查缓存库和数据库导致崩溃。

解决:热点数据不设置过期时间,或者互斥锁(当一个线程访问该数据时,其他线程只能等待)。

缓存雪崩:大量数据同时过期,高并发查缓存库和数据库导致崩溃。解决办法:

  1. 过期时间附加随机数
  2. 熔断(服务异常后,阻止上游服务调用该服务)和降级(上游服务远程调用发现下游熔断后,执行后备方法)
  3. 采用哨兵或集群模式,从而构建高可用的Redis服务

标准回答

缓存穿透:是指客户端查询了根本不存在的数据,使得这个请求直达存储层,导致其负载过大甚至造成宕机。这种情况可能是由于业务层误将缓存和库中的数据删除造成的,当然也不排除有人恶意攻击,专门访问库中不存在的数据导致缓存穿透。

我们可以通过缓存空对象的方式和布隆过滤器两种方式来解决这一问题。缓存空对象是指当存储层未命中后,仍然将空值存入缓存层 ,当客户端再次访问数据时,缓存层直接返回空值。

还可以将数据存入布隆过滤器。当一个请求到来时,先对其进行布隆过滤器的查询,如果查询结果为不存在,则该请求直接返回空结果。 当布隆过滤器判断元素存在时,此时需要进行进一步的验证,即访问数据库或缓存,获取具体值并返回,同时将该值加入缓存中。

缓存击穿:一份访问量非常大的热点数据缓存失效的瞬间,大量的请求直达存储层,导致服务崩溃。

缓存击穿可以通过热点数据不设置过期时间来解决,这样就不会出现上述的问题,这是“物理”上的永不过期。或者为每个数据设置逻辑过期时间,当发现该数据逻辑过期时,使用单独的线程重建缓存。除了永不过期的方式,我们也可以通过加互斥锁的方式来解决缓存击穿,即对数据的访问加互斥锁,当一个线程访问该数据时,其他线程只能等待。这个线程访问过后,缓存中的数据将被重建,届时其他线程就可以直接从缓存中取值。

缓存雪崩:是指当某一时刻缓存层无法继续提供服务,导致所有的请求直达存储层,造成数据库宕机。可能是缓存中有大量数据同时过期,也可能是Redis节点发生故障,导致大量请求无法得到处理。

缓存雪崩的解决方式有三种;第一种是在设置过期时间时,附加一个随机数,避免大量的key同时过期。第二种是启用降级和熔断措施,即发生雪崩时,若应用访问的不是核心数据,则直接返回预定义信息/空值/错误信息。或者在发生雪崩时,对于访问缓存接口的请求,客户端并不会把请求发给Redis,而是直接返回。第三种是构建高可用的Redis服务,也就是采用哨兵或集群模式,部署多个Redis实例,这样即使个别节点宕机,依然可以保持服务的整体可用。

如何利用Redis实现一个分布式锁?

得分点

为什么要实现分布式锁、实现分布式锁的方式

加锁:setnx,原子性加锁和设置过期时间,value设为随机uuid

set key value nx ex seconds

解锁:lua脚本原子性判断和解锁,根据比较value和自己加锁时设置的随机id判断是否自己的锁。

业务流程:先加锁,如果加锁成功就查数据库数据、解锁、返回数据;如果加锁失败就重新调用本方法,再次加锁解锁,直到成功拿到数据库的数据。

标准回答

在分布式的环境下,会发生多个server并发修改同一个资源的情况,这种情况下,由于多个server是多个不同的JRE环境,而Java自带的锁局限于当前JRE,所以Java自带的锁机制在这个场景下是无效的,那么就需要我们自己来实现一个分布式锁。

采用Redis实现分布式锁,我们可以在Redis中存一份代表锁的数据,数据格式通常使用字符串即可。

首先加锁的逻辑可以通过`setnx key value`来实现,但如果客户端忘记解锁,那么这种情况就很有可能造成死锁,但如果直接给锁增加过期时间即新增`expire key seconds`又会发生其他问题,即这两个命令并不是原子性的,那么如果第二步失败,依然无法避免死锁问题。考虑到如上问题,我们最终可以通过`set...nx...`命令,将加锁、过期命令编排到一起,把他们变成原子操作,这样就可以避免死锁。写法为`set key value nx ex seconds` 。

解锁就是将代表锁的那份数据删除,但不能用简单的`del key`,因为会出现一些问题。比如此时有进程A,如果进程A在任务没有执行完毕时,锁被到期释放了。这种情况下进程A在任务完成后依然会尝试释放锁,因为它的代码逻辑规定它在任务结束后释放锁,但是它的锁早已经被释放过了,那这种情况它释放的就可能是其他线程的锁。为解决这种情况,我们可以在加锁时为key赋一个随机值,来充当进程的标识,进程要记住这个标识。当进程解锁的时候进行判断,是自己持有的锁才能释放,否则不能释放。另外判断,释放这两步需要保持原子性,否则如果第二步失败,就会造成死锁。而获取和删除命令不是原子的,这就需要采用Lua脚本,通过Lua脚本将两个命令编排在一起,而整个Lua脚本的执行是原子的。综上所述,优化后的实现分布式锁命令如下:

# 加锁 
set key random-value nx ex seconds 
# 解锁 
if redis.call(“get“,KEYS【1】) == ARGV【1】 then return redis.call(“del“,KEYS【1】) else return 0 end
	//分布式锁
    public Object fun() {
        // 1、分布式锁。去redis占坑,同时设置过期时间
 
        //每个线程设置随机的UUID,也可以成为token
        String uuid = UUID.randomUUID().toString();
 
        //只有键key不存在的时候才会设置key的值。保证分布式情况下一个锁能进线程
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        if (lock) {
            // 加锁成功....执行业务【内部会判断一次redis是否有值】
            System.out.println("获取分布式锁成功....");
            Object ans= null;
            try {
                ans= getDataFromDb();
            } finally {
                // 2、查询UUID是否是自己,是自己的lock就删除
                // 查询+删除 必须是原子操作:lua脚本解锁
                String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call('del',KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                // 删除锁
                Long lock1 = redisTemplate.execute(
                        new DefaultRedisScript<Long>(luaScript, Long.class),
                        Arrays.asList("lock"), uuid);    //把key和value传给lua脚本
            }
            return ans;
        } else {
            System.out.println("获取分布式锁失败....等待重试...");
            // 加锁失败....重试
            // 休眠100ms重试
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return fun();// 自旋的方式
        }
    }

加分回答

上述的分布式锁实现方式是建立在单节点之上的,它可能存在一些问题,比如有一种情况,进程A在主节点加锁成功,但主节点宕机了,那么从节点就会晋升为主节点。那如果此时另一个进程B在新的主节点上加锁成功而原主节点重启了,成为了从节点,系统中就会出现两把锁,这违背了锁的唯一性原则。

总之,就是在单个主节点的架构上实现分布式锁,是无法保证高可用的。若要保证分布式锁的高可用,则可以采用多个节点的实现方案。这种方案有很多,而Redis的官方给出的建议是采用RedLock算法的实现方案。该算法基于多个Redis节点,它的基本逻辑如下:

- 这些节点相互独立,不存在主从复制或者集群协调机制;

- 加锁:以相同的KEY向N个实例加锁,只要超过一半节点成功,则认定加锁成功;

- 解锁:向所有的实例发送DEL命令,进行解锁;

我们可以自己实现该算法,也可以直接使用Redisson框架。

Redis如何与数据库保持双写一致性

得分点

四种同步策略及其可能出现的问题,重试机制

四种同步策略:

先更新缓存再更新数据库

先更新数据库再更新缓存

先删除缓存再更新数据库

先更新数据库、再删除缓存(推荐)

标准回答

保证缓存和数据库的双写一致性,共有四种同步策略,即先更新缓存再更新数据库、先更新数据库再更新缓存、先删除缓存再更新数据库、先更新数据库再删除缓存。

先更新缓存的优点是每次数据变化时都能及时地更新缓存,这样不容易出现查询未命中的情况,但这种操作的消耗很大,如果数据需要经过复杂的计算再写入缓存的话,频繁的更新缓存会影响到服务器的性能。如果是写入数据比较频繁的场景,可能会导致频繁的更新缓存却没有业务来读取该数据。

删除缓存的优点是操作简单,无论更新的操作复杂与否,都是直接删除缓存中的数据。这种做法的缺点则是,当删除了缓存之后,下一次容易出现未命中的情况,那么这时就需要再次读取数据库。查询

那么对比而言,删除缓存无疑是更好的选择

那么我们再来看一下先操作数据库和后操作数据库的区别:

先删除缓存再操作数据库的话,如果第二步骤失败可能导致缓存和数据库得到相同的旧数据。先操作数据库但删除缓存失败的话则会导致缓存和数据库得到的结果不一致。

出现上述问题的时候,我们一般采用重试机制解决,而为了避免重试机制影响主要业务的执行,一般建议重试机制采用异步的方式执行。当我们采用重试机制之后由于存在并发,先删除缓存依然可能存在缓存中存储了旧的数据,而数据库中存储了新的数据,二者数据不一致的情况。

所以我们得到结论:先更新数据库、再删除缓存是影响更小的方案。如果第二步出现失败的情况,则可以采用重试机制解决问题。

说说Redis的主从同步机制

得分点

psync,全量复制、部分复制

主从同步:从节点可以连接主节点同步数据。同步过程中,主从节点通过心跳、断开重连等机制确保同步的高可靠性和稳定性。

主从节点实现了数据备份和读写分离。

主从同步流程:

  1. 从节点启动,给主节点发psync命令请求部分同步;
  2. 如果是第一次连接到主节点,主节点会启动子线程生成rdb文件发给从节点,并缓存新收到的写命令;
  3. 从节点把rdb文件写入磁盘并加载到内存,实现全量复制。
  4. 主节点将缓存的新写命令发给从节点,实现部分复制(增量复制)。

标准回答

Redis主从同步是指任意数量的从节点(slave node)都可以从主节点上(master node)同步数据。而除了多个 slave 可以连接到同一个 master 之外,slave 还可以接受其他 slave 的连接,这就形成一个树形结构,使得Redis可执行单层树复制。

在同步过程中,主节点和从节点还会保持心跳、断开重连等机制,以确保同步的高可靠性和稳定性。Redis主从同步机制可以实现数据的备份和读写分离,提高Redis系统的可用性和性能。 

从2.8版本开始,当启动一个 slave node 的时候,它会发送一个 `PSYNC` 命令给 master node,PSYNC译为部分同步。

如果slave node 是第一次连接到 master node,那么会触发一次全量复制。此时 master 会启动一个后台线程,开始生成一份 `RDB` 快照文件,同时还会将从客户端 client 新收到的所有写命令缓存在内存中

`RDB` 文件生成完毕后, master 会将这个 `RDB` 发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中,接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。

slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据,即部分复制

说说Redis的单线程架构

得分点

单线程的前提,单线程的优劣,简单的io模型

Redis的网络IO和键值对读写是由单线程来完成的。持久化、异步删除、集群数据同步等操作是多线程完成的。

单线程性能高的原因:

  1. 避免了线程切换和竞争所产生的消耗;
  2. 内存读写快;
  3. IO多路复用机制(单线程能同时监听多个IO事件);

标准回答

Redis的网络IO和键值对读写是由一个线程来完成的,但Redis的其他功能,例如持久化、异步删除、集群数据同步等操作依赖于其他线程来执行。单线程可以简化数据结构和算法的实现,并且可以避免线程切换和竞争造成的消耗。但要注意如果某个命令执行时间过长,会造成其他命令的阻塞

Redis采用了io多路复用机制,这带给了Redis并发处理大量客户端请求的能力。它允许单个线程同时监听多个IO事件,并在有事件发生时及时通知程序进行相应的处理。常见的IO多路复用技术包括:select、poll和epoll等。

Redis单线程高性能的原因:

因为对服务端程序来说,线程切换和锁通常是性能杀手,而单线程避免了线程切换和竞争所产生的消耗。另外Redis的大部分操作是在内存上完成的,这是它实现高性能的一个重要原因;Redis还采用了IO多路复用机制,使其在网络IO操作中能并发处理大量的客户端请求,实现高吞吐率

加分回答

Redis的单线程主要是指Redis的网络IO和键值对读写是由一个线程来完成的。而Redis的其他功能,如持久化、异步删除、集群数据同步等,则是依赖其他线程来执行的。所以,说Redis是单线程的只是一种习惯的说法,事实上它的底层不是单线程的。

如何实现Redis高可用

得分点

哨兵模式、集群模式

实现Redis高可用的方法: 哨兵和分片集群。主从和哨兵可以解决高可用、高并发读的问题,分片集群解决海量数据存储问题和高并发写的问题。

哨兵(sentinel):哨兵节点是不存储数据的Redis节点,多个哨兵节点组成哨兵系统。哨兵的作用如下:

  • 监控:Sentinel基于心跳机制每隔1秒向集群的每个实例发送ping命令,监控实例状态;如果一个实例被单个哨兵ping不通,被认为主观下线;如果一个实例被大多数哨兵ping不通,被认为客观下线;
  • 自动故障恢复:如果master客观下线,Sentinel会根据slave-priority值、offset值、id值将一个slave提升为master,将旧master标记为slave。当故障实例恢复后也以新的master为主
  • 通知:当集群发生故障转移时,Sentinel会将最新信息推送给Redis的客户端。

分片集群:

  • 集群中有多个master,每个master通过映射的插槽保存对应的数据

  • 每个master都可以有多个slave节点

  • master之间通过ping监测彼此健康状态

  • 客户端请求可以访问集群任意节点,最终都会被转发到正确节点

散列插槽:Redis会把所有master映射到0~16383共16384个插槽(hash slot)上,每个master负责一部分插槽。

读写时,key做哈希运算并取余后确定插槽位置,该位置负责的master实现读写。

标准回答

主要有哨兵和集群两种方式可以实现Redis高可用。

哨兵: 哨兵模式是Redis的高可用的解决方案,它由一个或多个Sentinel实例组成Sentinel系统,可以监视任意多个主服务器以及这些主服务器属下的所有从服务器。当哨兵节点发现有节点不可达时,会对该节点做下线标识。如果是主节点下线,它还会和其他Sentinel节点进行“协商”,当大多数Sentinel节点都认为主节点不可达时,它们会选举出一个Sentinel节点来完成自动故障转移的工作,同时会将这个变化实时通知给Redis应用方。

哨兵节点包含如下的特征:

 1. 哨兵节点会定期监控数据节点,其他哨兵节点是否可达;

 2. 哨兵节点会将故障转移的结果通知给应用方;

 3. 哨兵节点可以将从节点晋升为主节点,并维护后续正确的主从关系;

 4. 哨兵模式下,客户端连接的是哨兵节点集合,从中获取主节点信息;

 5. 节点的故障判断是由多个哨兵节点共同完成的,可有效地防止误判;

 6. 哨兵节点集合是由多个哨兵节点组成的,即使个别哨兵节点不可用,整个集合依然是健壮的;

 7. 哨兵节点也是独立的Redis节点,是特殊的Redis节点,它们不存储数据,只支持部分命令

集群: Redis集群采用虚拟槽分区来实现数据分片,它把所有的键根据CRC16算法和哈希取余映射到`0-16383`整数槽内,计算公式为`slot=CRC16(key)%16383`,每一个master负责维护一部分槽以及槽所映射的键值数据。虚拟槽分区具有如下特点:

1. 解耦数据和节点之间的关系,简化了节点扩容和收缩的难度;

2. 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据;

3. 支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景。

说说Redis的缓存淘汰策略

得分点

惰性删除、定期删除,maxmemory-policy

惰性删除:访问时检查过期,若过期则删除;

定期删除: 将指定了过期时间的数据放在过期字典里,每10秒扫描一次,取随机20个key,删除过期key,若过期比例超过25%,则重复扫描。

超出最大内存淘汰策略:通过  maxmemory-policy 参数,配置Redis超过指定的最大内存时的淘汰策略。

noeviction(默认)不淘汰直接返回错误,其他参数前缀是volatile和allkeys,表示淘汰范围是有寿命key还是所有key;参数后缀是ttl,lru,lfu,random,表示根据存活时间、最近最少使用、随机。

标准回答

Redis有如下两种过期策略:

惰性删除:客户端访问一个key的时候,Redis会先检查它的过期时间,如果发现过期就立刻删除这个key。

定期删除:Redis会将设置了过期时间的key放到一个独立的字典中,并对该字典进行每秒10次的过期扫描, 过期扫描不会遍历字典中所有的key,而是采用了一种简单的贪心策略。该策略的删除逻辑如下:

1. 从过期字典中随机选择20个key;

2. 删除这20个key中已过期的key;

3. 如果已过期key的比例超过25%,则重复步骤1。

超出最大内存淘汰策略:

当写入数据将导致超出最大内存maxmemory限制时,Redis会采用maxmemory-policy所指定的策略进行数据淘汰,该策略一共包含8种选项:noeviction、volatile-lru、volatile-lfu、volatile-ttl、volatile-random、allkeys-lru、allkeys-lfu、allkeys-random

其中除了noeviction直接返回错误之外,筛选键的方式分为volatile和allkeys两种,volatile前缀代表从设置了过期时间的键中淘汰数据,allkeys前缀代表从所有的键中淘汰数据关于后缀ttl(time to live)代表选择存活时间最小的键,random代表随机选择键,需要我们额外关注的是lru和lfu后缀,它们分别代表采用最近最少使用lru(Least Recently Used)算法和访问次数最低lfu(Least Frequently Used)算法来淘汰数据。因为allkeys是筛选所有的键,所以不存在ttl,余下三个后缀二者都有,lfu算法是再Redis4版本才提出来的。

加分回答

LRU(Least Recently Used)是按照最近最少使用原则来筛选数据,即最不常用的数据会被筛选出来

- 标准LRU:把所有的数据组成一个链表,表头和表尾分别表示MRU和LRU端,即最常使用端和最少使用端。刚被访问的数据会被移动到MRU端,而新增的数据也是刚被访问的数据,也会被移动到MRU端。当链表的空间被占满时,它会删除LRU端的数据。

- 近似LRU:Redis会记录每个数据的最近一次访问的时间戳(LRU)。Redis执行写入操作时,若发现内存超出maxmemory,就会执行一次近似LRU淘汰算法。近似LRU会随机采样N个key,然后淘汰掉最旧的key,若淘汰后内存依然超出限制,则继续采样淘汰。可以通过maxmemory_samples配置项,设置近似LRU每次采样的数据个数,该配置项的默认值为5。

LRU算法的不足之处在于,若一个key很少被访问,只是刚刚偶尔被访问了一次,则它就被认为是热点数据,短时间内不会被淘汰。

LFU算法正式用于解决上述问题,LFU(Least Frequently Used)是Redis4新增的淘汰策略,它根据key的最近访问频率进行淘汰。LFU在LRU的基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用LFU策略淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出内存。如果两个数据的访问次数相同,LFU再比较这两个数据的访问时间,把访问时间更早的数据淘汰出内存。

猜你喜欢

转载自blog.csdn.net/qq_40991313/article/details/130279363