《7天学会Redis》Day 6 - 内存&性能调优

本期内容为自己总结归档,7天学会Redis。其中本人遇到过的面试问题会重点标记。

Day 1 - Redis核心架构与线程模型

Day2 - 深入Redis数据结构与底层实现

Day 3 - 持久化机制深度解析

Day 4 - 高可用架构设计与实践

Day 5 - Redis Cluster集群架构

Day 6 - 内存&性能调优

Day 7 - Redisson 框架

(若有任何疑问,可在评论区告诉我,看到就回复)

Day 6 - 内存&性能调优

6.1 缓存设计模式

6.1.1 缓存穿透解决方案:布隆过滤器

什么是缓存穿透?

缓存穿透是指查询一个不存在的数据,由于缓存中没有,请求会穿透到数据库,如果大量这样的请求,会给数据库带来巨大压力甚至崩溃。

典型场景:

  • 恶意攻击:故意请求不存在的ID

  • 业务逻辑缺陷:查询已删除或未创建的数据

  • 示例

⭐布隆过滤器原理

布隆过滤器(Bloom Filter)是一种概率型数据结构,用于快速判断一个元素是否一定不存在于集合中。

核心特点:

  1. 空间效率极高:使用位数组和多个哈希函数

  2. 存在假阳性:可能误判为存在(但不会误判为不存在)

  3. 不支持删除:标准布隆过滤器不支持删除操作

工作流程:

Redis布隆过滤器实现

Redis 4.0+通过BF.RESERVEBF.ADDBF.EXISTS等命令支持布隆过滤器:

bash 复制代码
# 创建布隆过滤器,指定误差率和容量
BF.RESERVE user_filter 0.01 100000

# 添加元素
BF.ADD user_filter user:1001
BF.ADD user_filter user:1002

# 检查元素是否存在
BF.EXISTS user_filter user:1001  # 返回1(可能存在)
BF.EXISTS user_filter user:9999  # 返回0(一定不存在)

# 批量操作
BF.MADD user_filter user:1003 user:1004 user:1005
BF.MEXISTS user_filter user:1003 user:9999
缓存穿透解决方案

方案1:缓存空对象

bash 复制代码
public Object getData(String key) {
    // 1. 查询缓存
    Object value = cache.get(key);
    if (value != null) {
        if (value instanceof NullObject) {
            return null;  // 缓存了空值
        }
        return value;
    }
    
    // 2. 查询数据库
    value = database.get(key);
    
    // 3. 处理结果
    if (value == null) {
        // 缓存空值,设置较短过期时间
        cache.set(key, new NullObject(), 300);  // 5分钟
    } else {
        cache.set(key, value, 3600);  // 1小时
    }
    
    return value;
}

方案2:布隆过滤器拦截

java 复制代码
public class BloomFilterCache {
    private BloomFilter<String> bloomFilter;
    private Cache<String, Object> cache;
    
    public Object getWithBloomFilter(String key) {
        // 1. 布隆过滤器检查
        if (!bloomFilter.mightContain(key)) {
            return null;  // 一定不存在
        }
        
        // 2. 查询缓存
        Object value = cache.get(key);
        if (value != null) {
            return value;
        }
        
        // 3. 查询数据库
        value = database.get(key);
        
        if (value == null) {
            // 数据库也没有,可能是布隆过滤器误判
            // 记录日志,考虑重建布隆过滤器
            log.warn("Bloom filter false positive for key: {}", key);
        } else {
            cache.set(key, value, 3600);
        }
        
        return value;
    }
    
    public void addToBloomFilter(String key) {
        bloomFilter.put(key);
    }
}

方案3:多层缓存防御

性能对比
方案 优点 缺点 适用场景
缓存空对象 实现简单,拦截准确 可能缓存大量无效数据 数据ID范围有限
布隆过滤器 内存占用小,效率高 存在误判率,不支持删除 大规模数据,可接受误判
多层防御 防护全面,性能好 实现复杂 对安全性要求高

