readis部分面试问题(面试篇)

主从同步原理

作为判断依据:

  • Replication Id:简称replid,是数据集的标记,replid一致则是同一数据集。每个master都有唯一的replidslave则会继承master节点的replid

  • offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slaveoffset小于masteroffset,说明slave数据落后于master,需要更新。

全量同步

主从第一次建立连接时,会执行全量同步,将master节点的所有数据都拷贝给slave节点;

如何判断是第一次建立连接?

当搭建redis主从关系时,会在master节点创建一个新的replid,其他slave节点的replid与其保持一致,当slave节点请求增量同步,master节点会判断该slave节点的replid与自己是否一致,不一致就可以认为是第一次连接,master会执行bgsave命令把所有数据保存到一个RDB文件当中,slave节点接收到该文件,slave就清空自己本地原有的数据,然会执行加载RDB文件实现全量同步。

增量同步

全量同步需要先做RDB,然后将RDB文件通过网络传输个slave,成本太高了。因此除了第一次做全量同步,其它大多数时候slave与master都是做增量同步

什么是增量同步?就是只更新slave与master存在差异的部分数据,根据偏移量差值来同步数据

repl_baklog原理

这个文件是一个固定大小的数组,只不过数组是环形,也就是说角标到达数组末尾后,会再次从0开始读写,这样数组头部的数据就会被覆盖。

repl_baklog中会记录Redis处理过的命令及offset,包括master当前的offset,和slave已经拷贝到的offset

slave与master的offset之间的差异,就是salve需要增量拷贝的数据了。

如果master继续写入新数据,master的offset就会覆盖repl_baklog中旧的数据,直到将slave现在的offset也覆盖:

棕色框中的红色部分,就是尚未同步,但是却已经被覆盖的数据。此时如果slave恢复,需要同步,却发现自己的offset都没有了,无法完成增量同步了。只能做全量同步

repl_baklog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于repl_baklog做增量同步,只能再次全量同步。

主从同步优化

可以从以下几个方面来优化Redis主从就集群:

  • 在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO。

  • Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO

  • 适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步

  • 限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力

总结(常见问题)

简述全量同步和增量同步区别?

  • 全量同步:master将完整内存数据生成RDB,发送RDB到slave。后续命令则记录在repl_baklog,逐个发送给slave。

  • 增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave

什么时候执行全量同步?

  • slave节点第一次连接master节点时

  • slave节点断开时间太久,repl_baklog中的offset已经被覆盖时

什么时候执行增量同步?

  • slave节点断开又恢复,并且在repl_baklog中能找到offset时

Redis哨兵

保证redis集群的高可用性

哨兵工作原理

Redis提供了哨兵Sentinel)机制来监控主从集群监控状态,确保集群的高可用性。

哨兵作用

哨兵的作用如下:

  • 状态监控Sentinel 会不断检查您的masterslave是否按预期工作

  • 故障恢复(failover):如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后会成为slave

  • 状态通知Sentinel充当Redis客户端的服务发现来源,当集群发生failover时,会将最新集群信息推送给Redis的客户端

状态监控(如何知道某个节点宕机)

Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个节点发送ping命令,并通过实例的响应结果来做出判断:

  • 主观下线(sdown):如果某sentinel节点发现某Redis节点未在规定时间响应,则认为该节点主观下线。

  • 客观下线(odown):若超过指定数量(通过quorum设置)的sentinel都认为该节点主观下线,则该节点客观下线。quorum值最好超过Sentinel节点数量的一半,Sentinel节点数量至少3台。

一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:

  • 首先会判断slave节点与master节点断开时间长短,如果超过down-after-milliseconds * 10则会排除该slave节点

  • 然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举(默认都是1)。

  • 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高

  • 最后是判断slave节点的run_id大小,越小优先级越高(通过info server可以查看run_id)。

选举leader

首先,Sentinel集群要选出一个执行failover的Sentinel节点,可以成为leader。要成为leader要满足两个条件:

  • 最先获得超过半数的投票

  • 获得的投票数不小于quorum

而sentinel投票的原则有两条:

  • 优先投票给目前得票最多的

  • 如果目前没有任何节点的票,就投给自己

比如有3个sentinel节点,s1s2s3,假如s2先投票:

  • 此时发现没有任何人在投票,那就投给自己。s2得1票

  • 接着s1s3开始投票,发现目前s2票最多,于是也投给s2s2得3票

  • s2称为leader,开始故障转移

