0%

redis基础知识

Redis基础

本文收录了一些常见的redis基础题和场景题,作为个人笔记。

  • 问题:缓存穿透、缓存击穿、缓存雪崩是什么?如何解决?

    缓存穿透、缓存击穿、缓存雪崩是缓存与数据库交互中常见的性能与可用性问题,核心都是 “缓存未能有效拦截请求,导致数据库压力骤增”,但三者场景与解决思路不同:

    一、缓存穿透

    • 定义:查询一个不存在的数据(如 ID=-1 的用户),由于缓存中无此数据(无法命中),请求会直接穿透到数据库,且数据库也无此数据,导致每次请求都打到数据库。
    • 危害:若被恶意利用(如高频查询不存在的 ID),可能导致数据库被击垮。
    产生原因
    1. 业务逻辑误操作(如查询不存在的记录);
    2. 恶意攻击(如批量查询无效 ID,模拟高并发请求)。
    解决方法
    1. 缓存空值(短期有效)

      • 数据库查询结果为空时,仍将空值存入缓存(如key: null),并设置较短过期时间(如 5 分钟),避免同一无效 key 反复穿透。
      • 注意:需设置过期时间,防止缓存中积累大量空值占用空间。
    2. 布隆过滤器(Bloom Filter)前置拦截

      • 原理:在缓存前部署布隆过滤器,预先存储所有

        有效 key

        (如数据库中存在的用户 ID),请求先经过布隆过滤器校验:

        • 若布隆过滤器判断 key 不存在,直接返回空(无需查缓存和数据库);
        • 若判断存在,再走 “缓存→数据库” 流程。
      • 适用场景:有效 key 集合固定且不频繁变更(如用户 ID、商品 ID),存在一定误判率(可接受)。

    3. 接口层校验与限流

      • 对输入参数做合法性校验(如 ID 必须为正整数),直接拦截无效请求;
      • 对高频异常请求(如同一 IP 短时间大量查询无效 key)进行限流(如通过 Redis 实现 IP 级限流)。

    二、缓存击穿

    • 定义:一个热点 key(如热门商品 ID)的缓存突然失效(过期或被删除),此时大量并发请求同时访问该 key,因缓存未命中,所有请求瞬间穿透到数据库,导致数据库压力骤增。
    • 区别于穿透:击穿的 key 是真实存在的(数据库有数据),只是缓存临时失效;穿透的 key 是不存在的。
    产生原因
    1. 热点 key 的缓存过期时间设置不合理(如集中过期);
    2. 缓存服务异常(如手动删除热点 key、缓存节点宕机导致热点 key 丢失)。
    解决方法
    1. 热点 key 永不过期

      • 对核心热点 key(如秒杀商品)不设置过期时间,避免因过期导致的击穿;
      • 需配合后台定时任务更新缓存(如每小时从数据库刷新一次数据),确保缓存数据新鲜。
    2. 互斥锁(Mutex Lock)控制并发

      • 当缓存未命中时,先尝试获取分布式锁(如 Redis 的

        1
        SETNX

        ),只有获取锁的请求才能查询数据库并更新缓存,其他请求等待重试:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        // 伪代码
        String value = redis.get(key);
        if (value == null) {
        if (redis.setNx(lockKey, "1", 5000)) { // 获取锁,5秒过期
        try {
        value = db.query(key); // 查询数据库
        redis.set(key, value, 3600); // 更新缓存
        } finally {
        redis.del(lockKey); // 释放锁
        }
        } else {
        Thread.sleep(100); // 等待100ms后重试
        return query(key); // 重试获取缓存
        }
        }
        return value;
      • 注意:锁的过期时间需大于数据库查询时间,避免锁提前释放导致并发问题。

    3. 后台主动更新缓存

      • 对热点 key,在缓存过期前(如过期前 10 分钟),通过后台任务主动从数据库查询最新数据并更新缓存,避免缓存失效的 “真空期”。

    三、缓存雪崩

    • 定义:在某一时刻,大量缓存 key 同时过期,或缓存服务整体宕机(如 Redis 集群崩溃),导致所有请求无法命中缓存,全部涌向数据库,造成数据库瞬间压力过大而崩溃。
    • 区别于击穿:雪崩是 “批量 key 失效或缓存整体不可用”,影响面大;击穿是 “单个热点 key 失效”,影响范围较小。
    产生原因
    1. 大量 key 设置了相同的过期时间(如整点批量过期);
    2. 缓存集群部署在单一节点或机房,遭遇硬件故障、网络中断等导致整体不可用;
    3. 缓存服务自身 bug 或负载过高(如内存溢出)引发崩溃。
    解决方法
    1. 过期时间随机化,避免批量过期

      • 为 key 设置基础过期时间(如 30 分钟),再叠加一个随机值(如 0-10 分钟),使过期时间分散,避免同一时刻大量 key 失效:

        1
        2
        3
        int baseExpire = 30 * 60; // 基础30分钟
        int randomExpire = new Random().nextInt(10 * 60); // 随机0-10分钟
        redis.set(key, value, baseExpire + randomExpire);
    2. 多级缓存架构

      • 引入本地缓存(如 Caffeine、Guava)+ 分布式缓存(如 Redis)的多级架构:
        • 本地缓存:抗瞬时高并发,避免所有请求直接访问分布式缓存;
        • 分布式缓存:保证数据一致性,作为本地缓存的 “数据源”。
      • 即使分布式缓存雪崩,本地缓存仍能拦截部分请求。
    3. 缓存集群高可用

      • 分布式缓存(如 Redis)采用集群部署(主从 + 哨兵 / Cluster),确保单个节点宕机后,从节点自动切换为主节点,避免缓存服务整体不可用;
      • 跨机房部署缓存集群,抵御单机房故障。
    4. 熔断降级与限流

      • 当缓存服务不可用或数据库压力过高时,通过熔断组件(如 Sentinel、Hystrix)暂停部分非核心请求,仅允许核心请求访问数据库;
      • 对数据库设置限流阈值(如每秒最大 1000 次请求),超过阈值则返回降级响应(如 “系统繁忙,请稍后再试”)。
    5. 缓存预热与快速恢复

      • 系统启动或低峰期,通过脚本批量加载热点数据到缓存(缓存预热),避免高峰期缓存为空;
      • 缓存崩溃后,通过备份数据(如 RDB/AOF 文件)快速恢复缓存,减少数据库承压时间。

    总结:三者均需通过 “增强缓存拦截能力” 和 “保护数据库” 双维度解决 —— 穿透需拦截无效请求,击穿需保护热点 key,雪崩需分散风险并提升缓存可用性。实际应用中需结合业务场景选择方案(如核心业务优先用布隆过滤器 + 多级缓存,高并发场景必用互斥锁和随机过期时间)。

  • 问题:如何使用 Redis 统计上亿用户的连续登录天数?

    核心需求是高效记录登录状态+低成本存储+快速计算连续天数,Redis 通过紧凑数据结构和增量计算实现,适合上亿用户规模。

    一、核心数据结构(节省空间 + 高效操作)

    数据结构 用途 键格式示例 存储逻辑
    BitMap 记录每日登录状态 login:20250807 以用户 ID 为位偏移量,1 表示登录、0 表示未登录;1 亿用户仅需 12MB / 天。
    String 存储用户当前连续登录天数 user:continue:days:100 直接存储整数(如5表示用户 100 连续登录 5 天)。
    Set 记录当天登录用户 ID(去重) daily:login:20250807 存储当天登录的用户 ID,避免重复记录,用于后续增量计算。

    二、实现流程

    1. 实时记录用户登录状态(高并发场景)

    用户登录时,通过两步操作记录:

    • 去重:用 Set 判断用户是否已记录当天登录(避免重复操作);
    • 标记登录:若未记录,在 Set 中添加用户 ID,并在当日 BitMap 中标记该用户为 “登录”(位值设为 1)。
    2. 每日增量计算连续天数(凌晨执行)

    仅针对 “前一天登录的用户” 计算,避免全量扫描:

    • 取前一天登录用户:从 Set 中获取前一天登录的所有用户 ID;

    • 判断连续登录

      :检查该用户 “前天” 是否登录(通过前天的 BitMap):

      • 若前天登录:当前连续天数 = 原天数 + 1;
      • 若前天未登录:连续天数重置为 1;
    • 更新结果:将新的连续天数存入 String 中。

    三、关键优化(支撑上亿用户)

    1. 分片存储:按用户 ID 哈希分片(如分散到 16 个 Redis 节点),降低单节点压力;
    2. 异步批量计算:凌晨计算时,将用户分批用多线程处理,配合批量操作减少网络开销;
    3. 冷数据清理:仅保留最近 30 天的 BitMap 和 Set, older 数据归档压缩,节省空间;
    4. 容错机制:若某天计算失败,可通过 “当天登录状态 + 前一天连续天数” 重新计算。

    四、查询方式

    直接读取 String 类型的连续天数:通过user:continue:days:{用户ID}键获取整数结果。

    总结:通过 BitMap 紧凑记录登录状态、Set 实现增量计算、String 存储结果,配合分片和异步处理,高效支撑上亿用户的连续登录天数统计,核心是 “只处理当天登录用户”,避免全量扫描。

  • 问题:如何用 Redis 统计一亿个 key 场景下的双方共同好友?

    核心是利用 Redis 的 Set 数据结构高效存储好友关系,并通过集合交集运算快速计算共同好友,需兼顾存储效率计算性能

    一、数据结构设计(存储好友关系)

    用 Redis 的Set存储每个用户的好友列表,适合场景:

    • 好友关系具有 “唯一性”(不会重复添加);
    • Set 原生支持交集运算(求共同好友的核心)。
    键格式 类型 含义 示例
    user:friends:{uid} Set 存储用户uid的所有好友 ID user:friends:100{200, 300, 400}

    二、计算共同好友的核心方法

    通过 Redis 的交集命令直接计算两个用户的共同好友,无需全量扫描:

    1. 基础命令:SINTER

      • 功能:返回多个 Set 的交集(即共同元素)。

      • 示例:计算用户 100 和用户 200 的共同好友:

        1
        2
        3
        # 返回两个用户好友列表的交集
        SINTER user:friends:100 user:friends:200
        # 结果:如 {300, 500}(表示300和500是双方共同好友)

    三、亿级 key 场景的性能优化

    当用户量达亿级、好友列表庞大(如每个用户平均 100 个好友),需优化计算效率:

    1. 利用 “小集合优先” 原则

      • Redis 的SINTER内部会优化计算:优先遍历 smaller Set,再检查元素是否在 larger Set 中。

      • 实际使用时,可先通过

        1
        SCARD

        命令获取两个 Set 的大小,手动指定小 Set 在前,减少遍历次数:

        1
        2
        3
        4
        5
        # 先查两个用户的好友数
        SCARD user:friends:100 # 假设返回80
        SCARD user:friends:200 # 假设返回120
        # 小Set在前,执行交集
        SINTER user:friends:100 user:friends:200
    2. 分片存储,分散计算压力

      • 亿级 key 需 Redis 集群分片(如按用户 ID 哈希分片),避免单节点存储和计算过载;
      • 若两个用户的好友 Set 在不同分片,集群会自动协同计算交集(依赖 Redis Cluster 的跨节点命令支持)。
    3. 限制单次返回数量,分页查询

      • 若共同好友数量过多(如超过 1000),直接返回全部会占用大量带宽,可结合

        1
        SINTERSTORE
        1
        SSCAN

        分页

        1
        2
        3
        4
        5
        6
        # 1. 将交集结果暂存到临时Set(过期时间10分钟)
        SINTERSTORE temp:common:100:200 user:friends:100 user:friends:200
        EXPIRE temp:common:100:200 600

        # 2. 分页查询临时Set(每次查20个)
        SSCAN temp:common:100:200 0 COUNT 20
    4. 缓存高频查询结果

      • 对高频查询的用户对(如明星用户与粉丝),将共同好友结果缓存到 Set(设置过期时间,如 1 小时),减少重复计算:

        1
        2
        3
        4
        5
        # 若缓存存在,直接查缓存
        EXISTS cache:common:100:200
        # 不存在则计算并缓存
        SINTERSTORE cache:common:100:200 user:friends:100 user:friends:200
        EXPIRE cache:common:100:200 3600

    四、适用场与限制

    • 适用场景:社交产品(如微信、微博)的共同好友推荐、互动场景(如 “你们有 3 个共同好友”);
    • 限制:若用户好友列表极大(如超 10 万),SINTER仍可能耗时(毫秒级增至秒级),需结合业务限制好友数或异步计算。

    总结:核心是用 Set 存储好友关系,通过SINTER求交集,配合 “小集合优先”、分片存储、结果缓存等优化,在亿级 key 场景下高效统计共同好友,平衡性能与资源消耗。

  • 问题:如何用 Redis 实现上亿用户的实时积分榜?

    核心需求是高频更新分数(如用户行为实时加分)和快速查询排名(如个人排名、Top N 用户),Redis 的 Sorted Set(有序集合)是最优选择,需解决亿级规模下的性能与内存挑战。

    一、核心数据结构:Sorted Set(有序集合)

    Sorted Set 天生适合排行榜场景,通过 “成员 - 分数” 键值对存储,支持按分数排序和快速排名计算:

    键格式 类型 含义 核心命令(优化点)
    rank:board:global Sorted Set 全局积分榜(用户 ID 为成员,分数为值) ZADD(更新分数)、ZREVRANK(查排名)、ZREVRANGE(查 Top N)

    二、亿级用户的核心挑战与解决方案

    1. 单集合过大导致的性能问题(核心优化)
    • 问题:单个 Sorted Set 存储上亿用户时,ZADD(更新)和ZREVRANK(查排名)的 O (log N) 复杂度会因 N 过大(亿级)导致延迟升高(如从微秒级增至毫秒级)。
    • 解决方案:分片存储
      • 按用户 ID 哈希分片(如hash(uid) % 100),将全局榜拆分为 100 个分片(rank:board:0rank:board:99),每个分片存储约 1000 万用户;
      • 优势:单分片规模缩小 100 倍,操作延迟显著降低,且支持水平扩容(增加分片数)。
    2. 高频分数更新的效率优化
    • 批量更新用 Pipeline:用户行为(如点赞、完成任务)触发分数更新时,用 Pipeline 批量执行ZADD(如一次更新 100 个用户分数),减少网络往返次数;
    • 分数更新策略:
      • 增量更新:仅传递分数变化量(如ZINCRBY rank:board:1 5 uid100,直接加 5 分),避免全量传递分数;
      • 异步更新:非核心场景(如浏览量)可通过消息队列异步更新 Redis,降低实时写入压力。
    3. 排名查询的灵活实现

    需支持三类查询场景,结合分片策略处理:

    查询场景 实现方式
    个人排名 1. 计算用户 ID 所属分片(如shardId = uid % 100); 2. 用ZREVRANK rank:board:{shardId} uid获取分片内排名; 3. 若需全局排名,累加所有 “分数高于该用户” 的分片用户数(可缓存高频用户的全局排名)。
    Top N 用户(全局) 1. 分别查询每个分片的 Top N(ZREVRANGE rank:board:{shardId} 0 N-1); 2. 合并所有分片结果,取全局 Top N(客户端或中间层处理)。
    附近用户排名 1. 获取用户分数(ZSCORE); 2. 在分片内查询 “分数在 [score-10, score+10]” 的用户(ZRANGEBYSCORE),返回前后 N 名。
    4. 内存占用控制
    • 用户 ID 压缩:若用户 ID 为长字符串(如 UUID),映射为整数 ID(如通过数据库自增 ID),减少 Sorted Set 中 “成员” 的存储体积;
    • 冷热数据分离:长期低活跃用户(如 30 天未更新分数)迁移至 “冷榜”(单独的 Sorted Set),仅保留活跃用户在 “热榜”,降低热榜规模;
    • 过期清理:非永久榜单(如活动榜)设置过期时间(EXPIRE),自动清理无效数据。

    三、高可用与容错

    • 集群部署:用 Redis Cluster 管理分片,每个分片配置主从节点,主节点故障时从节点自动切换,避免单点失效;
    • 持久化策略:开启 AOF+RDB 混合持久化,确保分数更新不丢失(AOF 记录实时操作,RDB 做全量备份);
    • 监控告警:监控各分片的内存占用、ZADD延迟、查询 QPS,超过阈值时告警(如分片内存超 80% 触发扩容)。

    总结:核心是用 Sorted Set 存储分数,通过哈希分片解决亿级规模性能问题,结合批量更新、冷热分离优化效率,最终实现支持高频更新和灵活查询的实时积分榜。分片策略是关键,需根据用户规模动态调整分片数量(如从 100 增至 200)。

  • 问题:秒杀系统如何设计(核心目标:抗高并发、防超卖、保稳定)

    秒杀系统需应对 “瞬时流量峰值(如 10 万 QPS)”“库存精确控制”“系统不崩溃” 三大核心挑战,需从流量拦截→请求处理→库存控制→兜底防护全链路设计,具体方案如下:

    一、前端层:减少无效请求,降低入口压力

    前端是流量的第一关,通过交互限制和资源优化过滤大部分无效请求:

    1. 限流与防重复提交
      • 按钮置灰:点击后立即置灰,禁止重复点击(避免用户快速多次提交);
      • 验证码 / 排队机制:秒杀开始前弹出验证码(如滑块验证),或显示 “排队中” 提示,延缓请求发送,分散流量峰值;
      • 前端倒计时:精准同步服务器时间,避免用户因本地时间偏差提前请求(减少无效请求)。
    2. 静态资源优化
      • 秒杀页面静态化:商品图片、描述等静态资源通过 CDN 分发,减轻应用服务器压力;
      • 懒加载:非核心内容(如商品详情)延迟加载,优先加载秒杀按钮和倒计时组件。

    二、接入层:流量过滤与限流,挡住大部分请求

    通过 Nginx 和网关拦截异常流量,只允许合法请求进入后端:

    1. Nginx 限流(第一层拦截)

      • 基于 IP 限流:用

        1
        limit_req

        模块限制单 IP 每秒请求数(如 10 次 / 秒),过滤恶意刷请求的 IP;

        1
        2
        3
        4
        5
        6
        7
        8
        # Nginx配置示例:单IP每秒最多10个请求,超过则返回503
        limit_req_zone $binary_remote_addr zone=seckill:10m rate=10r/s;
        server {
        location /seckill {
        limit_req zone=seckill burst=20 nodelay; # 允许20个突发请求
        proxy_pass http://backend;
        }
        }
      • 黑名单:通过 Nginx 配置或 Lua 脚本,拦截历史恶意 IP(如频繁超时、参数异常的 IP)。

    2. 网关层处理(第二层拦截)

      • 用 Spring Cloud Gateway 或 Kong 做路由转发,同时进行:
        • 参数校验:检查商品 ID、用户 ID 是否合法(如商品是否存在、用户是否登录),直接拦截无效参数;
        • 令牌桶限流:对秒杀接口设置全局 QPS 阈值(如 5 万 QPS),超过则返回 “系统繁忙”;
        • 灰度分流:大促时将部分流量引流到备用集群,避免单集群过载。

    三、服务层:异步化 + 集群化,扛住有效请求

    后端服务需轻量、高效,聚焦 “快速处理有效请求”:

    1. 秒杀服务独立部署
      • 将秒杀接口从主业务服务中拆分,独立部署集群(如 20 台服务器),避免秒杀流量冲击其他业务(如购物车、支付)。
    2. 异步化削峰(核心)
      • 用消息队列(RabbitMQ/Kafka)承接请求:用户请求到达后,先校验库存(Redis 预减),通过后发送消息到队列,立即返回 “排队中”;
      • 消费端(独立的订单服务)从队列中取消息,异步创建订单、扣减数据库库存,避免同步处理导致的服务阻塞;
      • 优势:消息队列缓冲瞬时流量(如 10 万请求在队列中排队,消费端按 5 万 / 秒处理),防止服务被压垮。
    3. 服务集群与负载均衡
      • 秒杀服务和订单服务均集群部署,通过负载均衡(如 Nginx、K8s Service)分发请求,避免单节点过载;
      • 无状态设计:服务不存储本地数据(如会话、库存),依赖 Redis 和数据库,支持随时扩容。

    四、数据层:库存精准控制,防超卖

    库存超卖是秒杀的致命问题,需通过 “Redis 预减 + 数据库兜底 + 原子操作” 多层防护:

    1. Redis 预减库存(快速判断)

      • 秒杀前预热:将商品库存加载到 Redis(如seckill:stock:1001 → 100,1001 为商品 ID);

      • 请求到达时,用 Redis 原子命令

        1
        DECR

        预减库存(如

        1
        DECR seckill:stock:1001

        ):

        • 若结果≥0:库存充足,允许进入消息队列;
        • 若结果 <0:库存不足,直接返回 “已抢完”,无需进入后续流程;
      • 优势:Redis 单命令原子性,避免并发减库存导致的超卖(如 100 个库存,101 个请求同时减,最终结果会正确为 - 1)。

    2. 数据库兜底防超卖

      • 消息队列消费端创建订单时,执行 SQL 扣减数据库库存,用乐观锁确保最终库存正确:

        1
        2
        3
        4
        -- 仅当库存>0时才扣减,version确保并发安全
        UPDATE seckill_stock
        SET stock = stock - 1, version = version + 1
        WHERE goods_id = 1001 AND stock > 0 AND version = #{version};
      • 若 SQL 影响行数 = 0:说明库存已空,回滚订单,Redis 库存补回(INCR),避免 Redis 与数据库不一致。

    3. 库存预热与动态调整

      • 秒杀前 10 分钟,通过脚本将数据库库存同步到 Redis(避免秒杀开始时 Redis 查库压力);
      • 若秒杀中发现 Redis 与数据库库存不一致(如网络延迟导致),用定时任务(如每 10 秒)校准一次。

    五、监控与兜底:确保系统不崩溃

    1. 实时监控
      • 监控指标:接口 QPS、响应时间、Redis 库存、消息队列堆积量、数据库连接数;
      • 告警触发:队列堆积超 10 万条、Redis 内存使用率超 80%、接口错误率超 5% 时,立即告警(短信 / 邮件)。
    2. 降级与熔断
      • 降级:当系统压力过大(如 CPU 超 90%),关闭非核心功能(如商品详情页),优先保障秒杀接口;
      • 熔断:若数据库或 Redis 响应超时,暂时停止请求处理,返回 “稍后再试”,避免级联失败。
    3. 事后复盘
      • 记录秒杀日志(用户请求、库存变化、订单创建),用于分析流量峰值、超卖原因;
      • 压测优化:定期用 JMeter 模拟 10 倍流量压测,发现瓶颈(如 Redis 性能、数据库锁冲突)并优化。

    总结:秒杀系统设计核心是 “层层拦截流量 + 异步削峰 + 库存精准控制”—— 前端减少无效请求,接入层过滤异常流量,服务层用消息队列异步扛峰,数据层用 Redis + 数据库双层防超卖,最终通过监控和兜底确保系统稳定。核心原则:“能在前面挡的,绝不放后面;能异步的,绝不同步”。

  • 问题:Redis 的 RDB 和 AOF 机制是什么?有何区别?

    Redis 是内存数据库,需通过持久化机制将数据从内存写入磁盘,防止重启后数据丢失。RDB 和 AOF 是两种核心持久化方式,分别通过 “快照” 和 “命令日志” 实现,各有优劣。

    一、RDB(Redis Database):基于快照的持久化

    • 定义:在指定时间间隔内,将内存中的全量数据生成快照(二进制文件)并写入磁盘,恢复时直接加载快照文件到内存。
    核心机制
    1. 触发方式
      • 自动触发:通过redis.conf配置快照规则(如save 900 1表示 900 秒内有 1 次写操作则触发);
      • 手动触发:执行SAVE(阻塞 Redis,直到快照生成,不建议生产用)或BGSAVE(fork 子进程生成快照,主进程继续处理请求)。
    2. 文件格式:单一二进制文件(默认dump.rdb),存储数据的键值对压缩形式,体积小。
    3. 恢复过程:Redis 启动时,若检测到dump.rdb文件,自动加载该文件到内存(加载期间会阻塞客户端请求)。
    优缺点
    优点 缺点
    1. 文件体积小,适合备份(如每日备份); 2. 恢复速度快(直接加载二进制文件); 3. 对 Redis 性能影响小(BGSAVE 通过子进程处理,不阻塞主进程)。 1. 数据安全性低:若 Redis 崩溃,最近一次快照后的数据会丢失(如配置save 300 10,则可能丢失 300 秒内的数据); 2. 大内存场景下,BGSAVE fork 子进程可能阻塞主进程(毫秒级,取决于内存大小)。
    适用场景
    • 对数据完整性要求不高(允许丢失几分钟数据);
    • 需要频繁备份(如灾备场景);
    • 内存数据量大,追求快速恢复。

    二、AOF(Append Only File):基于命令日志的持久化

    • 定义:将所有写操作命令(如SETHSET)以文本形式追加到日志文件中,恢复时重新执行日志中的命令以重建数据。
    核心机制
    1. 触发方式:默认关闭,需在redis.conf中开启(appendonly yes),命令实时追加到appendonly.aof文件。
    2. 命令同步策略(通过appendfsync配置,平衡安全性与性能):
      • always:每次写命令都同步到磁盘(最安全,性能最差);
      • everysec:每秒同步一次(默认,允许丢失 1 秒内数据,性能适中);
      • no:由操作系统决定何时同步(性能最好,安全性最差)。
    3. 文件重写(Rewrite)
      • 问题:AOF 文件会随命令增多而膨胀(如多次INCR同一键会记录多条命令);
      • 解决:通过BGREWRITEAOF命令(自动或手动触发),生成 “最终状态命令”(如INCR x 10替换 10 条INCR x),压缩文件体积。
    4. 恢复过程:Redis 启动时,逐行执行 AOF 文件中的命令,重建数据(命令多则恢复慢)。
    优缺点
    优点 缺点
    1. 数据安全性高:默认每秒同步,最多丢失 1 秒数据; 2. 日志文件是文本命令,易理解和修复(如手动删除错误命令)。 1. 文件体积大(相同数据,AOF 文件通常比 RDB 大); 2. 恢复速度慢(需重新执行所有命令); 3. 高并发写场景下,always策略可能影响性能。
    适用场景
    • 对数据完整性要求高(如金融场景,不允许丢失过多数据);
    • 可接受稍慢的恢复速度和较大的文件体积。

    三、混合持久化(Redis 4.0+):结合 RDB 和 AOF 的优势

    • 机制:AOF 文件中,前半部分是 RDB 快照(全量数据),后半部分是快照生成后的增量命令日志;
    • 优势:恢复时先加载 RDB(快),再执行增量命令(数据全),兼顾 RDB 的快速恢复和 AOF 的高安全性;
    • 配置aof-use-rdb-preamble yes(默认开启)。

    四、如何选择?

    • 单种方案:追求性能和快速恢复选 RDB;追求数据安全选 AOF(everysec策略);
    • 生产环境推荐:开启混合持久化,同时保留 RDB 作为备份(如每日生成 RDB,AOF 实时记录),兼顾安全性与恢复效率。

    总结:RDB 是 “全量快照”,适合备份和快速恢复;AOF 是 “命令日志”,适合高安全性场景;混合持久化结合两者优势,是生产环境的优选。

  • 问题:Redis 单线程为什么这么快?

    Redis 的 “单线程” 指的是处理客户端请求的核心线程是单线程(后台持久化、集群同步等操作由其他线程处理)。尽管单线程理论上无法利用多核 CPU,但 Redis 凭借内存操作、高效设计和 I/O 模型优化,实现了极高的性能(单机 QPS 可达 10 万 +),核心原因如下:

    一、基于内存操作,避免磁盘 I/O 瓶颈

    Redis 的所有数据都存储在内存中,内存读写速度(微秒级)远快于磁盘(毫秒级)。单线程处理内存操作时,无需等待磁盘 I/O,天然具备高性能基础。

    二、避免多线程的 “上下文切换” 和 “锁竞争” 开销

    多线程模型中,线程切换(保存 / 恢复上下文)和锁竞争(如共享资源加锁)会消耗大量 CPU 资源。而 Redis 单线程:

    • 无需切换线程,减少了切换带来的时间损耗;
    • 无需为共享数据加锁(单线程操作天然线程安全),避免了锁竞争和死锁风险。

    三、高效的数据结构设计

    Redis 为核心场景优化了数据结构,操作复杂度低,减少了单线程的计算耗时:

    • 哈希表:Redis 的 KV 存储基于哈希表,平均查找 / 插入复杂度为 O (1);
    • 跳表:有序集合(Sorted Set)使用跳表,范围查询和排序操作复杂度为 O (logN),优于平衡树;
    • 压缩列表(ZipList)、整数集合(IntSet):对短列表、小整数集合采用紧凑存储,减少内存占用和操作耗时。

    四、I/O 多路复用技术,高效处理并发连接

    Redis 通过I/O 多路复用(如 Linux 的 epoll、Windows 的 IOCP)处理大量客户端连接:

    • 单线程通过一个事件循环,同时监听多个客户端的 Socket 连接;
    • 当某个 Socket 有数据可读 / 可写时,事件循环会触发相应操作,避免单线程因等待 I/O 而阻塞;
    • 本质是 “用单线程管理多连接”,而非 “单线程串行处理所有请求”,兼顾了并发处理和单线程的简洁性。

    五、精简的命令处理逻辑

    Redis 的命令处理流程极简:接收命令→解析命令→执行操作→返回结果,无复杂的业务逻辑或计算。单线程专注于高效执行这些轻量操作,进一步提升速度。

    六、后台线程处理 “耗时操作”

    Redis 将耗时操作(如 RDB 持久化、AOF 重写、大 key 删除)交给后台线程处理,不阻塞核心单线程:

    • 例如BGSAVE通过 fork 子进程生成快照,主进程继续处理请求;
    • 避免了单线程被长耗时任务拖累。

    总结:Redis 单线程快的核心是 “扬长避短”—— 利用内存速度优势,避免多线程开销,通过高效数据结构和 I/O 模型优化,让单线程专注于快速处理核心命令,同时将耗时操作异步化。这使得 Redis 在单线程模型下,反而比多线程模型更高效地处理高并发请求。

  • 问题:Redis 底层的多路复用(I/O Multiplexing)机制是什么?

    Redis 的单线程能高效处理数万并发连接,核心依赖I/O 多路复用技术。它允许单线程同时监控多个客户端的 Socket 连接,仅在连接有数据可读 / 可写时才进行处理,避免了单线程因等待 I/O 而阻塞,极大提升了并发处理能力。

    一、为什么需要多路复用?

    Redis 是单线程处理客户端请求的(核心逻辑单线程),而客户端与 Redis 的通信基于 Socket,存在以下问题:

    • 若单线程逐个处理 Socket,当某个 Socket 无数据时,会陷入阻塞(等待数据),导致其他有数据的 Socket 无法被及时处理;
    • 若为每个 Socket 创建线程,会引发线程切换和锁竞争的巨大开销,反而降低性能。

    多路复用技术解决了这一矛盾:单线程通过一个 “监听器” 同时监控所有 Socket,仅处理 “有数据就绪” 的 Socket,无需阻塞等待,实现 “单线程高效处理多连接”。

    二、Redis 的多路复用模型:适配不同操作系统

    Redis 会根据操作系统自动选择最优的多路复用模型,核心实现如下:

    操作系统 多路复用模型 特点
    Linux epoll 性能最优,支持海量连接(无最大描述符限制),事件驱动型
    BSD(Mac OS) kqueue 与 epoll 类似,高效支持大量连接
    Solaris /dev/poll 适用于 Solaris 系统,支持高并发
    Windows IOCP Windows 平台的异步 I/O 模型

    Linux 下的 epoll 是最常用的实现,也是 Redis 高性能的关键,以下重点讲解 epoll 的工作原理。

    三、epoll 的工作原理(以 Linux 为例)

    epoll 通过 “事件驱动” 模式实现多路复用,核心是 3 个系统调用和 “就绪事件列表”:

    1. epoll_create:创建一个 epoll 实例(内核中的事件表),用于管理需要监控的 Socket。
    2. epoll_ctl:向 epoll 实例注册 / 修改 / 删除需要监控的 Socket 及事件类型(如 “读事件”:Socket 有数据可读;“写事件”:Socket 可写入数据)。
    3. epoll_wait:阻塞等待 epoll 实例中 “就绪的事件”(如某 Socket 有数据到达),返回就绪事件列表(仅包含有数据的 Socket)。

    四、Redis 如何使用 epoll 处理客户端请求?

    Redis 的事件循环(Event Loop)是多路复用的核心载体,流程如下:

    1. 注册事件

      • 客户端连接 Redis 时,Redis 会创建一个 Socket,并通过epoll_ctl向 epoll 实例注册 “读事件”(等待客户端发送命令);
      • 当 Redis 需要向客户端发送响应时,注册 “写事件”(等待 Socket 可写入)。
    2. 事件循环(核心流程)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      while (1) {
      // 1. 等待就绪事件(通过epoll_wait获取)
      就绪事件列表 = epoll_wait(epoll实例, 超时时间);

      // 2. 处理就绪事件
      对于每个就绪事件:
      if (是读事件):
      从Socket读取客户端命令 → 解析并执行 → 生成响应;
      若有响应需要发送,注册“写事件”;
      if (是写事件):
      将响应写入Socket → 完成后移除“写事件”;

      // 3. 处理时间事件(如定时任务:过期键清理、AOF重写检查等)
      处理到期的时间事件;
      }
    3. 高效性关键

      • 只处理就绪事件epoll_wait返回的是 “已就绪” 的 Socket,无需遍历所有连接,复杂度为 O (1);
      • 边缘触发(ET 模式):Redis 使用 epoll 的边缘触发模式,仅在 Socket 状态从 “无数据” 变为 “有数据” 时触发一次事件,减少事件处理次数(比水平触发更高效)。

    五、Redis 的事件类型:文件事件与时间事件

    多路复用监控的事件分为两类,由事件循环统一处理:

    • 文件事件:与 Socket I/O 相关的事件(客户端连接、命令读取、响应发送),是 epoll 监控的核心;
    • 时间事件:定时任务(如每隔 100ms 检查过期键、每隔 1s 触发 AOF 重写检查),由 Redis 自己维护的定时器驱动,在事件循环中穿插处理。

    总结:Redis 的多路复用通过 epoll(或其他系统的同类模型)实现 “单线程监控多 Socket”,仅处理就绪事件,避免 I/O 阻塞;配合事件循环统一调度文件事件和时间事件,最终让单线程高效支撑数万并发连接,是 Redis 高性能的核心技术之一。

  • 问题:Redis 的过期键删除策略是什么?

    Redis 的过期键删除并非依赖单一机制,而是通过三种策略配合,平衡 CPU 资源消耗与内存占用,核心包括:惰性删除定期删除内存淘汰机制

    一、惰性删除(Lazy Expiration)

    核心逻辑:键过期后不主动删除,仅在 “被访问时” 才检查是否过期,若过期则删除并返回空。

    • 触发时机:客户端执行GETHGET等访问命令时,Redis 会先校验键的过期时间。
    • 优点:
      • 完全按需删除,不占用额外 CPU 资源(无需主动扫描过期键),对 CPU 友好。
    • 缺点:
      • 若过期键长期未被访问,会一直占用内存,可能导致 “内存泄漏”(过期键堆积)。

    二、定期删除(Periodic Expiration)

    核心逻辑:每隔一段时间主动扫描部分过期键并删除,弥补惰性删除的内存占用问题。

    • 实现机制
      1. 定时触发:默认每 100ms(通过hz配置调整,1-500 范围)执行一次。
      2. 抽样扫描:每次随机抽取 20 个设置了过期时间的键,删除其中已过期的。
      3. 循环重试:若这 20 个键中过期比例超 25%,重复抽样(直到比例≤25% 或达时间上限),避免过期键集中堆积。
      4. 时间控制:每次执行不超过 25ms,防止阻塞主线程(单线程模型下,过长阻塞影响响应)。
    • 优点
      • 主动清理部分过期键,减少内存浪费,缓解惰性删除的内存泄漏风险。
    • 缺点
      • 抽样扫描可能漏掉部分过期键(仍需惰性删除兜底);扫描频率过高会占用 CPU,过低则清理不及时。

    三、内存淘汰机制(Memory Eviction Policies)

    核心逻辑:当 Redis 内存达到maxmemory(最大内存限制)时,即使过期键未被删除,也会强制删除部分键释放内存,作为前两种策略的兜底。

    • 常见策略(Redis 6.2+):

      策略 说明
      volatile-lru 从 “设过期时间的键” 中,删除最近最少使用的键
      allkeys-lru 从 “所有键” 中,删除最近最少使用的键
      volatile-lfu 从 “设过期时间的键” 中,删除最近最少频率使用的键
      allkeys-lfu 从 “所有键” 中,删除最近最少频率使用的键
      noeviction(默认) 不删除键,内存不足时拒绝新写入(返回错误)
    • 作用:确保 Redis 在内存满时仍能运行,避免因内存溢出崩溃。

    总结:三种策略协同工作 —— 惰性删除按需清理(省 CPU),定期删除主动抽样(控内存),内存淘汰兜底(保运行),最终在 “CPU 效率” 与 “内存占用” 之间找到平衡,支撑 Redis 高并发场景下的稳定运行。

  • 问题:Redis 分布式锁如何实现?

    分布式锁用于解决多节点(如微服务集群)对共享资源的并发访问问题,核心需求是互斥性(同一时间仅一个节点持有锁)、安全性(不被其他节点误释放)、防死锁(锁最终能被释放)。Redis 通过原子命令、红最终能被释放)。Redis 通过原子命令、红锁算法等实现,具体方案如下:

    一、基础实现:基于单节点 Redis 的分布式锁

    利用 Redis 的原子命令保证锁的互斥性,是最常用的基础方案。

    1. 获取锁(加锁)

    通过SET命令的扩展参数,确保 “判断锁是否存在 + 设置锁” 的原子性:

    • NX:仅当锁键不存在时才设置(保证互斥,只有第一个请求能成功);
    • PX:设置过期时间(避免节点宕机导致锁永久存在,防死锁);
    • 随机值:作为锁值,标识持有锁的节点(避免释放其他节点的锁)。
    1
    2
    3
    # 命令格式:SET 锁键 随机值 NX PX 过期时间(毫秒)
    SET lock:resource "uuid-123" NX PX 30000
    # 成功返回OK(获取锁成功),失败返回nil(锁已被持有)
    2. 释放锁(解锁)

    需先验证锁的持有者是否为自己,再删除锁,必须用 Lua 脚本保证两步原子性(避免 “验证后锁过期,误删新锁”):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    -- Lua脚本:仅当锁值匹配时才删除锁
    if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1]) -- 释放锁
    else
    return 0 -- 不操作(锁已被其他节点持有)
    end

    # 执行脚本:KEYS[1]为锁键,ARGV[1]为加锁时的随机值
    EVAL "上述脚本" 1 lock:resource "uuid-123"

    二、关键问题与优化方案

    1. 锁超时问题(锁释放但业务未完成)
    • 问题:若业务处理时间超过锁的过期时间,锁会自动释放,可能导致多个节点同时操作资源。
    • 优化:
      • 锁续约:使用 “看门狗” 机制(如 Redisson 的watch dog),持有锁的节点定期(如每 10 秒)延长锁的过期时间(前提是业务仍在处理);
      • 合理设置过期时间:根据业务最大耗时设置(如实际耗时 5 秒,设 10 秒过期)。
    2. 主从架构下的锁丢失问题
    • 问题:Redis 主从同步为异步,主节点加锁后未同步到从节点即宕机,从节点升级为主节点后,新节点会允许其他请求加锁(导致锁丢失)。
    • 优化:引入Redlock(红锁)算法,通过多实例投票解决单节点依赖问题。

    三、Redlock(红锁)算法:解决主从一致性问题

    Redlock 是 Redis 官方提出的增强方案,基于 “多个独立 Redis 实例” 实现,适用于对锁可靠性要求极高的场景。

    1. 核心设计思路
    • 部署 5 个完全独立的 Redis 节点(无主从、无集群关系);
    • 加锁时,向所有节点尝试加锁,仅当超过半数(≥3 个)节点加锁成功,且总耗时不超过锁过期时间的 1/3,才算整体加锁成功;
    • 解锁时,向所有节点发送解锁命令(无论该节点是否加锁成功)。
    2. 具体步骤
    1. 客户端获取当前时间戳(毫秒)

    2. 向 5 个节点依次发送加锁请求(使用基础方案的SET NX PX命令,锁值相同,过期时间统一,如 30 秒);

    3. 计算加锁总耗时

      (当前时间 - 步骤 1 的时间戳):

      • 若总耗时 > 锁过期时间 → 加锁失败,向所有节点发送解锁命令;
      • 若成功加锁的节点数 ≥3 → 加锁成功,锁的实际有效期 = 过期时间 - 总耗时;
    4. 执行业务逻辑:需在 “实际有效期” 内完成,否则锁可能失效;

    5. 解锁:向所有 5 个节点发送解锁命令(用 Lua 脚本验证并删除锁)。

    3. 优缺点
    • 优点:通过多实例投票,大幅降低单节点故障导致的锁丢失风险,可靠性更高;
    • 缺点:
      • 部署成本高(需 5 个独立节点);
      • 加锁耗时增加(需等待多个节点响应);
      • 仍存在极端场景漏洞(如时钟漂移导致的锁有效性判断错误)。

    四、生产环境推荐:使用成熟框架(如 Redisson)

    手动实现分布式锁易遗漏细节(如续约、红锁逻辑),生产环境建议使用封装好的框架,以 Redisson 为例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // Redisson基础锁示例(自动续约、防死锁)
    RedissonClient redisson = Redisson.create(config);
    RLock lock = redisson.getLock("lock:resource");
    lock.lock(30, TimeUnit.SECONDS); // 加锁,30秒过期(自动续约)
    try {
    // 执行业务逻辑
    } finally {
    lock.unlock(); // 释放锁
    }

    // Redisson红锁示例
    RLock lock1 = redisson1.getLock("lock:resource");
    RLock lock2 = redisson2.getLock("lock:resource");
    RLock lock3 = redisson3.getLock("lock:resource");
    RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
    redLock.lock(30, TimeUnit.SECONDS); // 红锁加锁
    try {
    // 执行业务逻辑
    } finally {
    redLock.unlock(); // 红锁释放
    }
    • 特性:自动实现锁续约、支持红锁算法、可重入锁(同一节点可多次获取同一锁)、公平锁等。

    总结:基础方案通过SET NX PX加锁 + Lua 解锁实现,适合大多数场景;Redlock 通过多实例投票提升可靠性,适合高要求场景;生产环境优先使用 Redisson 等框架,避免手动实现的细节漏洞。核心原则:确保互斥性、防死锁、兼容分布式环境的一致性问题。

  • 问题:Redis(缓存)和 MySQL(数据库)如何保证数据一致性?

    Redis 作为缓存加速读取,MySQL 作为持久化存储,两者的数据一致性指 “缓存中的数据与数据库中的数据保持一致”。核心挑战是读写顺序冲突(如更新数据库后未同步更新缓存)和并发操作干扰(如多线程同时读写),需通过合理的 “读写策略 + 异常处理” 保障一致性。

    一、核心原则:明确缓存的角色与更新策略

    缓存的核心是 “加速读取”,而非存储权威数据(权威数据在 MySQL),因此一致性策略需围绕 “以数据库为准,缓存按需同步” 设计,避免缓存成为数据不一致的源头。

    二、读取策略:缓存未命中时的正确处理

    读取数据时,需按 “先查缓存,缓存缺失再查数据库并更新缓存” 的流程,避免直接读取数据库导致缓存失效:

    1. 基础流程

      1
      2
      3
      4
      1. 客户端请求数据时,先查询Redis缓存;
      2. 若缓存命中(存在且未过期),直接返回缓存数据;
      3. 若缓存未命中,查询MySQL数据库;
      4. 将数据库查询结果写入Redis(设置合理过期时间),再返回给客户端。
    2. 并发读优化

      • 当缓存失效且高并发查询同一数据时,可能导致 “缓存击穿”(大量请求直接查数据库),需加分布式锁控制:仅允许一个线程查库并更新缓存,其他线程等待重试。

    三、更新策略:数据变更时的缓存同步

    更新数据(新增 / 修改 / 删除)时,需同步处理缓存,核心是 “如何协调数据库更新与缓存更新的顺序”,常见方案如下:

    1. Cache Aside Pattern(缓存旁路模式,最常用)

    核心逻辑:更新数据库后,删除缓存(而非直接更新缓存),下次读取时再从数据库加载最新数据到缓存。

    • 步骤

      1
      2
      3
      1. 更新操作:先更新MySQL数据库;
      2. 再删除Redis中对应的缓存(而非更新缓存);
      3. 后续读取时,缓存未命中,从数据库加载新数据并写入缓存。
    • 为什么删缓存而非更新缓存?

      • 避免 “更新缓存” 与 “更新数据库” 的内容不一致(如复杂数据结构更新容易出错);
      • 减少一次写缓存的开销,尤其当更新频率远高于读取频率时。
    • 潜在问题与解决

      • 问题 1

        :先更数据库→删缓存之间,若有新请求读取,可能读到旧缓存(概率低,因时间窗口短)。

        • 解决:缓存设置较短过期时间,即使出现旧数据,也会很快过期。
      • 问题 2

        :若删缓存失败(如 Redis 宕机),会导致缓存中一直是旧数据。

        • 解决:通过重试机制(如消息队列)确保缓存删除成功,或定期全量同步缓存(兜底)。
    2. 延迟双删:解决并发更新与读取的冲突

    当 “更新数据库” 和 “读取数据” 并发执行时,可能出现以下异常流程:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    线程A:更新数据库(旧→新)→ 准备删缓存  
    线程B:查询缓存(未命中)→ 查询数据库(恰好读到线程A更新后的新数据)→ 写入缓存(新数据)
    线程A:删除缓存(删除的是线程B刚写入的新数据)
    → 后续读取会重新加载新数据,无问题?不,若顺序相反:

    线程A:更新数据库(旧→新)
    线程B:查询缓存(未命中)→ 查询数据库(旧数据,因线程A的更新未提交)→ 写入缓存(旧数据)
    线程A:删除缓存(未执行或失败)
    → 缓存中留存旧数据,与数据库新数据不一致。

    延迟双删方案

    1
    2
    3
    1. 先删除缓存;  
    2. 更新数据库;
    3. 延迟一段时间(如500ms),再次删除缓存。
    • 第一次删缓存:避免更新数据库期间,旧缓存被读取;
    • 延迟再次删缓存:清除可能被并发线程写入的旧缓存(如上述线程 B 的情况);
    • 延迟时间:根据业务处理耗时设置(略大于一次数据库事务的时间)。
    3. Write Through(写透模式)

    核心逻辑:更新数据时,先更新缓存,再更新数据库,确保缓存与数据库同时写入成功。

    • 步骤

      1
      2
      3
      1. 客户端更新数据时,先更新Redis缓存;  
      2. Redis同步更新MySQL数据库;
      3. 两者都成功后,返回更新成功。
    • 优点:缓存与数据库强一致(同时更新);

    • 缺点:增加一次写缓存的耗时,降低更新性能;若数据库更新失败,需回滚缓存(复杂度高)。

    • 适用场景:对一致性要求极高,但更新频率低的场景(如金融核心数据)。

    4. 基于 Binlog 的异步同步(最终一致性)

    通过监听 MySQL 的 Binlog(二进制日志),异步更新 Redis 缓存,适合高并发场景(牺牲强一致性,保证最终一致)。

    • 实现
      1. 部署中间件(如 Canal)监听 MySQL 的 Binlog,解析数据变更;
      2. 中间件将变更信息发送到消息队列(如 Kafka);
      3. 消费端从队列获取变更信息,异步更新 Redis 缓存。
    • 优点
      • 解耦数据库与缓存更新,不影响主业务链路性能;
      • 可批量处理更新,适合高并发写入场景。
    • 缺点:存在短暂的数据不一致(从数据库更新到缓存同步有延迟);
    • 适用场景:允许短暂不一致的非核心业务(如商品详情、用户动态)。

    四、异常处理:确保极端情况的一致性

    1. 缓存宕机
      • 降级策略:直接查询数据库,不写入缓存(避免缓存恢复后数据混乱);
      • 恢复后:通过定时任务从数据库全量加载热点数据到缓存。
    2. 数据库宕机
      • 只读缓存:若数据库不可用,暂时返回缓存数据(需容忍可能的旧数据);
      • 禁止写入:避免缓存更新后,数据库恢复时无法同步(导致数据丢失)。
    3. 网络分区
      • 若 Redis 与 MySQL 之间网络中断,暂停缓存更新,仅更新数据库;
      • 网络恢复后,通过 Binlog 同步或全量校验修复缓存。

    五、总结:根据业务选择策略

    • 强一致性场景(如支付、库存):优先用 Cache Aside + 延迟双删,或 Write Through,确保数据实时一致;
    • 高并发场景(如电商商品):用 Cache Aside+Binlog 异步同步,接受短暂不一致,换取性能;
    • 核心原则:以数据库为权威,缓存仅作为加速层;通过 “删除缓存而非更新” 减少不一致风险;结合异步同步和定时校验作为兜底。

    没有完美的方案,需在 “一致性”“性能”“复杂度” 之间权衡,优先保证核心业务的数据正确性。

  • 问题:Redis 集群方案有哪些?各有什么特点?(含 Redis Sharding)

    Redis 集群方案用于解决单节点的性能瓶颈容量限制单点故障风险,主流方案包括:主从复制哨兵模式(Sentinel)Redis ClusterRedis Sharding(客户端分片),分别适用于不同规模和需求场景。

    一、主从复制(Master-Slave Replication)

    核心逻辑:通过 “一主多从” 架构,主节点处理写操作,从节点复制主节点数据并分担读操作,实现读写分离和数据备份。

    1. 架构与原理
    • 角色:1 个主节点(可写)+ N 个从节点(只读);
    • 数据同步:
      • 初始化:从节点启动时发送SYNC命令,主节点生成 RDB 快照并同步,后续通过 “命令缓冲区” 增量同步新写命令;
      • 偏移量机制:主从节点通过 “复制偏移量” 确保数据同步完整性。
    2. 核心作用
    • 读写分离:主节点承担写请求,从节点分担读请求(如 90% 读请求分配到从节点);
    • 数据备份:从节点存储完整数据,避免单节点故障导致数据丢失;
    • 负载均衡:分散读压力,突破单节点 CPU / 网络瓶颈。
    3. 优缺点
    优点 缺点
    1. 架构简单,易部署; 2. 有效提升读性能; 3. 实现数据冗余备份。 1. 主节点故障需手动切换(无自动故障转移); 2. 主节点写入压力集中; 3. 从节点同步存在延迟(可能导致读写不一致)。
    4. 适用场景
    • 读多写少的场景(如电商商品详情页、新闻资讯);
    • 对可用性要求不高(可接受手动故障转移);
    • 数据量中等(单主节点内存可容纳)。

    二、哨兵模式(Sentinel)

    核心逻辑:在主从复制基础上,增加 “哨兵节点” 监控主从状态,主节点故障时自动将从节点升级为主节点,解决主从复制的 “手动故障转移” 问题。

    1. 架构与原理
    • 角色:1 个主节点 + N 个从节点 + M 个哨兵节点(通常 3 个,奇数,避免脑裂);
    • 哨兵功能:
      • 监控:定期PING节点判断存活状态;
      • 通知:节点故障时通过 API 通知应用或其他哨兵;
      • 自动故障转移:主节点宕机后,投票选举新主节点并重新配置从节点;
      • 配置管理:客户端通过哨兵获取当前主节点地址(无需硬编码)。
    2. 核心作用
    • 自动故障转移:主节点故障后秒级切换,减少人工干预;
    • 高可用保障:多哨兵节点避免单点判断错误(如网络抖动误判);
    • 简化客户端接入:客户端只需连接哨兵,无需关心主节点变更。
    3. 优缺点
    优点 缺点
    1. 实现自动故障转移,提升可用性; 2. 兼容主从复制的读写分离和备份能力; 3. 部署难度低于分布式集群。 1. 仍存在单主节点写入瓶颈; 2. 数据存储受限于单主节点内存(无法分片); 3. 哨兵集群本身需维护。
    4. 适用场景
    • 对可用性要求高(需自动故障转移),但数据量中等(单主节点可容纳);
    • 读多写少,写操作压力未超过单主节点瓶颈(如 QPS≤10 万);
    • 如社交应用的用户会话存储、中小规模电商的库存缓存。

    三、Redis Cluster(官方分布式集群)

    核心逻辑:Redis 官方分布式方案,通过 “分片存储” 将数据分散到多个主节点,每个主节点对应从节点,支持自动故障转移,解决海量数据和高并发问题。

    1. 架构与原理
    • 角色:N 个主节点(默认 3 个以上)+ 每个主节点对应 1 个以上从节点;
    • 分片机制:
      • 数据划分为 16384 个 “哈希槽”,每个主节点负责一部分槽(如 3 主节点各负责 5461/5461/5462 个槽);
      • 路由规则:槽编号 = CRC16(key) % 16384,按槽路由到对应主节点;
    • 故障转移:主节点宕机后,其从节点通过选举升级为主节点,接管哈希槽。
    2. 核心作用
    • 分片存储:突破单节点内存限制(如 10 个主节点支持 10 倍容量);
    • 分布式高可用:多主节点分担写压力,单个节点故障不影响整体;
    • 自动扩缩容:支持动态添加 / 删除节点,自动重新分配哈希槽(无需停机)。
    3. 优缺点
    优点 缺点
    1. 支持海量数据存储(分片机制); 2. 多主节点分担写压力,提升并发; 3. 自带故障转移,高可用; 4. 官方原生支持,兼容性好。 1. 架构复杂,部署和维护成本高; 2. 跨槽操作(如MGET多 key 分属不同槽)需特殊处理; 3. 数据迁移(扩缩容)可能短暂影响性能。
    4. 适用场景
    • 数据量大(单节点内存不足,如超过 100GB);
    • 高并发读写(单主节点无法承载,如写 QPS>10 万);
    • 大规模分布式系统(如电商平台、支付系统的核心缓存)。

    四、Redis Sharding(客户端分片)

    核心逻辑:由客户端(或 SDK)通过哈希算法将数据分散到多个独立 Redis 实例,实现数据分片,本质是 “客户端主导的分布式存储”,无中心化协调节点。

    1. 架构与原理
    • 架构:多个独立 Redis 实例(无主从关系)+ 带分片逻辑的客户端(如 Jedis、redis-py);
    • 分片机制:
      • 客户端通过哈希算法(如CRC32(key) % 实例数量、一致性哈希)计算 key 对应的实例;
      • 读写时直接路由到目标实例,各实例独立存储数据(无同步)。
    2. 核心作用
    • 数据分片:将海量数据分散到多个实例,突破单实例内存限制;
    • 性能分摊:读写请求分散到多个实例,提升整体吞吐量;
    • 架构简单:无需部署额外组件,仅需客户端实现分片逻辑。
    3. 优缺点
    优点 缺点
    1. 架构极简,无额外集群组件(运维成本低); 2. 客户端直接操作实例,无中间代理开销; 3. 兼容所有 Redis 版本,灵活性高。 1. 无自动故障转移(实例宕机后对应数据不可用); 2. 扩缩容困难(增减实例导致路由规则变化,需迁移大量数据); 3. 客户端逻辑复杂(需实现分片、故障重试等); 4. 不支持跨实例操作(如MGET多 key 分布在不同实例时需多次请求)。
    4. 适用场景
    • 早期分布式场景,数据量中等且增长稳定(扩缩容频率低);
    • 客户端可控(如自研客户端或成熟 SDK 支持分片);
    • 对可用性要求不高(可接受手动处理故障),如内部系统的缓存、日志存储。

    总结:四种方案对比与选择

    方案 核心能力 适用场景
    主从复制 读写分离 + 数据备份 读多写少,可用性要求低(手动故障转移)
    哨兵模式 自动故障转移 + 主从能力 中规模数据,需高可用(自动切换)
    Redis Cluster 分片存储 + 分布式高可用 海量数据 + 高并发,需自动扩缩容
    Redis Sharding 客户端分片,无中心化 早期简单分布式,扩缩容少,客户端可控

    选择时需权衡数据量并发压力可用性要求运维成本,避免过度设计(如小场景用 Cluster)或功能不足(如高可用场景用 Sharding)。

  • 问题:Redis 如何配置过期时间?删除过期键的原理是什么?

    一、Redis 配置过期时间的方法

    Redis 通过特定命令为键设置过期时间(生存时间或过期时刻),过期后键会被标记为 “过期”,最终通过删除机制清理。核心命令如下:

    1. 为已存在的键设置过期时间(相对时间)
    • **EXPIRE key seconds**:设置键的生存时间(秒级),到期后键过期。
      示例:EXPIRE user:100 3600 → 用户 100 的信息 1 小时后过期。
    • **PEXPIRE key milliseconds**:设置键的生存时间(毫秒级),精度更高。
      示例:PEXPIRE order:200 15000 → 订单 200 的信息 15 秒后过期。
    2. 为已存在的键设置过期时刻(绝对时间)
    • **EXPIREAT key timestamp**:设置键的过期 Unix 时间戳(秒级),到达该时刻后过期。
      示例:EXPIREAT task:300 1691234567 → 任务 300 在指定时间戳(2023-08-05 12:02:47)过期。
    • **PEXPIREAT key milliseconds-timestamp**:设置键的过期 Unix 时间戳(毫秒级)。
    3. 新建键时直接指定过期时间

    通过SET命令的扩展参数,创建键的同时设置过期时间,避免 “先创建再设过期” 的两步操作:

    • SET key value EX seconds:秒级过期(等价于SET+EXPIRE)。
      示例:SET code:400 "123456" EX 60 → 验证码 123456 60 秒后过期。
    • SET key value PX milliseconds:毫秒级过期。
    4. 查看与移除过期时间
    • **TTL key**:返回键的剩余生存时间(秒级,-1 表示永不过期,-2 表示已过期)。
    • **PTTL key**:返回键的剩余生存时间(毫秒级)。
    • **PERSIST key**:移除键的过期时间(键变为永不过期)。

    二、删除过期键的原理:三种机制协同工作

    Redis 不会在键 “恰好过期时” 立即删除,而是通过惰性删除定期删除内存淘汰机制三种策略配合,平衡 CPU 资源与内存占用。

    1. 惰性删除(Lazy Expiration)
    • 核心逻辑:键过期后不主动删除,仅在 “被访问时” 才检查是否过期,若过期则删除并返回空。
    • 触发时机:客户端执行GETHGET等访问命令时,Redis 会先校验键的过期时间。
    • 优点:完全按需删除,不占用额外 CPU 资源(无需主动扫描)。
    • 缺点:若过期键长期未被访问,会一直占用内存(可能导致 “内存泄漏”)。
    2. 定期删除(Periodic Expiration)
    • 核心逻辑:每隔一段时间主动扫描部分过期键并删除,弥补惰性删除的内存占用问题。
    • 执行机制:
      1. 默认每 100ms(通过hz配置调整,范围 1-500)执行一次;
      2. 随机抽取 20 个设置了过期时间的键,删除其中已过期的;
      3. 若这 20 个键中过期比例超过 25%,重复抽样扫描(直到比例≤25% 或达时间上限);
      4. 每次执行时间不超过 25ms(避免阻塞主线程)。
    • 优点:主动清理部分过期键,减少内存浪费。
    • 缺点:抽样可能漏掉部分过期键(需惰性删除兜底)。
    3. 内存淘汰机制(Memory Eviction)
    • 核心逻辑:当 Redis 内存达到maxmemory(最大内存限制)时,即使过期键未被删除,也会强制删除部分键释放内存(作为前两种机制的兜底)。

    • 常见策略

      (Redis 6.2+):

      • volatile-lru:从 “设过期时间的键” 中删除最近最少使用的键;
      • allkeys-lru:从 “所有键” 中删除最近最少使用的键;
      • noeviction(默认):不删除键,内存不足时拒绝新写入。

    三、总结

    • 配置过期时间:通过EXPIRESET EX等命令设置相对 / 绝对过期时间,支持秒级 / 毫秒级精度。
    • 删除过期键原理:三种机制协同 —— 惰性删除按需清理(省 CPU),定期删除主动抽样(控内存),内存淘汰兜底(保运行),最终在 “CPU 效率” 与 “内存占用” 之间平衡,确保 Redis 高效稳定运行。
  • 问题:Redis 主从复制的核心原理是什么?

    Redis 主从复制(Master-Slave Replication)是通过 “主节点数据同步到从节点” 实现的分布式部署方案,核心是从节点自动复制主节点的数据,从而实现读写分离、数据备份和负载均衡。其核心原理可拆解为 “连接建立→初始化同步→增量同步→状态维护” 四个阶段。

    一、核心目标

    • 数据一致性:从节点数据与主节点保持一致(最终一致,允许短暂延迟);
    • 读写分离:主节点处理写操作,从节点处理读操作,分散压力;
    • 冗余备份:从节点存储完整数据,主节点故障时可作为备份。

    二、核心原理:四阶段同步流程

    1. 连接建立:从节点主动连接主节点

    从节点通过配置(如slaveof master_ip master_port)指定主节点地址,启动后主动发起连接:

    • 从节点向主节点发送PING命令,确认主节点存活;
    • 主节点返回PONG后,从节点发送身份验证命令(若主节点配置requirepass);
    • 验证通过后,从节点发送REPLCONF listening-port <port>告知主节点自己的监听端口,主从连接正式建立。
    2. 初始化同步:全量复制主节点数据

    首次连接或从节点数据与主节点差异过大时,触发全量同步,确保从节点初始化完整数据:

    • 步骤 1:主节点生成 RDB 快照
      从节点发送SYNC(Redis 2.8 前)或PSYNC(Redis 2.8 后,支持部分同步)命令,请求同步数据;
      主节点收到命令后,执行BGSAVE生成 RDB 快照(后台异步,不阻塞主节点处理写请求),同时将快照生成期间的新写命令存入 “复制缓冲区”(repl buffer)。
    • 步骤 2:主节点发送 RDB 快照给从节点
      RDB 生成后,主节点将快照文件发送给从节点;
      从节点接收完成后,清空本地旧数据,加载 RDB 快照(此过程会阻塞从节点,无法处理读请求)。
    • 步骤 3:主节点同步缓冲区命令
      从节点加载完 RDB 后,主节点将 “复制缓冲区” 中快照生成期间的新写命令发送给从节点;
      从节点执行这些命令,最终与主节点数据完全一致。
    3. 增量同步:实时同步新写命令

    初始化同步完成后,进入增量同步阶段,主节点实时将新写命令同步给从节点:

    • 主节点记录写命令:主节点每处理一个写命令(如SETHSET),都会将命令写入 “复制缓冲区”,并记录自己的 “复制偏移量”(offset,累计命令字节数)。
    • 从节点确认同步进度:从节点执行完主节点发送的命令后,会向主节点汇报自己的 “复制偏移量”(表示已处理到哪个位置)。
    • 主节点按需发送命令:主节点对比自身偏移量与从节点偏移量,将从节点未处理的命令(复制缓冲区中偏移量之后的部分)发送给从节点,从节点执行后完成同步。
    4. 状态维护:心跳检测与断线重连
    • 心跳检测
      主从连接建立后,从节点每隔 1 秒向主节点发送REPLCONF ACK <offset>命令,包含自己的复制偏移量:
      • 主节点通过该命令确认从节点存活;
      • 主节点对比偏移量,若发现从节点落后,触发增量同步。
    • 断线重连
      若网络中断,从节点会定期重试连接主节点;
      重连成功后,从节点发送PSYNC命令,主节点通过 “复制积压缓冲区”(repl_backlog_buffer)判断是否可进行增量同步(若从节点偏移量仍在缓冲区范围内,则增量同步;否则触发全量同步)。

    三、关键技术:避免全量同步的核心机制

    • 复制积压缓冲区:主节点维护一个固定大小的环形缓冲区(默认 1MB,可通过repl-backlog-size配置),存储最近的写命令;从节点断线重连时,若其偏移量仍在缓冲区范围内,主节点直接发送偏移量后的命令(增量同步),避免全量同步。
    • PSYNC命令:相比旧版SYNC(仅支持全量同步),PSYNC通过 “主节点运行 ID” 和 “从节点偏移量” 判断同步方式,大幅减少全量同步的频率(全量同步耗时且占用带宽)。

    四、总结

    Redis 主从复制的核心是 “初始化全量复制 + 实时增量同步”:

    1. 从节点主动连接主节点,通过PSYNC触发同步;
    2. 首次同步时,主节点生成 RDB 并发送,同时缓存期间的新命令,从节点加载 RDB 后执行缓存命令;
    3. 日常通过增量同步,主节点实时发送新写命令,从节点通过偏移量确认进度;
    4. 心跳检测和断线重连确保连接稳定,复制积压缓冲区减少全量同步开销。

    这种机制实现了数据的最终一致性,支撑了读写分离和数据备份,是 Redis 集群方案的基础。

  • 问题:Redis 的核心数据结构及底层实现是什么?

    Redis 提供五大核心数据结构(String、List、Hash、Set、Sorted Set),其底层实现会根据数据规模(数量、大小)动态切换,以平衡操作性能与内存效率。核心逻辑是 “小数据用紧凑存储节省内存,大数据用高效结构保证速度”,支撑多样化的业务场景。

    一、核心目标

    • 适配多样场景:满足字符串存储、列表操作、对象存储、集合运算、有序排序等不同业务需求;
    • 平衡性能与内存:小数据采用紧凑结构减少内存占用,大数据采用高效结构保证操作复杂度(多为 O (1) 或 O (log n));
    • 支持灵活操作:提供丰富的命令接口(如增删改查、范围查询、集合运算等),覆盖各类业务操作。

    二、核心内容:五大数据结构及底层实现

    1. String(字符串)

    功能:存储字符串、整数或浮点数,支持增删改、自增(INCR)、拼接(APPEND)、截取(SUBSTR)等操作,是 Redis 最基础的数据结构。

    底层实现:基于SDS(Simple Dynamic String,简单动态字符串),而非 C 语言原生字符串。

    • 结构组成:
      • len:记录字符串长度(字节数),支持 O (1) 时间获取长度;
      • free:记录未使用的空闲空间(字节数),减少内存重分配;
      • buf:字节数组,存储实际数据(以\0结尾,兼容 C 语言字符串函数)。
    • 核心特点:
      • 二进制安全:可存储任意二进制数据(如图片、序列化对象),不依赖\0判断结束;
      • 预分配空间:修改字符串时,会预分配free空间(如字符串增长时按 “加倍” 规则扩容),减少频繁内存重分配;
      • 惰性释放:缩短字符串时,不立即回收多余空间,通过free记录供后续使用。
    2. List(列表)

    功能:有序、可重复的元素集合,支持两端插入(LPUSH/RPUSH)、两端删除(LPOP/RPOP)、索引访问(LINDEX)、范围查询(LRANGE)等,类似双向链表。

    底层实现:根据元素规模动态切换两种结构:

    • ziplist(压缩列表):
      • 触发条件:元素数量≤512 个,且单个元素长度≤64 字节(可通过list-max-ziplist-entrieslist-max-ziplist-value配置)。
      • 结构特点:内存连续的数组,元素紧密排列(无指针开销),每个元素前存储 “前一个元素长度” 和 “自身编码”,通过偏移量定位元素。
      • 优缺点:节省内存,但插入 / 删除可能触发 “连锁更新”(因内存连续,修改一个元素需调整后续所有元素的偏移量)。
    • linkedlist(双向链表):
      • 触发条件:元素数量或单个元素长度超过 ziplist 阈值。
      • 结构特点:每个节点包含prev(前驱指针)、next(后继指针)和value(元素值),节点不连续存储。
      • 优缺点:两端操作效率 O (1),但指针占用额外内存,随机访问效率低(O (n))。
    3. Hash(哈希)

    功能:键值对集合(field-value),适合存储对象(如用户信息:name"张三"age20),支持单字段操作(HSET/HGET)、批量操作(HMSET/HMGET)等。

    底层实现:根据元素规模动态切换两种结构:

    • ziplist(压缩列表):

      • 触发条件:键值对数量≤512 个,且单个 value 长度≤64 字节(可通过hash-max-ziplist-entrieshash-max-ziplist-value配置)。
      • 结构特点:键值对按 “field-value-field-value” 顺序连续存储,通过偏移量定位字段和值。
      • 优缺点:内存紧凑,适合小对象存储;但字段越多,查询效率越低(需遍历查找)。
    • dict(哈希表):

      • 触发条件:键值对数量或单个 value 长度超过 ziplist 阈值。

      • 结构特点

        :类似 Java HashMap,由 “数组 + 链表” 组成:

        • 数组:每个元素是一个链表头(解决哈希冲突);
        • 哈希函数:通过hash(field) % 数组长度定位 field 所在链表;
        • 渐进式 rehash:扩容时不一次性迁移所有数据,而是分批次迁移,避免阻塞主线程。
      • 优缺点:单字段操作效率 O (1),适合大数据量;但内存占用高于 ziplist。

    4. Set(集合)

    功能:无序、不可重复的元素集合,支持交集(SINTER)、并集(SUNION)、差集(SDIFF)等运算,适合标签存储、去重场景。

    底层实现:根据元素类型和规模动态切换两种结构:

    • intset(整数集合):
      • 触发条件:元素全为整数(int16/int32/int64),且数量≤512 个(可通过set-max-intset-entries配置)。
      • 结构特点:有序数组存储,支持二分查找(O (log n)),元素类型统一(如全为 int16,若插入 int32 则整体升级)。
      • 优缺点:内存占用极低,查询高效;但不支持非整数元素,类型升级会消耗额外资源。
    • dict(哈希表):
      • 触发条件:元素含非整数,或数量超过 intset 阈值。
      • 结构特点:键为集合元素,值为NULL(利用哈希表去重特性),与 Hash 的 dict 结构一致。
      • 优缺点:增删查效率 O (1),支持任意类型元素;但内存占用高于 intset。
    5. Sorted Set(有序集合)

    功能:元素不可重复,关联 “分数(score)” 并按分数排序,支持按分数范围查询(ZRANGEBYSCORE)、排名查询(ZRANK)等,适合排行榜场景。

    底层实现:根据元素规模动态切换两种结构:

    • ziplist(压缩列表):
      • 触发条件:元素数量≤128 个,且单个元素长度≤64 字节(可通过zset-max-ziplist-entrieszset-max-ziplist-value配置)。
      • 结构特点:按 “元素 + 分数” 顺序有序存储(分数从小到大),通过偏移量遍历元素。
      • 优缺点:内存紧凑;但范围查询需遍历整个列表(效率低,O (n))。
    • skiplist(跳表)+ dict(哈希表):
      • 触发条件:元素数量或单个元素长度超过 ziplist 阈值。
      • 结构特点:
        • 跳表:按分数排序,通过多层索引实现快速范围查询(O (log n)),每层索引是下层的子集;
        • 哈希表:映射元素到分数,支持快速获取元素分数(O (1))。
      • 优缺点:兼顾有序性和查询效率,支持复杂排序操作;但内存占用较高(跳表索引需额外空间)。

    三、关键技术:动态切换的核心逻辑

    • 阈值驱动:每种结构通过配置参数(如max-ziplist-entries)定义切换阈值,当数据规模超过阈值时,自动从 “紧凑结构”(ziplist/intset)切换到 “高效结构”(linkedlist/dict/skiplist)。
    • 透明转换:切换过程对用户透明,无需手动干预,Redis 内部自动完成数据迁移(如 ziplist 满后转为 linkedlist 时,会将原有元素重新存储)。

    四、总结

    Redis 核心数据结构的底层实现遵循 “动态适配” 原则:小数据用 ziplist/intset 紧凑存储(省内存),大数据用 linkedlist/dict/skiplist 高效存储(保性能)。这种设计既满足了小数据场景的内存效率,又保证了大数据场景的操作性能,使 Redis 能灵活支撑从简单缓存到复杂排行榜的多样化业务需求。

  • 问题:如何通过 Lua 脚本在 Redis 中实现布隆过滤器?

    利用 Redis 的 Lua 脚本可以将布隆过滤器的 “多步哈希 + 位操作” 封装为原子操作,避免手动执行多个命令的并发问题,同时无需依赖 RedisBloom 模块。核心逻辑是通过 Lua 脚本实现哈希函数映射、位设置与检查,基于 Redis 的 BitMap(位图)存储数据。

    一、核心目标

    • 原子性操作:将 “多哈希 + 多位操作” 封装为单个脚本,确保添加 / 检查元素的原子性,避免并发冲突;
    • 简化调用:通过脚本参数动态传入布隆过滤器配置(位数组长度、哈希函数数量),无需手动执行多次SETBIT/GETBIT
    • 轻量实现:不依赖外部模块,仅使用 Redis 原生 BitMap 和 Lua 脚本功能,适合轻量化场景。

    二、实现步骤

    1. 设计哈希函数

    布隆过滤器需要多个独立的哈希函数,将元素映射到位数组的不同索引。Lua 脚本中可通过以下方式实现简单哈希(示例采用简化的 MurmurHash 思路,实际可根据需求替换):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    -- 简化的哈希函数,返回哈希值(需根据元素和种子生成不同结果)
    local function hash(element, seed, max_bits)
    local hash = 0
    for i = 1, #element do
    hash = (hash * 31 + string.byte(element, i)) % max_bits
    end
    -- 结合种子增加哈希多样性
    hash = (hash + seed) % max_bits
    return hash
    end
    2. 实现添加元素的 Lua 脚本

    脚本功能:接收布隆过滤器键名、元素、位数组长度(m)、哈希函数数量(k),对元素执行 k 次哈希,设置对应位为 1。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    -- 布隆过滤器添加元素脚本
    -- 参数:KEYS[1] = 布隆过滤器键名,ARGV[1] = 元素,ARGV[2] = 位数组长度(m),ARGV[3] = 哈希函数数量(k)
    local key = KEYS[1]
    local element = ARGV[1]
    local max_bits = tonumber(ARGV[2])
    local k = tonumber(ARGV[3])

    -- 哈希函数(同上)
    local function hash(element, seed, max_bits)
    local hash = 0
    for i = 1, #element do
    hash = (hash * 31 + string.byte(element, i)) % max_bits
    end
    hash = (hash + seed) % max_bits
    return hash
    end

    -- 执行k次哈希并设置位
    for seed = 1, k do
    local index = hash(element, seed, max_bits)
    redis.call('SETBIT', key, index, 1)
    end

    return 1 -- 返回成功标识
    3. 实现检查元素的 Lua 脚本

    脚本功能:接收相同参数,对元素执行 k 次哈希,检查所有对应位是否为 1(全为 1 则可能存在,否则一定不存在)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    -- 布隆过滤器检查元素脚本
    -- 参数:KEYS[1] = 布隆过滤器键名,ARGV[1] = 元素,ARGV[2] = 位数组长度(m),ARGV[3] = 哈希函数数量(k)
    local key = KEYS[1]
    local element = ARGV[1]
    local max_bits = tonumber(ARGV[2])
    local k = tonumber(ARGV[3])

    -- 哈希函数(同上)
    local function hash(element, seed, max_bits)
    local hash = 0
    for i = 1, #element do
    hash = (hash * 31 + string.byte(element, i)) % max_bits
    end
    hash = (hash + seed) % max_bits
    return hash
    end

    -- 执行k次哈希并检查位
    for seed = 1, k do
    local index = hash(element, seed, max_bits)
    if redis.call('GETBIT', key, index) == 0 then
    return 0 -- 存在位为0,一定不存在
    end
    end

    return 1 -- 所有位为1,可能存在(假阳性可能)
    4. 调用 Lua 脚本(Redis 客户端)
    • 添加元素:通过EVAL命令执行添加脚本,参数为布隆过滤器键名、元素、m、k。
      示例(m=1000000,k=7,添加元素 “user123”):

      1
      EVAL "【添加脚本内容】" 1 bloom_filter "user123" 1000000 7
    • 检查元素:执行检查脚本,返回 1 表示可能存在,0 表示一定不存在。
      示例(检查 “user123”):

      1
      EVAL "【检查脚本内容】" 1 bloom_filter "user123" 1000000 7

    三、关键参数与假阳性率控制

    • 位数组长度(m):越大,假阳性率越低,但内存占用越高,需根据预期元素数量(n)和可接受假阳性率(p)计算:
      m ≈ -n * ln(p) / (ln(2))²
    • 哈希函数数量(k):最优值为k ≈ m/n * ln(2),过多会增加计算量和位冲突,过少会提高假阳性率。

    四、优缺点

    • 优点:
      • 无需安装额外模块,依赖 Redis 原生功能,部署简单;
      • 脚本保证操作原子性,避免并发场景下的位操作冲突;
      • 可灵活自定义哈希函数和参数,适配不同场景。
    • 缺点:
      • 哈希函数实现简单(示例为简化版),可能导致分布不均,需优化哈希算法;
      • 不支持动态扩容,元素数量超过预期时假阳性率会骤升;
      • 脚本执行耗时随 k 增大而增加,可能阻塞 Redis(需控制 k 值,建议 k≤10)。

    五、总结

    通过 Lua 脚本实现 Redis 布隆过滤器的核心是用脚本封装多哈希 + 位操作的原子逻辑,基于 BitMap 存储映射结果。适合轻量化场景(如中小规模数据去重、缓存穿透防护),且不希望依赖外部模块时使用。实际应用中需根据数据量精心设计 m 和 k,优化哈希函数分布,并控制脚本复杂度以避免性能问题。

  • 问题:Redis 的 Bitmap、GeoHash、HyperLogLog、Streams 这几种数据结构的功能、底层实现及适用场景是什么?

    除了五大核心数据结构,Redis 还提供了几种特殊数据结构,分别针对位操作、地理位置、基数统计、消息队列等场景优化,兼顾性能与内存效率。

    一、Bitmap(位图)

    功能:通过二进制位(bit)存储数据,用于高效处理 “是 / 否” 类型的标记(如 “用户是否签到”“设备是否在线”),支持位级别的逻辑运算。

    1. 底层实现

    基于String(SDS) 实现:Redis 的字符串(SDS)是二进制安全的字节数组,Bitmap 通过对字节数组的位操作(如第 n 位的 0/1)实现功能。例如,一个长度为 100 字节的字符串可表示 800 个二进制位(1 字节 = 8 位),直接通过位索引操作。

    2. 核心命令
    • **SETBIT key offset value**:设置指定偏移量(offset)的位值(0 或 1)。
      示例:SETBIT sign:user:100 5 1 → 用户 100 在第 5 天签到(位 5 设为 1)。
    • **GETBIT key offset**:获取指定偏移量的位值。
      示例:GETBIT sign:user:100 5 → 返回 1(已签到)。
    • **BITCOUNT key [start end]**:统计指定范围内的 “1” 的个数(默认统计全部)。
      示例:BITCOUNT sign:user:100 → 统计用户 100 的总签到天数。
    • **BITOP op destkey key1 [key2...]**:对多个 Bitmap 执行位运算(AND/OR/XOR/NOT),结果存入 destkey。
      示例:BITOP AND active:users sign:user:100 sign:user:101 → 求两个用户共同签到的天数。
    3. 适用场景
    • 用户行为标记:如签到记录(1 位 / 天,1 年仅需 46 字节)、在线状态(1 位 / 用户,100 万用户仅需 125KB);
    • 数据压缩存储:如黑白名单(用位标记 ID 是否在名单中);
    • 快速统计与交集:如统计 “连续签到 7 天的用户”(通过 BITOP AND 多个日期的 Bitmap)。
    4. 优缺点
    • 优点:内存效率极高(1 位 / 状态),位运算速度快(底层是连续内存操作);
    • 缺点:偏移量过大时(如 offset=1 亿)会占用额外内存(即使高位全为 0),需合理规划 key 的粒度。

    二、GeoHash(地理位置)

    功能:存储地理位置信息(经纬度),支持距离计算、范围查询(如 “查找附近 1km 的商家”),本质是对地理坐标的编码与索引。

    1. 底层实现

    基于Sorted Set实现:

    • 经纬度(longitude, latitude)通过 GeoHash 算法编码为一个 64 位整数(作为 ZSet 的 “分数”),元素值为地理位置 ID;
    • 编码原理:将地球表面划分为网格,通过二分法对经纬度递归划分,用二进制表示网格位置,最终合并为一个整数(值越接近,地理位置越近)。
    2. 核心命令
    • **GEOADD key longitude latitude member [longitude latitude member...]**:添加地理位置。
      示例:GEOADD shops 116.403874 39.914885 "shop1" → 添加 “shop1” 的坐标(北京天安门附近)。
    • **GEODIST key member1 member2 [unit]**:计算两个位置的直线距离(unit 支持 m/km/mi/ft)。
      示例:GEODIST shops shop1 shop2 km → 计算 shop1 与 shop2 的距离(公里)。
    • **GEORADIUS key longitude latitude radius unit [WITHCOORD] [WITHDIST] [COUNT count]**:根据坐标查询范围内的位置。
      示例:GEORADIUS shops 116.403874 39.914885 1 km WITHCOORD WITHDIST → 查找天安门 1km 内的商家及坐标、距离。
    • **GEOPOS key member [member...]**:获取指定位置的经纬度。
    3. 适用场景
    • LBS 服务:如 “附近的人”“周边商家”“同城配送范围判断”;
    • 地理围栏:如 “用户进入某区域时触发通知”(结合 GEORADIUS 定时查询)。
    4. 优缺点
    • 优点:复用 Sorted Set 的高效排序能力,范围查询复杂度 O (log n);
    • 缺点:GeoHash 编码存在 “边缘误差”(相邻网格的编码可能不连续),高精度场景需结合其他算法修正。

    三、HyperLogLog(基数统计)

    功能:用于估算 “基数”(集合中不重复元素的个数,如独立用户数 UV),以极小的内存占用(约 12KB)支持海量数据统计,允许约 0.81% 的误差。

    1. 底层实现

    基于概率算法哈希函数

    • 核心原理:通过哈希函数将元素映射为随机二进制串,统计 “最长连续前导 0 的个数”(如某元素哈希后为 “000101”,前导 0 长度为 3);
    • 用多个 “寄存器” 存储不同分组的最长前导 0 长度,通过调和平均估算整体基数(公式:基数 ≈ 常数 × 2^平均前导0长度)。
    2. 核心命令
    • **PFADD key element [element...]**:向 HyperLogLog 添加元素(重复元素不影响结果)。
      示例:PFADD uv:20231001 "user1" "user2" "user1" → 统计 2023-10-01 的 UV(实际基数为 2)。
    • **PFCOUNT key [key...]**:估算基数(返回近似值)。
      示例:PFCOUNT uv:20231001 → 返回 2(误差范围内)。
    • **PFMERGE destkey sourcekey [sourcekey...]**:合并多个 HyperLogLog,结果存入 destkey(基数为所有源集合的并集估算)。
      示例:PFMERGE uv:202310 uv:20231001 uv:20231002 → 合并 10 月 1 日和 2 日的 UV。
    3. 适用场景
    • 海量数据基数统计:如网站 UV(独立访客)、APP 日活(DAU)、搜索关键词去重次数;
    • 资源受限场景:替代 Set(存储 100 万独立元素需约 16MB,HyperLogLog 仅需 12KB)。
    4. 优缺点
    • 优点:内存占用极低(固定 12KB),支持海量数据,合并操作高效;
    • 缺点:存在约 0.81% 的误差(不适合精确统计,如金融交易笔数),不存储原始元素(无法查询具体元素)。

    四、Streams(流)

    功能:Redis 5.0 新增的消息队列数据结构,支持持久化消息多消费者组消息确认(ACK)回溯消费等,适合分布式消息传递场景。

    1. 底层实现

    基于日志结构的双向链表:

    • 每条消息包含唯一 ID(格式:时间戳-序列号,如1696666666-0,确保有序性和唯一性);
    • 消息内容为 Hash 结构(field-value 键值对);
    • 支持 “消费者组”(Consumer Group)机制:每个组维护自己的消费偏移量,同组内消费者竞争消费,不同组可独立消费同批消息。
    2. 核心命令
    • **XADD key [MAXLEN [~] count] \*|ID field value [field value...]**:添加消息到流(*表示自动生成 ID,MAXLEN限制消息最大数量)。
      示例:XADD order_log * user 100 goods "phone" → 向 order_log 流添加一条消息(自动生成 ID)。
    • **XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key...] ID [ID...]**:读取消息(支持阻塞等待新消息)。
      示例:XREAD BLOCK 5000 STREAMS order_log $ → 阻塞 5 秒,读取 order_log 流中最新的消息($表示从末尾开始)。
    • **XGROUP CREATE key groupname ID**:创建消费者组(ID 为起始消费位置,$表示从最新消息开始)。
      示例:XGROUP CREATE order_log group1 $ → 为 order_log 流创建 group1 消费组。
    • **XREADGROUP GROUP groupname consumername [COUNT count] [BLOCK milliseconds] STREAMS key [key...] >**:消费者组内读取消息(>表示未被组内消费的消息)。
      示例:XREADGROUP GROUP group1 consumer1 STREAMS order_log > → group1 的 consumer1 消费新消息。
    • **XACK key groupname ID [ID...]**:确认消息已处理(从组内 pending 列表移除)。
    3. 适用场景
    • 分布式消息队列:如订单状态变更通知、日志收集(替代 Kafka 轻量场景);
    • 多角色消费:不同消费者组处理同一批消息的不同逻辑(如订单流同时被 “库存组” 和 “支付组” 消费);
    • 可靠消息传递:支持消息持久化和 ACK,避免消息丢失。
    4. 优缺点
    • 优点:支持持久化(消息不会因 Redis 重启丢失),多组消费灵活,消息可回溯(通过 ID 重新消费);
    • 缺点:相比 List(简单队列),命令复杂度高,高吞吐场景性能略低于专业消息队列(如 Kafka)。

    五、总结

    数据结构 核心能力 底层依赖 典型场景 核心优势
    Bitmap 位级标记与统计 String 签到、在线状态、黑白名单 内存效率极高(1 位 / 状态)
    GeoHash 地理位置存储与范围查询 Sorted Set 附近的人、LBS 服务 复用 ZSet 高效排序能力
    HyperLogLog 海量数据基数估算 概率算法 UV/DAU 统计、关键词去重 内存占用极低(12KB)
    Streams 持久化消息队列 日志结构链表 分布式消息传递、多组消费 支持 ACK、多组独立消费

    这些结构均针对特定场景优化,实际使用需根据 “精度要求”“内存限制”“功能需求” 选择 —— 例如,精确统计用 Set,模糊统计用 HyperLogLog;简单队列用 List,复杂多组消费用 Streams。

  • 问题:Redis 事务的实现原理是什么?

    Redis 事务是一组命令的集合,通过 “批量执行 + 顺序保证” 实现基本的原子性操作,核心是确保事务中的命令要么全部执行(即使部分命令出错),要么全部不执行(因入队错误或并发修改)。其实现依赖 “事务队列” 和 “乐观锁监控” 机制,与传统数据库事务的 ACID 特性有显著差异。

    一、核心目标

    • 批量执行:将多个命令打包为一个整体,按入队顺序依次执行,避免中间被其他客户端命令插入;
    • 基础原子性:事务中的命令要么全部执行(无入队错误且未被并发修改打断),要么全部不执行(入队错误或被WATCH机制打断);
    • 并发控制:通过WATCH机制监控关键键,防止事务执行时数据已被其他客户端修改,实现类似乐观锁的效果。

    二、核心原理:四阶段执行流程

    1. 开启事务(MULTI命令)
    • 作用:标记当前客户端进入 “事务上下文”,后续命令不再立即执行,而是进入事务队列;

    • 底层操作:Redis 为当前客户端创建一个空的 “事务命令队列”(链表结构),用于缓存后续命令;

    • 示例:

      1
      2
      127.0.0.1:6379> MULTI  # 开启事务
      OK # 进入事务模式
    2. 命令入队(事务内命令)
    • 作用:客户端发送的所有命令(如SETINCRHSET等)被缓存到事务队列,而非立即执行;

    • 入队检查

      :Redis 会对命令进行

      语法校验

      (如命令是否存在、参数数量是否正确):

      • 若语法错误,返回具体错误信息(如(error) ERR unknown command 'XXX'),且后续EXEC执行时整个事务会被放弃;
      • 若语法正确,返回QUEUED,表示成功入队;
    • 示例:

      1
      2
      3
      4
      5
      6
      127.0.0.1:6379> SET user:100 "Alice"  # 入队,语法正确
      QUEUED
      127.0.0.1:6379> INCR score:100 # 入队,语法正确
      QUEUED
      127.0.0.1:6379> INCR user:100 # 入队,语法正确(运行时可能出错,但入队不检查)
      QUEUED
    3. 执行事务(EXEC命令)
    • 作用:触发事务队列中所有命令的执行,按入队顺序依次执行,执行结果按顺序返回;

    • 执行逻辑:

      1. 若事务队列中存在语法错误(如步骤 2 中返回错误的命令),Redis 直接清空队列,返回(error) EXECABORT Transaction discarded because of previous errors.,事务不执行;

      2. 若存在

        1
        WATCH

        监控的键,先检查这些键是否被其他客户端修改:

        • 若已被修改,事务被打断,返回(nil),所有命令不执行;
        • 若未被修改,继续执行;
      3. 按入队顺序依次执行所有命令,将每个命令的结果存入结果数组;

      4. 执行完毕后,清空事务队列,退出事务上下文;

    • 示例

      (无错误且未被

      1
      WATCH

      打断):

      1
      2
      3
      4
      127.0.0.1:6379> EXEC  # 执行事务
      1) OK # SET命令结果
      2) (integer) 1 # INCR score:100结果
      3) (error) ERR value is not an integer or out of range # INCR user:100运行时错误(但仍执行)
    4. 取消事务(DISCARD命令)
    • 作用:终止当前事务,清空事务队列,退出事务上下文,所有入队命令均不执行;

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      127.0.0.1:6379> MULTI
      OK
      127.0.0.1:6379> SET a 100
      QUEUED
      127.0.0.1:6379> DISCARD # 取消事务
      OK
      127.0.0.1:6379> EXEC # 事务已取消,执行无效
      (error) ERR EXEC without MULTI
    5. 乐观锁监控(WATCH命令)
    • 作用:在事务执行前监控一个或多个键,若这些键在WATCH后、EXEC前被其他客户端修改,则事务被打断(EXEC返回nil);

    • 底层原理:

      • WATCH key1 key2...会记录这些键当前的 “版本号”(Redis 通过键的 “修改次数” 隐式标记);

      • EXEC
        
        1
        2
        3

        执行时,Redis 对比这些键的当前版本号与

        WATCH
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23

        时的版本号:

        - 若版本号一致(未被修改),执行事务;
        - 若任一键版本号不一致(已被修改),放弃事务;

        - 示例:

        ```bash
        # 客户端A
        127.0.0.1:6379> WATCH stock # 监控库存键
        OK
        127.0.0.1:6379> MULTI
        OK
        127.0.0.1:6379> DECR stock # 入队:减少库存
        QUEUED

        # 客户端B同时修改stock
        127.0.0.1:6379> SET stock 99 # 修改被监控的键

        # 客户端A执行事务(因stock被修改,事务被打断)
        127.0.0.1:6379> EXEC
        (nil) # 事务未执行

    三、关键特性与局限性

    1. 原子性的特殊性
    • 传统数据库事务:要么全成功,要么全回滚;
    • Redis 事务:
      • 若存在语法错误(入队时检查),事务全不执行;
      • 若存在运行时错误(如对字符串执行INCR),错误命令返回异常,但其他命令仍会执行(无回滚机制);
      • 本质是 “按顺序执行的批量命令”,而非严格的原子性。
    2. 隔离性的简化
    • 事务执行期间,其他客户端的命令不会插入到事务中间(保证顺序性),但可以并行修改事务外的键;
    • 若需隔离事务涉及的键,需通过WATCH手动实现(类似乐观锁)。
    3. 无持久性保证
    • Redis 事务的持久性依赖 Redis 的持久化配置(RDB/AOF),事务本身不额外提供持久性保证;
    • 若事务执行后 Redis 宕机,未持久化的命令可能丢失。

    四、总结

    Redis 事务通过 “MULTI开启→命令入队→EXEC执行 /DISCARD取消” 的流程,结合WATCH的乐观锁机制,实现了 “批量命令按顺序执行” 的基础原子性。其核心价值在于简化批量操作处理并发修改,但不支持传统事务的回滚和完整隔离性。

    适用场景:需批量执行多个命令且需防止并发修改的场景(如库存扣减、余额转账);不适用场景:需严格原子性(如金融交易的精确回滚)。

  • 问题:为什么要使用缓存?

    缓存是系统设计中用于临时存储高频访问数据的组件,核心目标是通过 “空间换时间” 优化系统性能,解决数据访问效率低、后端存储压力大等问题。其必要性可从以下几个核心场景展开:

    1. 加速数据访问,降低延迟

    • 底层原因:数据的存储介质速度差异极大 —— 内存(缓存常用介质)的读写速度通常是磁盘(如数据库、文件系统)的10 万~100 万倍(内存微秒级,磁盘毫秒级)。
    • 效果:对于高频访问的数据(如用户信息、商品详情),缓存可将数据从磁盘 “迁移” 到内存,让请求跳过缓慢的磁盘 IO,直接从内存读取,显著降低响应时间(例如:从数据库查询需 100ms,缓存查询仅需 1ms)。

    2. 减轻后端存储压力,避免过载

    • 问题:后端存储(如数据库、分布式文件系统)的并发处理能力有限(例如:MySQL 单机每秒能处理的查询通常在万级以内),若大量请求直接访问,会导致存储负载过高,出现响应变慢、连接超时甚至崩溃。
    • 缓存的作用:作为 “请求拦截器”,缓存可承接大部分高频请求(例如:80% 的请求访问 20% 的热点数据),大幅减少对后端存储的直接访问次数。例如:一个日均 1000 万请求的电商网站,若缓存命中率达 90%,则数据库仅需处理 100 万请求,压力降低 90%。

    3. 提升系统并发能力与吞吐量

    • 原理:缓存(尤其是分布式缓存如 Redis、Memcached)的并发处理能力远高于后端存储(内存操作支持更高的并发量)。例如:Redis 单机可轻松支持每秒 10 万 + 请求,而同等配置的数据库可能仅支持 1 万 +。
    • 效果:通过缓存承接高频请求,系统能处理更多并发用户,吞吐量(单位时间处理的请求数)显著提升。例如:秒杀场景中,缓存可快速返回商品库存状态,避免大量请求冲击数据库。

    4. 优化用户体验,减少等待

    • 用户感知:延迟直接影响用户体验 —— 研究表明,网页加载延迟每增加 1 秒,用户流失率可能上升 7%。
    • 缓存的作用:通过加速数据返回(如页面静态资源、API 响应结果),减少用户等待时间。例如:CDN 缓存静态图片后,用户打开网页的时间从 3 秒缩短到 0.5 秒,体验显著提升。

    5. 减少重复计算 / 读取,节约资源

    • 场景:部分数据需要通过复杂计算生成(如实时统计报表、复杂算法结果),重复计算会消耗大量 CPU / 内存资源。
    • 缓存的作用:将计算结果临时存储,后续请求直接复用结果,避免重复计算。例如:某数据分析接口需扫描 100 万条数据计算结果(耗时 5 秒),缓存后可直接返回结果(耗时 0.1 秒),节省 98% 的计算资源。

    6. 支持分布式 / 异地访问场景

    • 问题:分布式系统中,数据可能存储在异地机房,跨机房访问的网络延迟高(例如:跨洲际访问延迟可达 100ms+)。
    • 缓存的作用:通过分布式缓存(如 Redis Cluster)或 CDN(内容分发网络),在用户就近的节点缓存数据,减少跨网络访问。例如:全球用户访问某视频网站时,CDN 在各地区节点缓存视频片段,用户直接从本地节点加载,避免跨洋数据传输。

    总结

    缓存的核心价值是通过 “牺牲部分存储空间”,换取更快的访问速度、更低的后端压力、更高的系统并发能力,最终优化用户体验并降低系统资源消耗。它是应对高并发、大数据场景的 “标配” 技术,也是系统性能优化的关键手段之一。