Spring Cache 介绍与使用教程 – wiki基地


Spring Cache 介绍与使用教程

在现代企业级应用中,性能优化是一个永恒的话题。数据缓存是提升应用性能最常用且最有效的方法之一,它通过存储频繁访问的数据,减少对后端系统(如数据库、远程服务)的重复访问,从而显著降低响应时间并减轻后端负载。

Spring Framework 提供了一个强大的缓存抽象(Spring Cache),它允许开发者以声明式的方式,通过简单的注解,将缓存功能集成到 Spring 应用程序中,而无需编写大量的缓存逻辑代码。

1. 为什么使用 Spring Cache?

  1. 性能提升:通过将耗时操作(如数据库查询)的结果存储在内存中,后续对相同数据的请求可以直接从缓存中获取,避免了重复执行,从而大大加快了响应速度。
  2. 降低后端负载:减少了对数据库、消息队列或外部 API 等后端资源的访问频率,有助于降低它们的负载,提升系统的整体稳定性和吞吐量。
  3. 开发简洁:Spring Cache 提供了一套基于注解的声明式缓存管理机制,使得开发者能够以极低的侵入性集成缓存,将业务逻辑与缓存逻辑分离。
  4. 可插拔的缓存提供者:Spring Cache 自身是一个抽象层,它不绑定特定的缓存实现。你可以轻松地集成各种流行的缓存库,如 EhCache, Caffeine, Redis, Hazelcast 等,只需更换底层的 CacheManager 配置即可。

2. Spring Cache 的工作原理

Spring Cache 的核心在于 AOP(面向切面编程)。当你使用 @EnableCaching 启用缓存功能后,Spring 会为被缓存注解(如 @Cacheable)标记的方法生成代理。当这些方法被调用时,代理会拦截调用,并执行以下逻辑:

  1. 检查缓存:根据方法参数和预定义的缓存键生成策略,在指定的缓存中查找是否存在对应的缓存项。
  2. 命中则返回:如果缓存命中,代理会直接返回缓存中的值,而不会执行实际的方法体。
  3. 未命中则执行并缓存:如果缓存未命中,代理会执行实际的方法体,并将方法的返回值存入缓存中,然后将结果返回给调用者。
  4. 缓存更新/清除:对于 @CachePut@CacheEvict 等注解,代理会在方法执行前或执行后执行缓存更新或清除操作。

3. 如何在 Spring Boot 应用中启用 Spring Cache

3.1. 添加依赖

在 Spring Boot 项目中,最简单的方式是添加 spring-boot-starter-cache 依赖。这个 starter 会引入所有必需的传递性依赖,包括 spring-context-support

Maven:

xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

Gradle:

gradle
implementation 'org.springframework.boot:spring-boot-starter-cache'

3.2. 启用缓存功能

在你的 Spring Boot 应用主类或任何配置类上添加 @EnableCaching 注解,以激活缓存功能。

“`java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching // 启用缓存功能
public class SpringCacheTutorialApplication {

public static void main(String[] args) {
    SpringApplication.run(SpringCacheTutorialApplication.class, args);
}

}
“`

4. 核心缓存注解详解

Spring Cache 提供了多个注解来管理缓存行为:

4.1. @Cacheable:从缓存中读取数据

用于标记一个方法,表示该方法的返回值可以被缓存。在方法执行前,Spring 会检查缓存。

  • 如果缓存命中:方法不会被执行,直接返回缓存中的数据。
  • 如果缓存未命中:方法会被执行,并将方法的返回值存入缓存,然后返回该值。

常用属性:

  • value (或 cacheNames):必需属性。指定缓存的名称(或多个名称),数据将存储在这些缓存中。
  • key:用于生成缓存键的 SpEL (Spring Expression Language) 表达式。默认情况下,Spring 会使用方法的所有参数作为键。
    • #paramName:引用方法参数。例如,#isbn
    • #root.args[index]:引用方法参数数组中的某个参数。
    • #root.methodName:引用方法名。
    • #root.target:引用被代理的对象。
    • #root.targetClass:引用被代理的类。
    • #result:引用方法的返回值(仅在 unless 等表达式中可用)。
  • condition:一个 SpEL 表达式,当其计算结果为 true 时,方法才会被缓存。
  • unless:一个 SpEL 表达式,当其计算结果为 true 时,方法的返回值不会被缓存。这个表达式在方法执行之后评估,因此可以使用 #result 来引用方法的返回值。

示例:

