Skip to content

面试煎熬成蛋_Redis

常见面试题:

  • Redis 的数据类型
  • 谈一下布隆过滤器
  • Redis 为什么这么快
  • Redis 为什么要引入多线程
  • 什么是 IO 多路复用
  • 讲一下 Redis 的 Reactor 模型
  • Redis 如何获取所有 key
  • 讲一下 Redis 持久化
  • Redis 怎么实现高可用的
  • Redisi主从复制(同步)原理
  • 哨兵选主过程
  • Redis Cluster主从选举过程
  • 主从选举的脑裂问题
  • 缓存雪崩、缓存击穿、缓存穿透
  • 热点缓存并发重建
  • 数据库和缓存双写不一致
  • Redis分布式锁实现
  • 过期键的删除策略
  • 内存淘汰策略有哪些

Redis 在你项目中是怎么使用的

讲一下 Redis 的用法

缓冲、分布式锁

缓冲 → Jwt

常用的缓存读写策略

缓存常用的三种读写策略:Cache Aside Pattern(旁路缓存模式)、Read/Write Through Pattern(读写穿透)、Write Behind Pattern(异步缓存写入)

讲一下 Redis 事务

不是很推荐使用Redis事务,跟我们常见的关系型数据库事务不同:

  • 1、Redis 事务是不支持回滚(roll back)操作的,Redis 事务不满足原子性。
  • 2、Redis 事务的持久性是没办法保证的

image.png

Lua 脚本

与事务相对的话,更推荐使用 Lua 脚本执行批量任务;

  • 一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。

不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此, 严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。


Redis的Lua脚本功能是其强大特性之一,提供了在Redis服务器端执行脚本的能力。这允许执行一系列操作,这些操作作为一个整体原子地执行,而不是由客户端逐一发送命令然后由服务器逐一执行。

Lua是一种轻量级的编程语言,被嵌入到Redis中用于执行复杂的逻辑,这在多个命令需要作为单个原子操作执行时特别有用。

使用示例:

假设我们需要更新一个计数器,但只有在另一个键存在时才进行更新。不使用Lua脚本,我们可能需要先检查键是否存在,然后再更新计数器,这两步操作不能保证原子性。

使用Lua脚本,我们可以这样做:

if redis.call("EXISTS", KEYS[1]) == 1 then
    return redis.call("INCR", KEYS[2])
else
    return 0
end
  • 这个脚本接受两个键作为输入:KEYS[1]用于检查存在性,KEYS[2]是需要增加的计数器。如果KEYS[1]存在,脚本将增加KEYS[2]的值并返回新值;如果不存在,返回0。

执行脚本操作(使用EVAL命令执行Lua脚本):

EVAL <script> <numkeys> <key> [<key2> ...] [<arg> [<arg2> ...]]
  • <script>:Lua脚本文本。
  • <numkeys>:脚本中将要处理的键的数量。
  • <key>:脚本中用到的Redis键。
  • <arg>:传递给脚本的其他参数。

Redis 的常用命令

配置项

  • maxmemory: 设置 Redis 使用的最大内存量。
  • maxmemory-policy: 定义当内存达到上限时 Redis 的行为策略(淘汰策略)
  • appendonly: 是否启用 AOF(Append Only File)持久化。
  • appendfsync: 设置 AOF 持久化的同步策略。
  • save: 设置 RDB 快照的触发条件,格式为 seconds_to_elapsed changes_to_record
  • cluster-enabled: 是否启用 Redis 集群模式。

数据结构

