Redis面试问题缓存相关详解

发布于:2025-04-16 ⋅ 阅读:(14) ⋅ 点赞:(0)

Redis面试问题缓存相关详解

一、缓存三兄弟(穿透、击穿、雪崩)

1. 穿透

问题描述
缓存穿透是指查询一个数据库中不存在的数据,由于缓存不会保存这样的数据,每次都会穿透到数据库,导致数据库压力增大。例如,用户请求一个不存在的用户ID,每次请求都会直接查询数据库。

解决方法

1.1、存储特殊标记值
当数据库中没有查询到数据时,可以将一个特殊标记值(如"not_found")存储到Redis中,并设置一个较短的过期时间(如5分钟)。这样,后续的请求可以直接返回"not_found",而不会再次查询数据库。
优点:实现简单,减少数据库压力。
缺点:如果数据库中后来插入了数据,而Redis中仍然缓存了"not_found",会导致查询结果不一致。
示例

String value = redis.get(key);
if (value == null) {
    value = database.get(key);
    if (value == null) {
        redis.set(key, "not_found", 300); // 缓存5分钟
        return "not_found";
    } else {
        redis.set(key, value, 3600); // 缓存1小时
    }
}
return value;

1.2、布隆过滤器
在缓存和数据库之间加入一个布隆过滤器,它可以预存储一些可能存在的键。如果查询的键不在布隆过滤器中,直接返回不存在,避免查询数据库。布隆过滤器通过哈希函数实现,误判率可以通过调整其大小和哈希函数的数量来控制。
优点:减少对数据库的无效查询。
缺点:实现复杂,有一定的误判率。
示例

if (!bloomFilter.contains(key)) {
    return "not_found";
}
String value = redis.get(key);
if (value == null) {
    value = database.get(key);
    redis.set(key, value, 3600); // 缓存1小时
}
return value;

2. 击穿

问题描述
缓存击穿是指一个热点数据的缓存过期后,大量请求同时查询数据库,导致数据库压力过大。例如,一个热门商品的详情页缓存过期,大量用户同时请求该页面。

解决方法

2.1、互斥锁
使用互斥锁确保只有一个线程可以查询数据库并更新缓存。其他线程等待锁释放后直接从缓存中获取数据。可以使用Redis的SETNX命令实现分布式锁。
示例

String lockKey = "lock:" + key;
if (redis.setnx(lockKey, "1", 30) == 1) { // 尝试获取锁,超时30秒
    String value = database.get(key);
    redis.set(key, value, 3600); // 更新缓存
    redis.del(lockKey); // 释放锁
    return value;
} else {
    // 等待锁释放
    Thread.sleep(100);
    return redis.get(key);
}

2.2、逻辑过期
在缓存中存储一个额外的过期时间字段,每次读取时检查逻辑过期时间。即使Redis的物理过期时间到了,逻辑过期时间仍然有效,可以避免频繁更新缓存。
示例

String value = redis.get(key);
if (value == null) {
    String lockKey = "lock:" + key;
    if (redis.setnx(lockKey, "1", 30) == 1) {
        value = database.get(key);
        redis.set(key, value, 3600); // 更新缓存
        redis.del(lockKey); // 释放锁
    } else {
        Thread.sleep(100);
        return redis.get(key);
    }
}
return value;

3. 雪崩

问题描述
缓存雪崩是指大量缓存数据在同一时间过期,导致大量请求同时查询数据库,造成数据库压力过大。例如,多个热点数据的缓存同时过期。

解决方法

3.1、随机过期时间
为每个缓存设置不同的过期时间,避免大量缓存同时过期。例如,可以使用SETEX命令为每个key设置随机的过期时间(如1-5分钟)。
示例

int randomTTL = new Random().nextInt(300) + 60; // 随机过期时间1-5分钟
redis.setex(key, randomTTL, value);

3.2、本地缓存
在应用层使用本地缓存(如Guava Cache)作为二级缓存,减轻Redis的压力。本地缓存可以快速响应,减少对Redis的依赖。
示例

LoadingCache<String, String> localCache = CacheBuilder.newBuilder()
    .expireAfterWrite(1, TimeUnit.MINUTES)
    .build(new CacheLoader<String, String>() {
        @Override
        public String load(String key) throws Exception {
            return redis.get(key);
        }
    });

