3830 字
19 分钟
Redis Interview

谈谈你对Redis的理解#

Redis是一个高性能的基于Key-Value结构存储的NoSQL开源数据库。大部分公司 采用Redis来实现分布式缓存,用来提高数据查询效率。

Redis为什么这么快?#

决定Redis请求效率的因素主要是三个方面:分别是网络、CPU、内存。

1.运行在内存中:cpu>内存>磁盘

2.redis是运行线程是单线程。

为什么采用单线程?

redis运行中性能瓶颈不是cpu数(也就是多线程作用不大,是内存大小),单线程的好处可以避免多线程时的上下文切换。

如果采用多线程,对于Redis中的数据操作,都需要通过同步的方式来保证线程安全性,这反而会影响到redis的性能

3.采用多路复用I/O模式

RDB和AOF的实现原理以及优缺点?#

两种持久化机制的特性:
RDB和AOF都是Redis里面提供的持久化机制,RDB是通过快照方式实现持久化、 AOF是通过命令追加的方式实现持久化。
这两种机制的工作原理:
RDB持久化机制会根据快照触发条件,把内存里面的数据快照写入到磁盘,以二进制的压缩文件进行存储。
RDB快照的触发方式有很多,比如:
1.执行bgsave命令触发异步快照,执行save命令触发同步快照,同步快照会阻塞客户端的执行指令。
2.根据redis.conf文件里面的配置,自动触发bgsave
3.主从复制的时候触发
AOF持久化机制是近乎实时的方式来完成持久化的,就是客户端执行一个数据变更的操作,Redis Server就会把这个命令追加到aof缓冲区的末尾,然后再把缓冲区的数据写入到磁盘的AOF文件里面,至于最终什么时候真正持久化到磁盘,是根据刷盘的策略来决定的。
为了避免追加的方式导致AOF文件过大的问题,Redis提供了AOF重写机制,也就是说当AOF文件的大小达到某个阈值的时候,就会把这个文件里面相同的指令进行压缩。
AOF和RDB的优缺点分析
RDB和AOF的优缺点有两个:
RDB是每隔一段时间触发持久化,因此数据安全性低,AOF可以做到实时持久化,数据安全性较高;
RDB文件默认采用压缩的方式持久化,AOF存储的是执行指令,所以RDB在数据恢复的时候性能比AOF要好

redis五种基本类型,及其应用场景?#

String
String最常规的 set/get 操作,Value 可以是 String 也可以是数字。可以设置TTL过期时间与自动续期,一般做一些复杂的计数功能的缓存。
String类型的key,是有最小内存大小的,512mb,如果有很多个key但没有512MB就有些浪费内存。如果key超过512位就会扩容
- 分布式锁:利用 SETNX(Set if Not eXists)实现基础的分布式锁功能。
- 计数器:利用 INCR 指令实现点赞数、访问量统计。
Hash
缓存的数据具有相同的大key
存储java对象 存储要秒杀的商品
这里 Value 存放的是结构化的对象,比较方便的就是操作其中的某个字段。
list
Redis中的list数据结构是队列,底层实现是一个双向链表,(特点是先进先出)
​ 应用场景
​ 有限资源抢购(优惠券,满减券)
​ 按照顺序执行的场景(已经淘汰,有更好的技术替代了,rabbitmq)
消息队列:利用 LPUSH 和 BRPOP 实现简单的异步消息处理。
set 无序,不允许重复
​ 应用场景
​ 点赞,收藏,访问次数,需要去重等功能
- 去重统计:统计 IP 访问量(UV)。
set中
添加是add
移除是remove
访问查看是members
Zset
​ 保留了set的特点,但是是有序集合,可以对数据排序,根据数据中的权重来排序
​ 应用场景:
​ 排行榜,新闻排行
添加
​ 添加是add方法
获取
​ range方法
​ 根据数据下标取,权重从小到大
​ reverseRange方法
​ 根据数据下标取,权重从大到小
​ rangeByScore
​ 根据权重的范围取,权重从小到大
​ reverseRangeByScore
​ 根据权重的范围取,权重从大到小
高级数据类型
Bitmaps (位图)
- 极度节省空间。一个 bit 只有 0 和 1 两种状态。
- 布隆过滤器 (Bloom Filter) 的底层实现。
GEO (地理位置)
- 它的底层实现其实是 Sorted Set (ZSet),通过 GeoHash 算法将二维经纬度转换为一维的分数。
- 附近的人/店铺:如美团找附近美食、微信附近的人。