Redis 常见的数据结构

  • String
    • string 主要是⽤来存储字符串,底层是基于 动态字符串sds 实 现的,sds 通过动态调整⻓度来节省内存
    • 应⽤场景
      • 分布式session
      • 分布式锁
    • 常用指令:
      • set、get、setnx、setex ..
  • hash
    • hash 类似于 Map,底层采⽤两种⽅式来实现,当数据量较少 并且元素占⽤内存少(⼩整数或短字符串)时,采⽤ ziplist(压缩 列表),反之采⽤ hashtable(哈希表),两种⽅式都是为了节省 内存
      • ziplist 是⼀个连续的内存空间,通过紧凑的存储来节省空 间
      • hashtable 是基于dict(字典)实现,采⽤拉链法解决hash冲 突
    • 应用场景
      • 实现购物⻋
    • 常用指令
      • hset、hget、hgetall、hdel、hincrby ..
  • list
    • list 是⼀个有序可重复集合,底层采⽤两种⽅式实现,当数据 量较少并且元素占⽤内存少(⼩整数或短字符串)时,采⽤ ziplist(压缩列表),反之采⽤ quicklist(快速列表),两种⽅式都 是为了节省内存
      • ziplist 是⼀个连续的内存空间,通过紧凑的存储来节省空 间
      • quicklist 是基于 ziplist 和 双向链表 实现的,可以在节省空 间的同时保证⾼效增删
    • 应用场景
      • 栈(lpush + lpop)
      • 队列(lpush + rpop)
      • 阻塞队列(lpush + brpop)
      • 发布订阅
    • 常用指令
      • lpush、lpop、rpush、rpop、blpop、brpop、lrange ..
  • set
    • set 是⼀个⽆序不可重复集合,底层采⽤两种⽅式实现,当数 据量较少且元素为整数时,采⽤ intset(整数集合),反之采⽤ hashtable(哈希表),两种⽅式都是为了节省内存
      • intset 是⼀个有序的整数数组,通过紧凑的存储来节省空 间
      • hashtable 是基于dict(字典)实现,采⽤拉链法解决hash冲 突
    • 应用场景
      • 抽奖(srandmember)
      • 点赞收藏关注(sadd)
      • 共同关注(sinter)
      • 可能认识的⼈(sdiff)
    • 常用指令
      • sadd、srem、smembers、scard、srandmember、 sismember、spop、sinter、sunion、sdiff、sinterstore、 sdiffstore
  • zset(sorted set)
    • zset 是⼀个有序不可重复集合,底层采⽤两种⽅式实现,当数 据量较少并且元素占⽤内存少(⼩整数或短字符串),采⽤ ziplist(压缩列表),反之采⽤skiplist(跳表) + dict(字典),两种 ⽅式都是为了节省内存,另外skiplist主要是为了提升score查 询效率
      • ziplist 是⼀个连续的内存空间,通过紧凑的存储来节省空 间
      • skiplist 是⼀个有序链表配上多级索引,通过多级索引位置 的跳转来实现快速查找元素,主要⽤于按照分值对元素进 ⾏排序,同样也⽀持范围查询
        • 跳表如何定位元素
          • 每隔⼀个元素建⽴⼀个索引,通过建⽴多个索引, 利⽤⼀次索引定位到需要查询的元素,如果觉得 慢,可以在⼀级索引的基础上建⽴⼆级索引,依次 类推,在多级索引之间来回转跳实现快速定位,当 数据量特别⼤的时候,查找时间复杂度为O(logN), 因为它本身的思想就类似⼆分查找
    • 应用场景
      • 排⾏榜(zrange/zreverange/zunionscore)
    • 常用指令
      • zadd、zrem、zsore、zincrby、zrange、zreverange、 zrangebyscore、zreverangescore、zunionscore、 zinterscore
  • bitmap
    • bitmap是⼀个位图
    • 应⽤场景
      • ⽉打卡、⽉活跃
        • ⽉打卡可以通过将 当前第⼏天 作为 偏移量,如果打卡 对应的位置为1,反之为0
      • 布隆过滤器
  • stream
    • stream 是参考kafka设计的消息队列,⽀持持久化,适合⼩基 数的消息队列场景

String 数据类型在项目中的实际使用

image.png

ZSet 为什么使用跳表

谈一下布隆过滤器

布隆过滤器 是基于bitmap实现的,主要⽤于粗略的数据过滤

  • 添加数据时,经过hash运算得到对应的 bit位,将该 bit位 置为1
    • bit位为1 表示可能存在
    • bit位为0 表示⼀定不存在

Redis 字符串最大不能超过多少

512 MB

内存、读写