6.1.2 缓存击穿应对:互斥锁设计

什么是缓存击穿?

缓存击穿是指一个热点key在缓存过期瞬间,有大量并发请求同时查询数据库,导致数据库压力骤增。

与缓存穿透的区别:

  • 缓存穿透:查询不存在的数据

  • 缓存击穿:查询存在但过期的热点数据

互斥锁解决方案

核心思想:只允许一个线程去查询数据库,其他线程等待并复用结果。

方案1:Redis分布式锁(关注专栏,后面单独讲)

java 复制代码
public class CacheBreakdownSolution {
    private static final String LOCK_PREFIX = "lock:";
    private static final int LOCK_EXPIRE = 10;  // 锁过期时间(秒)
    
    public Object getDataWithDistributedLock(String key) {
        // 1. 查询缓存
        Object value = redis.get(key);
        if (value != null) {
            return value;
        }
        
        // 2. 尝试获取分布式锁
        String lockKey = LOCK_PREFIX + key;
        String requestId = UUID.randomUUID().toString();
        
        try {
            // 使用SET NX EX获取锁
            boolean locked = redis.setnxex(lockKey, requestId, LOCK_EXPIRE);
            
            if (locked) {
                try {
                    // 3. 再次检查缓存(double check)
                    value = redis.get(key);
                    if (value != null) {
                        return value;
                    }
                    
                    // 4. 查询数据库
                    value = database.get(key);
                    
                    // 5. 写入缓存
                    if (value != null) {
                        redis.setex(key, 3600, value);  // 缓存1小时
                    } else {
                        // 数据库也没有,缓存空值防止穿透
                        redis.setex(key, 300, new NullObject());
                    }
                    
                    return value;
                } finally {
                    // 释放锁(使用Lua脚本保证原子性)
                    String script = 
                        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                        "   return redis.call('del', KEYS[1]) " +
                        "else " +
                        "   return 0 " +
                        "end";
                    redis.eval(script, Arrays.asList(lockKey), Arrays.asList(requestId));
                }
            } else {
                // 6. 未获取到锁,等待并重试
                Thread.sleep(100);
                return getDataWithDistributedLock(key);  // 递归重试
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException("获取数据中断", e);
        }
    }
}

方案2:本地锁+分布式锁组合

java 复制代码
public class HybridLockSolution {
    // 本地锁(针对单JVM内的并发)
    private final ConcurrentHashMap<String, Object> localLocks = new ConcurrentHashMap<>();
    
    public Object getDataWithHybridLock(String key) {
        // 1. 查询缓存
        Object value = redis.get(key);
        if (value != null) {
            return value;
        }
        
        // 2. 获取本地锁(减少分布式锁竞争)
        Object localLock = localLocks.computeIfAbsent(key, k -> new Object());
        
        synchronized (localLock) {
            try {
                // 再次检查缓存
                value = redis.get(key);
                if (value != null) {
                    return value;
                }
                
                // 3. 获取分布式锁
                String lockKey = "lock:" + key;
                String requestId = UUID.randomUUID().toString();
                
                try {
                    if (redis.setnxex(lockKey, requestId, 10)) {
                        // 查询数据库
                        value = database.get(key);
                        
                        // 写入缓存
                        if (value != null) {
                            redis.setex(key, 3600, value);
                        }
                        
                        return value;
                    } else {
                        // 等待其他线程完成
                        Thread.sleep(50);
                        return getDataWithHybridLock(key);
                    }
                } finally {
                    // 释放分布式锁
                    releaseDistributedLock(lockKey, requestId);
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException(e);
            } finally {
                localLocks.remove(key);
            }
        }
    }
}

方案3:逻辑过期时间

java 复制代码
public class LogicalExpirationSolution {
    
    static class CacheData {
        private Object data;
        private long expireTime;  // 逻辑过期时间
        
        // getters and setters
    }
    
