一站式了解本地缓存Guava(内含面试点)

引言

在后端开发中或者面试中,我们经常会谈到多级缓存这个概念,那么这里就要来说一说缓存的分类了。缓存分为本地缓存和分布式缓存(数据库缓存已经被废弃了),本地缓存常见的有Guava,Ehcache,Caffeine,分布式缓存常见的有Redis,Memcached。那么我们今天主要来聊聊本地缓存Guava。

本地缓存的必要性🤤

不是有了像redis这样的分布式缓存了吗,为什么还要本地缓存呢?因为本地缓存有这样的优势:

  1. 提升性能
  • 减少延迟:通过在应用层引入本地缓存作为第一级缓存(L1),可以直接在进程内部快速响应查询请求,而不需要每次都访问更慢的外部存储(如远程缓存或数据库)。这显著减少了响应时间。
  • 减少IO:由于本地缓存可以命中部分数据,减少了与分布式缓存的交互,也就减少了IO次数,进而减少了网络开销
  1. 减轻后端压力
  • 降低负载:多级缓存策略可以有效减少对后端数据库或其他外部服务的直接访问次数,从而减轻这些资源的压力,提高整体系统的稳定性和可用性。
  1. 增加缓存命中率
  • 优化缓存利用率:不同的缓存级别可以根据其特点存储不同热度的数据。例如,本地缓存可以存放最常访问的数据,而分布式缓存则用于存放相对较少访问但仍然重要的数据。这种方式能够最大化利用各个层级缓存的优势,提高整体缓存命中率。
  1. 容错能力增强
  • 提供冗余:当一个级别的缓存发生故障或失效时,其他级别的缓存仍然可以提供一定程度的服务,避免了单点失败带来的严重影响,提高了系统的容错能力和可靠性。
  1. 灵活性与可扩展性
  • 易于调整和扩展:根据业务需求的变化,可以灵活地调整各级缓存的大小、过期策略等参数,甚至添加新的缓存层次,以适应不断变化的工作负载。

多级缓存访问流程🥹

多级缓存架构中,一般的设计模式是:

先查本地缓存(L1缓存) → 未命中则查分布式缓存(L2缓存) → 还未命中才查询数据库。

这种设计的目的在于兼顾性能与一致性

使用Guava作为本地缓存😈

首先要注意的是,Guava本身并不提供注解来使用缓存功能。Guava 提供了 CacheBuilder API 来以编程方式配置和使用缓存。

所以我们这里可以使用Spring Cache 集成 Guava 的方式来使用guava的本地缓存功能,这样非常方便,因为它允许通过注解来(如 @Cacheable@CachePut@CacheEvict)来声明性地配置缓存。

添加Spring Cache 和 Guava 依赖

在pom文件添加以下依赖:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>33.4.8-jre</version>
</dependency>

配置Guava CacheManager

创建一个配置类,用于配置 GuavaCacheManager 作为 Spring 的缓存管理器。

记得在配置类或者启动类加上@EnableCaching

java 复制代码
@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        GuavaCacheManager cacheManager = new GuavaCacheManager();
        CacheBuilder<Object, Object> cacheBuilder = CacheBuilder.newBuilder()
                .maximumSize(100)
                .expireAfterWrite(10, TimeUnit.MINUTES);
        cacheManager.setCacheBuilder(cacheBuilder);
        return cacheManager;
    }
}

使用@Cacheable 注解

@Cacheable 是 Spring 框架中用于缓存管理的一个注解。它被用来标记一个方法,表示该方法的返回值可以被缓存起来。当再次调用这个方法,并且传入相同的参数时,框架会直接从缓存中获取结果,而不是重新执行方法体内的逻辑。这在减少数据库查询次数、提高应用性能方面非常有用。

主要属性

  • value/cacheNames: 指定缓存的名称。这是必需的属性,因为它定义了缓存存储的位置(即哪个缓存)。
  • key: 定义缓存的键,默认是使用方法的所有参数来生成。可以通过 SpEL(Spring Expression Language)自定义键值。
  • condition: 可选属性,指定一个条件表达式,只有满足条件时才会进行缓存。例如,可以根据方法参数决定是否缓存结果。
  • unless : 与 condition 相似,但它是在方法成功执行之后评估的。如果条件为真,则不会缓存方法的结果

使用示例:

java 复制代码
@Cacheable(value = "books", key = "#isbn")
public Book findBookByIsbn(String isbn) {
    // 方法逻辑...
}

在这个例子中,findBookByIsbn 方法的结果会被缓存到名为 "books" 的缓存中。缓存的键是传入的 isbn 参数值。如果多次调用 findBookByIsbn 方法并传递相同的 ISBN 值,那么第二次及以后的调用将直接从缓存中获取结果,而不会执行方法体中的逻辑。