讲一下 Redis 持久化 🚩

image.png

Redis 为什么这么快 🚩

  • 1、主要还是基于内存操作
  • 2、是单线程的,避免了不必要的上下文切换可竞争条件,多线程需要考虑线程安全问题
  • 3、使用 IO 多路复用模型,非阻塞 IO
  • 4、Redis 的每种数据结构进行了优化处理

什么是 IO 多路复用

Rdis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,

I/O多路复用模型主要就是实现了高效的网络请求

  • 用户空间和内核空间
  • 常见的 IO 模型
    • 阻塞IO(Blocking IO)
    • 非阻塞IO(Nonblocking IO)
    • IO多路复用(IO Multiplexing)
  • Redis网络模型

阻塞IO 是常见 的IO 操作模型,但是它实际的效率并不是很高;用户态在内核读取数据和从内核拷贝到用户缓冲区的两个阶段都需要等待阻塞运行。

image.png

非阻塞IO,在内核态读取这一阶段,用户阶段并不会持续阻塞等待,而是自旋询问是否数据准备就绪,如果未就绪,就循环尝试读取。

阶段二仍然会被阻塞,但由于忙等机制实际效率并没有提高很多。

image.png

IO 多路复用

监听了多个 socker,并且其中只要有一个 socket 就绪,就可以进行系统调用 recvfrom

和前者最大的区别是:非阻塞 iO 或者 阻塞 IO 需要等待它目前在读取的 socket 数据准备就绪才可以下一步操作(而实际上可能它后面的 socket 是已经准备就绪了)

通过这个监听机制,可以有效提高效率

image.png

常见 的 IO 多路复用模式有: select、poll、epoll;

然后前两个的机制:select 和 poll 会有一个监听,然后只是监听里面有没有就绪的,如果有,遍历逐个来判断; 而 epoll 机制能够精确定位到具体是哪一个,而不用遍历。

image.png

Reids 网络模型:IO多路复用 + 事件派发机制

image.png

Redis 为什么要引入多线程

影响效率最大的是 网络 IO

Redis 多线程 在命令回复处理器和 接受参数数据,转换Redis 命令 (网络IO 方面的内容)进行了优化(加入了多线程)

image.png

  • Redis引⼊多线程主要想发挥多核处理器的能⼒,处理在⼤数据量下 ⽹络IO读写速度慢的问题,但是指令的执⾏依旧采⽤的是单线程

image.png

面试官:Redis是单线程的,但是为什么还那么快?

候选人:

嗯,这个有几个原因吧~~~ 1、完全基于内存的,C语言编写 2、采用单线程,避免不必要的上下文切换可竞争条件 3、使用多路1/0复用模型,非阻塞I0

例如:bgsave和bgrewriteaof都是在后台执行操作,不影响主线程的正常使用,不会产生阻塞

面试官:能解释一下1/O多路复用模型?

候选人:嗯~,I/O多路复用是指利用单个线程来同时监听多个Socket,并在某个Socketi可读、可写时 得到通知,从而避免无效的等待,充分利用CPU资源。目前的1/O多路复用都是采用的epo模式实现, 它会在通知用户进程Socket就绪的同时,把己就绪的Socket写入用户空间,不需要挨个遍历Socket来判 断是否就绪,提升了性能。 其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socketi请求,比如,提供了连 接应答处理器、命令回复处理器,命令请求处理器: 在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求 处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程

讲一下 Redis 的 Reactor 模型

Redis 的过期策略

  • 定时过期:每个设置过期时间的 key 都需要创建一个定时器, 到过期时间就会立即对 key 进行清除
  • 惰性过期:只有当访问一个 key 时,才会判断该 key 是否已过 期,过期则清除
  • 定期过期:每隔一定的时间,会扫描一定数量的数据库的 expires 字典中一定数量的 key,并清除其中已过期的 ke

过期键的删除策略 🚩

image.png

假如redis的 key 过期之后,会立即删除吗? set name heima 10

  • Redis 对数据设置数据的有效时间,数据过期以后,就需要将数据从内存中删除掉。
  • 可以按照不同的规则进行删除,这种删除规则就被称之为数据的删除策略(数据过期策略)。

