Redis八股
数据库和缓存一致性
缓存和数据库一致性问题,是有很多种方案解决的,首先是旁路缓存,在写操作的时候先更新数据库,再删除缓存,读操作的时候如果没读到缓存就读数据库,再写入缓存。这样在高并发场景下也会出现一些问题,比如请求1执行写操作了,还没删除缓存,请求2读缓存了,请求2读到的就是旧数据。这个问题解决可以通过延迟双删解决,在写操作的时候先删除缓存,再更新数据库,过段时间再删除缓存。这样在线程1写操作的时候,其他线程读操作就无法命中旧数据的缓存了。但是这样高并发还是有一定的问题,首先在删除缓存准备修改数据库的时候,还没来及改,线程2查询发现没命中缓存,数据库查旧数据,更新缓存了,这个时候线程1更新数据库,还没来得及第二次删除缓存,又有读操作来了,又命中了旧数据。所以在写操作到延迟删缓存这段时间缓存还是有不一致问题,而且这个延迟时间无法确定设置多少,并且降低了吞吐量。如果业务对于并发情况数据一致性要求并没有那么高,上面两种方案其实就够用了,如果要保证强一致性问题就可以上分布式锁,在读写操作之前必须要获取到锁才能操作,线程1写操作的时候获取锁,这个时候其他线程就无法读,等线程1执行完更新操作删除了缓存,再释放锁,其他线程读操作获取到锁,查询数据库更新缓存释放锁。缺点就是强行改成了串行处理,性能非常差。还有一种保证最终一致性的方案就是使用阿里的Canal,订阅数据库的Binlog日志,当更新数据库的时候Binlog发生变更,Canal监听到消息删除缓存。优点是解耦了,缺点增加了运维成本,而且依赖Binlog,如果Canal 消费 Binlog 时可能失败(如网络波动),需对接消息队列(如 RabbitMQ),将删除缓存的请求存入队列,失败后重试,避免漏删。
Redis持久化
RDB存储的Redis数据的二进制格式,会每隔一段时间进行一次数据快照,保存rdb文件到磁盘,他的缺点是在两次快照之间如果重启或者网络波动会导致数据丢失。第二个AOF 以日志形式记录每一条写命令,需要设置刷盘策略到磁盘。Redis重启的时候重新执行日志中的写命令,实现持久化,缺点是数据量大了导致日志越来越大,恢复也会越来越慢。Redis4.0以后引入了混合持久化,通过RDB+AOF两种的方式达到一个不错的效果,融合 RDB 的 “快速恢复” 和 AOF 的 “数据完整” 优势,将全量数据用RDB用二进制存储,增量数据用AOF,以后统一放入AOF文件中,恢复先加载二进制的RDB再加载AOF。
Redis为什么快
- 纯内存操作:数据全存内存,避免磁盘 IO(数据库慢的核心原因),内存读写速度比磁盘快万倍以上;
- 单线程模型:无多线程上下文切换开销,也无需处理线程安全问题,减少性能损耗;
- 高效数据结构:底层用跳表、哈希表等结构,查询 / 插入 / 删除效率均为 O (1) 或 O (logn);
- IO 模型优化:用 IO 多路复用(epoll/kqueue),单线程处理上万并发连接,无连接阻塞;
- 轻量设计:代码简洁(核心代码仅几万行),无复杂逻辑,减少运行时资源占用。
讲一下缓存穿透,缓存击穿,缓存雪崩
缓存穿透就是因为一般设计业务的时候,会优先访问缓存中数据是否存在,如果不存在访问数据库,攻击者就是可以用这一点构造恶意请求,多次访问数据库,导致数据库压力过大。这种情况可以对访问请求进行合法性检查,过滤非法字符或者使用布隆过滤器过滤,再决定是否访问数据库。或者给redis设置null值或者空字符串。同时如果查询数据库没数据,将该key缓存并设置空值标识,比如NULL,防止业务支持空值,设置短期的过期时间,实际落地可以通过缓存空值+布隆过滤器解决缓存缓存穿透的问题,缓存空值能处理布隆过滤器的假阳性数据。
缓存击穿就是当缓存数据过期或者失效的时候,攻击者并发访问失效数据,这样会直接访问数据库,导致高并发,给数据库造成压力。解决方式可以在缓存失效或者过期前,进行预更新或者延迟更新,让攻击者不知道更新的时间。第二个就是解决并发攻击,这里可以使用锁,互斥锁和分布式锁,过期或者失效的时候有线程没有访问到缓存中数据,则给该线程一个锁, 这个时候就可以增加一个判断如果有锁,则查询数据,同时释放锁,并将数据更新到缓存,如果没有锁就线程等待。或者逻辑过期,添加逻辑时间字段,当现在请求接口的时候,先判断当前时间是否在过期时间之前,如果未过期,将数据直接返回,如果过期了,进行缓存重建,加上互斥锁,重新查找一次数据库,封装新的过期时间,将数据放入缓存中。那么在过期时间以后的所有线程,只有拿到锁的线程进行了缓存修改,后面线程发现时间都没过期,就拿修改后的数据。所以会有一个数据一致性的问题,在过期时间内的数据都是旧数据。
缓存雪崩是缓存中大量的数据全部失效,导致非常多的请求直接访问数据库,导致数据库压力剧增。最简单的是给每个key的TTL增加随机值,缓存预热的时候给缓存数据的设置过期时间TTL的时候定义一个范围,追加该范围的随机数。这种情况一般使用分布式集群提高可靠性或者限流,要么多级缓存。
布隆过滤器的问题
布隆过滤器的核心缺陷是存在假阳性(False Positive):即一个不存在于数据库中的 key,可能被误判为 “存在”。无法处理 “已删除数据” 的查询。商品 A 已下架(数据库删除),但布隆过滤器仍认为 “product:A” 存在,当缓存中 “product:A” 过期后,所有查询都会打到数据库查无结果。需要提前设计好预期存储的元素数量p和可接受的假阳性百分比,并计算出对应的P(数组长度)和K(哈希函数数量)。
redis的分布式锁如何设计的
首先我在系统发起流程审批的时候通过redisson对流程节点ID加上了分布式锁,防止了因为某个节点被多端同时处理或者多用户同时审批引起的并发问题。之所以使用redisson是因为它是基于redis的分布锁方案,底层依旧是通过set NX上锁,lua脚本释放锁,底层lua脚本通过hash类型fieid为线程标识,key为锁重入次数,加锁和释放锁判断保证了锁的可重入性,并且它引入了看门狗机制,防止了网络问题导致锁提前过期,死锁的问题。主从一致性问题通过redlock保证,比如5个从节点要有三个拿到锁才算成功,增加了可靠性。
Redisson分布式锁原理
Redisson 分布式锁是基于 Redis 实现的高性能、高可用分布式锁方案,核心原理可概括为:利用 Redis 的单线程特性 + 原子命令 + Lua 脚本保证锁的安全性,通过可重入设计、自动续期、集群容错等机制解决分布式场景下的各种问题 。
IO多路复用
IO多路复用解决了传统单线程处理IO任务的时候,其他任务会等待阻塞,多线程之间创建和切换线程上下文资源消耗问题。他能让单个同时监控多个IO文件描述符,一旦某个描述符就绪,就通知应用程序进行相应的 IO 操作,避免线程在单个 IO 上阻塞等待。因为Linux中认为万物皆是文件,所以通过文件描述符处理对应的文件,Linux中三个主流方案是select poll和epoll。select比较古老,有设置文件描述符上限。poll和epoll没有,IO就绪通知方式select和poll都是轮训通知,epoll是事件驱动只通知就绪的IO。数据拷贝中selecet和poll都要从内核态到用户态,epoll则是共享内存。
| 对比维度 | select | poll | epoll(推荐) |
|---|---|---|---|
| 文件描述符上限 | 有上限(默认 1024) | 无上限 | 无上限 |
| IO 就绪通知方式 | 轮询(遍历所有描述符) | 轮询(同 select) | 事件驱动(只通知就绪的) |
| 效率 | 低(随连接数增加下降) | 低(同 select) | 高(不受连接数影响) |
| 数据拷贝 | 内核→用户空间(每次就绪) | 内核→用户空间(每次就绪) | 无需拷贝(共享内存) |
epoll 是目前性能最优的实现,其工作流程可分为三步,核心是 “事件注册 + 内核通知”:
- 创建 epoll 实例:调用
epoll_create()创建一个 epoll 对象(本质是内核中的一个事件表),用于管理需要监控的 IO 描述符。 - 注册事件:调用
epoll_ctl()向 epoll 实例中注册需要监控的 IO 描述符,以及关注的事件类型(如 “读就绪”EPOLLIN、“写就绪”EPOLLOUT)。 - 等待事件就绪:调用
epoll_wait()阻塞等待,内核会监控所有注册的描述符。一旦某个描述符就绪,内核会将其对应的事件加入到 “就绪事件列表”,并唤醒epoll_wait(),最后应用程序从列表中获取就绪的描述符并处理 IO。
redis的pub/sub机制
Redis 的 Pub/Sub(发布 / 订阅)机制是一种基于消息传递的通信模式,用于实现多个客户端之间的异步通信。它的核心思想是将消息的发送者(发布者)和接收者(订阅者)解耦,发布者无需知道订阅者的存在,订阅者也无需关注消息的来源,只需关注自己感兴趣的消息类型(频道)。优点轻量级缺点消息可靠性不高,对于业务要求持久化或者复杂的消费模式还是使用常规的MQ。