ps:要注意缓存机制依赖于代理模式(AOP),因此 @Cacheable 注解不能应用于本类内部的方法调用,否则会失效😵

使用@CachePut 注解

@CachePut 的作用是:无论缓存中是否存在数据,都会执行方法体,并将方法的返回值更新到缓存中

  • 方法一定会执行
  • 返回结果会写入缓存
  • 常用于更新操作后刷新缓存
value / cacheNames 必填,指定缓存的名字(可以是一个或多个),如 "users"
key 可选,默认使用参数生成缓存键,可以通过 SpEL 表达式自定义,如 #user.id
condition 可选,只有满足条件时才更新缓存,如 #user.age > 18
unless 可选,在方法执行之后判断,如果表达式为 true,则不更新缓存

使用示例:

java 复制代码
@Service
public class UserService {

    @CachePut(value = "users", key = "#user.id")
    public User updateUser(User user) {
        // 模拟更新数据库操作
        System.out.println("Updating user in database: " + user);
        return user; // 返回值会被放入缓存
    }
}
  • 当调用 updateUser() 方法时:

    • 不管缓存中有没有 user.id 对应的数据,方法都会执行
    • 方法执行完毕后,返回值(即 user)会被写入名为 "users" 的缓存中,键为 #user.id
  • 下次通过 @Cacheable 获取这个用户时,就会直接命中缓存

使用@CacheEvict 注解

@CacheEvict 用于清除缓存。它通常用于在执行某些操作(如删除或更新数据)后,清理相关的缓存条目,以保证缓存和底层数据源(如数据库)之间的一致性

@CacheEvict 的作用是:从缓存中移除一个或多个条目,你可以根据方法参数动态决定要清除的缓存键,也可以选择是否清除整个缓存区域(即清空整个缓存)。

value / cacheNames 必填,指定要清除的缓存名称,如 "users"
key 可选,默认使用参数生成缓存键,可以通过 SpEL 自定义
allEntries 布尔值,默认 false,若为 true,则清除整个缓存区域的所有条目
beforeInvocation 默认 false,表示在方法执行之后 清除缓存;若设为 true,则在方法执行之前清除

简单示例:

java 复制代码
@CacheEvict(value = "users", key = "#userId", beforeInvocation = true)
public void deleteUserById(String userId) {
    // 删除数据库记录
}

即使方法抛出异常,缓存也会被清除(因为是在方法执行前进行的)

示例:按 key 清除单个缓存项

调用 deleteUserById("123") 时,会从 "users" 缓存中移除键为 "123" 的缓存项。

java 复制代码
@Service
public class UserService {

    @CacheEvict(value = "users", key = "#userId")
    public void deleteUserById(String userId) {
        // 模拟删除用户操作
        System.out.println("Deleting user from database: " + userId);
    }
}

示例:清除整个缓存区域

使用 allEntries = true 表示清除 "users" 缓存下的所有条目。

java 复制代码
@CacheEvict(value = "users", allEntries = true)
public void clearAllUsersCache() {
    // 不需要执行任何业务逻辑,仅用于清空缓存
}

示例:结合条件判断是否清除缓存

只有当 #user.shouldBeRemoved == true 时,才会清除该用户的缓存。

java 复制代码
@CacheEvict(value = "users", key = "#user.id", condition = "#user.shouldBeRemoved")
public void removeUserIfNecessary(User user) {
    // 删除逻辑
}

总结❤️

如果你看了这篇文章有收获可以点赞+关注+收藏🤩,这是对笔者更新的最大鼓励!如果你有更多方案或者文章中有错漏之处,请在评论区提出帮助笔者勘误,祝你拿到更好的offer!

相关推荐
恸流失17 分钟前
DJango项目
后端·python·django
互联网搬砖老肖2 小时前
Web 架构相关文章目录(持续更新中)
架构
计算机毕设定制辅导-无忧学长2 小时前
Kafka 核心架构与消息模型深度解析(二)
架构·kafka·linq
计算机毕设定制辅导-无忧学长2 小时前
Kafka 核心架构与消息模型深度解析(一)
分布式·架构·kafka
Mr Aokey3 小时前
Spring MVC参数绑定终极手册:单&多参/对象/集合/JSON/文件上传精讲
java·后端·spring
地藏Kelvin3 小时前
Spring Ai 从Demo到搭建套壳项目(二)实现deepseek+MCP client让高德生成昆明游玩4天攻略
人工智能·spring boot·后端
菠萝014 小时前
共识算法Raft系列(1)——什么是Raft?
c++·后端·算法·区块链·共识算法
长勺4 小时前
Spring中@Primary注解的作用与使用
java·后端·spring
小奏技术5 小时前
基于 Spring AI 和 MCP:用自然语言查询 RocketMQ 消息
后端·aigc·mcp
编程轨迹5 小时前
面试官:如何在 Java 中读取和解析 JSON 文件
后端