面试题答案
一键面试缓存分层设计
- 数据访问层缓存(一级缓存):
- 特点:靠近应用程序,通常在应用服务器本地。缓存数据的读取直接在本地进行,减少远程调用开销。
- 优点:响应速度极快,能快速返回频繁访问的数据。
- 缺点:存储容量有限,且由于每个应用服务器都有自己的缓存,数据一致性维护较困难。
- 示例:在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");
- 分布式缓存层(二级缓存):
- 特点:独立于应用服务器,作为共享缓存供多个应用实例使用。常见的如Redis。
- 优点:具有高可用、可扩展性,能存储大量数据,且可以在多个应用之间共享数据,减少数据冗余。
- 缺点:存在网络开销,相比本地缓存响应速度稍慢。
- 示例:使用Jedis操作Redis缓存。
Jedis jedis = new Jedis("localhost", 6379); jedis.set("key", "value"); String value = jedis.get("key"); jedis.close();
- 持久化存储层(三级缓存):
- 特点:通常是数据库,如关系型数据库(MySQL)或非关系型数据库(MongoDB)。数据持久化存储,容量大,但读写速度相对较慢。
- 优点:数据安全性高,能长期保存大量数据。
- 缺点:读写性能相对缓存较差,不适合高并发读操作。
结合微服务特点的缓存节点动态管理
- 基于服务发现:
- 原理:微服务架构中通常有服务发现组件(如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();
- 负载均衡:
- 原理:在多个缓存节点间进行负载均衡,避免单个缓存节点压力过大。常见的负载均衡算法有轮询、随机、加权轮询等。可以在应用程序客户端实现负载均衡,也可以使用专门的负载均衡器(如Nginx)。
- 示例:在客户端使用轮询算法实现缓存节点负载均衡。
List<ServiceInstance> instances = discoveryClient.getInstances("redis - service"); int instanceIndex = counter % instances.size(); ServiceInstance instance = instances.get(instanceIndex); counter++;
避免缓存雪崩和缓存击穿问题
- 缓存雪崩:
- 问题描述:大量缓存数据在同一时间过期,导致大量请求直接访问数据库,使数据库压力骤增甚至崩溃。
- 解决方案:
- 设置随机过期时间:在原有的过期时间基础上,加上一个随机值。例如,原本过期时间为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。
- 缓存击穿:
- 问题描述:单个热点数据缓存过期瞬间,大量请求同时访问该数据,直接穿透到数据库。
- 解决方案:
- 互斥锁:在缓存过期时,使用互斥锁(如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"); }
- 热点数据永不过期:对于热点数据不设置过期时间,同时可以异步更新缓存数据,保证数据的实时性。
架构图说明
[此处可以手绘一个简单架构图,或者用文字描述架构图结构]
- 应用层:多个微服务应用实例,每个实例包含本地缓存(一级缓存)。
- 分布式缓存层:由多个Redis节点组成的集群,通过负载均衡器(如Nginx)对外提供服务,实现缓存节点的负载均衡。缓存节点注册到服务发现组件(如Eureka),应用程序通过服务发现获取缓存节点地址。
- 持久化存储层:数据库,如MySQL或MongoDB,作为数据的最终存储,当缓存中没有数据时,从数据库读取并更新到缓存。