MST

星途 面试题库

面试题:缓存设计与微服务架构的深度融合优化

当微服务架构中的服务数量众多且业务复杂时,缓存设计需要更深入的优化以提升系统性能和稳定性。请详细阐述如何对缓存进行分层设计,如何结合微服务的特点(如服务发现、负载均衡等)进行缓存节点的动态管理,以及在高并发场景下如何避免缓存雪崩和缓存击穿问题,同时提供相应的代码示例或架构图说明。
33.3万 热度难度
后端开发缓存设计

知识考点

AI 面试

面试题答案

一键面试

缓存分层设计

  1. 数据访问层缓存(一级缓存)
    • 特点:靠近应用程序,通常在应用服务器本地。缓存数据的读取直接在本地进行,减少远程调用开销。
    • 优点:响应速度极快,能快速返回频繁访问的数据。
    • 缺点:存储容量有限,且由于每个应用服务器都有自己的缓存,数据一致性维护较困难。
    • 示例:在Java应用中,可以使用Guava Cache作为本地缓存。
    LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
          .maximumSize(1000)
          .expireAfterWrite(10, TimeUnit.MINUTES)
          .build(
               new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) throws Exception {
                        // 从数据库或其他数据源加载数据
                        return loadDataFromDB(key);
                    }
                }
          );
    Object value = cache.get("someKey");
    
  2. 分布式缓存层(二级缓存)
    • 特点:独立于应用服务器,作为共享缓存供多个应用实例使用。常见的如Redis。
    • 优点:具有高可用、可扩展性,能存储大量数据,且可以在多个应用之间共享数据,减少数据冗余。
    • 缺点:存在网络开销,相比本地缓存响应速度稍慢。
    • 示例:使用Jedis操作Redis缓存。
    Jedis jedis = new Jedis("localhost", 6379);
    jedis.set("key", "value");
    String value = jedis.get("key");
    jedis.close();
    
  3. 持久化存储层(三级缓存)
    • 特点:通常是数据库,如关系型数据库(MySQL)或非关系型数据库(MongoDB)。数据持久化存储,容量大,但读写速度相对较慢。
    • 优点:数据安全性高,能长期保存大量数据。
    • 缺点:读写性能相对缓存较差,不适合高并发读操作。

结合微服务特点的缓存节点动态管理

  1. 基于服务发现
    • 原理:微服务架构中通常有服务发现组件(如Eureka、Consul)。缓存节点可以注册到服务发现组件中,应用程序通过服务发现组件获取缓存节点的地址。当缓存节点发生变化(如新增、移除、故障转移)时,服务发现组件会更新信息,应用程序可以重新获取最新的缓存节点地址。
    • 示例:在Spring Cloud Eureka环境下,假设缓存服务注册到Eureka。
    • 缓存服务注册配置(如Redis服务):
    eureka:
      client:
        service - url:
          defaultZone: http://eureka - server1:8761/eureka/,http://eureka - server2:8761/eureka/
    
    • 应用程序获取缓存节点:
    @Autowired
    private DiscoveryClient discoveryClient;
    List<ServiceInstance> instances = discoveryClient.getInstances("redis - service");
    ServiceInstance instance = instances.get(0);
    String host = instance.getHost();
    int port = instance.getPort();
    
  2. 负载均衡
    • 原理:在多个缓存节点间进行负载均衡,避免单个缓存节点压力过大。常见的负载均衡算法有轮询、随机、加权轮询等。可以在应用程序客户端实现负载均衡,也可以使用专门的负载均衡器(如Nginx)。
    • 示例:在客户端使用轮询算法实现缓存节点负载均衡。
    List<ServiceInstance> instances = discoveryClient.getInstances("redis - service");
    int instanceIndex = counter % instances.size();
    ServiceInstance instance = instances.get(instanceIndex);
    counter++;
    

避免缓存雪崩和缓存击穿问题

  1. 缓存雪崩
    • 问题描述:大量缓存数据在同一时间过期,导致大量请求直接访问数据库,使数据库压力骤增甚至崩溃。
    • 解决方案
      • 设置随机过期时间:在原有的过期时间基础上,加上一个随机值。例如,原本过期时间为1小时,可以设置为1小时到1小时10分钟之间的随机值。
      int baseExpireTime = 3600; // 1小时
      int randomExpireTime = new Random().nextInt(600); // 0到10分钟之间随机值
      int totalExpireTime = baseExpireTime + randomExpireTime;
      jedis.setex("key", totalExpireTime, "value");
      
      • 使用缓存集群:将数据分散到多个缓存节点上,避免所有数据集中过期。同时配置高可用,如Redis Sentinel或Redis Cluster。
  2. 缓存击穿
    • 问题描述:单个热点数据缓存过期瞬间,大量请求同时访问该数据,直接穿透到数据库。
    • 解决方案
      • 互斥锁:在缓存过期时,使用互斥锁(如Redis的SETNX命令)保证只有一个请求能去加载数据到缓存。其他请求等待,等数据加载到缓存后再从缓存获取。
      String lockKey = "lock:key";
      String uniqueValue = UUID.randomUUID().toString();
      if (jedis.set(lockKey, uniqueValue, "NX", "EX", 10) != null) {
          try {
              Object value = loadDataFromDB("key");
              jedis.set("key", value.toString());
          } finally {
              jedis.del(lockKey);
          }
      }
      Object value = jedis.get("key");
      if (value == null) {
          // 再次尝试获取,可能在等待锁期间数据已被加载
          value = jedis.get("key");
      }
      
      • 热点数据永不过期:对于热点数据不设置过期时间,同时可以异步更新缓存数据,保证数据的实时性。

架构图说明

[此处可以手绘一个简单架构图,或者用文字描述架构图结构]

  1. 应用层:多个微服务应用实例,每个实例包含本地缓存(一级缓存)。
  2. 分布式缓存层:由多个Redis节点组成的集群,通过负载均衡器(如Nginx)对外提供服务,实现缓存节点的负载均衡。缓存节点注册到服务发现组件(如Eureka),应用程序通过服务发现获取缓存节点地址。
  3. 持久化存储层:数据库,如MySQL或MongoDB,作为数据的最终存储,当缓存中没有数据时,从数据库读取并更新到缓存。