Redis面试问题缓存相关详解
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>() {@Overridepublic 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的INCR
和EXPIRE
命令实现限流。例如,每秒最多允许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较短的键 | 临时数据缓存 |