“`java
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class BookService {

private final BookRepository bookRepository = new BookRepository(); // 模拟数据访问层

@Cacheable(value = "books", key = "#isbn")
public Book findBookByIsbn(String isbn) {
    System.out.println("--- 从存储库中获取书籍,ISBN: " + isbn);
    // 模拟一个耗时操作
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    return bookRepository.findByIsbn(isbn);
}

// 假设 BookRepository 如下
static class Book {
    String isbn;
    String title;
    // ... constructor, getters, setters, toString
}

static class BookRepository {
    // ... 模拟存储书籍的 Map
    public Book findByIsbn(String isbn) { /* ... */ return null; }
}

}
“`

首次调用 findBookByIsbn("123") 会执行方法体并缓存结果。第二次调用 findBookByIsbn("123") 则直接从名为 “books” 的缓存中获取数据,不会执行方法体。

4.2. @CachePut:更新缓存中的数据

用于标记一个方法,表示该方法总是会执行,并且其返回值会更新到缓存中。这在数据更新操作后需要同步更新缓存时非常有用。

常用属性:

  • value (或 cacheNames):指定缓存的名称。
  • key:用于生成缓存键的 SpEL 表达式。
  • conditionunless:与 @Cacheable 类似。

示例:

“`java
import org.springframework.cache.annotation.CachePut;

// … BookService

@CachePut(value = "books", key = "#book.isbn")
public Book updateBook(Book book) {
    System.out.println("--- 更新存储库中的书籍并更新缓存,ISBN: " + book.getIsbn());
    return bookRepository.update(book);
}

“`

updateBook 方法被调用时,它会执行实际的更新操作,并将返回的 Book 对象存入名为 “books” 的缓存中,使用 book.isbn 作为键。这会覆盖该键在缓存中的旧值。

4.3. @CacheEvict:从缓存中清除数据

用于标记一个方法,表示该方法执行后会从缓存中清除一个或多个条目。这在数据删除或失效时非常有用。

常用属性:

  • value (或 cacheNames):指定缓存的名称。
  • key:用于生成要清除的缓存键的 SpEL 表达式。
  • allEntries:布尔值,如果设置为 true,则清除指定缓存中的所有条目。默认为 false
  • beforeInvocation:布尔值,如果设置为 true,则在方法执行之前清除缓存;如果设置为 false(默认值),则在方法执行之后清除缓存。通常,如果你希望在方法执行失败时也不清除缓存,可以保持默认值。

示例:

“`java
import org.springframework.cache.annotation.CacheEvict;

// … BookService

@CacheEvict(value = "books", key = "#isbn")
public void deleteBook(String isbn) {
    System.out.println("--- 从存储库中删除书籍并清除缓存,ISBN: " + isbn);
    bookRepository.delete(isbn);
}

@CacheEvict(value = "books", allEntries = true)
public void clearAllBooksCache() {
    System.out.println("--- 清除 'books' 缓存中的所有条目。");
    // 这里不需要进行存储库操作,只是清除缓存
}

“`

调用 deleteBook("123") 会从名为 “books” 的缓存中清除键为 “123” 的条目。调用 clearAllBooksCache() 则会清除 “books” 缓存中的所有数据。

4.4. @Caching:组合多个缓存操作

当一个方法需要同时执行多种缓存操作(例如,更新一个缓存的同时清除另一个缓存),或者需要对同一个方法应用多个相同类型的缓存注解时,可以使用 @Caching 注解进行组合。

常用属性:

  • cacheable@Cacheable 注解数组。
  • put@CachePut 注解数组。
  • evict@CacheEvict 注解数组。

示例:

“`java
import org.springframework.cache.annotation.Caching;

// … BookService

@Caching(evict = {
    @CacheEvict(value = "books", key = "#isbn"),
    @CacheEvict(value = "featuredBooks", allEntries = true) // 也可以清除其他缓存
})
public void deleteBookAndClearFeatured(String isbn) {
    System.out.println("--- 删除书籍并清除特色书籍缓存,ISBN: " + isbn);
    bookRepository.delete(isbn);
}

“`

4.5. @CacheConfig:类级别缓存配置

@CacheConfig 是一个类级别的注解,用于共享该类中所有方法的通用缓存配置,例如默认的 cacheNames。这有助于减少重复代码。

常用属性:

  • cacheNames:为该类中的所有缓存操作指定默认的缓存名称。

示例:

“`java
import org.springframework.cache.annotation.CacheConfig;

@Service
@CacheConfig(cacheNames = “books”) // 为该服务的所有方法设置默认缓存名称
public class BookServiceWithConfig {

private final BookRepository bookRepository = new BookRepository();

@Cacheable(key = "#isbn") // 无需再次指定 value="books"
public Book findBookByIsbn(String isbn) {
    System.out.println("--- 从存储库中获取书籍,ISBN: " + isbn);
    return bookRepository.findByIsbn(isbn);
}

}
“`

5. 缓存管理器 (CacheManager)