请说一下你对分布式锁的理解,以及分布式锁的实现#

分布式锁和线程锁本质上是一样的,线程锁的生命周期是单进程多线程,分布式锁的声明周期是多进程多机器节点。 在本质上,他们都需要满足锁的几个重要特性:

  • 排他性,也就是说,同一时刻只能有一个节点去访问共享资源。
  • 可重入性,允许一个已经获得锁的进程,在没有释放锁之前再次重新获得锁。
  • 锁的获取、释放的方法
  • 锁的失效机制,避免死锁的问题
  1. 关系型数据库,可以使用唯一约束来实现锁的排他性,如果要针对某个方法加锁,就可以创建一个表包含方法名称字段,并且把方法名设置成唯一的约束。 那抢占锁的逻辑就是:往表里面插入一条数据,如果已经有其他的线程获得了某个方法的锁,那这个时候插入数据会失败,从而保证了互斥性。 这种方式虽然简单啊,但是要实现比较完整的分布式锁,还需要考虑重入性、锁超时(防死锁)、没抢占到锁的线程要实现阻塞等,就会比较麻烦。

  2. Redis,它里面提供了SETNX命令可以实现锁的排他性,当key不存在就返回1,存在就返回0。然后还可以用expire命令设置锁的失效时间,从而避免死锁问题。

当然有可能存在锁过期了,但是业务逻辑还没执行完的情况。所以这种情况,可以写一个定时任务对指定的key进行续期。

  1. Redisson这个开源组件,就提供了分布式锁的封装实现,并且也内置了一个Watch Dog机制来对key做续期(默认锁的时间是30s,还会开启一个子线程去检测主线程是否执行完,会自动续期)。 释放锁(内部用lua脚本实现的原子性删除) Redis里面这种分布式锁设计已经能够解决99%的问题了,当然如果在Redis搭建了高可用集群的情况下出现主从切换导致key失效,这个问题也有可能造成,多个线程抢占到同一个锁资源的情况,所以Redis官方也提供了一个RedLock的解决办法,但是实现会相对复杂一些,或者使用Redisson中的MultiLock;
TIP

为了提高Redis的可用性,我们会搭建集群或者主从,以主从为例,主节点负责增删改,从节点负责读,主机会将数据同步给从机,但在主从同步完成之前,如果主节点宕机,Redis的哨兵机制会选择一个新的从节点作为主节点。然而,这个新的主节点上并没有之前的锁信息,导致锁失效。这样,当新的线程发来请求时,又可以获取到锁,从而出现两个线程并发访问安全问题。

说说缓存雪崩和缓存穿透的理解,以及如何避免?#

缓存穿透指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

常见解决方案有两种:

  1. 缓存空对象 当我们发现请求的数据即不存在于缓存,也不存于与数据库时,将空值缓存到Redis,并设置过期时间,避免频繁查询数据库。
  • 优点: 实现简单,维护⽅便

  • 缺点: 额外的内存消耗;可能发生不一致问题(在TTL内真的有对应数据存入数据库中)

假如用户刚好请求了一个id,但是这个id的数据不存在,我们给缓存了个null,就在此时我们真的给这个id插入了一条数据,但是缓存中缓存的是null,出现了数据不一致的问题。

我们可以在新增数据的时候,我们主动把redis中的数据进行覆盖掉,也可以解决这个问题。

  1. 布隆过滤 内存占用少,空间利用率高,因为每个位置只需要占用一个bit位,不需要存实际数据,但实现复杂,并且因为hash冲突存在,可能有误判
  • 优点:内存占用较少,没有多余key
  • 缺点: 实现复杂 存在误判可能

实现原理:是用redis 中bitmap 数据类型来实现的。 bitmap: 很长的二进制数组

  1. 会把需要做缓存的数据,会根据查询条件(把该条件字段的值获取出来)在用hash算法算这个值,比如hash(20)得到一个二进制数组的下标,在把该下标值改成1
  2. 在接口查询之前(过滤器/拦截器) 得到条件的值比如id=20,对值进行hash(20)运行,此时也会得到数组下标,判断下标对应的值是0 还是1,如果是0那么数据库一定没有该值,可以拦截。那么等于1数据可能有这个值。

