Redis
目录
1.讲一下Redis底层的数据结构
2.ZSet底层是怎么实现的?
3.Redis为什么使用跳表而不是用B+树?
4.Redis为什么快?
5.Redis是怎么实现的IO多路复用?
6.为什么redis设计为单线程,却要在6.0版本引入多线程?
7.redis中有没有事务?
8.Redis如何保证数据的持久化?
9.谈谈Redis的内存淘汰和过期删除?
10.Redis的缓存失效会不会立即删除?
11.Redis主从同步中的增量和完全同步怎么实现?
12.要是主从架构中的主节点宕机了怎么办?
13.Redis集群的模式了解吗?讲一下?
14.redis应用场景是什么?
15.Redis支持并发操作吗?
16.什么是big key?
17.Redis 中的缓存击穿、缓存穿透和缓存雪崩是什么?
18.Redis 中如何保证缓存与数据库的数据一致性?
19.Redis 中原生批处理命令(MSET、MGET)与 Pipeline 的区别是什么?
20.如何在 Redis 中实现队列和栈数据结构?
1.讲一下Redis底层的数据结构
Redis 提供了丰富的数据类型,常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)
结构类型 | 结构存储的值 | 结构的读写能力 |
---|---|---|
String 字符串 | 可以是字符串、整数或浮点数 | 对整个字符串或字符串的一部分进行操作;对整数或浮点数进行自增或自减操作; |
List 列表 | 一个链表,链表上的每个节点都包含一个字符串 | 对链表的两端进行push和pop操作,读取单个或多个元素;根据值查找或删除元素; |
Set 集合 | 包含字符串的无序集合 | 字符串的集合,包含基础的方法有查看是否存在添加、获取、删除;还包含计算交集、并集、差集等 |
Hash 散列 | 包含键值对的无序散列表 | 包含方法有添加、获取、删除单个元素 |
Zset 有序集合 | 和散列一样,用于存储键值对 | 字符串成员与浮点数分数之间的有序映射;元素的排列顺序由分数的大小决定;包含方法有添加、获取、删除单个元素以及根据分值范围或成员来获取元素 |
2.ZSet底层是怎么实现的?
Zset底层数据结构是有压缩列表或跳表实现的:
-
当有序集合的元素个数小于128个,并且每个元素的值小于64个字节时,Redis会使用压缩列表作为Zset类型的底层数据结构
-
如果有序集合的元素不满足以上条件,Redis会使用调表作为Zset类型的底层数据结构;
延伸→跳表是怎么实现的?
跳表主要是通过多层链表来实现,底层链表保存所有元素,而每一层链表都是下一层的子集。
在进行查找的时候,会先在有着最高层级的头结点的最高层开始查找,一旦下个节点的值为null或者大于当前节点,此时就会在当前节点降一层继续往前走,以此类推,直到找到最终节点。查找效率还是很高的,时间复杂度是O(logn)。
在插入的时候,会从最高层逐层去查找插入的位置,查找的逻辑跟前面的一样,找到插入位置之后会随机决定节点的层级,最后插入节点并更新指针。
删除的时候,也是从最高层逐层去查找插入的位置,最后删除结点并更新指针。
延伸→层级是如何决定的?
一个节点的层级是有一个概率函数来决定的,这个概率是25%,能够让跳表的层级分布在一个比较理想的金字塔形,下多上少。
延伸→为什么redis跳表实现还多了个回退指针?
回退指针主要是为了提高跳表的操作效率和灵活性。 例如在进行删除操作时,需要找到要删除节点的前驱节点以便更新指针。回退指针使得这一过程更为高效,避免了从最高层开始逐层查找。 尤其是在频繁插入和删除的场景中,回退指针减少了节点之间指针的更新复杂度,提升性能。
延伸→要是面试官想拷打你的话,这里可能会让你直接手撕跳表...
3.Redis为什么使用跳表而不是用B+树?
这个问题有很多个说法,我去搜索了以下最主流的说法就是从内存占用和范围查找两个方面,跳表会比平衡树更加灵活和简单,但是我在B站上看了一个up主对这两个数据结构的性能分析,似乎在查找和插入上,两个数据结构的性能都差不多,在删除上,跳表就相对比B+树快一点,我个人想法就是之所以不用B+树而采用跳表,可能就是Redis作者觉得用跳表实现比B+树实现更简单方便一点吧。
4.Redis为什么快?
Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了;
Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销而且也不会导致死锁问题。
Redis 采用了I/O 多路复用机制处理大量的客户端 Socket 请求,I/O 多路复用机制是指一个线程处理多个I/O 流。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 I/O流的效果。
相当于就是去餐馆点餐,不用排队,想好了直接跟服务员说,避免前面的人一直在考虑吃什么导致后面的人一直在等。
5.Redis是怎么实现的IO多路复用?
Redis 的服务端,他会通过 一个线程 来处理 多个 Socket 中的事件,包括连接建立、读、写事件等,当有 Socket 发生了事件,就会将该 Socket 加入到 任务队列 中等待事件分发器来处理即可; IO 多路复用 的核心就是通过一个线程处理多个 Socket 上的事件,常见的 IO 多路复用底层实现 有:select、poll、epoll。
select实现IO多路复用的思路如下:
简单来说就是将每个Socket都封装成一个FD(linux中的文件描述符 ),然后在用户空间创建一个大小为1024的位图来表示,1表示监听,0表示不监听,例如有三个socket,处理过后的fd=1,2,5,那么在这个位图的第一位、第二位、第五位为1。 位图准备好了就开始调用select函数将位图作为参数带过去内核空间,将位图从用户态拷贝到内核态,此时内核空间会遍历这个位图并监测,如果某个socket就绪了,就遍历整个位图找出监测的fd,然后保留就绪的fd,其他fd设置为0,然后把位图结果拷贝回去覆盖掉用户态的位图。 然后用户态再遍历新位图,找出标记为1的fd,然后去读里面的数据。
总结来说:select来实现IO多路复用,在内存上是有优势的,但是在时间上来说会相对比较慢,毕竟涉及到很多次遍历和拷贝。
poll实现IO多路复用的思路如下:(换汤不换药,位图换数组)
poll模式对select模式做了简单改进,但性能提升不明显,部分关键代码如下:
IO流程:
-
创建pollfd数组,向其中添加关注的fd信息,数组大小自定义
-
调用poll函数,将pollfd数组拷贝到内核空间,转链表存储,无上限
-
内核遍历fd,判断是否就绪
-
数据就绪或超时后,拷贝pollfd数组到用户空间,返回就绪fd数量n
-
用户进程判断n是否大于0,大于0则遍历pollfd数组,找到就绪的fd
与select对比:
-
select模式中的fd_set大小固定为1024,而pollfd在内核中采用链表,理论上无上限
-
监听FD越多,每次遍历消耗时间也越久,性能反而会下降
epoll实现IO多路复用的思路如下:
epoll模式是对select和poll的改进,它提供了三个函数:
第一个是:eventpoll的函数,他内部包含两个东西
一个是:
1、红黑树-> 记录的事要监听的FD
2、一个是链表->一个链表,记录的是就绪的FD
紧接着调用epoll_ctl操作,将要监听的数据添加到红黑树上去,并且给每个fd设置一个监听函数,这个函数会在fd数据就绪时触发,就是准备好了,现在就把fd把数据添加到list_head中去
3、调用epoll_wait函数
就去等待,在用户态创建一个空的events数组,当就绪之后,我们的回调函数会把数据添加到list_head中去,当调用这个函数的时候,会去检查list_head,当然这个过程需要参考配置的等待时间,可以等一定时间,也可以一直等, 如果在此过程中,检查到了list_head中有数据会将数据添加到链表中,此时将数据放入到events数组中,并且返回对应的操作的数量,用户态的此时收到响应后,从events中拿到对应准备好的数据的节点,再去调用方法去拿数据。
小总结:
select模式存在的三个问题:
-
能监听的FD最大不超过1024
-
每次select都需要把所有要监听的FD都拷贝到内核空间
-
每次都要遍历所有FD来判断就绪状态
poll模式的问题:
-
poll利用链表解决了select中监听FD上限的问题,但依然要遍历所有FD,如果监听较多,性能会下降
epoll模式中如何解决这些问题的?
-
基于epoll实例中的红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高
-
每个FD只需要执行一次epoll_ctl添加到红黑树,以后每次epol_wait无需传递任何参数,无需重复拷贝FD到内核空间
-
利用ep_poll_callback机制来监听FD状态,无需遍历所有FD,因此性能不会随监听的FD数量增多而下降
6.为什么redis设计为单线程,却要在6.0版本引入多线程?
redis在6.0版本之前使用单线程的是因为redis是基于内存的,性能瓶颈一般不在CPU上而是在内存和网络带宽上,因此使用单线程能在简化代码的同时减少上下文切换的开销和多线程之间因数据争夺导致的性能问题,同时redis在单线程中使用了IO多路复用在提高IO利用率。
而在6.0版本在网络IO上去引入多线程,其实为了解决高并发的场景,进一步去提供IO利用率,因为随着硬件提升,redis的性能瓶颈就卡在了网络上,使用多线程能够很好地解决这一个问题。
延伸→那引入了多线程,会不会引发一些线程安全呢?
不会的,因为这个多线程是应用在网络IO上,对于redis的命令执行,仍旧是单线程的。
7.redis中有没有事务?
有的,但是redis中的事务跟我们理解的MySQL中的事务不太一样,MySQL中的事务符合ACID,但是redis中的事务只能保证多条命令执行的原子性。
我们可以使用MULTI 和 EXEC和实现原子性,MULTI命令就是开启一个事务,之后的所有命令会排队执行。EXEC命令就是执行队列里的所有命令,确保原子性。
延伸→redis中的事务有没有回滚机制?
没有的,EXEC命令虽然能保证“要么全部执行,要么全部不执行”,但是这里的“全部不执行”并不是说执行到一半发现命令错误然后回滚,而是在执行EXEC命令之前会先检查队列中的命令,只要没有语法错误,那就会全部执行,在确定执行之后,即使队列中的命令临时出错,也会继续执行下去。所以redis的事务是没有回滚机制的。
延伸→那为什么redis不搞个回滚机制呢?
因为redis设计的初衷就是简单、快速、高效,虽然说有回滚机制可能会让整个系统更加“完美”,但是也会增加系统的复杂性,不符合设计初衷,所以索性就不搞回滚了。
延伸→除了这两个命令还能怎么样来实现原子性呢?
还可以编写Lua脚本,redis会将Lua脚本看成一个整体,因此我们可以在Lua脚本里去编写多条执行命令。
8.Redis如何保证数据的持久化?
有两种方式来保证数据持久化,分别是RDB快照和AOF日志。
-
RDB快照就是在将Redis某个时刻的数据快照存储在磁盘文件当中,当下次启动Redis之后读取RDB文件,将数据恢复。可以是用save和bgsave,两者的区别就是save会在主线程执行,而bgsave会在单独创建一个子线程执行。
-
AOF日志就是将Redis的每一次写命令追加到AOF文件当中,下次重启Redis的时候执行文件里面的写命令同样也可以恢复数据,通常可以在redis中设置AOF的写入时机,
写回策略 | 写回时机 | 优点 | 缺点 |
---|---|---|---|
Always | 同步写回 | 可靠性高,最大程度保证数据不丢失 | 每个写命令都要写回硬盘,性能开销大 |
Everysec | 每秒写回 | 性能适中 | 宕机时会丢失1秒内的数据 |
No | 由操作系统控制写回 | 性能好 | 宕机时丢失的数据可能会很多 |
RDB和AOF的优缺点分别是什么?
-
RDB的优点就是文件体积小,恢复速度快,而且是有主进程fork出来的子进程去恢复的,不会阻塞服务器执行当前命令。
-
RDB的缺点就是两次RDB之间如果出现Redis宕机,那之间的数据就会丢失。以及在RDB快照恢复期间有其他写操作执行,也会导致数据的不一致。
-
AOF的优点就是提供了更好地数据安全性,因为它默认是每次写操作都会追加到AOF文件末尾,即使Redis宕机,也只会丢失宕机前的一条写操作数据。
-
AOF的缺点就是如果写操作过多,可能会导致AOF文件太大,占用过多的磁盘空间。而且如果给写如AOF策略设置为always的话,可能会影响redis的性能
9.谈谈Redis的内存淘汰和过期删除?
-
内存淘汰就是当redis的内存满了的时候,就会触发内存淘汰机制,去淘汰掉一些不必要的内存资源,腾出空间去存新的数据。
-
过期删除就是对过期的键值对进行删除,删除策略是定期删除+惰性删除。
延伸→讲一下内存淘汰机制有哪些策略?
内存淘汰策略有八种,分为两类:不进行数据淘汰的策略和进行数据淘汰的策略
不进行数据淘汰的策略:
noeviction:当redis内存达到设置的最大内存时,不会淘汰数据,会对之后的写操作报错通知禁止写入,此时是可以查询删除修改的,唯独写不了。
进行数据淘汰的策略(分为有过期值和所有数据范围)
有过期值:
volatile-random:在有过期值的数据中随机删除
volatile-ttl:删除离过期时间最短的数据
volatile-lru:删除有过期值且最长时间未使用的数据
volatile-lfu:删除有过期值且使用次数最少的数据
所有数据范围:
allkeys-random:在所有数据范围内随机删除
allkeys-lru:删除在所有数据范围内最长时间未使用的数据
allkeys-lfu:删除在所有数据范围内使用次数最少得数据
延伸→讲一讲过期删除策略?
删除策略为定期删除+惰性删除
-
所谓的惰性删除就是在每次访问和修改键值对前,reids会调用expireIfNeeded函数去判断键值是否过期,如果发现过期就直接删除键值对并返回过期信息,没过期就正常操作返回数据。
-
而定期删除就是redis会隔一段时间(默认一百毫秒)去抽取二十个键值对来检查是否过期,过期删除反之不删除,如果发现抽到的20个键中超过25%的键过期了,那会再抽取20个,直到过期比率小于25%。
两者的优缺点如下:
惰性删除就是及时但是占用CPU,定期删除就是CPU花销小但占用内存空间多。
10.Redis的缓存失效会不会立即删除?
不会,Redis 的过期删除策略是选择「惰性删除+定期删除」这两种策略配和使用。
-
惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
-
定期删除策略的做法是,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
延伸→为什么不选择过期直接删除呢?非要搞这些策略干嘛?
在过期 key 比较多的情况下,删除过期 key 可能会占用相当一部分 CPU 时间,在内存不紧张但 CPU 时间紧张的情况下,将 CPU 时间用于删除和当前任务无关的过期键上,无疑会对服务器的响应时间和吞吐量造成影响。所以,定时删除策略对 CPU 不友好。
11.Redis主从同步中的增量和完全同步怎么实现?
完全同步:
-
slave节点请求增量同步
-
master节点判断runid,发现不一致,判断为第一次连接,拒绝增量同步,进行全量同步
-
master调用bgsave命令将完整内存数据生成RDB,发送RDB到slave
-
slave清空本地数据,加载master的RDB
-
master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave
-
slave执行接收到的命令,保持与master之间的同步
增量同步:
从节点也是发送psync命令给主节点,同时带上主节点的runid以及offset,主节点判断从节点传递过来的runid是否和主节点一致;如果一致,则根据ofset判断数据是否还在repl backlog buffer中,如果在,则通过offset进行定位,将offset之后的命令数据写入到复制缓冲区中;然后通过复制缓冲中区发给从节点,进行同步。如果offset判断对应的数据在repl backlog buffer不存在了,则进行完全同步。
12.要是主从架构中的主节点宕机了怎么办?
Redis的主从架构中实现了读写分离,在从节点读,在主节点写,然后数据同步到从节点,当主节点宕机了之后,就需要在从节点中选出一个节点作为主节点,这个过程可以由Redis的哨兵机制实现。
哨兵(Sentinel)其实是一个运行在特殊模式下的Redis进程,是属于一个观察者节点,观察的是主从节点,哨兵节点主要负责三件事:监控、选举、通知。
监控:
这里的监控就是哨兵会每隔一秒发送一个ping指令给所有节点,如果没收到对应节点的pong回应,那么这个节点就会被哨兵认为是主观下线。这时候这个哨兵节点会询问其他哨兵节点,如果哨兵集群中超过一定数量的哨兵都判断该节点是主观下线,那么此时这个节点就是客观下线。
如果客观下线的redis节点是从节点或者是Sentinel节点,则操作到此为止,没有后续的操作了;如果客观下线的redis节点为主节点,则开始故障转移,从从节点中选举一个节点升级为主节点。
选举:
Sentinel Leader选举:
判断主节点主观下线的Sentinel节点为候选人,此时他想成为Leader。如果有多个Sentinel 节点判断主节点主观下线,那么就有多个候选人。候选人会给自己先投一票,其他Sentinel节点会给候选人投一票,投票涉及到raft算法。为了避免平票,Sentinel 节点的数量一般会设置为奇数。
Redis主节点选举:
选出Sentinel Leader之后,由Sentinel Leader去从节点中选出主节点:
1.过滤故障的节点 2.选择优先级高(slave-priority最小)的从节点作为主节点,如不存在则继续 3.选择复制偏移量(数据写入量的字节,记录写了多少数据。主服务器会把偏移量同步给从服务器,当主从的偏移量一致,则数据是完全同步)最大的从节点作为主节点,如不存在则继续 4.选择runid(redis每次启动的时候生成随机的runid作为redis的标识)最小的从节点作为主节点
总结来说就是:先剔除下线的从节点,然后从从节点中选出优先级高、复制偏移量大、runid小的从节点作为主节点。
通知:
选好主节点之后,哨兵 leader 会让其他从节点全部成为新 master 节点的 slave 节点。最后利用 redis 的发布/订阅机制,把新主节点的 IP 和端口信息推送给客户端,此时主从切换就结束了
延伸→那“苏醒”过来的旧主节点怎么办?
会成为新主节点的从节点,并开始进行全量同步。
13.Redis集群的模式了解吗?讲一下?
主从和哨兵已经解决高可用、高并发读的问题,但仍然有海量数据存储和高并发写的问题没有解决,因此采用Redis集群作为这写问题的解决方案。
Redis集群中有多个master,而每个master又连接多个slave节点,master之间通过ping来检测彼此健康状态,客户端请求可以访问集群任意节点,最终都会被转发到正确节点去。
分片集群引入了哈希槽的概念,一共有16384个哈希槽,平均分配给不同的redis实例,读写的时候,将key值根据CRC 16算法计算得到一个16bit的值,将值向16384取余,得出来的结果就是该key在槽位中的位置。
优缺点如下:
优点:
-
高可用性:节点之间采用主从复制,即使集群中一个节点挂掉了,仍然不影响集群的工作。
-
高性能:redis集群读写效率高,当数据比较大的时候就可以考虑搭建集群。
缺点:
-
部署和维护起来比较复杂,要考虑很多配置。
-
集群同步问题:通过某个节点出现故障或者网络故障,集群中数据同步的问题也会出现,随着节点数量越来越多,会带来一定的读写延迟。
14.redis应用场景是什么?
数据库、缓存、排行版、分布式锁、计数器、轻量级消息队列。
具体内容请看我的另一篇文章:最适合使用Redis作为解决方案的几个应用场景-CSDN博客
15.Redis支持并发操作吗?
单个 Redis 命令的原子性:Redis 的单个命令是原子性的,这意味着一个命令要么完全执行成功,要么完全不执行,确保操作的一致性。这对于并发操作非常重要。 多个操作的事务:Redis 支持事务,可以将一系列的操作放在一个事务中执行,使用 MULTI、EXEC、DISCARD 和 WATCH 等命令来管理事务。这样可以确保一系列操作的原子性。
16.什么是big key?
Redis大key问题指的是某个key对应的value值所占的内存空间比较大,导致Redis的性能下降、内存不足、数据不均衡以及主从同步延迟等问题。
到底多大的数据量才算是大key?
没有固定的判别标准,通常认为字符串类型的key对应的value值占用空间大于1M,,或者集合类型的元素数量超过1万个,就算是大key。
延伸→big key带来的缺点是什么?
-
内存占用过高。大Key占用过多的内存空间,可能导致可用内存不足,从而触发内存淘汰策略。在极端情况下,可能导致内存耗尽,Redis实例崩溃,影响系统的稳定性。
-
性能下降。大Key会占用大量内存空间,对于大Key的操作,如读取、写入、删除等,都会消耗更多的CPU时间和内存资源,进一步降低系统性能。
-
阻塞其他操作。某些对大Key的操作可能会导致Redis实例阻塞。例如,使用DEL命令删除一个大Key时,可能会导致Redis实例在一段时间内无法响应其他客户端请求,从而影响系统的响应时间和吞吐量。
-
网络拥塞。每次获取大key产生的网络流量较大,可能造成机器或局域网的带宽被打满,同时波及其他服务。例如:一个大key占用空间是1MB,每秒访问1000次,就有1000MB的流量。
延伸→怎么解决big key?
-
对大Key进行拆分。例如将含有数万成员的一个HASH Key拆分为多个HASH Key,并确保每个Key的成员数量在合理范围。在Redis集群架构中,拆分大Key能对数据分片间的内存平衡起到显著作用。
-
对大Key进行清理。将不适用Redis能力的数据存至其它存储,并在Redis中删除此类数据。注意,要使用异步删除。
17.Redis 中的缓存击穿、缓存穿透和缓存雪崩是什么?
-
缓存击穿:指某个热点数据在缓存中失效,导致大量请求直接访问数据库。此时,由于瞬间的高并发,可能导致数据库崩溃。
-
缓存穿透:指查询一个不存在的数据,缓存中没有相应的记录,每次请求都会去数据库查询,造成数据库负担加重。
-
缓存雪崩:指多个缓存数据在同一时间过期,导致大量请求同时访问数据库,从而造成数据库瞬间负载激增。
缓存击穿解决方案:
-
互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
-
不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
缓存穿透解决方案:
-
非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
-
缓存空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
-
布隆过滤器:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。
缓存雪崩解决方案:
-
均匀设置过期时间:如果要给缓存数据设置过期时间,应该避免将大量的数据设置成同一个过期时间。给这些数据的过期时间加上一个随机数,这样就保证数据不会我们可以在对缓存数据设置过期时间时,在同一时间过期。
-
互斥锁:当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。
-
后台更新缓存:业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。
18.Redis 中如何保证缓存与数据库的数据一致性?
-
先更新数据库,再删除缓存,后续等查询把数据库的数据回种到缓存中
-
缓存双删策略:更新数据库之前,删除一次缓存,更新完数据库后,再进行一次延迟删除
-
使用 Binlog 异步更新缓存,监听数据库的 Binlog 变化,通过异步方式更新 Redis 缓存
-
如果是要考虑实时一致性的话,先写 MySQL,再删除 Redis 应该是较为优的方案,虽然短期内数据可能不一致,不过其能尽量保证数据的一致性。
-
如果考虑最终一致性的话,推荐的是使用 binlog +消息队列的方式,这个方案其有重试和顺序消费,能够最大限度地保证缓存与数据库的最终一致性。
19.Redis 中原生批处理命令(MSET、MGET)与 Pipeline 的区别是什么?
mset和mget是原生命令,用于一次set和get多个键值对。是单个命令。原子性。 pipeline是一种机制,用于在一次网络连接中,发送多个命令到服务端执行。是机制。能减少网络往返时间。非原子性。
20.如何在 Redis 中实现队列和栈数据结构?
可以通过 List 类型 来实现 队列 和 栈
实现队列(FIFO)
-
队列是一种 先进先出(FIFO)的数据结构。在 Redis中,可以使用 LPUSH和 RPOP命令组合来实现队列。
-
LPUSH 向列表的左侧推入元素,而 RPOP从列表的右侧弹出元素,这样可以保证最先进入的元素最先被弹出。
实现栈(LIFO):
-
栈是一种 后进先出(LIFO)的数据结构。在Redis中,可以使用LPUSH和 RPOP命令组合来实现栈.
-
LPUSH 向列表的左侧推入元素,而 LPOP从列表的左侧弹出元素,这样可以保证最后进入的元素最先被弹出。