缓存设计模式有哪些?
缓存设计模式在软件开发中用于提升数据访问效率和系统性能。常见的缓存设计模式包括:
- Cache Aside(旁路缓存)模式:应用程序直接与缓存和数据库交互。
- 读操作流程:
- 应用程序先从缓存读取数据。
- 如果缓存命中,返回数据。
- 如果缓存未命中,从数据库读取数据,将数据写入缓存,然后返回数据。
- 写操作流程:
- 应用程序更新数据库中的数据。
- 然后删除或更新缓存中的旧数据。
- 读操作流程:
- Read-Through 模式:应用程序只与缓存交互,缓存系统负责从数据库加载数据。
- 操作流程:
- 应用程序从缓存读取数据。
- 如果缓存命中,返回数据。
- 如果缓存未命中,缓存系统从数据库加载数据,写入缓存,然后返回数据。
- 操作流程:
- Write-Through 模式:应用程序只与缓存交互,缓存系统负责同步更新数据库。
- 操作流程:
- 应用程序更新缓存中的数据。
- 缓存系统同步将数据写入数据库,确保数据一致性。
- 操作流程:
- Write-Behind(Write-Back)模式:应用程序更新缓存,缓存系统异步批量更新数据库。
- 操作流程:
- 应用程序更新缓存中的数据。
- 缓存系统在后台异步批量将数据写入数据库。
- 操作流程:
本地缓存的优缺点
本地缓存是一种将缓存数据存储在应用程序内存中的方式,主要通过工具(如 Caffeine
、Guava
)实现。以下是本地缓存的主要优缺点:
优点
1. 访问速度快
- 原因:数据直接存储在应用程序的内存中,避免了网络传输和外部系统调用。
- 适用场景:访问频率高、实时性要求强的场景(如
热点数据
、会话数据
)。
2. 实现简单
- 原因:本地缓存依赖轻量级工具,无需部署独立的缓存服务(如
Redis
)。 - 适用场景:中小型应用或对性能需求适中的场景。
3. 降低外部依赖
- 原因:数据在本地内存中,减少对数据库和分布式缓存系统的访问,降低外部系统的压力。
- 适用场景:分布式缓存系统性能瓶颈的场景。
4. 支持复杂数据结构
- 原因:许多本地缓存工具(如
Caffeine
)支持复杂的数据存储和操作(如缓存过期策略、统计功能)。 - 适用场景:需要定制缓存策略或操作复杂数据的场景。
缺点
1. 数据量有限
- 原因:受限于服务器内存,缓存数据量不能过大,否则可能导致内存溢出。
- 适用建议:适合小数据量的热点数据,无法缓存大规模数据。
2. 分布式场景不适用
- 原因:本地缓存只适用于单实例,无法在分布式环境中共享数据。
- 适用建议:分布式应用需要引入分布式缓存(如 Redis、Memcached)。
3. 数据一致性问题
- 原因:缓存与数据库数据可能不同步,尤其在数据更新频繁时,本地缓存难以实时更新。
- 适用建议:对一致性要求较高的场景需要谨慎使用。
4. 缓存命中率依赖初始化
- 原因:本地缓存的初始状态为空,需要逐步填充数据,可能导致首次访问命中率低。
- 适用建议:可以在应用启动时预加载常用数据。
5. 增加内存压力
- 原因:缓存占用内存资源过多可能影响其他业务逻辑的运行。
- 适用建议:设置合理的缓存大小和过期策略,防止缓存滥用。
总结对比
优点 | 缺点 |
---|---|
访问速度快 | 数据量有限,受内存限制 |
实现简单,依赖轻 | 不适合分布式环境 |
减少外部系统压力 | 数据一致性问题 |
支持复杂数据结构和策略 | 缓存初始命中率低 |
场景适用建议
-
适合场景:
- 热点数据(如商品分类、配置数据、用户会话等)。
- 中小型系统或单节点服务。
- 数据更新频率低、读取频率高的场景。
-
不适合场景:
- 分布式系统,需要共享缓存数据。
- 数据量特别大,内存无法承载。
- 对数据一致性要求极高的场景。
本地缓存的应用
本地缓存是指将缓存数据存储在应用程序的内存中,具有高速访问、简单实现等优点,常用于以下场景:
1. 热点数据缓存
- 场景描述:访问频率极高的数据(如热门商品、用户会话信息等),对性能要求高。
- 适用原因:热点数据的访问次数多,存储在本地缓存中可以减少网络调用和数据库压力。
Java 示例代码(使用 Caffeine 缓存实现):
@Service
public class HotDataService {
private final Cache<Long, String> hotDataCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
public String getHotData(Long id) {
return hotDataCache.get(id, this::fetchFromDatabase);
}
private String fetchFromDatabase(Long id) {
// 模拟从数据库获取数据
return "HotData-" + id;
}
}
2. 配置数据缓存
- 场景描述:应用程序的配置数据(如特定阈值、特性开关)更新频率较低,但读取频率高。
- 适用原因:配置数据的更新不频繁,本地缓存可以快速提供访问而无需每次请求配置服务或数据库。
Java 示例代码:
@Service
public class ConfigService {
private final Cache<String, String> configCache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
public String getConfigValue(String key) {
return configCache.get(key, this::fetchConfigFromDb);
}
private String fetchConfigFromDb(String key) {
// 模拟从数据库获取配置
return "ConfigValue-" + key;
}
}
3. 会话数据缓存
- 场景描述:需要频繁访问的会话数据(如登录用户的会话状态)。
- 适用原因:会话数据通常与请求绑定,使用本地缓存可以避免频繁查询分布式存储或数据库。
Java 示例代码:
@Service
public class SessionService {
private final Cache<String, String> sessionCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterAccess(30, TimeUnit.MINUTES)
.build();
public void storeSession(String sessionId, String userInfo) {
sessionCache.put(sessionId, userInfo);
}
public String getSession(String sessionId) {
return sessionCache.getIfPresent(sessionId);
}
public void removeSession(String sessionId) {
sessionCache.invalidate(sessionId);
}
}
缓存设计模式结合场景应用及示例
1. Cache Aside 模式(旁路缓存) 应用于热点数据
- 场景:电商平台的热门商品详情页访问量高。
- 适用原因:Cache Aside 模式灵活,允许缓存和数据库数据不同步的短暂窗口,适合热点数据。
Java 示例代码:
@Service
public class ProductService {
private final Cache<Long, Product> productCache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
public Product getProduct(Long productId) {
return productCache.get(productId, this::fetchProductFromDb);
}
private Product fetchProductFromDb(Long productId) {
// 从数据库获取数据
return productRepository.findById(productId).orElse(null);
}
public void updateProduct(Product product) {
// 更新数据库
productRepository.save(product);
// 删除缓存中的数据
productCache.invalidate(product.getId());
}
}
2. Read-Through 模式 应用于配置数据
- 场景:系统启动时加载配置数据,用户访问时直接从缓存读取。
- 适用原因:配置数据通常从缓存读取,更新较少,Read-Through 模式简化了缓存操作。
Java 示例代码:
@Service
public class ConfigReadThroughService {
private final Cache<String, String> configCache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
public String getConfig(String key) {
return configCache.get(key, this::fetchConfigFromSource);
}
private String fetchConfigFromSource(String key) {
// 从配置中心或数据库获取配置
return "ConfigValue-" + key;
}
}
3. Write-Through 模式 应用于用户权限数据
- 场景:后台管理系统中,管理员更新用户权限时,同时更新缓存和数据库。
- 适用原因:Write-Through 模式保证数据一致性,适合敏感数据的读写操作。
Java 示例代码:
@Service
public class PermissionService {
private final Cache<Long, List<String>> permissionCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
public List<String> getUserPermissions(Long userId) {
return permissionCache.get(userId, this::fetchPermissionsFromDb);
}
private List<String> fetchPermissionsFromDb(Long userId) {
// 从数据库获取用户权限
return permissionRepository.findPermissionsByUserId(userId);
}
public void updatePermissions(Long userId, List<String> permissions) {
// 同步更新数据库和缓存
permissionRepository.updatePermissions(userId, permissions);
permissionCache.put(userId, permissions);
}
}
- 本地缓存的典型应用场景:热点数据、配置数据、会话数据。
- 缓存设计模式的结合场景:
- Cache Aside:适合灵活处理数据一致性的场景。
- Read-Through:适合读取频繁、更新较少的场景。
- Write-Through:适合对一致性要求高的数据。
二级缓存是指在系统中设置两级缓存:一级缓存通常是本地缓存(如 Caffeine)
,位于应用程序内部,提供快速访问;二级缓存通常是分布式缓存(如 Redis)
,用于共享
和持久化
数据。
二级缓存的优缺点:
-
优点:
- 提高数据访问速度,减少数据库压力。
- 本地缓存减少网络延迟,提升性能。
- 分布式缓存提供数据一致性和持久性。
-
缺点:
- 增加系统复杂度,需要处理缓存同步和一致性问题。
- 可能出现缓存不一致的情况,需要设计有效的失效策略。
Redis+Caffeine 实现两级缓存:
在这种架构中,Caffeine 作为一级缓存,存储于应用程序内部,提供快速访问;Redis 作为二级缓存,存储于分布式系统中,提供共享和持久化。
应用场景:
-
商品详情页缓存:电商平台的商品详情页访问频繁,使用两级缓存可以提高响应速度,减少数据库压力。
-
用户信息缓存:社交平台的用户信息查询频繁,采用两级缓存可以提升查询效率,改善用户体验。
-
配置数据缓存:系统的配置数据读取频繁且更新较少,使用两级缓存可以提高读取速度,确保配置的及时性。
代码示例:
以下是一个使用 Spring Boot
、Caffeine
和 Redis
实现两级缓存的示例,应用于查询商品详情的场景。
-
引入依赖(在
pom.xml
中添加):<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency>
-
配置缓存:
@Configuration public class CacheConfig extends CachingConfigurerSupport { @Bean public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { // 配置 Redis 缓存 RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(redisCacheConfiguration) .build(); // 配置 Caffeine 缓存 CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(); caffeineCacheManager.setCaffeine(Caffeine.newBuilder() .initialCapacity(100) .maximumSize(1000) .expireAfterWrite(60, TimeUnit.SECONDS)); // 将两者结合 CompositeCacheManager cacheManager = new CompositeCacheManager(caffeineCacheManager, redisCacheManager); cacheManager.setFallbackToNoOpCache(false); return cacheManager; } }
-
使用缓存:
@Service public class ProductService { @Cacheable(value = "productCache", key = "#productId") public Product getProductById(Long productId) { // 从数据库获取商品信息的逻辑 return productRepository.findById(productId).orElse(null); } @CacheEvict(value = "productCache", key = "#product.id") public void updateProduct(Product product) { // 更新数据库中的商品信息 productRepository.save(product); } @CacheEvict(value = "productCache", key = "#productId") public void deleteProduct(Long productId) { // 删除数据库中的商品信息 productRepository.deleteById(productId); } }
二级缓存流程:
以下是 Redis + Caffeine
两级缓存的流程图,展示了查询和写入数据的操作过程。
-
查询操作流程:
- 先从 Caffeine 一级缓存中查找数据。
- 如果命中,直接返回数据。
- 如果未命中,从 Redis 二级缓存中查找。
- 如果 Redis 命中,数据加载到 Caffeine 缓存,同时返回数据。
- 如果 Redis 未命中,从数据库读取数据,数据加载到 Redis 和 Caffeine 缓存,然后返回数据。
-
写入操作流程:
- 更新数据库中的数据。
- 同步更新或失效 Redis 和 Caffeine 缓存中的数据。
- 写入操作:
常见应用场景:
1. 新闻热点缓存
- 新闻热点的访问频率很高且更新及时,适合使用两级缓存架构。
Java 示例:
@Cacheable(value = "newsCache", key = "#newsId")
public News getNewsById(Long newsId) {
return newsRepository.findById(newsId).orElse(null);
}
@CacheEvict(value = "newsCache", key = "#news.id")
public void updateNews(News news) {
newsRepository.save(news);
}
@CacheEvict(value = "newsCache", key = "#newsId")
public void deleteNews(Long newsId) {
newsRepository.deleteById(newsId);
}
2. 金融交易缓存
- 金融系统中的交易记录
查询需求高
,但写入频率较低
,可使用两级缓存提高查询效率。
Java 示例:
@Cacheable(value = "transactionCache", key = "#transactionId")
public Transaction getTransactionById(Long transactionId) {
return transactionRepository.findById(transactionId).orElse(null);
}
@CacheEvict(value = "transactionCache", key = "#transaction.id")
public void updateTransaction(Transaction transaction) {
transactionRepository.save(transaction);
}
@CacheEvict(value = "transactionCache", key = "#transactionId")
public void deleteTransaction(Long transactionId) {
transactionRepository.deleteById(transactionId);
}
3. 电商购物车缓存
- 用户的购物车数据需要
频繁读取和更新
,适合使用两级缓存保证性能。
Java 示例:
@Cacheable(value = "cartCache", key = "#userId")
public ShoppingCart getCartByUserId(Long userId) {
return shoppingCartRepository.findByUserId(userId);
}
@CacheEvict(value = "cartCache", key = "#userId")
public void updateCart(Long userId, ShoppingCart cart) {
shoppingCartRepository.save(cart);
}
4. 商品分类缓存
场景:商品分类信息更新少
,但读取频繁
。适合使用两级缓存减少分类查询对数据库的压力。
Java 代码实现:
@Service
public class CategoryService {
@Cacheable(value = "categoryCache", key = "#categoryId")
public Category getCategoryById(Long categoryId) {
// 从数据库查询分类信息
return categoryRepository.findById(categoryId).orElse(null);
}
@CacheEvict(value = "categoryCache", key = "#category.id")
public void updateCategory(Category category) {
// 更新数据库中的分类信息
categoryRepository.save(category);
}
@CacheEvict(value = "categoryCache", key = "#categoryId")
public void deleteCategory(Long categoryId) {
// 删除数据库中的分类信息
categoryRepository.deleteById(categoryId);
}
}
5. 文章详情缓存
场景:博客系统中的文章详情读取频繁
,使用两级缓存加速访问。
Java 代码实现:
@Service
public class ArticleService {
@Cacheable(value = "articleCache", key = "#articleId")
public Article getArticleById(Long articleId) {
// 从数据库查询文章详情
return articleRepository.findById(articleId).orElse(null);
}
@CacheEvict(value = "articleCache", key = "#article.id")
public void updateArticle(Article article) {
// 更新数据库中的文章详情
articleRepository.save(article);
}
@CacheEvict(value = "articleCache", key = "#articleId")
public void deleteArticle(Long articleId) {
// 删除数据库中的文章详情
articleRepository.deleteById(articleId);
}
}
6. 用户权限缓存
场景:后台系统中用户权限查询频繁,且权限更新较少,适合使用两级缓存提高查询效率。
Java 代码实现:
@Service
public class UserPermissionService {
@Cacheable(value = "permissionCache", key = "#userId")
public List<Permission> getPermissionsByUserId(Long userId) {
// 从数据库查询用户权限信息
return permissionRepository.findPermissionsByUserId(userId);
}
@CacheEvict(value = "permissionCache", key = "#userId")
public void updatePermissions(Long userId, List<Permission> permissions) {
// 更新用户权限信息
permissionRepository.updatePermissions(userId, permissions);
}
@CacheEvict(value = "permissionCache", key = "#userId")
public void deletePermissions(Long userId) {
// 删除用户权限信息
permissionRepository.deletePermissionsByUserId(userId);
}
}
总结:
使用 Redis
+ Caffeine
构建两级缓存,能够满足多种高频读操作场景(如商品、用户、配置等)的性能需求,同时减少数据库压力。通过合理设计缓存失效策略与数据同步机制,可以有效规避缓存不一致问题。