常见的过期策略

  • 惰性删除
    • 只有当访问一个key的时候,才会判断的当前key是否已过期,已过期就会删除
    • 这个策略可以节省CPU资源,但是占用内存,可能因为大量的key不被再次访问,导致一直不清除从而占用内存
  • 定期删除
    • 每隔一段时间会扫描一定数量的过期key,并且清除已过期的key。
    • 这个策略属于折中方案,可以有效的平衡CPU资源以及内存资源
    • 定期清理有两种模式:
      • SLOW 模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf的hz选项来调整这个次数
      • FAST 模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms
  • 强制删除
    • 当已使用内存超过Redis:最大允许内存,会触发内存淘汰策略
  • Redis中同时使用了惰性删除和定期删除

内存淘汰策略有哪些

数据的淘汰策略:当Redis中的内存不够用时,此时在向Redisr中添加新的key,那么Redis 就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。

  • 默认策略noeviction,当内存不⾜以容纳新写⼊数据时,新写⼊操作 会报错
  • 针对设置过期时间的key
    • volatile-lru,按照LRU算法删除
    • volatile-lfu,按照LFU算法删除
    • volatile-radom,随机删除
    • volatile-ttl,按过期时间顺序删除
  • 针对所有key
    • allkeys-random,随机删除
    • allkeys-lru,按照LRU算法删除
    • allkeys-lfu,按照LFU算法删除

image.png

关于数据淘汰策略其他的面试问题

  • 1.数据库有1000万数据,Redis只能缓存20w数据,如何保证Redis 中的数据都是热点数据?
    • 使用allkeys-lru(挑选最近最少使用的数据淘汰)淘汰策略,留下来的都是经常访问的热点数据
  • 2.Redis的内存用完了会发生什么?
    • 主要看数据淘汰策略是什么?如果是默认的配置(noeviction),会直接报错

记忆点:

数据淘汰策略

  • 1.Redis提供了8种不同的数据淘汰策略,默认是 noeviction 不删除任何数据,内存不足直接报错
  • 2.LRU: 最少最近使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
  • 3.LFU:最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高

平时开发过程中用的比较多的就是allkeys-lru(结合自己的业务场景)

高可用

Redis 怎么实现高可用的

Redisi主从复制(同步)原理

  • 通过 RDB 日志文件和偏移量操作

全量同步操作

  • 1、从节点发起同步请求
  • 2、主节点判断是否是第一次同步,如果是第一次,进行全量同步,如果不是,判断 数据集 replid 是否一致
    • 是,第一次,会返回 master 的数据版本信息 replid、offset
  • 3、从节点会保存版本信息
  • 4、主节点会生成 RDB 文件进行发送操作
  • 5、从节点会清空本地数据,加载 RDB 文件
  • 6、从节点加载过程会主节点会生成一个 repl_baklog 的文件(中间执行操作),并发送到从节点
  • 7、从节点再进行加载进行同步

image.png

增量同步操作

  • 从 reol_baklog 获取数据发送到从节点

image.png

参考

image.png

讲一下 哨兵选主过程

哨兵:高可用,提供监控范围和自动故障恢复与通知

image.png

  • 选取操作

image.png

哨兵模式中会遇到一个问题:脑裂问题

讲一下 主从选举的脑裂问题

集群脑裂是由于主节点和从节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到主节点,所以通过选举的方式提升了一个从节点为主,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将老的主节点降为从节点,这时再从新master 同步数据,就会导致数据丢失

解决:我们可以修改rds的配置,可以设置最少的从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求就可以避免大量的数据丢失。

image.png

参考

image.png

分片集群

image.png

参考

image.png

讲一下Redis Cluster主从选举过程

生产问题

数据一致性

  • 查询
    • 当需要获取数据时,首先查询缓存。
    • 如果缓存中有数据(缓存命中),则直接返回缓存中的数据。
    • 如果缓存中没有数据(缓存未命中),则查询数据库,将查询结果存入缓存,并返回数据。
    • 设置合理的缓存过期时间,以保证数据的时效性。
  • 更新
    • 遇到写请求的时候是先更新数据库,再删除 cache
  • 删除
    • 当数据需要被删除时,首先删除数据库中的数据。
    • 然后删除缓存中对应的数据,保持数据库和缓存的一致性。