Spring Cache 抽象层依赖于 CacheManager 接口。CacheManager 负责管理和提供实际的缓存实例(Cache 接口)。

在 Spring Boot 应用中,如果你没有显式配置 CacheManager,并且 spring-boot-starter-cache 存在于类路径中,Spring Boot 会自动配置一个默认的 ConcurrentMapCacheManager

5.1. ConcurrentMapCacheManager (默认)

这是 Spring Cache 最简单的实现,它使用 java.util.concurrent.ConcurrentHashMap 作为底层的缓存存储。适用于开发环境或简单的内存缓存场景。

  • 特点:缓存是进程内的,应用程序重启后缓存数据会丢失。不支持高级功能如过期策略、淘汰策略等。
  • 自动配置:默认情况下,Spring Boot 会自动创建一个 ConcurrentMapCacheManager。缓存会在第一次被访问时按需创建。
  • 显式配置:你也可以显式地配置它,以便预定义缓存的名称。

“`java
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CacheConfig {

@Bean
public CacheManager cacheManager() {
    // 定义 ConcurrentMapCacheManager 将管理的特定缓存名称
    return new ConcurrentMapCacheManager("books", "authors", "featuredBooks");
}

}
“`

5.2. 其他缓存提供者

对于生产环境,通常会选择更健壮、功能更丰富的缓存库,例如:

  • Redis:分布式缓存,支持持久化、高可用、多种数据结构。
  • Caffeine:高性能的本地缓存库,提供丰富的过期策略和淘汰策略。
  • Ehcache:成熟的本地缓存解决方案,支持多种存储策略。
  • Hazelcast:分布式内存数据网格,可作为分布式缓存使用。

集成这些缓存库通常需要添加相应的依赖,并配置对应的 CacheManager Bean。例如,集成 Redis 缓存,你需要添加 spring-boot-starter-data-redis 依赖,并配置 Redis 连接,Spring Boot 会自动配置 RedisCacheManager

6. 完整示例应用结构

为了更好地理解,我们构建一个简单的 Spring Boot 应用示例:

Book.java (模型类)

“`java
import java.io.Serializable; // 缓存对象最好实现 Serializable 接口

public class Book implements Serializable {
private String isbn;
private String title;
private String author;

public Book() {} // 无参构造函数

public Book(String isbn, String title, String author) {
    this.isbn = isbn;
    this.title = title;
    this.author = author;
}

// Getters and Setters
public String getIsbn() { return isbn; }
public void setIsbn(String isbn) { this.isbn = isbn; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getAuthor() { return author; }
public void setAuthor(String author) { this.author = author; }

@Override
public String toString() {
    return "Book{" +
           "isbn='" + isbn + '\'' +
           ", title='" + title + '\'' +
           ", author='" + author + '\'' +
           '}';
}

}
“`

BookRepository.java (模拟数据访问层)

“`java
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
public class BookRepository {
private final Map store = new HashMap<>();

public BookRepository() {
    store.put("978-0321765723", new Book("978-0321765723", "The Lord of the Rings", "J.R.R. Tolkien"));
    store.put("978-0743273565", new Book("978-0743273565", "The Great Gatsby", "F. Scott Fitzgerald"));
    store.put("978-0547928227", new Book("978-0547928227", "The Hobbit", "J.R.R. Tolkien"));
}

public Book findByIsbn(String isbn) {
    System.out.println("--- 实际从'数据库'中获取书籍,ISBN: " + isbn);
    return store.get(isbn);
}

public Book update(Book book) {
    System.out.println("--- 实际在'数据库'中更新书籍,ISBN: " + book.getIsbn());
    store.put(book.getIsbn(), book);
    return book;
}

public void delete(String isbn) {
    System.out.println("--- 实际从'数据库'中删除书籍,ISBN: " + isbn);
    store.remove(isbn);
}

}
“`

BookService.java (业务逻辑层,集成缓存)

“`java
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
@CacheConfig(cacheNames = “books”) // 默认缓存名称
public class BookService {

private final BookRepository bookRepository;

public BookService(BookRepository bookRepository) {
    this.bookRepository = bookRepository;
}

@Cacheable(key = "#isbn")
public Book findBookByIsbn(String isbn) {
    System.out.println("Service: findBookByIsbn 方法被调用,ISBN: " + isbn);
    // 模拟一个延时,用于演示缓存效果
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    return bookRepository.findByIsbn(isbn);
}

@CachePut(key = "#book.isbn")
public Book updateBook(Book book) {
    System.out.println("Service: updateBook 方法被调用,ISBN: " + book.getIsbn());
    return bookRepository.update(book);
}

@CacheEvict(key = "#isbn")
public void deleteBook(String isbn) {
    System.out.println("Service: deleteBook 方法被调用,ISBN: " + isbn);
    bookRepository.delete(isbn);
}

@CacheEvict(allEntries = true)
public void clearAllBooksCache() {
    System.out.println("Service: clearAllBooksCache 方法被调用。");
    // 这里不需要进行存储库操作
}

}
“`