String value = localCache.get(key);
if (value == null) {
    value = database.get(key);
    redis.setex(key, 3600, value); // 更新Redis缓存
    localCache.put(key, value); // 更新本地缓存
}
return value;

3.3、降级限流
降级和限流是应对高并发场景的通用策略。可以使用Guava RateLimiter或Redis的INCREXPIRE命令实现限流。例如,每秒最多允许100次请求:
示例

RateLimiter rateLimiter = RateLimiter.create(100); // 每秒100次
if (rateLimiter.tryAcquire()) {
    return handleRequest();
} else {
    return "Too many requests";
}

二、双写一致

问题描述
双写一致是指保持Redis和数据库的更新操作一致。如果操作顺序不当,可能会导致数据不一致。例如:

  • 先更新Redis,再更新数据库:可能导致Redis中的数据丢失。
  • 先更新数据库,再更新Redis:可能导致Redis中的旧数据覆盖新数据。

解决方法

1、延迟双删
先删除Redis中的缓存,然后更新数据库,最后再删除一次Redis中的缓存。这样可以确保Redis中的数据是最新的。
示例

redis.del(key); // 删除缓存
database.update(key, value); // 更新数据库
Thread.sleep(300); // 延迟300毫秒
redis.del(key); // 再次删除缓存

2、分布式锁
使用分布式锁确保同一时间只有一个线程可以更新数据。可以使用Redisson等库实现分布式锁。
示例

RLock lock = redisson.getLock("lock:" + key);
try {
    lock.lock();
    database.update(key, value); // 更新数据库
    redis.del(key); // 删除缓存
} finally {
    lock.unlock();
}

3、共享锁和排它锁

共享锁:当一个线程在读取数据时,其他线程可以同时读取,但不能写入。

排它锁:当一个线程在写入数据时,其他线程不能读取或写入。
示例

// 使用Redis的SET命令实现共享锁和排它锁
String lockKey = "lock:" + key;
if (redis.set(lockKey, "1", 30, NX, EX)) { // 获取排它锁
    database.update(key, value); // 更新数据库
    redis.del(key); // 删除缓存
    redis.del(lockKey); // 释放锁
} else {
    // 等待锁释放
    Thread.sleep(100);
}

三、持久化

Redis提供了两种持久化方式:RDB和AOF。

1、RDB(快照模式):

优点:恢复速度快,数据完整性好,方便使用。

缺点:可能会丢失最后一次快照之后的数据,占用磁盘空间较大。

配置示例

save 900 1  # 900秒内至少有1个键被修改时保存快照
save 300 10 # 300秒内至少有10个键被修改时保存快照

2、AOF(追加模式):

优点:数据不易丢失,恢复时可以逐条执行命令。

缺点:文件体积大,恢复速度慢。

配置示例

appendonly yes
appendfsync everysec  # 每秒同步一次

3、混合持久化:
Redis 4.0引入了混合持久化模式,结合了RDB和AOF的优点。可以同时使用两种持久化方式,提高数据安全性和恢复速度。

四、数据过期策略

Redis提供了两种主要的数据过期策略:惰性删除和定期删除。

1、惰性删除:

优点:占用CPU资源少。

缺点:可能会导致内存中积累大量过期数据。

原理:只有当访问某个键时,才会检查该键是否过期,如果过期则删除。

2、定期删除:

原理:Redis会定期扫描内存中的键,删除过期的键。

配置示例

hz 10  # 设置Redis的事件循环频率,单位为每秒

五、数据淘汰策略

Redis支持多种数据淘汰策略,可以通过maxmemory-policy配置。

策略 描述 适用场景
noeviction 不淘汰任何数据,当内存不足时返回错误 内存足够大,不需要淘汰数据
allkeys-lru 对所有键使用LRU(最近最少使用)算法淘汰 通用缓存场景
volatile-lru 对设置了过期时间的键使用LRU算法淘汰 热点数据缓存
allkeys-random 随机淘汰所有键 对数据一致性要求不高的场景
volatile-random 随机淘汰设置了过期时间的键 热点数据缓存
allkeys-lfu 对所有键使用LFU(最不经常使用)算法淘汰 访问频率差异较大的缓存
volatile-lfu 对设置了过期时间的键使用LFU算法淘汰 热点数据缓存
volatile-ttl 优先淘汰TTL较短的键 临时数据缓存

网站公告

今日签到

点亮在社区的每一天
去签到