招聘面试季-- 高频出现的Redis经典题目及核心知识点解析

一、基础核心题

  1. Redis为什么快?

    • 纯内存操作,无磁盘I/O瓶颈‌
    • 单线程模型避免上下文切换和锁竞争‌
    • I/O多路复用技术(如epoll)处理高并发连接‌
    • 高效数据结构(SDS、跳跃表、压缩列表等)‌
  2. Redis与Memcached的区别

    • 数据类型:Redis支持5种基础+3种扩展结构,Memcached仅支持String‌
    • 持久化:Redis支持RDB/AOF,Memcached无持久化‌
    • 内存管理:Memcached使用Slab Allocation,Redis支持多种淘汰策略‌
  3. 数据持久化机制

    • RDB‌:全量快照,恢复快但可能丢数据(配置触发条件)‌
    • AOF‌:追加操作日志,数据安全但文件较大(支持重写优化)‌
    • 混合模式(Redis 4.0+):RDB+AOF结合使用‌

二、高级特性题

  1. 缓存三大异常场景

    • 缓存穿透‌:非法请求击穿DB → 布隆过滤器/空值缓存‌
    • 缓存击穿‌:热点Key失效 → 互斥锁/永不过期+逻辑过期‌
    • 缓存雪崩‌:大量Key同时失效 → 随机过期时间/多级缓存‌
  2. 内存淘汰策略

    • 8种策略(如volatile-lru、allkeys-lfu等),默认noeviction‌
    • 适用场景:LRU适合时间局部性热点数据,LFU适合访问频次差异大的场景‌
  3. 分布式锁实现

    • SETNX + Lua脚本保证原子性‌
    • Redlock算法解决主从切换锁失效问题‌
    • 锁续期问题(Redisson Watchdog机制)‌

三、架构设计题

  1. 高可用方案对比

    • 主从复制‌:异步复制,数据可能丢失‌
    • 哨兵模式‌:自动故障转移,解决HA问题‌
    • Cluster集群‌:数据分片(16384 Slot),支持横向扩展‌
  2. 双写一致性方案

    • 延迟双删策略(先删缓存→更新DB→再删缓存)‌
    • 异步监听Binlog(如Canal组件)‌
  3. 热Key处理方案

    • 本地缓存+随机过期时间‌
    • 分片存储(如Key添加随机后缀)‌

四、扩展技术题

  1. 线程模型演进

    • 6.0前:单线程处理命令+多线程处理网络I/O‌
    • 6.0后:多线程网络I/O(核心命令仍单线程执行)
  2. 数据结构底层实现

    • String:SDS动态字符串(预分配/惰性删除)‌
    • Hash:ziplist(小数据)→ hashtable(大数据)‌
    • Zset:跳跃表+字典实现范围查询‌

五、场景应用题(高频)

  • 会话存储‌:利用expire特性管理登录状态‌
  • 排行榜‌:ZSET实现实时排序‌
  • 消息队列‌:List实现简单队列,Stream实现高级特性‌
  • 分布式ID‌:INCR原子操作生成唯一ID‌

延迟双删策略(先删缓存→更新DB→再删缓存)‌,请举个栗子

延迟双删策略实现过程详解(以用户信息更新场景为例)

一、核心步骤与原理
  1. 第一次删除缓存

    • 动作‌:在更新数据库操作前,先删除缓存中的旧数据(如用户ID=10的缓存)
    • 目的‌:防止后续并发读请求命中旧缓存,直接穿透到数据库获取最新数据 ‌13
  2. 更新数据库

    • 动作‌:执行数据库更新操作(如将用户姓名从"张三"改为"李四")
    • 注意‌:需保证数据库操作在事务内完成,避免部分更新成功导致数据不一致 ‌14
  3. 延迟二次删除缓存

    • 动作‌:在数据库更新成功后,通过异步任务延迟500ms-1s再次删除缓存
    • 目的‌:清除在第一次删除缓存后、数据库更新完成前的短暂窗口期内,可能被其他线程写入缓存的旧数据 ‌23
二、典型代码实现逻辑(伪代码)
def update_user(user_id, new_name):
    # 第一次删除缓存
    redis.delete(f"user:{user_id}")  # ‌:ml-citation{ref="1,3" data="citationList"}
    
    # 更新数据库(事务内操作)
    with db.transaction():
        db.execute("UPDATE users SET name=? WHERE id=?", new_name, user_id)  # ‌:ml-citation{ref="1,4" data="citationList"}
    
    # 启动异步任务执行延迟双删
    async_task(delay=500ms, task=delete_cache_again, args=(user_id))  # ‌:ml-citation{ref="3,5" data="citationList"}

def delete_cache_again(user_id):
    redis.delete(f"user:{user_id}")  # ‌:ml-citation{ref="1,3" data="citationList"}
三、关键实现细节
  1. 延迟时间设定

    • 经验值‌:通常取500ms-1s,需覆盖数据库主从同步耗时 + 并发读写时间窗口
    • 计算依据‌:统计业务读写链路的95%耗时(如数据库更新到从库同步耗时200ms,则延迟至少300ms)
  2. 异步任务实现方式

    • 消息队列‌:将二次删除任务写入Kafka/RabbitMQ,消费者延迟处理
    • 定时线程池‌:通过ScheduledExecutorService等工具实现延迟执行 ‌
  3. 异常处理机制

    • 重试策略‌:对二次删除失败的操作进行最多3次指数退避重试
    • 补偿监控‌:记录删除失败日志并告警,触发人工干预流程
