字节二面挂!面试官追问 Redis 内存淘汰策略 LRU 和传统 LRU 差异,我答懵了

面试都背过道八股题:Redis 的内存淘汰策略 LRU 和 LFU 是什么?怎么选好?

很多同学对这两个算法的理解,只停留在都是缓存淘汰,但说不清它们具体区别,概念混淆,更不知道实际场景该怎么选?

而且 Redis 的 key 淘汰算法其实还不是正统的 LRU 和 LFU 算法,而是基于 LRU/LFU 的一个变种。所以我们先了解下 LRU/LFU 基础算法,再看看它和变种的 Redis LRU 算法有何不同。

一、什么是 LRU 算法?

LRU 的全称是 Least Recently Used(最近最少使用),核心逻辑特别好记:最近没被用过的,下次也大概率用不上

举个例子:你电脑桌面上放着常用的软件图标(微信、IDE),这些是最近常用的;而几个月没打开过的压缩工具,会被你拖到文件夹里。这就是 LRU 的思路:保留最近使用的,淘汰最近最少使用的。

假设缓存容量只有 3,依次存入 A、B、C,此时缓存是 [A,B,C];

若此时访问 A(A 变成最近使用),缓存顺序变为 [B,C,A]

若再存入D(缓存满了),需要淘汰最近最少使用的 B,最终缓存是 [C,A,D]

LRU 的优缺点

优点:
  • 逻辑简单:只关注使用时间,实现成本低,容易理解

  • 响应快:插入、删除、查询的时间复杂度可以做到 O (1)(用链表 + 哈希表实现)

  • 贴合短期局部性:很多业务场景中,最近用的数据,确实接下来更可能用(比如你刚打开的文档,接下来大概率会继续编辑)。

缺点:
  • 突发访问误淘汰:如果突然有大量一次性数据访问,会把原本常用的缓存挤掉。

    比如缓存容量 3,原本缓存 [A,B,C](A 是高频使用),突然连续访问 D、E、F,此时会淘汰 A、B、C,缓存变成 [D,E,F];但后续再访问 A 时,A 已经被淘汰,需要重新从数据库加载,导致缓存命中率骤降

  • 不考虑使用频率:如果 A 每天用 100 次,B 每天用 1 次,但 B 是 1 分钟前刚用,A 是 2 分钟前用,LRU 会优先淘汰 A,这显然不合理(高频使用的 A 不该被淘汰)。

实现 LRU 两种方案

方案 1:基于 LinkedHashMap

Java 的LinkedHashMap自带按访问顺序排序的功能,只需重写removeEldestEntry方法,就能实现 LRU 缓存。这是最简洁的实现方式,适合业务中不需要极致性能的场景,快速开发。

import java.util.LinkedHashMap;
import java.util.Map;

publicclass LRUCache<K, V> extends LinkedHashMap<K, V> {
    // 缓存最大容量
    privatefinalint maxCapacity;

    // 构造函数:accessOrder=true 表示“按访问顺序排序”(核心)
    public LRUCache(int maxCapacity) {
        super(maxCapacity, 0.75f, true);
        this.maxCapacity = maxCapacity;
    }

    // 核心:当缓存大小超过maxCapacity时,自动删除“最老的 entry”(最近最少使用的)
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > maxCapacity;
    }

    // 测试
    public static void main(String[] args) {
        LRUCache<Integer, String> cache = new LRUCache<>(3);
        cache.put(1, "A");
        cache.put(2, "B");
        cache.put(3, "C");
        System.out.println(cache); // 输出:{1=A, 2=B, 3=C}(插入顺序)

        cache.get(1); // 访问1,1变成最近使用
        System.out.println(cache); // 输出:{2=B, 3=C, 1=A}(按访问顺序排序)

        cache.put(4, "D"); // 缓存满了,淘汰最老的2
        System.out.println(cache); // 输出:{3=C, 1=A, 4=D}
    }
}
方案 2:基于双向链表 + 哈希表

LinkedHashMap本质是哈希表 + 双向链表,但如果想深入理解 LRU 的实现原理,建议自己手写一个核心是用哈希表保证 O (1) 查询,用双向链表保证 O (1) 插入、删除(维护访问顺序)。适用于高并发场景和面试。

import java.util.HashMap;
import java.util.Map;