缓存读写策略一般选择使用旁路缓存模式

常用的缓存读写策略

  • Cache Aside Pattern(旁路缓存模式)
      • 从 cache 中读取数据,读取到就直接返回
      • cache 中读取不到的话,就从 db 中读取数据返回
      • 再把数据放到 cache 中。
      • 先更新 db,然后直接删除 cache
  • Read/Write Through Pattern(读写穿透)
      • 从 cache 中读取数据,读取到就直接返回 。
      • 读取不到的话,先从 db 加载,写入到 cache 后返回响应。
      • 先查 cache,cache 中不存在,直接更新 db。
      • cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(同步更新 cache 和 db)。
  • Write Behind Pattern(异步缓存写入)
      • **Read/Write Through 是同步更新 cache 和 db,
      • Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db

保持数据库和缓存数据一致性

  • 1、数据库中数据中全量刷写到缓存中,不设置失效时间 + 定时任务
  • 2、保留热数据(都设置一个合理的过期时间)
  • 3、旁路缓存模式 + 保证删除操作非异常执行(MQ异步 + 订阅日志机制)

怎么保存缓存和数据库数据一致(数据一致性怎么保证)

延迟双删 + 定时刷新

策略

  • 1、数据库中数据中全量刷写到缓存中,不设置失效时间 + 定时任务
  • 2、保留热数据

旁路缓存模式 + 保证删除操作非异常执行(MQ异步 + 订阅日志机制)

思考:

  • 一旦我们决定使用缓存,那必然要面临一致性问题。性能和一致性就像天平的两端,无法做到都满足要求。
  • 既然决定使用缓存,就必须容忍「一致性」问题,我们只能尽可能地去降低问题出现的概率。

缓存和数据库不一致

一般是采用:Cache Aside Pattern(旁路缓存模式): 遇到写请求的时候是先更新数据库,再删除 cache

这种情况下发生数据不一致的情况比较小,

在这种情况下,在数据库更新和缓存删除之间有读取请求,它也只会读到旧的数据,一旦缓存被删除,下一个读取请求将从数据库中获取最新的数据,并更新缓存,从而保持了一致性。

需要避免缓存和数据库不一致需要考虑的点:避免第二步操作【删除 cache】失败

解决方案:

  1. 缓存失效时间变短(不推荐,治标不治本)
    1. 我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。
  2. 增加 cache 更新重试机制(常用)
    1. 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,
    2. 重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。

第二种方式采用方案:

  1. 引入消息队列来进行异步重试操作(将删除 cache 操作放在消费者这步操作)
  2. 订阅数据库变更日志,再操作缓存

当缓存操作失败时,通过设置重试机制来确保缓存最终能够与数据库数据保持一致。具体步骤包括:

A. 直接重试

在缓存操作(如删除、更新)失败时,立即进行重试。可以设定一个最大重试次数,避免无限重试。

B. 延时重试

如果直接重试仍然失败,可以将失败的操作延时一段时间后再重试,延时重试可以通过定时任务或延时队列实现。

C. 失败队列

如果重试多次后仍然失败,将更新失败的key存入一个特定的队列中。等到缓存服务恢复正常后,再统一处理这些失败的操作。

具体实施方案

引入消息队列(异步重试)

  • 消息队列保证可靠性:写到队列中的消息,成功消费之前不会丢失(重启项目也不担心)
  • 消息队列保证消息成功投递:下游从队列拉取消息,成功消费后才会删除消息,否则还会继续投递消息给消费者(符合我们重试的场景)

使用消息队列作为异步处理机制,将需要更新或删除的缓存操作作为消息发送到队列中,由消费者负责执行实际的缓存更新任务。这样即使缓存服务暂时不可用,也不会影响主业务流程,待缓存服务恢复后再通过消费消息来同步缓存。