    public Object getDataWithLogicalExpiration(String key) {
        // 1. 查询缓存
        String cacheValue = redis.get(key);
        if (cacheValue == null) {
            // 缓存不存在,加载数据
            return loadData(key);
        }
        
        // 2. 反序列化
        CacheData cacheData = JSON.parseObject(cacheValue, CacheData.class);
        
        // 3. 检查是否逻辑过期
        if (cacheData.getExpireTime() <= System.currentTimeMillis()) {
            // 已过期,异步更新
            CompletableFuture.runAsync(() -> {
                updateData(key);
            });
        }
        
        // 4. 返回数据(即使过期也返回旧数据)
        return cacheData.getData();
    }
    
    private Object loadData(String key) {
        // 使用互斥锁加载数据
        // 实现略...
    }
    
    private void updateData(String key) {
        // 后台更新数据,使用互斥锁避免并发更新
        // 实现略...
    }
}
互斥锁实现细节

Redis分布式锁的三种实现方式对比:(关注专栏,细节单独讲)

实现方式 优点 缺点 推荐度
SETNX + EXPIRE 简单直接 非原子性,可能死锁 ⭐⭐
SET NX EX 原子操作 Redis 2.6.12+支持 ⭐⭐⭐⭐
RedLock算法 高可用,防单点故障 实现复杂,性能较低 ⭐⭐⭐

推荐实现:

bash 复制代码
# 使用SET命令的NX和EX选项
SET lock:user:1001 request_id NX EX 10

# 解锁使用Lua脚本保证原子性
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock:user:1001 request_id
热点key检测与预防

检测方法:

  1. 监控工具 :Redis的hotkeys参数或监控QPS

  2. 客户端统计:在客户端统计key的访问频率

  3. 网络分析:通过流量分析识别热点key

预防措施:

  1. 本地缓存:在应用层缓存热点数据

  2. 随机过期时间:避免同时过期

  3. 永不过期+异步更新:设置逻辑过期时间

  4. 多级缓存:使用本地缓存+Redis+数据库

6.1.3 缓存雪崩预防:分层缓存+过期时间分散

什么是缓存雪崩?

缓存雪崩是指大量缓存key同时过期,导致所有请求直接打到数据库,引起数据库压力过大甚至崩溃。

与缓存击穿的区别:

  • 缓存击穿:单个热点key过期

  • 缓存雪崩:大量key同时过期

分层缓存架构
过期时间分散策略

方案1:随机过期事件

方案2:分层过期策略,按业务需要分层过期,注意层级

6.2 内存优化策略

6.2.1 内存淘汰策略深度解析

Redis内存管理机制

Redis内存管理采用"分配器+淘汰策略 "的双层机制。当Redis使用内存达到maxmemory配置值时,会根据指定的淘汰策略自动删除键来释放内存。

内存淘汰触发时机:

8种内存淘汰策略

Redis提供了8种内存淘汰策略,当内存达到maxmemory限制时触发:

  1. noeviction:不淘汰,内存满时拒绝写请求

    适用场景:数据绝对不能丢失,有严格监控和扩展机制

  2. allkeys-lru:从所有键中使用LRU算法淘汰

    适用场景:缓存服务,有明显热点数据

  3. volatile-lru:从设置了过期时间的键中使用LRU算法淘汰

    适用场景:混合存储,需要保留永久数据

  4. allkeys-random:从所有键中随机淘汰

    适用场景:访问模式均匀,无明确热点

  5. volatile-random:从设置了过期时间的键中随机淘汰

    适用场景:缓存数据,不关心淘汰顺序

  6. volatile-ttl:从设置了过期时间的键中淘汰剩余时间最短的

    适用场景:希望优先淘汰即将过期的数据

  7. allkeys-lfu(Redis 4.0+):从所有键中使用LFU算法淘汰

    适用场景:需要基于访问频率进行淘汰,如Session存储

  8. volatile-lfu(Redis 4.0+):从设置了过期时间的键中使用LFU算法淘汰