第一个确认master客观下线的人会立刻发起投票,一定会成为leader

failover(如何让一个从节点代替宕机的master节点)

假如master发生故障,slave1当选。则故障转移的流程如下:

1)sentinel给备选的slave1节点发送slaveof no one命令,让该节点成为master

2)sentinel给所有其它slave发送slaveof 192.168.150.101 7002 命令,让这些节点成为新master,也就是7002slave节点,开始从新的master上同步数据。

3)最后,当故障节点恢复后会接收到哨兵信号,执行slaveof 192.168.150.101 7002命令

总结(常见问题)

Sentinel的三个作用是什么?

  • 集群监控

  • 故障恢复

  • 状态通知

Sentinel如何判断一个redis实例是否健康?

  • 每隔1秒发送一次ping命令,如果超过一定时间没有相向则认为是主观下线(sdown

  • 如果大多数sentinel都认为实例主观下线,则判定服务客观下线(odown

故障转移步骤有哪些?

  • 首先要在sentinel中选出一个leader,由leader执行failover

  • 选定一个slave作为新的master,执行slaveof noone,切换到master模式

  • 然后让所有节点都执行slaveof 新master

  • 修改故障节点配置,添加slaveof 新master

sentinel选举leader的依据是什么?

  • 票数超过sentinel节点数量1半

  • 票数超过quorum数量

  • 一般情况下最先发起failover的节点会当选

sentinel从slave中选取master的依据是什么?

  • 首先会判断slave节点与master节点断开时间长短,如果超过down-after-milliseconds * 10则会排除该slave节点

  • 然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举(默认都是1)。

  • 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高

  • 最后是判断slave节点的run_id大小,越小优先级越高(通过info server可以查看run_id)。

散列插槽

数据要分片存储到不同的Redis节点,肯定需要有分片的依据,这样下次查询的时候才能知道去哪个节点查询。很多数据分片都会采用一致性hash算法。而Redis则是利用散列插槽(hash slot)的方式实现数据分片。

在Redis集群中,共有16384个hash slots,集群中的每一个master节点都会分配一定数量的hash slots

当我们读写数据时,Redis基于CRC16 算法对keyhash运算,得到的结果与16384取余,就计算出了这个keyslot值。然后到slot所在的Redis节点执行读写操作。

不过hash slot的计算也分两种情况:

  • key中包含{}时,根据{}之间的字符串计算hash slot

  • key中不包含{}时,则根据整个key字符串计算hash slot

总结(常见问题)

Redis分片集群如何判断某个key应该在哪个实例?

  • 将16384个插槽分配到不同的实例

  • 根据key计算哈希值,对16384取余

  • 余数作为插槽,寻找插槽所在实例即可

如何将同一类数据固定的保存在同一个Redis实例?

  • Redis计算key的插槽值时会判断key中是否包含{},如果有则基于{}内的字符计算插槽

  • 数据的key中可以加入{类型},例如key都以{typeId}为前缀,这样同类型数据计算的插槽一定相同

Redis数据结构

我们常用的Redis数据类型有5种,分别是:

  • String

  • List

  • Set

  • SortedSet

  • Hash

还有一些高级数据类型,比如Bitmap、HyperLogLog、GEO等,其底层都是基于上述5种基本数据类型。因此在Redis的源码中,其实只有5种数据类型。

RedisObject

不管是任何一种数据类型,最终都会封装为RedisObject格式,它是一种结构体,C语言中的一种结构,可以理解为Java中的类。

结构大概是这样的:

属性中的encoding就是当前对象底层采用的数据结构编码方式,可选的有11种之多:

编号

编码方式

说明

0

OBJ_ENCODING_RAW

raw编码动态字符串

1

OBJ_ENCODING_INT

long类型的整数的字符串

2

OBJ_ENCODING_HT

hash表(也叫dict)

3

OBJ_ENCODING_ZIPMAP

已废弃

4

OBJ_ENCODING_LINKEDLIST

双端链表

5

OBJ_ENCODING_ZIPLIST

压缩列表

6

OBJ_ENCODING_INTSET

整数集合

7

OBJ_ENCODING_SKIPLIST

跳表

8

OBJ_ENCODING_EMBSTR

embstr编码的动态字符串

9

OBJ_ENCODING_QUICKLIST

快速列表

10

OBJ_ENCODING_STREAM

Stream流

11

OBJ_ENCODING_LISTPACK

紧凑列表

Redis中的5种不同的数据类型采用的底层数据结构和编码方式如下:

数据类型

编码方式

STRING

intembstrraw

LIST

LinkedList和ZipList(3.2以前)、QuickList(3.2以后)

SET

intsetHT

ZSET

ZipList(7.0以前)、Listpack(7.0以后)、HTSkipList

HASH

ZipList(7.0以前)、Listpack(7.0以后)、HT

 SkipList(跳表)

SkipList(跳表)首先是链表,但与传统链表相比有几点差异:

  • 元素按照升序排列存储

  • 节点可能包含多个指针,指针跨度不同。

传统链表只有指向前后元素的指针,因此只能顺序依次访问。如果查找的元素在链表中间,查询的效率会比较低。而SkipList则不同,它内部包含跨度不同的多级指针,可以让我们跳跃查找链表中间的元素,效率非常高。

跳表的特点:

多层链表:跳表由多层链表构成,每一层都是前一层的索引,可以快速地跳过一些元素。

随机化:在创建新节点时,会随机决定该节点在跳表中的层数,这增加了跳表的灵活性和性能。

有序:跳表中的元素是有序的,可以进行快速的顺序访问。

动态:跳表可以动态地添加和删除元素,而不需要重新构建整个结构。

跳表的操作:

查找:从顶层开始,逐层向下查找,直到找到目标元素或到达底层。查找的时间复杂度是O(log n),其中n是跳表中元素的数量。

插入:首先在底层进行插入操作,然后根据随机化过程决定是否在更高层创建索引节点。插入操作的时间复杂度也是O(log n)。

删除:首先查找要删除的元素,然后从所有层中删除该元素的节点。删除操作的时间复杂度同样是O(log n)。

SortedSet

面试题:Redis的SortedSet底层的数据结构是怎样的?

:SortedSet是有序集合,底层的存储的每个数据都包含element和score两个值。score是得分,element则是字符串值。SortedSet会根据每个element的score值排序,形成有序集合。

它支持的操作很多,比如:

  • 根据element查询score值

  • 按照score值升序或降序查询element

要实现根据element查询对应的score值,就必须实现element与score之间的键值映射。SortedSet底层是基于HashTable来实现的。

要实现对score值排序,并且查询效率还高,就需要有一种高效的有序数据结构,SortedSet是基于跳表实现的。

加分项:因为SortedSet底层需要用到两种数据结构,对内存占用比较高。因此Redis底层会对SortedSet中的元素大小做判断。如果元素大小小于128每个元素都小于64字节,SortedSet底层会采用ZipList,也就是压缩列表来代替HashTableSkipList

不过,ZipList存在连锁更新问题,因此而在Redis7.0版本以后,ZipList又被替换为Listpack(紧凑列表)。

Redis内存回收

当内存达到上限,就无法存储更多数据了。因此,Redis内部会有两套内存回收的策略:

  • 内存过期策略

  • 内存淘汰策略

内存过期处理

存入Redis中的数据可以配置过期时间,到期后再次访问会发现这些数据都不存在了,也就是被过期清理了。

Redis中通过expire命令可以给KEY设置TTL(过期时间)

过期策略

Redis不管有多少种数据类型,本质是一个KEY-VALUE的键值型数据库,而这种键值映射底层正式基于HashTable来实现的,在Redis中叫做Dict.

面试题:Redis如何判断KEY是否过期呢?

:在Redis中会有两个Dict,也就是HashTable,其中一个记录KEY-VALUE键值对,另一个记录KEY和过期时间。要判断一个KEY是否过期,只需要到记录过期时间的Dict中根据KEY查询即可。

Redis是何时删除过期KEY的呢?

Redis并不会在KEY过期时立刻删除KEY,因为要实现这样的效果就必须给每一个过期的KEY设置时钟,并监控这些KEY的过期状态。无论对CPU还是内存都会带来极大的负担。

Redis的过期KEY删除策略有两种:

  • 惰性删除

  • 周期删除

惰性删除,顾明思议就是过期后不会立刻删除。那在什么时候删除呢?

Redis会在每次访问KEY的时候判断当前KEY有没有设置过期时间,如果有,过期时间是否已经到期。

 周期删除:顾明思议是通过一个定时任务,周期性的抽样部分过期的key,然后执行删除。

执行周期有两种:

  • SLOW模式:Redis会设置一个定时任务serverCron(),按照server.hz的频率来执行过期key清理

  • FAST模式:Redis的每个事件循环前执行过期key清理(事件循环就是NIO事件处理的循环)。

SLOW模式规则:

  • ① 执行频率受server.hz影响,默认为10,即每秒执行10次,每个执行周期100ms。

  • ② 执行清理耗时不超过一次执行周期的25%,即25ms.

  • ③ 逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期

  • ④ 如果没达到时间上限(25ms)并且过期key比例大于10%,再进行一次抽样,否则结束

FAST模式规则(过期key比例小于10%不执行):

  • ① 执行频率受beforeSleep()调用频率影响,但两次FAST模式间隔不低于2ms

  • ② 执行清理耗时不超过1ms

  • ③ 逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期

  • ④ 如果没达到时间上限(1ms)并且过期key比例大于10%,再进行一次抽样,否则结束

内存淘汰策略

对于某些特别依赖于Redis的项目而言,仅仅依靠过期KEY清理是不够的,内存可能很快就达到上限。因此Redis允许设置内存告警阈值,当内存使用达到阈值时就会主动挑选部分KEY删除以释放更多内存。这叫做内存淘汰机制。

淘汰策略

Redis支持8种不同的内存淘汰策略:

  • noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。

  • volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰

  • allkeys-random:对全体key ,随机进行淘汰。也就是直接从db->dict中随机挑选

  • volatile-random:对设置了TTL的key ,随机进行淘汰。也就是从db->expires中随机挑选。

  • allkeys-lru: 对全体key,基于LRU算法进行淘汰

  • volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰

  • allkeys-lfu: 对全体key,基于LFU算法进行淘汰

  • volatile-lfu: 对设置了TTL的key,基于LFI算法进行淘汰

比较容易混淆的有两个算法:

  • LRULeast Recently Used),最近最久未使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。

  • LFULeast Frequently Used),最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。