publicclass LRUCache2<K, V> {
    // 双向链表节点:存储key、value,以及前后指针
    staticclass Node<K, V> {
        K key;
        V value;
        Node<K, V> prev;
        Node<K, V> next;

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }

    privatefinalint maxCapacity;
    privatefinal Map<K, Node<K, V>> map; // 哈希表:key→Node,O(1)查询
    privatefinal Node<K, V> head; // 虚拟头节点(简化链表操作)
    privatefinal Node<K, V> tail; // 虚拟尾节点

    public LRUCache2(int maxCapacity) {
        this.maxCapacity = maxCapacity;
        this.map = new HashMap<>();
        // 初始化虚拟头尾节点,避免处理null指针
        this.head = new Node<>(null, null);
        this.tail = new Node<>(null, null);
        head.next = tail;
        tail.prev = head;
    }

    // 1. 获取缓存:命中则把节点移到链表头部(标记为最近使用)
    public V get(K key) {
        Node<K, V> node = map.get(key);
        if (node == null) {
            returnnull; // 未命中
        }
        // 移除当前节点
        removeNode(node);
        // 移到头部
        addToHead(node);
        return node.value;
    }

    // 2. 存入缓存:不存在则新增,存在则更新并移到头部;满了则删除尾部节点
    public void put(K key, V value) {
        Node<K, V> node = map.get(key);
        if (node == null) {
            // 新增节点
            Node<K, V> newNode = new Node<>(key, value);
            map.put(key, newNode);
            addToHead(newNode);

            // 缓存满了,删除尾部节点(最近最少使用)
            if (map.size() > maxCapacity) {
                Node<K, V> tailNode = removeTail();
                map.remove(tailNode.key); // 同步删除哈希表中的key
            }
        } else {
            // 更新节点值,并移到头部
            node.value = value;
            removeNode(node);
            addToHead(node);
        }
    }

    // 辅助:把节点添加到链表头部
    private void addToHead(Node<K, V> node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    // 辅助:移除指定节点
    private void removeNode(Node<K, V> node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    // 辅助:移除尾部节点(最近最少使用)
    private Node<K, V> removeTail() {
        Node<K, V> tailNode = tail.prev;
        removeNode(tailNode);
        return tailNode;
    }

    // 测试
    public static void main(String[] args) {
        LRUCache2<Integer, String> cache = new LRUCache2<>(3);
        cache.put(1, "A");
        cache.put(2, "B");
        cache.put(3, "C");
        System.out.println(cache.get(1)); // 输出A,此时1移到头部
        cache.put(4, "D"); // 淘汰3
        System.out.println(cache.get(3)); // 输出null(已被淘汰)
    }
}

LRU 的使用场景

LRU 适合短期高频场景,比如:

  • 浏览器缓存:浏览器缓存网页时,会优先淘汰最近最少打开”的页面(比如你一周没打开的网页,会被优先清理);

  • Redis 默认内存淘汰策略:Redis 的allkeys-lruvolatile-lru是基于 LRU 的(实际是 “近似 LRU”,性能更高),适合 “缓存访问具有短期局部性” 的场景(比如电商商品详情页,用户打开后短时间内可能再次刷新);

  • 本地缓存基础版:如果业务中没有突发大量一次性访问,用 LRU 实现本地缓存足够满足需求(比如用 LinkedHashMap 快速开发)。

二、什么是 LFU 算法?

LFU 的全称是 Least Frequently Used(最不经常使用),核心逻辑和 LRU 完全不同:用得少的,下次也大概率用不上,它关注的是使用频率,而不是使用时间

还是用生活例子:你手机里的 APP,微信、抖音是高频使用的(每天打开几十次),而指南针是低频使用的(几个月打开一次)。当手机内存不足时,系统会优先卸载指南针这类低频 APP,这就是 LFU 的思路。

假设缓存容量 3,初始存入 A(使用 1 次)、B(使用 1 次)、C(使用 1 次),频率都是 1;

访问 A(A 频率变成 2),此时频率:A=2,B=1,C=1;

访问 A(A 频率变成 3),此时频率:A=3,B=1,C=1;

存入 D(缓存满了),需要淘汰 “频率最低” 的 B 或 C(若频率相同,淘汰 “最近最少使用” 的,即 LFU+LRU 结合),最终缓存是 A、C、D(频率分别为 3、1、1)。

LFU 的优缺点

优点:
  • 更贴合长期高频场景:能保留使用频率高的数据,即使它不是 最近使用的(比如 A 每天用 100 次,即使 10 分钟前没⽤,也不该被淘汰);

  • 避免突发访问误淘汰:面对一次性突发访问(比如访问 D、E、F),因为这些数据的频率低,不会把高频的 A、B、C 挤掉,缓存命中率更稳定。

缺点:
  • 实现复杂:需要维护使用频率,还需要处理频率相同的情况(通常结合 LRU),时间复杂度比 LRU 高(插入、删除约 O (log n));

  • 冷启动问题:新存入的缓存频率低,容易被淘汰。比如一个新功能的接口,刚开始访问频率低,会被 LFU 优先淘汰,但其实后续会变成高频接口;

  • 频率老化问题:一个数据过去高频,但现在不再使用(比如活动结束后的活动页面),它的高频率会一直占用缓存,导致新的高频数据无法进入。

实现 LFU:基于哈希表 + 优先级队列

LFU 的实现比 LRU 复杂,核心需要两个哈希表:

  • cache:key→Node(存储 value 和使用频率、最后使用时间);

  • freqMap:频率→LinkedHashSet(存储该频率下的所有 key,LinkedHashSet 保证插入顺序,用于处理频率相同的情况);

    再用一个变量minFreq记录当前最小频率,快速找到最该淘汰的 key。

import java.util.*;

publicclass LFUCache<K, V> {
    // 缓存节点:存储key、value、使用频率、最后使用时间(处理频率相同时的淘汰)
    staticclass Node<K, V> {
        K key;
        V value;
        int freq; // 使用频率
        long lastUseTime; // 最后使用时间(毫秒)

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
            this.freq = 1; // 初始频率1
            this.lastUseTime = System.currentTimeMillis();
        }
    }

    privatefinalint maxCapacity;
    privatefinal Map<K, Node<K, V>> cache; // key→Node
    privatefinal Map<Integer, LinkedHashSet<K>> freqMap; // 频率→key集合(LinkedHashSet保证顺序)
    privateint minFreq; // 当前最小频率(快速定位要淘汰的key)

    public LFUCache(int maxCapacity) {
        this.maxCapacity = maxCapacity;
        this.cache = new HashMap<>();
        this.freqMap = new HashMap<>();
        this.minFreq = 1;
    }

    // 1. 获取缓存:命中则更新频率和最后使用时间
    public V get(K key) {
        Node<K, V> node = cache.get(key);
        if (node == null) {
            returnnull;
        }
        // 更新节点频率
        updateNodeFreq(node);
        return node.value;
    }

    // 2. 存入缓存:不存在则新增,存在则更新;满了则淘汰最小频率的key
    public void put(K key, V value) {
        if (maxCapacity <= 0) {
            return;
        }

        Node<K, V> node = cache.get(key);
        if (node != null) {
            // 存在:更新value、频率、最后使用时间
            node.value = value;
            node.lastUseTime = System.currentTimeMillis();
            updateNodeFreq(node);
        } else {
            // 不存在:检查缓存是否满
            if (cache.size() >= maxCapacity) {
                // 淘汰最小频率的key(频率相同则淘汰最早使用的)
                evictMinFreqKey();
            }
            // 新增节点
            Node<K, V> newNode = new Node<>(key, value);
            cache.put(key, newNode);
            // 加入freqMap:频率1的集合
            freqMap.computeIfAbsent(1, k -> new LinkedHashSet<>()).add(key);
            // 新节点频率是1,minFreq重置为1
            minFreq = 1;
        }
    }

    // 辅助:更新节点频率
    private void updateNodeFreq(Node<K, V> node) {
        K key = node.key;
        int oldFreq = node.freq;
        int newFreq = oldFreq + 1;

        // 1. 从旧频率的集合中移除key
        LinkedHashSet<K> oldFreqSet = freqMap.get(oldFreq);
        oldFreqSet.remove(key);
        // 如果旧频率是minFreq,且集合为空,minFreq+1
        if (oldFreq == minFreq && oldFreqSet.isEmpty()) {
            minFreq = newFreq;
        }

        // 2. 加入新频率的集合
        freqMap.computeIfAbsent(newFreq, k -> new LinkedHashSet<>()).add(key);

        // 3. 更新节点的频率和最后使用时间
        node.freq = newFreq;
        node.lastUseTime = System.currentTimeMillis();
    }

    // 辅助:淘汰最小频率的key(频率相同则淘汰最早使用的)
    private void evictMinFreqKey() {
        // 1. 获取最小频率的key集合
        LinkedHashSet<K> minFreqSet = freqMap.get(minFreq);
        // 2. 淘汰集合中第一个key(LinkedHashSet按插入顺序,即最早使用的)
        K evictKey = minFreqSet.iterator().next();
        minFreqSet.remove(evictKey);

        // 3. 同步删除cache和freqMap(如果集合为空)
        cache.remove(evictKey);
        if (minFreqSet.isEmpty()) {
            freqMap.remove(minFreq);
        }

        System.out.println("淘汰key:" + evictKey);
    }

    // 测试
    public static void main(String[] args) {
        LFUCache<Integer, String> cache = new LFUCache<>(3);
        cache.put(1, "A");
        cache.put(2, "B");
        cache.put(3, "C");
        System.out.println(cache.get(1)); // 输出A,频率变成2,minFreq还是1
        cache.put(4, "D"); // 缓存满,淘汰minFreq=1的2(B)
        System.out.println(cache.get(2)); // 输出null(已淘汰)
        cache.get(3); // 3频率变成2,minFreq变成2
        cache.get(4); // 4频率变成2
        cache.put(5, "E"); // 缓存满,淘汰minFreq=2的3(C,最早使用)
    }

三、Redis 中的 LRU 和 LFU:实现与区别

Redis 作为缓存数据库,当内存达到内存maxmemory限制时,会触发内存淘汰策略,其中LRULFU是最常用的两种。

但 Redis 的实现并非 “严格版”,而是做了性能优化的 “近似版”, 这一点尤其需要注意。

Redis 的 LRU 实现

Redis 的 LRU 实现:近似 LRU(性能优先)

Redis 没有采用 “双向链表 + 哈希表” 的标准 LRU 实现,因为会增加内存开销和操作复杂度,而是用随机采样 + 时间戳实现近似 LRU:

  • 核心原理:每个 key 在内存中记录lru字段(最后一次访问的时间戳),当需要淘汰 key 时,Redis 会从所有候选 key 中随机采样 N 个(默认 5 个,可通过maxmemory-samples配置),然后淘汰这 N 个中lru值最小(最久未使用)的 key。

  • 为什么近似:严格 LRU 需要维护全量 key 的访问顺序,在 Redis 高并发场景下会成为性能瓶颈;近似 LRU 通过控制采样数(N 越大越接近严格 LRU,但性能稍差),在 “命中率” 和 “性能” 之间做了平衡。

  • 相关配置

# 淘汰所有key中最久未使用的
maxmemory-policy allkeys-lru
# 只淘汰设置了过期时间的key中最久未使用的
maxmemory-policy volatile-lru
# 采样数(默认5,范围3-10)
maxmemory-samples 5

Redis 的 LFU 实现

Redis 的 LFU 实现,结合频率与时间衰减。

Redis 4.0 引入了 LFU 策略,解决了标准 LFU 的频率老化问题,实现更贴合实际业务:

核心改进

  • 频率计数:用lfu_count记录访问频率(但不是简单累加,而是用对数计数:访问次数越多,lfu_count增长越慢,避免数值过大);

  • 时间衰减:用lfu_decay_time控制频率衰减(单位分钟),如果 key 在lfu_decay_time内没有被访问,lfu_count会随时间减少,避免过去高频但现在不用的 key 长期占用缓存。

  • 淘汰逻辑:当需要淘汰时,同样随机采样 N 个 key,淘汰lfu_count最小的(频率最低);若频率相同,淘汰最久未使用的(结合 LRU 逻辑)。

相关配置

# 淘汰所有key中访问频率最低的
maxmemory-policy allkeys-lfu
# 只淘汰设置了过期时间的key中访问频率最低的
maxmemory-policy volatile-lfu
# 频率衰减时间(默认1分钟,0表示不衰减)
lfu-decay-time 1
# 初始频率(新key的lfu_count,默认5,避免新key被立即淘汰)
lfu-log-factor 10

Redis 中 LRU 和 LFU 的核心区别

维度

Redis LRU

Redis LFU

判断依据

最后访问时间(越久未用越可能被淘汰)

访问频率(频率越低越可能被淘汰,结合时间衰减)

适用场景

短期局部性访问(如用户会话、临时数据)

长期高频访问(如热点商品、基础配置)

应对突发访问

弱(易被一次性访问挤掉常用 key)

强(低频的突发访问 key 不会淘汰高频 key)

冷启动友好度

较好(新 key 只要最近访问就不会被淘汰)

需配置lfu-log-factor(避免新 key 因初始频率低被淘汰)

内存 overhead

低(只存时间戳)

稍高(存频率和时间衰减信息)

典型业务案例

新闻 APP 的最近浏览列表

电商首页的热销商品缓存

怎么选?看业务访问模式

选 LRU:如果你的缓存访问具有短期集中性(比如用户打开一个页面后,短时间内会频繁刷新,但过几天可能再也不看),比如:

  • 会话缓存(用户登录后 1 小时内频繁操作,之后可能下线);

  • 临时活动页面(活动期间访问集中,活动结束后很少访问)。

选 LFU:如果你的缓存访问具有 “长期高频性”(比如某些基础数据每天都被大量访问),比如:

  • 商品类目、地区编码等基础配置数据;

  • 首页 banner、热销榜单等高频访问数据。

  • 排查技巧:可以先开启 Redis 的INFO stats监控keyspace_hits(缓存命中)和keyspace_misses(缓存未命中),如果切换策略后keyspace_hits提升,说明更适合当前业务。

总结:LRU 和 LFU 的本质区别与选择

从本质上看,LRU 和 LFU 的核心差异是判断价值的维度不同

  • LRU 用最近是否被使用衡量价值,适合短期热点;

  • LFU 用使用频率高低衡量价值,适合长期热点。

在 Redis 中,两者都做了性能优化(近似实现),选择时不用纠结 理论完美性,先看业务访问模式:短期集中访问用 LRU,长期高频访问用 LFU。如果实在不确定,不妨先试 LRU(实现更简单,兼容性更好),再根据监控数据调整。

看完等于学会,点个赞吧!!!

以下是针对 **Redis分布式缓存领域20道高频面试题** 的标准面试案详解,内容符合中高级Java开发工程师在真实技术面试中的表达规范:逻辑清晰、原理深入、关键词突出、语言专业。每道题均包含 **核心概念 + 实现机制 + 应用场景 + 注意事项**,便于应试者精准作。 --- ### 1. Redis的数据类型? Redis支持五种基本数据类型三种扩展类型: | 数据类型 | 说明 | 典型应用场景 | |---------|------|---------------| | **String(字符串)** | 最基础类型,可存储文本、数字、序列化对象 | 缓存、计数器、分布式锁 | | **Hash(哈希)** | 键值对集合,适合存储对象属性 | 用户信息、商品详情 | | **List(列表)** | 有序可重复队列,支持双向插入删除 | 消息队列、最新消息排行 | | **Set(集合)** | 无序不重复元素集合 | 好友关系、标签去重 | | **ZSet(有序集合)** | 带分数的Set,按分值排序 | 排行榜、延迟任务 | | **Bitmaps** | 位数组操作 | 用户签到、活跃统计 | | **HyperLogLog** | 基数估算结构 | UV统计(误差率<0.81%) | | **Geospatial(GEO)** | 地理位置坐标存储与计算 | 附近的人、距离计算 | ```java // Java示例(使用Jedis) jedis.set("name", "Tom"); jedis.hset("user:1", "name", "Jerry"); jedis.lpush("msg_queue", "task1"); ``` > ✅ 所有操作都是原子性的,天然适合高并发场景 --- ### 2. Redis的持久化机制? Redis提供两种持久化方式保障数据安全: #### (1)RDB(Redis Database) - **原理**:定时生成数据集的时间点快照(Snapshot) - **触发方式**: - 配置自动触发:`save 900 1`(900秒内至少1次修改) - 手动执行:`SAVE`(阻塞)、`BGSAVE`(fork子进程异步保存) - **优点**:文件紧凑、恢复快、适合备份 - **缺点**:可能丢失最后一次快照后的数据 #### (2)AOF(Append Only File) - **原理**:记录每条写命令日志,重启时重放重建数据 - **同步策略**: - `appendfsync always`:每次写都刷盘(最安全,性能差) - `appendfsync everysec`:每秒刷盘(推荐,默认) - `appendfsync no`:由操作系统决定 - **优点**:数据完整性高,可读性强 - **缺点**:文件大,恢复慢 > ✅ 生产建议:**同时开启RDB+AOF**,兼顾性能与可靠性 --- ### 3. Redis的主从复制原理? 主从复制用于实现数据冗余、读写分离故障转移。 #### 工作流程: 1. **建立连接**: - 从节点配置 `slaveof <masterip> <port>` 或使用 `REPLICAOF` - 发送PING确认主节点可达 2. **全量同步(Full Resynchronization)**: - 主节点执行 `BGSAVE` 生成RDB - 将RDB文件发送给从节点 - 从节点清空旧数据并加载RDB 3. **增量同步(Partial Resynchronization)**: - 主节点将后续写命令写入**复制积压缓冲区(Replication Backlog)** - 从节点通过偏移量(offset)请求缺失命令 - 使用PSYNC命令进行增量传输 #### 关键组件: - **run_id**:主节点唯一标识 - **replication offset**:复制流的字节偏移量 - **repl_backlog_buffer**:环形缓冲区,默认1MB > ✅ 支持级联复制(主→从→从),减少主库压力 --- ### 4. Redis的哨兵机制? **Sentinel(哨兵)** 是Redis的高可用解决方案,监控主从集群并在主节点宕机时自动切换。 #### 核心功能: - **监控(Monitoring)**:持续检查主从节点是否正常 - **通知(Notification)**:异常时发送告警 - **故障转移(Failover)**:主节点失败后提升一个从节点为新主 - **配置中心(Configuration Provider)**:客户端可通过哨兵获取最新主节点地址 #### 故障转移流程: 1. 多个哨兵对主节点进行主观下线判断 2. 达到法定数量(quorum)后标记为客观下线 3. 选举领导者哨兵执行failover 4. 选择优先级最高的从节点升级为主 5. 更新其他从节点指向新主 6. 对外广播新的主节点地址 ```bash # sentinel.conf 示例 sentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 15000 ``` > ✅ 至少部署3个哨兵节点,避免脑裂问题 --- ### 5. Redis的集群模式? **Redis Cluster** 是官方提供的分布式方案,支持数据分片高可用。 #### 核心特性: - **数据分片**:16384个哈希槽(hash slot),每个key通过CRC16算法映射到槽 - **节点通信**:使用Gossip协议传播节点状态 - **高可用**:每个主节点可配多个从节点,主故障时从节点接管 - **客户端路由**:客户端直接连接任意节点,收到MOVED或ASK重定向 #### 架构要求: - 至少6个节点(3主3从) - 所有节点两两互通(ping/pong消息) ```bash # 创建集群 redis-cli --cluster create 127.0.0.1:7000 ... --cluster-replicas 1 ``` > ✅ 不支持多数据库(SELECT db不可用),仅支持db0 --- ### 6. Redis的缓存穿透、缓存击穿、缓存雪崩? | 问题 | 定义 | 解决方案 | |------|------|----------| | **缓存穿透** | 查询不存在的数据,绕过缓存直击数据库 | 1. 缓存空值(设置短TTL)<br>2. 使用布隆过滤器预判是否存在 | | **缓存击穿** | 热点key过期瞬间大量请求涌入数据库 | 1. 设置永不过期或逻辑过期<br>2. 加互斥锁(如Redis分布式锁) | | **缓存雪崩** | 大量key在同一时间过期导致数据库压力激增 | 1. 过期时间加随机值(如TTL+30min±5min)<br>2. 多级缓存架构<br>3. 高可用集群部署 | > ✅ 防御原则:不让请求直接打到数据库 --- ### 7. 如何解决Redis的并发竞争? 并发竞争指多个客户端同时修改同一key,可能导致数据覆盖。 #### 解决方案: 1. **分布式锁(推荐)** ```java // 使用SETNX实现锁 Boolean locked = jedis.setnx("lock:update_user", "1"); if (locked) { jedis.expire("lock:update_user", 10); // 执行业务逻辑 jedis.del("lock:update_user"); } ``` 2. **Lua脚本保证原子性** ```lua -- 原子递增并限制最大值 local current = redis.call("GET", KEYS[1]) if not current or tonumber(current) < 100 then return redis.call("INCR", KEYS[1]) else return 0 end ``` 3. **消息队列串行化处理** - 将更新请求放入MQ,单消费者顺序处理 > ✅ 推荐:Redisson等框架封装的公平锁更可靠 --- ### 8. Redis的事务机制? Redis事务通过 `MULTI` / `EXEC` 实现一组命令的批量执行。 #### 特性: - **不支持回滚**:即使某条命令失败,其余命令仍会继续执行 - **原子性**:所有命令一次性提交(但非传统意义上的ACID事务) - **隔离性**:事务执行期间不会被其他命令打断 ```bash MULTI SET name Tom INCR age EXEC ``` #### 局限性: - 无法捕获语法错误以外的运行时错误 - 不支持条件判断循环 > ✅ 替代方案:使用Lua脚本实现复杂原子操作 --- ### 9. Redis的发布订阅机制? Pub/Sub模型实现进程间消息通信。 ```bash # 订阅频道 SUBSCRIBE news # 发布消息 PUBLISH news "Hello World" # 查看订阅者数量 PUBSUB NUMSUB news ``` #### 特点: - 实时性强,低延迟 - 消息不持久化,离线消息丢失 - 不保证送达(fire-and-forget) > ✅ 适用场景:实时通知、聊天室 > ⚠️ 不适用于需要可靠传递的消息系统(建议用Kafka/RocketMQ) --- ### 10. Redis的Lua脚本使用? Lua脚本可在Redis服务端原子执行复杂逻辑。 #### 示例:实现带过期时间的限流器 ```lua local key = KEYS[1] local limit = tonumber(ARGV[1]) local expire_time = ARGV[2] local current = redis.call('GET', key) if current and tonumber(current) > limit then return 0 else redis.call('INCRBY', key, 1) redis.call('EXPIRE', key, expire_time) return 1 end ``` 调用方式: ```java jedis.eval(luaScript, 1, "rate_limit:user_123", "100", "60"); ``` > ✅ 优势:网络开销小、原子性强、减少多次往返 --- ### 11. Redis内存淘汰策略? 当内存达到 `maxmemory` 限制时触发淘汰策略: | 策略 | 说明 | |------|------| | `noeviction` | 默认,内存不足时报错 | | `allkeys-lru` | 淘汰最近最少使用的任意key | | `volatile-lru` | 仅淘汰设置了过期时间的key中LRU的 | | `allkeys-random` | 随机淘汰任意key | | `volatile-random` | 随机淘汰带过期时间的key | | `volatile-ttl` | 优先淘汰剩余时间最短的key | | `allkeys-lfu` | 淘汰最不经常使用(Least Frequently Used)的key(Redis 4.0+) | ```conf maxmemory 2gb maxmemory-policy allkeys-lru ``` > ✅ 推荐:缓存场景用 `allkeys-lru`,有明确过期策略可用 `volatile-*` --- ### 12. Redis的Pipeline机制? Pipeline用于批量发送命令,减少网络RTT开销。 ```java // 传统方式:N次往返 for (int i = 0; i < 1000; i++) { jedis.set("key:" + i, "value:" + i); } // Pipeline:一次往返 try (Pipeline pipeline = jedis.pipelined()) { for (int i = 0; i < 1000; i++) { pipeline.set("key:" + i, "value:" + i); } pipeline.sync(); // 执行并等待响应 } ``` > ✅ 性能提升显著(特别是跨机房调用),但注意不能保证原子性 --- ### 13. Redis的布隆过滤器? 布隆过滤器是一种空间效率极高的概率型数据结构,用于判断元素是否存在。 #### 原理: - 使用多个哈希函数将元素映射到位数组 - 查询时所有位均为1 → 可能存在;任一位为0 → 一定不存在 - 存在误判率(可调),但不会漏判 ```bash # 使用RedisBloom模块 BF.ADD user_filter "user1001" BF.EXISTS user_filter "user1001" # 返回1 BF.EXISTS user_filter "user9999" # 可能返回1(误判) ``` > ✅ 应用:防止缓存穿透、垃圾邮件识别、爬虫URL去重 --- ### 14. Redis的分布式锁实现? 基于 `SETNX` + `EXPIRE` 实现简单分布式锁: ```java public boolean tryLock(String key, String value, int expireSec) { String result = jedis.set(key, value, "NX", "EX", expireSec); return "OK".equals(result); } public void unlock(String key, String value) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " + "return redis.call('del', KEYS[1]) else return 0 end"; jedis.eval(script, 1, key, value); } ``` #### 改进方案(Redisson): ```java RLock lock = redisson.getLock("business_lock"); lock.lock(); // 自动续期(watchdog机制) try { // 业务逻辑 } finally { lock.unlock(); } ``` > ✅ 注意事项: - 必须设置超时防止死锁 - 解锁需校验value防止误删 - 推荐使用Redisson等成熟框架 --- ### 15. Redis的持久化方式对比? | 对比项 | RDB | AOF | |--------|-----|-----| | 文件大小 | 小(压缩二进制) | 大(日志文本) | | 恢复速度 | 快 | 慢 | | 数据安全性 | 可能丢失最后一次快照后数据 | 更高(最多丢失1秒) | | 写性能影响 | BGSAVE时有影响 | appendfsync策略决定 | | 可读性 | 二进制,不可读 | 文本,可读可追加 | | 适用场景 | 备份、灾难恢复 | 数据完整性要求高的系统 | > ✅ 最佳实践:**双开**,RDB做定期备份,AOF保障在线数据安全 --- ### 16. Redis的高可用方案? 常见高可用架构: | 方案 | 说明 | |------|------| | **主从复制 + 哨兵(Sentinel)** | 自动故障检测与切换,适合中小规模 | | **Redis Cluster** | 官方分片集群,自带高可用负载均衡 | | **Codis / Twemproxy** | 中间件方案,支持大规模集群管理 | | **云托管Redis** | 如阿里云Redis、AWS ElastiCache,平台级保障 | > ✅ 推荐组合:Cluster用于大数据量,Sentinel用于小集群 --- ### 17. Redis的性能优化技巧? #### 关键优化点: 1. **合理选择数据结构**:用Hash代替多个String存储对象 2. **禁用危险命令**:`KEYS *` → 改用 `SCAN` 3. **启用Pipeline**:批量操作减少网络开销 4. **控制Key大小**:避免大Key(>10KB)导致阻塞 5. **设置合理的过期时间**:防止内存无限增长 6. **使用连接池**:HikariCP/Redisson优化客户端资源 7. **监控慢查询**:`slowlog get` 分析耗时命令 8. **调整TCP参数**:`tcp-keepalive` 保持长连接 > ✅ 工具推荐:`redis-benchmark` 压测,`redis-cli --stat` 实时监控 --- ### 18. Redis的热点数据处理? 热点数据是指访问频率极高的Key,可能导致单节点压力过大。 #### 解决方案: 1. **本地缓存 + Redis二级缓存** ```java Object data = localCache.get(key); if (data == null) { data = jedis.get(key); localCache.put(key, data, 10); // TTL=10s } ``` 2. **Key拆分(影子Key)** - 将 `hotkey` 拆为 `hotkey::1`, `hotkey::2`... - 随机访问其中一个,分散压力 3. **客户端缓存(Client Side Caching)** - Redis 6.0+支持tracking模式,允许客户端缓存数据 4. **多级缓存架构** - Nginx层 → Local Cache → Redis → DB > ✅ 核心思想:把流量挡在Redis之外 --- ### 19. Redis与Memcached的区别? | 对比维度 | Redis | Memcached | |---------|--------|-----------| | 数据类型 | 支持丰富类型(String/Hash/ZSet等) | 仅支持String | | 持久化 | 支持RDB/AOF | 不支持 | | 集群 | 原生Cluster支持 | 需第三方工具 | | 线程模型 | 单线程(6.0+网络IO多线程) | 多线程 | | 内存管理 | jemalloc,碎片较少 | slab分配器 | | 分布式 | 客户端分片或Cluster | 客户端一致性哈希 | | 高可用 | 支持主从、哨兵、Cluster | 无内置HA机制 | | 功能扩展 | 支持Lua、Pub/Sub、事务、GEO等 | 功能较单一 | > ✅ 推荐:现代应用优先选用Redis,除非追求极致吞吐且无需持久化 --- ### 20. Redis的持久化恢复机制? Redis启动时根据配置自动恢复数据: #### 恢复流程: 1. 如果启用了AOF且AOF文件存在: - 优先加载AOF文件(数据更完整) - 逐条重放写命令重建数据 2. 否则: - 加载最新的RDB快照文件 - 恢复当时的数据状态 #### 注意事项: - AOF重写不影响恢复过程(只保留最终状态) - 若RDBAOF都关闭,则启动为空实例 - 可通过 `INFO persistence` 查看持久化状态 ```bash # 强制生成RDB BGSAVE # 修复损坏的AOF redis-check-aof --fix appendonly.aof ``` > ✅ 生产环境务必开启持久化,并定期验证dump文件可用性 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值