因为有hash冲突的存在,两个不同的值,经过hash运算得到的值是一样hash(100) = 2hash(999) = 2

总结:因为hash冲突的存在,布隆过滤器判断没有,数据库一定没有,判断有的,数据库不一定有,但是这个机率可以控制0.000001(数组越小误判率就越大,数组越大误判率就越小,但是同时带来了更多的内存消耗。)


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

与缓存击穿的区别:雪崩是很多key,击穿是某一个key缓存。

常见的解决方案有:

  • 由于设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效。因此给不同的Key在原本TTL的基础上添加随机值,这样KEY的过期时间不同,不会大量KEY同时过期
  • 利用Redis集群提高服务的可用性,避免缓存服务宕机
  • 给缓存业务添加降级限流策略(服务降级、快速失败等)
  • 给业务添加多级缓存,比如先查询本地缓存,本地缓存未命中再查询Redis,Redis未命中再查询数据库。

缓存击穿问题也叫热点Key问题,就是⼀个被 ⾼并发访问 并且 缓存重建业务较复杂的key突然失效了,⽆数的请求访问会在瞬间给数据库带来巨⼤的冲击。

解决方案:

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

当线程1发现缓存过期并尝试重建缓存时,首先获取互斥锁,再查询数据库并写入缓存,之后释放锁。在重建过程中,有其他线程也发现缓存过期并尝试重建时,会获取互斥锁失败,休眠一会再尝试查询缓存和获取锁的操作,直到查询到新的缓存数据时直接返回。

  • 逻辑过期:热点key不要设置过期时间,通过逻辑过期字段标识是否过期。

当一个线程发现缓存已经过期时,获取互斥锁进行缓存重建,与前一种方案不同的是,缓存重建时会创建新的线程去完成,重建完成后释放互斥锁,自己直接返回过期数据。在重建缓存过程中,有新线程发现缓存过期并尝试重建时,会获取锁失败,此时直接返回过期数据。

缓存更新策略#

内存淘汰

不用自己维护,利用Redis的内存淘汰机制,当内存不足时自动淘汰部分数据。下次查询时更新缓存。

超时剔除

给缓存数据添加TTL时间,到期后自动删除缓存。下次查询时更新缓存。

主动更新

编写业务逻辑,在修改数据库的同时,更新缓存。

  • Cache Aside(缓存旁路模式):由缓存调用者,在更新数据库的同时更新缓存。(代码复杂,但可人为控制)
  • Read/Write Through(读写穿透模式):缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题。(维护服务复杂,无现成服务)
  • Write Behind Caching(写回模式):调用者只操作缓存,由其它线程异步的将缓存数据持久化到数据库,保证最终一致。(维护异步任务复杂,在异步进程修改数据库前,难以保证一致性,若服务器宕机,内存中的Redis数据将丢失 )

Redis和MySQL如何保证数据一致性#

延迟双删

public void write(id, newValue) {
// 1. 第一次删除缓存
cache.delete(id);
// 2. 更新数据库
db.update(id, newValue);
// 3. 休眠一小段时间(如500ms)
Thread.sleep(500);
// 4. 第二次删除缓存
cache.delete(id);
}

Cache Aside(缓存旁路模式)

读操作:
[应用] → ①读缓存 → 有数据 → 返回
↓ 无数据
②读数据库
③写回缓存
④返回数据
写操作:
[应用] → ①更新数据库
②删除缓存

为什么写操作是“删除缓存”而不是“更新缓存”?

  • 避免复杂计算
  • 避免并发写导致脏数据
// 场景:两个并发写请求
// 线程A:写操作1 → 更新缓存为value1
// 线程B:写操作2 → 更新缓存为value2
// 线程B的更新可能晚于线程A,但网络原因先完成
// 最终缓存 = value2,数据库 = value1 → 不一致!
// 如果用的是删除缓存:
// 无论顺序如何,缓存都被删除
// 下次读请求一定会从数据库拉取最新值
Redis Interview
https://zzyang.top/posts/redis-notes/
作者
张小阳
发布于
2025-03-07
许可协议
CC BY-NC-SA 4.0

评论区

评论区加载中...

如果长时间无法显示,请尝试刷新页面。