逻辑访问次数

Redis的逻辑访问次数算法了,LFU的访问次数之所以叫做逻辑访问次数,是因为并不是每次key被访问都计数,而是通过运算:

  • ① 生成[0,1)之间的随机数R

  • ② 计算 1/(旧次数 * lfu_log_factor + 1),记录为Plfu_log_factor默认为10

  • ③ 如果 R < P ,则计数器 +1,且最大不超过255

  • ④ 访问次数会随时间衰减,距离上一次访问时间每隔 lfu_decay_time 分钟(默认1) ,计数器-1

Redis采取的是抽样法,即每次抽样一定数量(maxmemory_smples)的key,然后基于内存策略做排序,找出淘汰优先级最高的,删除这个key。这就导致Redis的算法并不是真正的LRU,而是一种基于抽样的近似LRU算法

总结(常见问题)

面试题Redis如何判断KEY是否过期呢?

:在Redis中会有两个Dict,也就是HashTable,其中一个记录KEY-VALUE键值对,另一个记录KEY和过期时间。要判断一个KEY是否过期,只需要到记录过期时间的Dict中根据KEY查询即可。

面试题Redis何时删除过期KEY?如何删除?

:Redis的过期KEY处理有两种策略,分别是惰性删除和周期删除。

惰性删除是指在每次用户访问某个KEY时,判断KEY的过期时间:如果过期则删除;如果未过期则忽略。