    适用场景:需要基于频率淘汰,同时保留永久数据

6.2.2 内存碎片整理

Redis内存分配机制

Redis使用自己实现的内存分配器(jemalloc的修改版)来管理内存。内存碎片主要来自两个方面:

  1. 内部碎片:分配器为对齐而浪费的空间

  2. 外部碎片:已分配内存块之间的空闲空间

内存分配器层级:

bash 复制代码
# 查看内存信息
redis-cli info memory
# redis.conf
activedefrag yes                      # 开启主动整理
active-defrag-threshold-lower 10      # 碎片率>10%开始整理
active-defrag-threshold-upper 100     # 碎片率>100%强制整理
active-defrag-cycle-min 1             # 每次整理占用1% CPU
active-defrag-cycle-max 25            # 最多占用25% CPU

碎片率解读:

1.0 - 1.1:理想状态,几乎无碎片

1.1 - 1.5:正常范围,可接受

1.5 - 2.0:碎片率较高,可能需要干预

> 2.0:碎片率过高,建议采取措施

< 1.0:内存交换(swap)发生,非常危险

6.2.2 大Key识别与处理

大Key的定义
类型 大Key标准 风险
String value > 10KB 阻塞网络,慢查询
Hash/Set/ZSet 元素数 > 5000 操作慢,迁移困难
List 元素数 > 10000 阻塞,内存碎片
Stream 长度 > 1000 内存占用大
解决方案

方案1: 拆

方案2:冷热分离

6.3 面试高频考点

考点1:缓存穿透、击穿、雪崩的区别和解决方案?见上述6.1

考点2:Redis大Key和热Key问题如何排查和解决?

面试回答:

大Key排查:

  1. 使用redis-cliredis-cli --bigkeys

  2. 内存分析redis-cli memory usage key

  3. RDB分析:使用redis-rdb-tools分析dump文件

  4. 自定义扫描:使用SCAN命令遍历所有key

大Key解决方案:

  1. 拆分:将大Hash、List、Set拆分为多个小key

  2. 压缩:对value进行压缩存储

  3. 删除:清理无用的大Key

  4. 数据结构优化:选择更合适的数据结构

  5. 冷热分离:将冷数据迁移到其他存储

热Key排查:

  1. Redis监控redis-cli --hotkeys(Redis 4.0+)

  2. 客户端统计:在客户端统计key访问频率

  3. 网络分析:分析网络流量识别热Key

  4. 监控命令redis-cli monitor(谨慎使用)

热Key解决方案:

  1. 本地缓存:在应用层缓存热Key数据

  2. 读写分离:将读请求分散到多个从节点

  3. 分片:将热Key拆分为多个子key

  4. 限流保护:对热Key请求进行限流

  5. 数据副本:创建多个副本分散请求

考点3:⭐如何保证缓存与数据库的数据一致性?

见《中间件》专栏 《7天学会Redis》特别篇: 如何保证缓存与数据库的数据一致性?

相关推荐
石头wang2 小时前
jmeter java.lang.OutOfMemoryError: Java heap space 修改内存大小,指定自己的JDK
java·开发语言·jmeter
鱼跃鹰飞2 小时前
面试题:解释一下什么是全字段排序和rowid排序
数据结构·数据库·mysql
yaoxin5211232 小时前
292. Java Stream API - 使用构建器模式创建 Stream
java·开发语言
阮松云2 小时前
code-server 配置maven
java·linux·maven
Aloudata技术团队2 小时前
完美应对千亿级明细数据计算:Aloudata CAN 双引擎架构详解
数据库·数据分析·数据可视化
Dxy12393102162 小时前
MySQL连表查询讲解:从基础到实战
数据库·mysql
DemonAvenger2 小时前
Redis数据迁移与扩容实战:平滑扩展的技术方案
数据库·redis·性能优化
木木木一2 小时前
Rust学习记录--C11 编写自动化测试
java·学习·rust
bug总结2 小时前
uniapp+动态设置顶部导航栏使用详解
java·前端·javascript