订阅数据库变更日志

另一个高级的方案是直接订阅数据库的变更日志(如果数据库支持),如MySQL的binlog。通过解析变更日志,对相关缓存进行更新或删除操作。这种方法可以确保缓存数据的最终一致性,但实现复杂,对数据库和缓存系统的要求较高。

数据双写不一致(数据一致性)

image.png

讲一下延迟双删

  • 延迟双删
    • 更新数据库智慧,等待一段时间,再次删除缓存中的数据

image.png

缓存雪崩、缓存击穿、缓存穿透

  • 缓存穿透
    • 缓存穿透是指当请求的数据既不在缓存中也不存在于数据库中时,导致请求直接到达数据库层的现象。
    • 成因:频繁查询不存在的数据(可能是由于错误的输入或恶意攻击)
    • 解决方案
      • 空对象缓存(缓存空结果)
        • 即使某个值在数据库中不存在,也可以在缓存中存储一个特殊的空对象或空值,并设置较短的过期时间。
        • 这样,相同的无效请求在这个过期时间内会直接得到缓存中的空结果,而不会再次查询数据库。
        • 缺点:可能会导致期间使用内存较高,缓存资源浪费
    • 布隆过滤器
      • 在查询之前使用布隆过滤器判断数据是否可能存在。布隆过滤器是一种空间效率高但可能有一定误判率的数据结构。
  • 缓存击穿
    • 缓存击穿是指缓存中某个热点key突然失效(过期),导致大量并发请求直接落到数据库上,造成数据库短时间内压力剧增的现象。
    • 与缓存穿透不同,缓存击穿是针对原本就存在于缓存中的热点数据。
    • 成因:热点数据在缓存中突然过期,而此时正有大量并发请求这些数据。
    • 解决方案
        • 设置热点数据永不过期
          • 对于一些热点数据,可以设置其缓存永不过期,或者设置一个非常长的过期时间,从而避免缓存击穿问题。
          • 但这种方法需要定期手动或通过程序更新缓存,以保证数据的新鲜度
      • 互斥锁
        • 在缓存失效的瞬间,使用互斥锁或分布式锁,确保只有一个请求去数据库查询数据并重新缓存。
        • 其他请求等待缓存加载完成后再访问缓存
      • 提前预热
        • 在缓存即将过期前,后台异步程序提前更新缓存中的数据,这样可以确保热点数据在缓存中始终是可用的
      • 二级缓存策略
        • 为热点数据设置两级缓存,第一级缓存正常设置过期时间;第二级缓存设置较长的过期时间。当第一级缓存失效时,请求可以访问第二级缓存,同时异步更新第一级缓存和第二级缓存数据。
  • 缓存雪崩
    • 缓存雪崩是指缓存中大量数据同时过期,导致所有的请求都直接访问数据库,可能会使数据库压力过大甚至崩溃。
    • 成因
      • 缓存设置了相同的过期时间,导致大量数据同时过期。
      • 缓存服务崩溃,所有数据丢失。
    • 解决方案
      • 不同的过期时间:为缓存数据设置不同的过期时间,避免同时过期。(可以在设置缓存过期时间时加入随机值)
      • 缓存数据预热:系统启动时预先加载热点数据到缓存中。
      • 限流降级:在系统入口处使用限流降级策略,避免在缓存失效时,突发流量直接打到数据库。
      • 使用多级缓存策略
      • 使用高可用的缓存架构:比如使用Redis集群,提高缓存系统的稳定性和容错能力。

总结一下上面的三个常见的生产问题:缓存穿透、缓存击穿、缓存雪崩;

  • 1、缓存穿透指的不存在的数据进行访问缓存,并打到数据库中,解决方案一般有:空对象缓存、布隆过滤器
  • 2、缓存击穿和缓存雪崩有点像,都是缓存失效,但前者一般是热点数据,后者是大量数据同时过期;
    • 热点数据建议做一下预热处理,在快要过期的时候进行处理一下;或者说常用的两种处理方案:
      • 设置热点数据不过期,不过后续需要手动进行更新缓存或者程序更新
      • 设置互斥锁,保证同一时刻访问缓存的数据(缓存中不存在的数据),此时只有一个
    • 大量数据过期建议使用随机过期时间,同时可以设置一下限流,防止大量数据同时请求;也可以设置多级的一个缓存策略操作。