周期删除有两种模式:

  • SLOW模式:通过一个定时任务,定期的抽样部分带有TTL的KEY,判断其是否过期。默认情况下定时任务的执行频率是每秒10次,但每次执行不能超过25毫秒。如果执行抽样后发现时间还有剩余,并且过期KEY的比例较高,则会多次抽样。

  • FAST模式:在Redis每次处理NIO事件之前,都会抽样部分带有TTL的KEY,判断是否过期,因此执行频率较高。但是每次执行时长不能超过1ms,如果时间充足并且过期KEY比例过高,也会多次抽样

面试题当Redis内存不足时会怎么做

:这取决于配置的内存淘汰策略,Redis支持很多种内存淘汰策略,例如LRU、LFU、Random. 但默认的策略是直接拒绝新的写入请求。而如果设置了其它策略,则会在每次执行命令后判断占用内存是否达到阈值。如果达到阈值则会基于配置的淘汰策略尝试进行内存淘汰,直到占用内存小于阈值为止。

面试题那你能聊聊LRULFU

LRU是最近最久未使用。Redis的Key都是RedisObject,当启用LRU算法后,Redis会在Key的头信息中使用24个bit记录每个key的最近一次使用的时间lru。每次需要内存淘汰时,就会抽样一部分KEY,找出其中空闲时间最长的,也就是now - lru结果最大的,然后将其删除。如果内存依然不足,就重复这个过程。

