MST
星途 面试题库

面试题:缓存设计之Redis在高并发分布式系统中的性能优化与缓存雪崩应对

假设你正在设计一个高并发的分布式系统,使用Redis作为缓存。在高并发场景下,缓存雪崩是一个严重的问题。请详细描述你会如何从缓存设计层面(如数据预热、缓存过期时间打散等)、系统架构层面(如引入多级缓存、使用缓存集群等)以及代码实现层面(如加锁机制等)来预防和应对缓存雪崩,确保系统的高可用性和高性能。同时,说明这些措施可能带来的新问题及解决方案。
11.9万 热度难度
后端开发缓存设计

知识考点

AI 面试

面试题答案

一键面试

缓存设计层面

  1. 数据预热 在系统上线前,将热点数据预先加载到Redis缓存中。可以通过批量读取数据库中的热点数据,然后使用MSET等命令一次性写入Redis。例如,电商系统在大促前预先加载热门商品信息。这样系统启动后,用户请求可以直接命中缓存,减少数据库压力。 新问题及解决方案:可能导致启动时间变长。解决方案是采用异步加载的方式,在系统启动的同时,在后台线程中进行数据预热,不影响系统正常启动流程。
  2. 缓存过期时间打散 避免大量缓存数据在同一时间过期。可以为每个缓存数据设置一个随机的过期时间,例如原本设置缓存过期时间为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)

新问题及解决方案:可能导致部分数据在较短时间内过期,影响缓存命中率。解决方案是根据业务特点合理调整随机范围,对于特别重要的热点数据,可以适当延长过期时间范围,保证其在较长时间内存在于缓存中。

系统架构层面

  1. 引入多级缓存 可以采用本地缓存(如Guava Cache)和Redis分布式缓存相结合的方式。当用户请求到达时,先查询本地缓存,如果命中则直接返回;未命中则查询Redis缓存。如果Redis也未命中,再查询数据库,并将结果依次写入Redis和本地缓存。 新问题及解决方案:本地缓存和Redis缓存一致性问题。可以通过设置本地缓存过期时间略短于Redis缓存,或者在数据更新时,同时更新本地缓存和Redis缓存。但同时更新可能在高并发下存在一致性风险,还可以采用发布 - 订阅模式,在数据更新时发布消息,让各个节点收到消息后主动更新本地缓存。
  2. 使用缓存集群 采用Redis集群(如Redis Cluster),将数据分布在多个节点上,避免单点故障。当某个节点出现问题时,集群可以自动进行故障转移,继续提供服务。 新问题及解决方案:数据分布不均匀问题。可以通过合理的哈希算法(如一致性哈希)来确保数据均匀分布在各个节点上。同时,在集群扩容或缩容时,需要考虑数据迁移问题,可以使用Redis Cluster自带的机制来进行平滑迁移。

代码实现层面

  1. 加锁机制 当缓存未命中时,使用分布式锁(如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来实现。另外,加锁机制会降低系统并发性能,可通过优化锁的粒度,如对不同类型的数据采用不同的锁,减少锁竞争。