MST

星途 面试题库

面试题:基于Redis和MySQL的复杂业务场景下缓存策略优化

给定一个复杂的业务场景,涉及到多个表之间的关联查询,并且数据更新操作频繁。部分数据在Redis中缓存以提高查询性能,但需要保证与MySQL数据的一致性。请设计一套完整的缓存策略,包括缓存更新机制、缓存失效策略以及如何应对缓存雪崩、缓存穿透等问题,并结合Redis的特性和MySQL的事务机制进行详细说明。
35.5万 热度难度
数据库Redis

知识考点

AI 面试

面试题答案

一键面试

缓存更新机制

  1. 读写穿透
    • 读操作:应用程序先从Redis缓存中读取数据。如果缓存命中,直接返回数据;若缓存未命中,从MySQL数据库查询数据,将查询结果写入Redis缓存,并返回给应用程序。
    • 写操作:应用程序先更新MySQL数据库,使用MySQL的事务机制保证数据的一致性。在事务提交成功后,立即更新Redis缓存,确保缓存数据与数据库数据一致。例如在Java中使用Spring Data JPA进行数据库操作,结合Jedis操作Redis:
@Transactional
public void updateData(YourData data) {
    yourDataRepository.save(data);
    jedis.set("key:" + data.getId(), data.toString());
}
  1. 异步更新(基于消息队列): 写操作时,先将更新操作发送到消息队列(如Kafka、RabbitMQ),应用程序立即返回成功响应给客户端。然后由专门的消息消费者从队列中读取更新消息,先更新MySQL数据库,再更新Redis缓存。这种方式可以提高写操作的响应速度,但需要注意消息的可靠性和幂等性。

缓存失效策略

  1. 定时失效:对于一些时效性较强的数据,在写入Redis缓存时设置过期时间(expire)。例如一些实时性要求不高的统计数据,可以设置较短的过期时间,如30分钟。
SET key value EX 1800
  1. 主动失效:当数据库中的数据发生更新时,主动删除Redis中对应的缓存数据。结合前面的缓存更新机制,在成功更新数据库后,主动删除缓存。
@Transactional
public void updateData(YourData data) {
    yourDataRepository.save(data);
    jedis.del("key:" + data.getId());
}

应对缓存雪崩

  1. 设置不同过期时间:避免大量缓存数据在同一时间过期。对于不同的数据,设置随机的过期时间,如在1 - 2小时之间随机。
int randomExpire = new Random().nextInt(3600) + 3600;
jedis.setex("key", randomExpire, value);
  1. 加锁排队:当缓存失效后,在大量请求涌入时,使用分布式锁(如Redis的SETNX命令实现),只允许一个请求去查询数据库并更新缓存,其他请求等待。待第一个请求更新完缓存后,其他请求直接从缓存读取数据。
String lockKey = "lock:key";
while (!jedis.set(lockKey, "locked", "NX", "EX", 10).equals("OK")) {
    Thread.sleep(100);
}
try {
    YourData data = yourDataRepository.findById(id);
    jedis.set("key:" + id, data.toString());
} finally {
    jedis.del(lockKey);
}
  1. 二级缓存:采用两级缓存,一级缓存使用Redis,二级缓存可以使用本地缓存(如Guava Cache)。当一级缓存失效时,先从二级缓存读取数据,如果二级缓存也没有,则查询数据库并更新两级缓存。

应对缓存穿透

  1. 布隆过滤器:在查询数据前,先通过布隆过滤器判断数据是否存在。如果布隆过滤器判断不存在,则直接返回,不再查询数据库。布隆过滤器可以使用Redis的Bitmap实现。例如使用Google的Guava库中的BloomFilter,在数据写入数据库时,同时写入布隆过滤器:
BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 100000, 0.01);
// 假设dataId是数据的ID
bloomFilter.put(dataId);

查询时:

if (!bloomFilter.mightContain(dataId)) {
    return null;
}
  1. 缓存空值:当查询数据库发现数据不存在时,也将空值缓存到Redis中,并设置较短的过期时间,如1分钟。这样下次查询同样的数据时,直接从缓存返回空值,避免重复查询数据库。
YourData data = yourDataRepository.findById(id);
if (data == null) {
    jedis.setex("key:" + id, 60, "");
    return null;
}
jedis.set("key:" + id, data.toString());
return data;