十一、多级缓存——多级缓存实战详解
缓存技术在性能优化中扮演了重要角色,尤其是在面试中,关于缓存的各种问题都是考察重点。本文将介绍多级缓存的概念、实现方案及其优化技巧,重点讨论如何通过多级缓存架构来提升系统性能和缓存命中率。
1. 多级缓存介绍
多级缓存是在系统的不同层级进行数据缓存的一种策略,旨在提高数据访问效率。以下是多级缓存的一般架构及其工作流程:
-
接入 Nginx:
- 将请求负载均衡到应用 Nginx。常见的负载均衡算法有轮询和一致性哈希。轮询可均衡负载,一致性哈希则能提升缓存命中率。
-
应用 Nginx 本地缓存:
- 读取本地缓存(可以是 Lua Shared Dict、Nginx Proxy Cache 等)。如果命中,则直接返回缓存数据。这种缓存可以提升吞吐量,降低后端压力,特别是应对热点数据时非常有效。
-
分布式缓存:
- 如果本地缓存未命中,则查询分布式缓存(如 Redis)。若分布式缓存命中,则将数据返回并写入到应用 Nginx 的本地缓存中。
-
回源到应用服务器:
- 如果分布式缓存也未命中,则请求回源到应用服务器(如 Tomcat 集群)。在此过程中,负载均衡算法(轮询或一致性哈希)可以再次发挥作用。
-
Tomcat 本地堆缓存:
- 在 Tomcat 应用中,首先检查本地堆缓存。如果有缓存,则返回数据并写入主 Redis 集群。此层缓存可以防止缓存失效后的冲击。
-
主 Redis 集群(可选):
- 如果 Tomcat 堆缓存未命中,再次尝试从主 Redis 集群读取数据,以防从 Redis 集群的问题引起的流量冲击。
-
查询数据库:
- 如果所有缓存都未命中,最终查询数据库或相关服务获取数据。
-
缓存更新:
- 步骤 7 返回的数据将异步写入主 Redis 集群。要处理多个 Tomcat 实例同时写入的问题,可以使用一致性哈希或分布式锁机制。
这种多级缓存方案包含了应用 Nginx 本地缓存、分布式缓存和 Tomcat 堆缓存,每一层缓存都用于解决特定问题,例如本地缓存用于应对热点数据,分布式缓存减少回源频率,而 Tomcat 堆缓存则用于处理缓存失效带来的冲击。
2. 如何缓存数据
2.1 过期与不过期
-
不过期缓存:
- 适用场景:主要用于访问频率高且稳定的数据,例如用户信息、分类、商品详情等。这些数据变化不频繁,且需要在缓存中长期保存,以降低对数据库的频繁访问。
- 实现方式:通常采用 Cache-Aside 模式(旁路缓存模式)。在更新数据时,首先更新数据库,然后更新缓存。当缓存容量不足时,可以采用 LRU(Least Recently Used,最近最少使用)策略驱逐旧数据,以释放空间。这种方式确保了热点数据能够常驻内存,减少缓存失效带来的查询压力。
-
过期缓存:
- 适用场景:用于缓存空间有限且数据变更频率较高的场景,如实时性要求较低的热点数据。典型的例子包括库存数据、商品价格等,这些数据的短期不一致性对业务影响不大,但需要定期刷新。
- 实现方式:采用懒加载模式(Lazy Loading),即当有请求时才查询缓存,缓存未命中时,再从数据库读取数据并写入缓存。同时设置过期时间(TTL,Time to Live)以确保缓存数据能及时更新。当缓存项过期或被淘汰时,新请求将触发数据重新加载。
在 Spring Boot 和 Spring Cloud 项目中,实现不过期缓存和过期缓存主要通过使用缓存框架,如 Ehcache、Caffeine 或 Redis 来完成。
以下是这两种缓存策略的实现方法:
2.1.1 不过期缓存实现
不过期缓存通常用于访问频率高且稳定的数据,可以使用 Cache-Aside
模式结合 Spring 的 @Cacheable
注解来实现。
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// 获取用户信息,并缓存结果
@Cacheable(value = "userCache", key = "#userId")
public User getUserById(Long userId) {
// 从数据库获取用户信息
return userRepository.findById(userId).orElse(null);
}
}
关键点:
@Cacheable
注解用于将方法的返回值缓存到指定的缓存中(这里是userCache
),缓存不会过期。- 当缓存满时,可配置如 LRU(Least Recently Used)机制来淘汰旧数据。
2.1.2 过期缓存实现
过期缓存适用于短期数据不一致性允许的场景,可以通过设置 TTL(Time to Live)来实现缓存自动过期。
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
// 获取商品信息,并缓存结果,设置缓存过期时间
@Cacheable(value = "productCache", key = "#productId")
public Product getProductById(Long productId) {
// 从数据库获取商品信息
return productRepository.findById(productId).orElse(null);
}
}
如果使用 Redis 作为缓存,则可以配置 Redis 缓存的 TTL:
spring:
cache:
type: redis
redis:
time-to-live: 60000 # 设置缓存过期时间为60秒
关键点:
time-to-live
配置项可以设置缓存的过期时间。- 在缓存失效时,新的请求将触发数据重新加载并更新缓存。
这两种缓存策略各有优劣,在不同的业务场景下应灵活应用。
2.1.3 配置淘汰策略
在 Spring 中,@Cacheable
注解本身并不直接处理缓存的淘汰策略,如 LRU(Least Recently Used)等。要配置 LRU 等缓存淘汰机制,通常需要结合具体的缓存实现工具,如 Ehcache
、Caffeine
、或 Guava
等。下面是如何通过不同缓存实现工具来配置 LRU 淘汰策略的示例。
1. 使用 Caffeine 配置 LRU 机制
Caffeine 是一个高性能的 Java 缓存库,可以很容易地与 Spring Cache 集成,并支持多种缓存策略,包括 LRU。
步骤如下:
-
添加 Caffeine 依赖:
<dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>3.1.8</version> <!-- 使用最新版本 --> </dependency>
-
配置 Caffeine 缓存:
import com.github.benmanes.caffeine.cache.Caffeine; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.caffeine.CaffeineCache; import org.springframework.cache.caffeine.CaffeineCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @EnableCaching public class CacheConfig { @Bean public CacheManager cacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager("userCache"); cacheManager.setCaffeine(caffeineCacheBuilder()); return cacheManager; } Caffeine<Object, Object> caffeineCacheBuilder() { return Caffeine.newBuilder() .maximumSize(1000) // 设置缓存的最大数量 .expireAfterWrite(10, TimeUnit.MINUTES) // 可选的过期时间配置 .evictionListener((key, value, cause) -> { System.out.println("Eviction cause: " + cause); }); } }
-
使用
@Cacheable
注解:@Service public class UserService { @Cacheable(value = "userCache", key = "#userId") public User getUserById(Long userId) { // 从数据库获取用户信息 return userRepository.findById(userId).orElse(null); } }
2. 使用 Ehcache 配置 LRU 机制
Ehcache 是另一个流行的缓存实现,支持各种缓存淘汰策略,包括 LRU。
步骤如下:
-
添加 Ehcache 依赖:
<dependency> <groupId>org.ehcache</groupId> <artifactId>ehcache</artifactId> <version>3.10.8</version> <!-- 使用最新版本 --> </dependency>
-
创建 Ehcache 配置文件
ehcache.xml
:<config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns='http://www.ehcache.org/v3' xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd"> <cache alias="userCache"> <key-type>java.lang.Long</key-type> <value-type>com.example.User</value-type> <expiry> <ttl unit="minutes">10</ttl> <!-- 可选的过期时间配置 --> </expiry> <resources> <heap unit="entries">1000</heap> <!-- 设置缓存的最大数量 --> </resources> <eviction strategy="LRU"/> <!-- 设置 LRU 淘汰策略 --> </cache> </config>
-
配置 Ehcache 管理器:
import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.ehcache.EhCacheCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @EnableCaching public class CacheConfig { @Bean public CacheManager cacheManager() { return new EhCacheCacheManager(ehCacheManagerFactoryBean().getObject()); } @Bean public EhCacheManagerFactoryBean ehCacheManagerFactoryBean() { EhCacheManagerFactoryBean factory = new EhCacheManagerFactoryBean(); factory.setConfigLocation(new ClassPathResource("ehcache.xml")); factory.setShared(true); return factory; } }
-
使用
@Cacheable
注解:@Service public class UserService { @Cacheable(value = "userCache", key = "#userId") public User getUserById(Long userId) { // 从数据库获取用户信息 return userRepository.findById(userId).orElse(null); } }
3. 使用 Redis 配置 LRU 机制
Redis 本身支持 LRU 机制,但需要通过配置 Redis 的最大内存使用量以及淘汰策略来实现。
步骤如下:
-
配置 Redis 服务器的
redis.conf
文件:maxmemory 256mb # 设置最大内存使用量 maxmemory-policy allkeys-lru # 设置全局 LRU 淘汰策略
-
Spring Boot Redis 配置:
在
application.yml
文件中配置 Redis 连接:spring: redis: host: localhost port: 6379
-
配置 RedisCacheManager:
import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.redis.RedisCacheConfiguration; import org.springframework.cache.redis.RedisCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration @EnableCaching public class CacheConfig { @Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())); return RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(config) .build(); } }
-
使用
@Cacheable
注解:@Service public class UserService { @Cacheable(value = "userCache", key = "#userId") public User getUserById(Long userId) { // 从数据库获取用户信息 return userRepository.findById(userId).orElse(null); } }
2.2 维度化缓存与增量缓存
缓存机制是提升分布式系统性能的关键之一。在分布式系统中,通过合理设计缓存策略,可以有效降低数据库压力,减少接口调用次数,提高系统的响应速度。维度化缓存与增量缓存是两种常见且高效的缓存策略,尤其适用于需要频繁更新数据的场景。
2.2.1 维度化缓存
维度化缓存 是指将数据按不同维度拆分,以减少数据更新的成本和复杂性。例如,在商品数据的缓存中,可以将数据按照不同的属性进行维度化拆分,如基础属性(价格、名称、描述等)、图片列表、库存信息、上下架状态等。这样,当某个维度的数据发生变化时,只需更新对应维度的数据,而无需重新缓存整个商品数据。
优点:
- 减少更新成本:在维度化缓存中,由于数据被分拆成不同维度,更新时只需修改变更的维度部分,而不必重建整个缓存。
- 提高缓存命中率:不同维度的数据可以单独缓存,这使得缓存更具细粒度,从而提高缓存的利用率和命中率。
- 降低网络带宽消耗:由于只需更新变化的维度数据,减少了不必要的数据传输,降低了网络带宽的消耗。
Spring Cloud 实现:
在 Spring Cloud 中,可以使用 Redis 作为缓存中间件,通过维度化设计实现缓存。具体实现步骤如下:
-
定义不同维度的数据结构:
// 商品基础属性类 public class Product { private Long id; // 商品ID private String name; // 商品名称 private BigDecimal price; // 商品价格 private String description; // 商品描述 // 其他基础属性 } // 商品图片信息类 public class ProductImage { private Long productId; // 商品ID private List<String> imageUrls; // 商品图片URL列表 } // 商品状态类 public class ProductStatus { private Long productId; // 商品ID private Boolean available; // 商品是否可用 }
-
将维度化数据分别存入 Redis:
@Autowired private RedisTemplate<String, Object> redisTemplate; // Redis操作模板 // 缓存商品基础属性数据 public void cacheProductData(Product product) { redisTemplate.opsForValue().set("product:info:" + product.getId(), product); /