Redis分布式锁实现 🚩

image.png

使用 Redis 可以实现分布式情况下对访问资源进行加锁

  • 1、通过 setnx 命令进行加锁
  • 2、是否锁通过 del 命令删除对应的key
    • 为了防止误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value(唯一值)来判断。
    • 选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性
  • 3、优化项
    • 考虑为了避免锁无法释放,一个解决方法是:给这个锁加一个过期时间
    • 一定要保证设置指定 key 的值和过期时间是一个原子操作!!! 不然的话,依然可能会出现锁无法被释放的问题。
    • 因为这个引入了现成的一个解决方案:Redission
  • 4、Redission
    • Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。
  • 5、如何实现可重入锁(锁里面又需要获取到另外一个需要锁的方法)
    • 不可重入的分布式锁基本可以满足绝大部分业务场景了,一些特殊的场景可能会需要使用可重入的分布式锁
    • 实际实现中,Redission 是提供了实现方案的
      • Redisson ,内置了多种类型的锁比如可重入锁(Reentrant Lock)、自旋锁(Spin Lock)、公平锁(Fair Lock)、多重锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)。

在面对高并发的库存扣减问题时,除了使用Redis分布式锁外,还可以考虑以下一些策略:

  1. 乐观锁:在数据表中使用版本号(或时间戳)字段,每次更新时检查版本号是否一致,如果一致则更新并增加版本号。这种方式减少了锁的使用,但在高冲突环境下可能导致大量的重试。
  2. 消息队列:将库存扣减操作异步化,通过消息队列来控制对库存的操作序列。这样可以平滑高峰期的请求,但需要处理好消息的可靠性和消费顺序。
  3. 令牌桶或漏斗算法:通过限流算法控制对库存操作的并发量,保证系统的稳定性。
  4. 分段锁:如果库存数据可以分段(例如不同的商品或仓库),可以对每一段使用独立的锁或Redis key,从而减少锁的竞争。
  5. 预扣减与补偿机制:在用户下单时先进行库存预扣减,然后异步处理订单,如果订单处理失败则进行库存补偿。这种方式可以快速响应用户请求,但需要处理好补偿逻辑。

大 Key 问题

遇到过大 Key 问题吗,说一下当时遇到的场景,以及当时是怎么处理的

参考: https://www.bilibili.com/video/BV1iu411v7fp

什么是大 Key:

string 类型的值大于 10 kb

hash、Iist、set、Zset元素个数超过 5000 个

如何找到 大 Key

  • redis -cli -h 127.0.0.1 -p6379 -a "password" -- bigkeys

image.png

如何删除 大 Key

  • 直接删除大key会造成阻塞,因为redis是单线程执行,阻塞期间,其他所有请求可能都会超时。超时越来越多,会造成 redis 连接会耗尽,产生各种异常
  • 低峰期删除:凌晨,观察 qps ,选择低的时候,无法彻底解决阻塞
  • 分批次删除:对于hash,使用 hscan 扫描法,对于集合采用 srandmember 每次随机取数据进行删除。对于有序集台可以使用 zremrangebyrank 直接删除,对于列表直接pop即可。
  • 异步删除法:用 unlink 代替del来删除,这样redis会将这个key 放入到一个异步线程中,进行删除,这样不会阻塞主线程。

参考: https://www.bilibili.com/video/BV1m54y1f74X

大 Key 一般会造成客户端阻塞,造成查询失败、网络阻塞等现象;

一般说这个通过 问题反馈 → 问题排查 → 问题解决

发现了 大 key 问题,一般需要逐步进行删除操作,避免影响其他业务服务

删除后,考虑如何对 redis 的大 key 进行设计和优化

常见面试题

Redis 默认分多少个数据库

16 个

说一下为什么要使用旁路缓存,使用其他的缓存读写策略有什么问题