由于采用了抽样来计算,这种算法只能说是一种近似LRU算法。因此在Redis4.0以后又引入了LFU算法,这种算法是统计最近最少使用,也就是按key的访问频率来统计。当启用LFU算法后,Redis会在key的头信息中使用24bit记录最近一次使用时间和逻辑访问频率。其中高16位是以分钟为单位的最近访问时间,后8位是逻辑访问次数。与LFU类似,每次需要内存淘汰时,就会抽样一部分KEY,找出其中逻辑访问次数最小的,将其淘汰。

面试题逻辑访问次数是如何计算的

:由于记录访问次数的只有8bit,即便是无符号数,最大值只有255,不可能记录真实的访问次数。因此Redis统计的其实是逻辑访问次数。这其中有一个计算公式,会根据当前的访问次数做计算,结果要么是次数+1,要么是次数不变。但随着当前访问次数越大,+1的概率也会越低,并且最大值不超过255.

除此以外,逻辑访问次数还有一个衰减周期,默认为1分钟,即每隔1分钟逻辑访问次数会-1。这样逻辑访问次数就能基本反映出一个key的访问热度了。

缓存问题

Redis经常被用作缓存,而缓存在使用的过程中存在很多问题需要解决。例如:

  • 缓存的数据一致性问题

  • 缓存击穿

  • 缓存穿透

  • 缓存雪崩

缓存一致性

缓存的通用模型有三种:

  • Cache Aside:有缓存调用者自己维护数据库与缓存的一致性。即:

    • 查询时:命中则直接返回,未命中则查询数据库并写入缓存

    • 更新时:更新数据库并删除缓存,查询时自然会更新缓存

  • Read/Write Through:数据库自己维护一份缓存,底层实现对调用者透明。底层实现:

    • 查询时:命中则直接返回,未命中则查询数据库并写入缓存

    • 更新时:判断缓存是否存在,不存在直接更新数据库。存在则更新缓存,同步更新数据库

  • Write Behind Cahing:读写操作都直接操作缓存,由线程异步的将缓存数据同步到数据库

目前企业中使用最多的就是Cache Aside模式

综上,添加缓存的目的是为了提高系统性能,而你要付出的代价就是缓存与数据库的强一致性。如果你要求数据库与缓存的强一致,那就需要加锁避免并行读写。但这就降低了性能,与缓存的目标背道而驰。

因此不管任何缓存同步方案最终的目的都是尽可能保证最终一致性,降低发生不一致的概率。我们采用先更新数据库再删除缓存的方案,已经将这种概率降到足够低,目的已经达到了。

同时我们还要给缓存加上过期时间,一旦发生缓存不一致,当缓存过期后会重新加载,数据最终还是能保证一致。这就可以作为一个兜底方案。

缓存穿透

开启很多线程频繁的访问一个数据库中也不存在的数据。由于缓存不可能生效,那么所有的请求都访问数据库,可能就会导致数据库因过高的压力而宕机。

解决这个问题有两种思路:

  • 缓存空值

  • 布隆过滤器

缓存空值

简单来说,就是当我们发现请求的数据即不存在与缓存,也不存在与数据库时,将空值缓存到Redis,避免频繁查询数据库。实现思路如下:

优点:

  • 实现简单,维护方便

缺点:

  • 额外的内存消耗

布隆过滤器

布隆过滤是一种数据统计的算法,用于检索一个元素是否存在一个集合中。 

布隆过滤首先需要一个很长的bit数组,默认数组中每一位都是0.

然后还需要Khash函数,将元素基于这些hash函数做运算的结果映射到bit数组的不同位置,并将这些位置置为1。

此时,我们要判断元素是否存在,只需要再次基于Khash函数做运算, 得到K个角标,判断每个角标的位置是不是1:

  • 只要全是1,就证明元素存在

  • 任意位置为0,就证明元素一定不存在

