面试题答案
一键面试缓存设计层面
- 数据预热
在系统上线前,将热点数据预先加载到Redis缓存中。可以通过批量读取数据库中的热点数据,然后使用
MSET
等命令一次性写入Redis。例如,电商系统在大促前预先加载热门商品信息。这样系统启动后,用户请求可以直接命中缓存,减少数据库压力。 新问题及解决方案:可能导致启动时间变长。解决方案是采用异步加载的方式,在系统启动的同时,在后台线程中进行数据预热,不影响系统正常启动流程。 - 缓存过期时间打散 避免大量缓存数据在同一时间过期。可以为每个缓存数据设置一个随机的过期时间,例如原本设置缓存过期时间为1小时,可以改为在50 - 70分钟之间随机设置。在代码中可以这样实现(以Python为例):
import redis
import random
r = redis.Redis(host='localhost', port=6379, db=0)
key = 'example_key'
value = 'example_value'
expire_time = random.randint(3000, 4200) # 50 - 70分钟
r.setex(key, expire_time, value)
新问题及解决方案:可能导致部分数据在较短时间内过期,影响缓存命中率。解决方案是根据业务特点合理调整随机范围,对于特别重要的热点数据,可以适当延长过期时间范围,保证其在较长时间内存在于缓存中。
系统架构层面
- 引入多级缓存 可以采用本地缓存(如Guava Cache)和Redis分布式缓存相结合的方式。当用户请求到达时,先查询本地缓存,如果命中则直接返回;未命中则查询Redis缓存。如果Redis也未命中,再查询数据库,并将结果依次写入Redis和本地缓存。 新问题及解决方案:本地缓存和Redis缓存一致性问题。可以通过设置本地缓存过期时间略短于Redis缓存,或者在数据更新时,同时更新本地缓存和Redis缓存。但同时更新可能在高并发下存在一致性风险,还可以采用发布 - 订阅模式,在数据更新时发布消息,让各个节点收到消息后主动更新本地缓存。
- 使用缓存集群 采用Redis集群(如Redis Cluster),将数据分布在多个节点上,避免单点故障。当某个节点出现问题时,集群可以自动进行故障转移,继续提供服务。 新问题及解决方案:数据分布不均匀问题。可以通过合理的哈希算法(如一致性哈希)来确保数据均匀分布在各个节点上。同时,在集群扩容或缩容时,需要考虑数据迁移问题,可以使用Redis Cluster自带的机制来进行平滑迁移。
代码实现层面
- 加锁机制 当缓存未命中时,使用分布式锁(如Redis的SETNX命令实现)来保证只有一个线程去查询数据库并更新缓存。以Java为例:
import redis.clients.jedis.Jedis;
public class CacheUtil {
private static final Jedis jedis = new Jedis("localhost", 6379);
public static String getValue(String key) {
String value = jedis.get(key);
if (value == null) {
String lockKey = "lock:" + key;
String requestId = UUID.randomUUID().toString();
try {
while (!jedis.set(lockKey, requestId, "NX", "EX", 10).equals("OK")) {
// 未获取到锁,等待一段时间后重试
Thread.sleep(100);
}
// 获取到锁,查询数据库
value = getDataFromDB(key);
if (value != null) {
jedis.setex(key, 3600, value); // 更新缓存
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (requestId.equals(jedis.get(lockKey))) {
jedis.del(lockKey); // 释放锁
}
}
}
return value;
}
private static String getDataFromDB(String key) {
// 模拟从数据库获取数据
return "data from db";
}
}
新问题及解决方案:可能出现死锁问题。设置合理的锁过期时间(如代码中的10秒),避免因程序异常导致锁一直未释放。同时,在释放锁时要确保是当前持有锁的线程释放,通过比较请求ID来实现。另外,加锁机制会降低系统并发性能,可通过优化锁的粒度,如对不同类型的数据采用不同的锁,减少锁竞争。