四、典型异常场景模拟

场景‌:线程A更新用户数据期间,线程B触发读操作

  • 时序‌:
    1. 线程A删除缓存 → 更新DB(耗时50ms)
    2. 线程B发现缓存缺失 → 查询旧DB数据(未提交事务) → 写入缓存旧值
    3. 线程A提交事务 → 延迟500ms后再次删除缓存
  • 结果‌:最终缓存被二次删除,下次请求加载最新数据 ‌
五、策略优缺点对比
优势 局限
显著降低并发场景脏数据概率 ‌ 需维护异步任务组件(增加系统复杂度)‌
相比单一删除策略,数据一致性更强 ‌ 极端高并发下仍存在短暂不一致窗口 ‌
兼容各种数据库类型(无需特定特性支持)‌ 延迟时间需根据业务动态调整 ‌

注:此策略适用于‌写多读少‌且对一致性要求较高的场景(如电商库存更新),若需更高一致性保障可结合‌监听数据库Binlog‌方案 ‌

更简化的延迟双删策略实现方案(2025年主流实践)


基于Redisson延时队列(依赖中间件)

实现步骤

  1. 配置Redisson客户端

    # application.yml
    spring:
      redis:
        password: your_password
        cluster:
          nodes: 192.168.1.100:7001,192.168.1.101:7001
    
    @Configuration
    public class RedissonConfig {
        @Bean
        public RedissonClient redissonClient() {
            Config config = new Config();
            config.useClusterServers().addNodeAddress("redis://192.168.1.100:7001");
            return Redisson.create(config);
        }
    }
    

  2. 发送延迟删除任务

    @Service
    public class CacheService {
        @Autowired
        private RedissonClient redissonClient;
    
        public void updateData(String key, Object value) {
            // 第一次删除缓存
            redissonClient.getBucket(key).delete();  // ‌:ml-citation{ref="8" data="citationList"}
    
            // 更新数据库
            db.update(value); 
    
            // 添加延时删除任务(500ms后执行)
            RDelayedQueue delayedQueue = 
                redissonClient.getDelayedQueue(redissonClient.getQueue("delay_queue"));
            delayedQueue.offer(key, 500, TimeUnit.MILLISECONDS);  // ‌:ml-citation{ref="8" data="citationList"}
        }
    }
    

    优点

  • 无需手动管理线程池或消息队列,Redisson封装底层复杂性‌
  • 支持分布式环境,天然适配集群场景‌

拓展思考

延迟双删的过程中,redis需要加锁吗,如果不加,两个并发线程同时修改数据,如何保持一致性呢

延迟双删策略中锁的必要性与一致性保障方案

一、是否需要加锁?

无需强制加锁‌,但需结合具体场景设计补偿机制实现最终一致性‌。

  • 无锁场景适用性‌:适用于允许短暂不一致但对性能要求高的业务(如资讯类应用)‌
  • 加锁场景适用性‌:对强一致性要求严格的场景(如金融交易)需结合分布式锁‌

二、无锁场景下的并发冲突处理

典型并发时序‌(线程A和线程B同时修改同一数据):

  1. 线程A‌:删除缓存 → 更新DB → 触发延迟双删
  2. 线程B‌:在A的DB更新完成前读取旧值 → 回写旧数据到缓存
  3. 线程A延迟双删生效‌:二次删除清空线程B写入的旧缓存‌

关键保障措施‌:

  1. 延迟时间动态计算

    • 公式:延迟时间 ≥ 主从同步耗时 + 业务平均响应时间 * 2(经验值500ms-1s)‌
    • 示例:若主从同步耗时200ms,业务读写耗时150ms,则延迟至少设为500ms‌
  2. 异步重试补偿

    • 第二次删除失败时,通过消息队列(如Kafka)重试3次,间隔采用指数退避策略‌
    // 伪代码示例(Spring + RabbitMQ)
    @RabbitListener(queues = "retry_queue")
    public void retryDelete(String key) {
        if (!redis.delete(key)) {  // 删除失败触发重试
            rabbitTemplate.convertAndSend("retry_queue", key, 
                message -> { message.getMessageProperties().setDelay(5000); return message; });
        }
    }
    

  3. 版本号校验(增强版)

    • 数据更新时携带版本号,缓存回写前校验版本号新旧
    -- 数据库操作示例
    UPDATE users SET name='李四', version=version+1 WHERE id=10 AND version=old_version
    
    • 若缓存回写时发现版本号已过期,则放弃写入‌

三、加锁与无锁方案对比
方案 优点 缺点 适用场景
无锁方案 高性能、实现简单‌ 短暂不一致窗口期存在‌ 允许最终一致性的业务
加锁方案 强一致性保障‌ 性能损耗增加20%-30%‌ 金融/库存等强一致性场景

四、实践建议
  1. 监控与告警
    • 监控缓存与DB差异率,超过阈值触发自动补偿‌
  2. 降级策略
    • 高并发下关闭延迟双删,直接走缓存过期机制‌
  3. 组合方案
    • 核心业务:无锁延迟双删 + 版本号校验 + 异步重试‌
    • 非核心业务:仅用延迟双删 + 固定过期时间‌

注:2025年主流框架(如Spring 6.0)已内置@RetryableCache注解支持自动化延迟双删‌

你可能感兴趣的:(分布式和技术框架们,面试,redis,分布式)