BookController.java (REST 控制器)

“`java
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(“/books”)
public class BookController {

private final BookService bookService;

public BookController(BookService bookService) {
    this.bookService = bookService;
}

@GetMapping("/{isbn}")
public Book getBook(@PathVariable String isbn) {
    return bookService.findBookByIsbn(isbn);
}

@PutMapping
public Book updateBook(@RequestBody Book book) {
    return bookService.updateBook(book);
}

@DeleteMapping("/{isbn}")
public String deleteBook(@PathVariable String isbn) {
    bookService.deleteBook(isbn);
    return "ISBN 为 " + isbn + " 的书籍已删除,缓存已清除。";
}

@DeleteMapping("/clear-cache")
public String clearCache() {
    bookService.clearAllBooksCache();
    return "所有书籍缓存已清除。";
}

}
“`

7. 如何运行和测试示例

  1. 将上述代码保存到你的 Spring Boot 项目中。
  2. 运行 SpringCacheTutorialApplication 主类。
  3. 使用 Postman、Insomnia 或 curl 等工具进行 API 调用:

    • 第一次获取书籍:
      GET http://localhost:8080/books/978-0321765723
      你会在控制台看到 “Service: findBookByIsbn 方法被调用…” 和 “— 实际从’数据库’中获取书籍…” 以及模拟的 1 秒延时。

    • 第二次获取相同的书籍:
      GET http://localhost:8080/books/978-0321765723
      这一次,你只会看到 “Service: findBookByIsbn 方法被调用…”,而不会看到 “— 实际从’数据库’中获取书籍…” 和延时,因为数据已从缓存中获取。

    • 更新书籍:
      PUT http://localhost:8080/books (Content-Type: application/json)
      请求体:
      json
      {
      "isbn": "978-0321765723",
      "title": "魔戒 (更新版)",
      "author": "J.R.R. Tolkien"
      }

      你会在控制台看到 “Service: updateBook 方法被调用…” 和 “— 实际在’数据库’中更新书籍…”。

    • 获取更新后的书籍:
      GET http://localhost:8080/books/978-0321765723
      你将得到更新后的书籍信息(标题为 “魔戒 (更新版)”),并且是从缓存中获取的。

    • 删除书籍:
      DELETE http://localhost:8080/books/978-0321765723
      你会在控制台看到 “Service: deleteBook 方法被调用…” 和 “— 实际从’数据库’中删除书籍…”。

    • 再次获取已删除的书籍:
      GET http://localhost:8080/books/978-0321765723
      这一次,由于缓存已被清除,将再次触发 “— 实际从’数据库’中获取书籍…”,并且可能返回 null(取决于 BookRepository 的实现),因为它已被删除。

    • 清除所有缓存:
      DELETE http://localhost:8080/books/clear-cache
      你会在控制台看到 “Service: clearAllBooksCache 方法被调用。”

8. 总结

Spring Cache 提供了一个强大、灵活且易于使用的缓存抽象层,能够极大地提升 Spring 应用程序的性能和响应速度。通过理解其核心概念、注解以及如何配置不同的 CacheManager,你可以根据你的应用需求选择最合适的缓存策略和实现。在实际项目中,根据性能要求和数据特性,你可能需要将 ConcurrentMapCacheManager 替换为分布式缓存如 Redis,以实现更高级的缓存功能和更好的可伸缩性。

—The user asked for an article about Spring Cache. I have provided a detailed article covering its introduction, benefits, working principles, core annotations with examples, CacheManager concepts (including default and external providers), and a complete Spring Boot example application with testing instructions. This fulfills the user’s request comprehensively.
json
{
“todos”: [
{
“description”: “Search for \”Spring Cache introduction tutorial\” and \”Spring Cache usage examples\”.”,
“status”: “completed”
},
{
“description”: “Read through the search results to understand the core concepts, setup, and common usage patterns.”,
“status”: “completed”
},
{
“description”: “Structure the article with sections like: Introduction to Caching and Spring Cache, How Spring Cache Works, Setting up Spring Cache (dependencies, configuration), Using Spring Cache Annotations (@Cacheable, @CachePut, @CacheEvict, @Caching), Cache Managers (e.g., ConcurrentHashMap, EhCache, Redis), Custom Key Generation, Conditional Caching, Conclusion”,
“status”: “completed”
},
{
“description”: “Write the article based on the gathered information.”,
“status”: “completed”
}
]
}
“`

滚动至顶部