缓存的作用
在项目实际上线后,如果有大量的用户同时请求,那么对数据库的频繁读写就可能会造成系统卡顿。
SpringBoot为我们提供多种缓存机制,如果请求的参数是一致的,那么就从缓存中读取数据,而不必对数据库进行重复的操作。
相关注解
SpringBoot的缓存允许我们使用多种缓存机制,但都涉及以下这些注解。
在我们配置缓存的时候,这些注解是通用的,使用不同缓存类型,只需要在全局配置文件中修改缓存提供者即可。
@EnableCaching
@EnableCaching 是 Spring 框架中启用缓存支持的注解,它用于开启基于注解的缓存机制。添加此注解后,Spring将扫描项目中的缓存注解(如 @Cacheable, @CachePut, @CacheEvict 等),并自动配置相应的缓存功能。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching // 启用缓存
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
@Cacheable
- 作用:当调用带有@Cacheable注解的方法时,Spring会先检查缓存中是否有结果。如果有,直接返回缓存中的结果;如果没有,则执行方法并将结果存入缓存。
- value:缓存的名称,通常是缓存区域的标识。
- key:缓存条目的唯一标识,可以使用SpEL表达式自定义。
- condition:指定一个SpEL表达式,仅当满足该条件时,缓存才会应用。
- 例如:condition = "#id > 10"表示只有id大于10时才缓存数据。
- unless:也是一个SpEL表达式,返回true时不会缓存结果
- 例如:unless = "#result == null"表示返回结果为空时不缓存。
@Cacheable(value = "books", key = "#id")
public Book getBookById(Long id) {
// 执行查询逻辑
return bookRepository.findById(id);
}
@CachePut
- 作用:每次调用方法时,方法都会执行,返回值也会被更新到缓存中。适用于需要更新缓存的场景。
- 这个注解适合在更新操作中使用,例如修改数据库中的记录并同步更新缓存。
- 主要属性:
- value:与@Cacheable中的作用一致,指定缓存名称。
- key:指定缓存条目的唯一标识符,类似于@Cacheable中的key。
- condition:类似于@Cacheable,用来指定缓存更新的条件。
- unless:类似于@Cacheable,当unless的条件为true时不更新缓存。
@CachePut(value = "books", key = "#book.id")
public Book updateBook(Book book) {
return bookRepository.save(book);
}
@CacheEvict
- 作用:用于移除缓存中的某个条目或整个缓存。
- 主要属性:
- value:缓存的名称。
- key:指定要清除的缓存条目,可以使用SpEL表达式。
- allEntries:如果设置为true,则清空缓存区域中的所有条目。默认为false。
- beforeInvocation:如果设置为true,则在方法执行之前清除缓存;如果为false(默认值),则在方法执行之后清除缓存。用于防止方法异常导致缓存数据不一致的情况。
@CacheEvict(value = "books", key = "#id")
public void deleteBook(Long id) {
bookRepository.deleteById(id);
}
@Caching
- 作用:允许在一个方法上同时使用多个缓存注解。适用于复杂缓存场景。
- 主要属性:
- cacheable、put、evict:分别对应多个@Cacheable、@CachePut和@CacheEvict注解,适用于在一个方法上应用多个缓存操作。
- 例如,可以同时缓存数据并清除另一个缓存区域的数据。
@Caching(
put = { @CachePut(value = "books", key = "#book.id") },
evict = { @CacheEvict(value = "authors", key = "#book.authorId") }
)
public Book saveBook(Book book) {
return bookRepository.save(book);
}
@CacheConfig
- 作用:类级别的注解,用于指定缓存的公共配置,减少重复配置。
- cacheNames/value:指定类中所有方法共享的缓存区域名称。
- 在@CacheConfig中,cacheNames属性可以用value代替,二者等价
//缓存区域为books,对应上方的注解的value
@CacheConfig(cacheNames = "books")
public class BookService {
@Cacheable
public Book getBookById(Long id) {
return bookRepository.findById(id);
}
}
缓存提供者
SpringBoot提供了多种缓存提供者,如Redis,EhCache,使用对应的缓存提供者即使用相应的缓存类型。
下面是常用的缓存提供者。
Ehcache
特点
- 适用场景:
- 本地缓存,适合单个应用节点,不涉及分布式环境。
- 读取频率高且数据变动少的场景,能够显著提升性能。
- 特点:
- 缓存在应用程序本地内存中,读取速度非常快,适用于减少对远程存储系统的访问。
- 支持持久化,将缓存数据存储到磁盘,确保数据在重启后能够恢复。
- 配置简单,可通过 XML 或 Java 配置进行灵活设置。
- 优点:
- 低延迟,访问速度极快。
- 部署简单,不需要额外的服务器或服务。
- 缺点:
- 缓存数据仅限于当前应用节点,无法在分布式系统中共享。
- 如果节点宕机,缓存数据将丢失(除非使用持久化)。
引入依赖
<!-- Spring Boot Cache Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Ehcache -->
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.10.6</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.1</version>
</dependency>
配置文件
EhCache的配置文件需要放在项目的resource文件夹中并通过classpath引用
<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.xsd">
<!-- 配置默认缓存设置 -->
<cache alias="defaultCache">
<expiry>
<ttl unit="seconds">600</ttl> <!-- 设置默认过期时间为600秒 -->
</expiry>
<heap unit="entries">500</heap> <!-- 缓存的最大条目数 -->
</cache>
<!-- 自定义缓存区域,EhCache无法直接在xml中直接配置磁盘路径-->
<cache alias="book">
<key-type>java.lang.Long</key-type> <!-- 缓存键类型 -->
<value-type>java.lang.Object</value-type> <!-- 缓存值类型 -->
<expiry>
<ttl unit="seconds">3600</ttl> <!-- 设置过期时间为3600秒 -->
</expiry>
<heap unit="entries">1000</heap> <!-- 使用内存堆存储的最大缓存条目数 -->
<disk persistent="true" directory="java.io.tmpdir"/> <!-- 将缓存数据持久化到磁盘 -->
</cache>
</config>
使用EhCache作为缓存提供者
全局配置文件
spring:
datasource:
url: "jdbc:mysql://localhost:3306/javaee?characterEncoding=utf-8&serverTimezone=Asia/Shanghai"
username: root
password: 123456
jpa:
show-sql: true
data:
redis:
host: localhost
port: 6379
password: 123456
# 使用EnCache作为缓存提供者,并指定EnCache的配置文件
cache:
jcache:
config: classpath:ehcache.xml
使用注解
package com.example.demochapter05.service.impl;
import com.example.demochapter05.dao.BookRepository;
import com.example.demochapter05.entity.Book;
import com.example.demochapter05.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
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;
import org.springframework.transaction.annotation.Transactional;
@Service
// 指定缓存空间为book
@CacheConfig(cacheNames = "book")
@Transactional
public class BookServiceImpl implements BookService {
@Autowired
private BookRepository bookRepository;
// 缓存条目的key,若key存在于book的缓存空间中,则返回缓存结果,否则正常执行语句并把key的条目放到缓存空间中。
@Cacheable(key = "#id")
public Book findById(Integer id){
//根据id查找图书信息
return bookRepository.findById(id).get();
}
// 根据key更新缓存空间条目
@CachePut(key = "#id")
public Book updateById(Integer id,String name){
Book book=this .findById(id);
book.setName(name);
//更新图书信息
return bookRepository.save(book);
}
// 根据key删除缓存空间条目
@CacheEvict(key = "#id")
public void delById(Integer id){
//根据id删除图书信息
bookRepository.deleteById(id);
}
}
单元测试
package com.example.demochapter05;
import com.example.demochapter05.service.BookService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class Demochapter06ApplicationTests {
@Autowired
private BookService bookService;
@Test
public void testCache(){
bookService.findById(1);
bookService.findById(1);
}
}
在控制台可以看到只执行了一次sql,缓存生效。
Redis
- 适用场景:
- 分布式缓存,适合多节点共享数据的分布式环境。
- 需要缓存复杂数据结构(如列表、集合、哈希)或需要高并发访问的场景。
- 特点:
- 作为远程缓存提供服务,所有节点都可以访问相同的缓存数据。
- 支持丰富的数据结构,适合多种缓存需求,如列表、集合、哈希等。
- 提供持久化功能,可以将数据写入磁盘,保证在服务器重启后数据仍然可用。
- 优点:
- 支持分布式部署,适合多实例、多节点系统。
- 支持数据持久化和高可用,通过主从复制与哨兵模式实现数据的高可用性。
- 丰富的数据类型支持,能够满足复杂的缓存需求。
- 缺点:
- 相比本地缓存,访问需要网络请求,存在一定的网络延迟。
- 部署复杂,需额外维护 Redis 服务,并配置集群以保证高可用性。
使用Redis作为缓存提供者,既可以是本机的Redis,也可以是分布式Redis集群。
引入依赖
<dependency>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-data-redis
</artifactId>
</dependency>
全局配置文件
使用Redis作为缓存提供者不需要在编写其它配置文件,下面是基于本机的Redis单机模式,只需要在全局配置文件中配置Redis相关配置即可
spring:
datasource:
url: "jdbc:mysql://localhost:3306/javaee?characterEncoding=utf-8&serverTimezone=Asia/Shanghai"
username: root
password: 123456
jpa:
show-sql: true
data:
redis:
host: localhost # Redis 服务器的主机地址
port: 6379 # Redis 服务器的端口
password: 123456 # Redis 服务器的密码(如果设置了密码)
# 使用Redis作为缓存提供者
cache:
type: redis
redis:
use-key-prefix: true #是否使用全局的 key 前缀
key-prefix: book_ # 自定义 Redis key 的前缀
cache-null-values: false # 是否缓存 null 值,如果为 false,则不缓存返回 null 的结果
time-to-live: 400s # 缓存数据的生存时间,单位为秒
使用注解
package com.example.demochapter05.service.impl;
import com.example.demochapter05.dao.BookRepository;
import com.example.demochapter05.entity.Book;
import com.example.demochapter05.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
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;
import org.springframework.transaction.annotation.Transactional;
@Service
// 指定缓存空间为book
@CacheConfig(cacheNames = "book")
@Transactional
public class BookServiceImpl implements BookService {
@Autowired
private BookRepository bookRepository;
// 缓存条目的key,若key存在于book的缓存空间中,则返回缓存结果,否则正常执行语句并把key的条目放到缓存空间中。
@Cacheable(key = "#id")
public Book findById(Integer id){
//根据id查找图书信息
return bookRepository.findById(id).get();
}
// 根据key更新缓存空间条目
@CachePut(key = "#id")
public Book updateById(Integer id,String name){
Book book=this .findById(id);
book.setName(name);
//更新图书信息
return bookRepository.save(book);
}
// 根据key删除缓存空间条目
@CacheEvict(key = "#id")
public void delById(Integer id){
//根据id删除图书信息
bookRepository.deleteById(id);
}
}
测试
package com.example.demochapter05;
import com.example.demochapter05.service.BookService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class Demochapter06ApplicationTests {
@Autowired
private BookService bookService;
@Test
public void testCache(){
bookService.findById(1);
bookService.findById(1);
}
}
控制台中仍然只打印了一次Sql,缓存机制生效