布隆过滤器的判断存在误差:

  • 当布隆过滤器认为元素不存在时,它肯定不存在

  • 当布隆过滤器认为元素存在时,它可能存在,也可能不存在

缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

常见的解决方案有:

  • 给不同的Key的TTL添加随机值,这样KEY的过期时间不同,不会大量KEY同时过期

  • 利用Redis集群提高服务的可用性,避免缓存服务宕机

  • 给缓存业务添加降级限流策略

  • 给业务添加多级缓存,比如先查询本地缓存,本地缓存未命中再查询Redis,Redis未命中再查询数据库。即便Redis宕机,也还有本地缓存可以抗压力(比如浏览器级别缓存、nginx级别缓存、jvm本地缓存等,然后就是redis缓存和数据库)

缓存击穿(热点Key问题)

就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

常见的解决方案有两种:

  • 互斥锁:给重建缓存逻辑加锁,避免多线程同时指向

  • 逻辑过期:热点key不要设置过期时间,在活动结束后手动删除。

基于互斥锁的方案如图:

逻辑过期的思路如图:

逻辑时间过期,就是说缓存数据还在,只需要重置逻辑过期时间就行.如果每次都要查数据库重建缓存数据,那这个逻辑时间就没有意义了 。

总结(常见问题) 

面试题如何保证缓存的双写一致性

:缓存的双写一致性很难保证强一致,只能尽可能降低不一致的概率,确保最终一致。我们项目中采用的是Cache Aside模式。简单来说,就是在更新数据库之后删除缓存;在查询时先查询缓存,如果未命中则查询数据库并写入缓存。同时我们会给缓存设置过期时间作为兜底方案,如果真的出现了不一致的情况,也可以通过缓存过期来保证最终一致。

追问:为什么不采用延迟双删机制?

:延迟双删的第一次删除并没有实际意义,第二次采用延迟删除主要是解决数据库主从同步的延迟问题,我认为这是数据库主从的一致性问题,与缓存同步无关。既然主节点数据已经更新,Redis的缓存理应更新。而且延迟双删会增加缓存业务复杂度,也没能完全避免缓存一致性问题,投入回报比太低。

面试题如何解决缓存穿透问题

:缓存穿透也可以说是穿透攻击,具体来说是因为请求访问到了数据库不存在的值,这样缓存无法命中,必然访问数据库。如果高并发的访问这样的接口,会给数据库带来巨大压力。

我们项目中都是基于布隆过滤器来解决缓存穿透问题的,当缓存未命中时基于布隆过滤器判断数据是否存在。如果不存在则不去访问数据库。

当然,也可以使用缓存空值的方式解决,不过这种方案比较浪费内存。

面试题如何解决缓存雪崩问题

:缓存雪崩的常见原因有两个,第一是因为大量key同时过期。针对问这个题我们可以可以给缓存key设置不同的TTL值,避免key同时过期。

第二个原因是Redis宕机导致缓存不可用。针对这个问题我们可以利用集群提高Redis的可用性。也可以添加多级缓存,当Redis宕机时还有本地缓存可用。

面试题如何解决缓存击穿问题

:缓存击穿往往是由热点Key引起的,当热点Key过期时,大量请求涌入同时查询,发现缓存未命中都会去访问数据库,导致数据库压力激增。解决这个问题的主要思路就是避免多线程并发去重建缓存,因此方案有两种。

第一种是基于互斥锁,当发现缓存未命中时需要先获取互斥锁,再重建缓存,缓存重建完成释放锁。这样就可以保证缓存重建同一时刻只会有一个线程执行。不过这种做法会导致缓存重建时性能下降严重。

第二种是基于逻辑过期,也就是不给热点Key设置过期时间,而是给数据添加一个过期时间的字段。这样热点Key就不会过期,缓存中永远有数据。

查询到数据时基于其中的过期时间判断key是否过期,如果过期开启独立新线程异步的重建缓存,而查询请求先返回旧数据即可。当然,这个过程也要加互斥锁,但由于重建缓存是异步的,而且获取锁失败也无需等待,而是返回旧数据,这样性能几乎不受影响。

需要注意的是,无论是采用哪种方式,在获取互斥锁后一定要再次判断缓存是否命中,做dubbo check. 因为当你获取锁成功时,可能是在你之前有其它线程已经重建缓存了。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值