Spring Cache和Redis 聲明式緩存指南(最新推薦)
Spring Cache + Redis 聲明式緩存指南
1. 背景與動(dòng)機(jī)
1.1 為什么需要緩存
在高并發(fā) Web 應(yīng)用中,數(shù)據(jù)庫(kù)往往成為性能瓶頸:
| 問(wèn)題 | 影響 | 解決方案 |
|---|---|---|
| 頻繁查詢(xún) | 數(shù)據(jù)庫(kù)連接耗盡 | 緩存熱點(diǎn)數(shù)據(jù) |
| 慢查詢(xún) | 接口響應(yīng)時(shí)間長(zhǎng) | 緩存計(jì)算結(jié)果 |
| 高并發(fā)讀 | 數(shù)據(jù)庫(kù) CPU/IO 打滿(mǎn) | 讀寫(xiě)分離 + 緩存 |
典型收益:
- 響應(yīng)時(shí)間:從 100ms 降至 5ms(20 倍提升)
- 數(shù)據(jù)庫(kù)負(fù)載:減少 60%-90%
- 并發(fā)能力:從 1000 QPS 提升至 10000+ QPS
1.2 Spring Cache 的設(shè)計(jì)理念
Spring Cache 是一個(gè) 抽象層,核心思想是:
應(yīng)用代碼 → Spring Cache 抽象層 → 具體緩存實(shí)現(xiàn)(Redis/EhCache/Caffeine)
優(yōu)勢(shì):
- ? 聲明式編程:通過(guò)注解而非侵入式代碼
- ? 實(shí)現(xiàn)無(wú)關(guān):可無(wú)縫切換緩存提供者
- ? AOP 實(shí)現(xiàn):自動(dòng)攔截方法調(diào)用
- ? 統(tǒng)一 API:學(xué)習(xí)成本低
1.3 為什么選擇 Redis
| 特性 | Redis | EhCache | Caffeine |
|---|---|---|---|
| 分布式支持 | ? 原生支持 | ? 僅單機(jī) | ? 僅單機(jī) |
| 持久化 | ? RDB/AOF | ?? 有限 | ? 純內(nèi)存 |
| 數(shù)據(jù)結(jié)構(gòu) | ? 豐富(String/Hash/List等) | ?? 簡(jiǎn)單 | ?? 簡(jiǎn)單 |
| 高可用 | ? 哨兵/集群 | ? | ? |
| 適用場(chǎng)景 | 分布式系統(tǒng) | 單體應(yīng)用 | 單體應(yīng)用 |
Redis 適用于:微服務(wù)架構(gòu)、多實(shí)例部署、需要數(shù)據(jù)共享的場(chǎng)景。
2. 核心概念與架構(gòu)
2.1 Spring Cache 抽象層
┌─────────────────────────────────────────┐ │ 應(yīng)用代碼(@Cacheable) │ ├─────────────────────────────────────────┤ │ Spring Cache 抽象層(AOP) │ │ ┌─────────────┐ ┌──────────────┐ │ │ │CacheManager │ ───> │ Cache │ │ │ └─────────────┘ └──────────────┘ │ ├─────────────────────────────────────────┤ │ 緩存提供者實(shí)現(xiàn)(SPI) │ │ ┌──────────┐ ┌──────────┐ ┌───────┐│ │ │ Redis │ │ EhCache │ │Caffeine││ │ └──────────┘ └──────────┘ └───────┘│ └─────────────────────────────────────────┘
2.2 核心接口
CacheManager(緩存管理器)
public interface CacheManager {
Cache getCache(String name); // 根據(jù)名稱(chēng)獲取緩存
Collection<String> getCacheNames();
}
Cache(緩存操作接口)
public interface Cache {
ValueWrapper get(Object key); // 獲取緩存
void put(Object key, Object value); // 寫(xiě)入緩存
void evict(Object key); // 清除緩存
}
2.3 Redis 作為緩存提供者
RedisCacheManager 是 Spring Data Redis 提供的實(shí)現(xiàn):
RedisCacheManager
├─ RedisCacheConfiguration (配置)
│ ├─ TTL (過(guò)期時(shí)間)
│ ├─ KeySerializer (Key 序列化)
│ └─ ValueSerializer (Value 序列化)
└─ RedisConnectionFactory (連接工廠)
3. 快速開(kāi)始
3.1 依賴(lài)配置
Maven(pom.xml):
<dependencies>
<!-- Spring Boot Starter -->
<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>
<!-- 連接池(推薦 Lettuce) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- JSON 序列化(推薦) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>Gradle(build.gradle):
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.apache.commons:commons-pool2'
implementation 'com.fasterxml.jackson.core:jackson-databind'
}
3.2 Redis 連接配置
application.yml:
spring:
# Redis 連接配置
data:
redis:
host: localhost
port: 6379
password: your_password # 如無(wú)密碼可省略
database: 0
timeout: 5000ms
# Lettuce 連接池配置(推薦)
lettuce:
pool:
max-active: 20 # 最大連接數(shù)
max-idle: 10 # 最大空閑連接
min-idle: 5 # 最小空閑連接
max-wait: 2000ms # 連接超時(shí)時(shí)間
# 緩存配置
cache:
type: redis # 指定緩存類(lèi)型
redis:
time-to-live: 600000 # 默認(rèn)過(guò)期時(shí)間 10 分鐘(毫秒)
cache-null-values: true # 是否緩存空值(防止緩存穿透)
key-prefix: "app:" # Key 前綴
use-key-prefix: true # 是否使用前綴3.3 啟用緩存注解
在啟動(dòng)類(lèi)或配置類(lèi)添加 @EnableCaching:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching // ? 啟用緩存功能
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}4. 核心注解詳解
4.1 @EnableCaching
作用:激活 Spring Cache 功能,觸發(fā) AOP 代理。
原理:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(CachingConfigurationSelector.class)
public @interface EnableCaching {
boolean proxyTargetClass() default false; // 是否強(qiáng)制使用 CGLIB 代理
AdviceMode mode() default AdviceMode.PROXY; // 代理模式
}
4.2 @Cacheable(查詢(xún)緩存)
作用:先查緩存,未命中則執(zhí)行方法并緩存結(jié)果。
常用屬性:
| 屬性 | 說(shuō)明 | 示例 |
|---|---|---|
cacheNames/value | 緩存名稱(chēng) | @Cacheable("users") |
key | 緩存 Key(SpEL) | key = "#id" |
condition | 緩存條件 | condition = "#id > 0" |
unless | 排除條件 | unless = "#result == null" |
sync | 同步加載(防擊穿) | sync = true |
示例:
@Service
public class UserService {
/**
* 根據(jù) ID 查詢(xún)用戶(hù)(帶緩存)
* Key: users::1
* TTL: 使用全局配置(10 分鐘)
*/
@Cacheable(cacheNames = "users", key = "#id")
public User getUserById(Long id) {
System.out.println("從數(shù)據(jù)庫(kù)查詢(xún)用戶(hù): " + id);
return userRepository.findById(id).orElse(null);
}
/**
* 條件緩存:僅緩存 VIP 用戶(hù)
*/
@Cacheable(cacheNames = "vipUsers",
key = "#id",
condition = "#user.isVip()")
public User getVipUser(Long id) {
return userRepository.findById(id).orElse(null);
}
/**
* 排除空值:不緩存查詢(xún)結(jié)果為 null 的情況
*/
@Cacheable(cacheNames = "users",
key = "#username",
unless = "#result == null")
public User getUserByUsername(String username) {
return userRepository.findByUsername(username);
}
}執(zhí)行流程:
1. 攔截方法調(diào)用 2. 根據(jù) Key 查詢(xún)緩存 3. 命中 → 直接返回 4. 未命中 → 執(zhí)行方法 → 緩存結(jié)果 → 返回
4.3 @CachePut(更新緩存)
作用:無(wú)論緩存是否存在,都執(zhí)行方法并更新緩存。
典型場(chǎng)景:更新操作后同步緩存。
@Service
public class UserService {
/**
* 更新用戶(hù)信息(同步更新緩存)
*/
@CachePut(cacheNames = "users", key = "#user.id")
public User updateUser(User user) {
System.out.println("更新數(shù)據(jù)庫(kù): " + user.getId());
return userRepository.save(user);
}
}與 @Cacheable 的區(qū)別:
| 注解 | 執(zhí)行方法 | 緩存操作 |
|---|---|---|
@Cacheable | 僅在緩存未命中時(shí)執(zhí)行 | 讀取 + 寫(xiě)入 |
@CachePut | 始終執(zhí)行 | 僅寫(xiě)入 |
4.4 @CacheEvict(清除緩存)
作用:刪除指定緩存數(shù)據(jù)。
常用屬性:
| 屬性 | 說(shuō)明 | 示例 |
|---|---|---|
allEntries | 清空整個(gè)緩存 | allEntries = true |
beforeInvocation | 方法執(zhí)行前清除 | beforeInvocation = true |
@Service
public class UserService {
/**
* 刪除用戶(hù)(清除緩存)
*/
@CacheEvict(cacheNames = "users", key = "#id")
public void deleteUser(Long id) {
System.out.println("刪除用戶(hù): " + id);
userRepository.deleteById(id);
}
/**
* 批量導(dǎo)入用戶(hù)(清空整個(gè)緩存)
*/
@CacheEvict(cacheNames = "users", allEntries = true)
public void importUsers(List<User> users) {
userRepository.saveAll(users);
}
/**
* 方法執(zhí)行前清除緩存(防止異常導(dǎo)致緩存不一致)
*/
@CacheEvict(cacheNames = "users",
key = "#id",
beforeInvocation = true)
public void dangerousOperation(Long id) {
// 可能拋出異常的操作
}
}4.5 @Caching(組合操作)
作用:組合多個(gè)緩存注解。
@Service
public class UserService {
/**
* 復(fù)雜場(chǎng)景:同時(shí)更新多個(gè)緩存
*/
@Caching(
put = {
@CachePut(cacheNames = "users", key = "#user.id"),
@CachePut(cacheNames = "usersByEmail", key = "#user.email")
},
evict = {
@CacheEvict(cacheNames = "userList", allEntries = true)
}
)
public User updateUserComplex(User user) {
return userRepository.save(user);
}
}5. 配置與定制化
5.1 RedisCacheConfiguration 核心配置
完整配置類(lèi):
package com.example.demo.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 默認(rèn)配置
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
// Key 序列化(String)
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()
)
)
// Value 序列化(JSON)
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()
)
)
// 默認(rèn)過(guò)期時(shí)間 10 分鐘
.entryTtl(Duration.ofMinutes(10))
// 禁用緩存空值
.disableCachingNullValues()
// Key 前綴
.prefixCacheNameWith("app:");
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.build();
}
}5.2 序列化方案選擇
方案對(duì)比
| 序列化器 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場(chǎng)景 |
|---|---|---|---|
| JDK | 原生支持 | 可讀性差、體積大 | 簡(jiǎn)單場(chǎng)景 |
| Jackson | 可讀性好、跨語(yǔ)言 | 性能中等 | ? 推薦 |
| Protobuf | 高性能、體積小 | 需要定義 proto | 高性能要求 |
| Kryo | 高性能 | 需要注冊(cè)類(lèi) | Java 專(zhuān)用 |
Jackson 序列化配置(推薦)
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
@Bean
public GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer() {
ObjectMapper objectMapper = new ObjectMapper();
// 啟用類(lèi)型信息(防止反序列化時(shí)類(lèi)型丟失)
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
return new GenericJackson2JsonRedisSerializer(objectMapper);
}Redis 中的 JSON 數(shù)據(jù)示例:
{
"@class": "com.example.demo.entity.User",
"id": 1,
"username": "alice",
"email": "alice@example.com",
"createTime": ["java.time.LocalDateTime", "2024-01-01T10:00:00"]
}5.3 TTL 過(guò)期時(shí)間配置
全局 TTL
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)); // 10 分鐘不同緩存空間使用不同 TTL
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 默認(rèn)配置(10 分鐘)
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10));
// 用戶(hù)緩存(1 小時(shí))
RedisCacheConfiguration userConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1));
// 熱點(diǎn)數(shù)據(jù)緩存(5 分鐘)
RedisCacheConfiguration hotConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withCacheConfiguration("users", userConfig) // users 緩存使用 1 小時(shí)
.withCacheConfiguration("products", hotConfig) // products 緩存使用 5 分鐘
.build();
}5.4 Key 生成策略
默認(rèn) Key 生成規(guī)則
cacheNames::key 例如:users::1
自定義 Key 生成器
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
@Component("customKeyGenerator")
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
// 格式:類(lèi)名:方法名:參數(shù)1_參數(shù)2
return target.getClass().getSimpleName() + ":" +
method.getName() + ":" +
Arrays.toString(params).replaceAll("[\\[\\]\\s]", "");
}
}使用自定義 Key 生成器:
@Cacheable(cacheNames = "users", keyGenerator = "customKeyGenerator")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
5.5 多 CacheManager 配置
場(chǎng)景:同時(shí)使用 Redis(分布式) + Caffeine(本地)。
@Configuration
public class MultiCacheConfig {
// Redis CacheManager
@Bean
@Primary
public CacheManager redisCacheManager(RedisConnectionFactory factory) {
return RedisCacheManager.builder(factory)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig())
.build();
}
// Caffeine CacheManager
@Bean
public CacheManager caffeineCacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(1000));
return cacheManager;
}
}指定使用哪個(gè) CacheManager:
@Cacheable(cacheNames = "users", cacheManager = "redisCacheManager")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
6. 完整代碼示例
6.1 項(xiàng)目結(jié)構(gòu)
src/main/java/com/example/demo
├── DemoApplication.java # 啟動(dòng)類(lèi)
├── config
│ └── CacheConfig.java # 緩存配置
├── entity
│ └── User.java # 實(shí)體類(lèi)
├── repository
│ └── UserRepository.java # 數(shù)據(jù)訪問(wèn)層
├── service
│ └── UserService.java # 業(yè)務(wù)層(帶緩存)
└── controller
└── UserController.java # 控制器
6.2 配置類(lèi)
package com.example.demo.config;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
// 配置 Jackson 序列化器
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY
);
GenericJackson2JsonRedisSerializer jsonSerializer =
new GenericJackson2JsonRedisSerializer(objectMapper);
// 默認(rèn)緩存配置
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()
)
)
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(jsonSerializer)
)
.entryTtl(Duration.ofMinutes(10))
.disableCachingNullValues()
.prefixCacheNameWith("app:");
// 用戶(hù)緩存配置(1 小時(shí))
RedisCacheConfiguration userConfig = defaultConfig
.entryTtl(Duration.ofHours(1));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withCacheConfiguration("users", userConfig)
.transactionAware() // 支持事務(wù)
.build();
}
}6.3 實(shí)體類(lèi)
package com.example.demo.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import jakarta.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "t_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String username;
@Column(nullable = false, length = 100)
private String email;
@Column(name = "create_time")
private LocalDateTime createTime;
@Column(name = "update_time")
private LocalDateTime updateTime;
}6.4 Repository 層
package com.example.demo.repository;
import com.example.demo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}6.5 Service 層(核心)
package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
/**
* 查詢(xún)用戶(hù)(帶緩存)
* Key: users::1
*/
@Cacheable(cacheNames = "users", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
log.info("從數(shù)據(jù)庫(kù)查詢(xún)用戶(hù): {}", id);
return userRepository.findById(id).orElse(null);
}
/**
* 根據(jù)用戶(hù)名查詢(xún)(帶緩存)
*/
@Cacheable(cacheNames = "usersByName", key = "#username")
public User getUserByUsername(String username) {
log.info("從數(shù)據(jù)庫(kù)查詢(xún)用戶(hù): {}", username);
return userRepository.findByUsername(username).orElse(null);
}
/**
* 查詢(xún)所有用戶(hù)(帶緩存)
*/
@Cacheable(cacheNames = "userList")
public List<User> getAllUsers() {
log.info("從數(shù)據(jù)庫(kù)查詢(xún)所有用戶(hù)");
return userRepository.findAll();
}
/**
* 創(chuàng)建用戶(hù)(清除列表緩存)
*/
@Transactional
@CacheEvict(cacheNames = "userList", allEntries = true)
public User createUser(User user) {
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
log.info("創(chuàng)建用戶(hù): {}", user.getUsername());
return userRepository.save(user);
}
/**
* 更新用戶(hù)(更新緩存 + 清除列表緩存)
*/
@Transactional
@Caching(
put = {
@CachePut(cacheNames = "users", key = "#user.id"),
@CachePut(cacheNames = "usersByName", key = "#user.username")
},
evict = {
@CacheEvict(cacheNames = "userList", allEntries = true)
}
)
public User updateUser(User user) {
user.setUpdateTime(LocalDateTime.now());
log.info("更新用戶(hù): {}", user.getId());
return userRepository.save(user);
}
/**
* 刪除用戶(hù)(清除所有相關(guān)緩存)
*/
@Transactional
@Caching(evict = {
@CacheEvict(cacheNames = "users", key = "#id"),
@CacheEvict(cacheNames = "userList", allEntries = true)
})
public void deleteUser(Long id) {
log.info("刪除用戶(hù): {}", id);
userRepository.deleteById(id);
}
}6.6 Controller 層
package com.example.demo.controller;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
@GetMapping("/username/{username}")
public User getUserByUsername(@PathVariable String username) {
return userService.getUserByUsername(username);
}
@GetMapping
public List<User> getAllUsers() {
return userService.getAllUsers();
}
@PostMapping
public User createUser(@RequestBody User user) {
return userService.createUser(user);
}
@PutMapping
public User updateUser(@RequestBody User user) {
return userService.updateUser(user);
}
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
}
}6.7 測(cè)試驗(yàn)證
測(cè)試類(lèi):
package com.example.demo.service;
import com.example.demo.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class UserServiceTest {
@Autowired
private UserService userService;
@Test
void testCache() {
// 第一次查詢(xún)(從數(shù)據(jù)庫(kù))
User user1 = userService.getUserById(1L);
assertNotNull(user1);
// 第二次查詢(xún)(從緩存,不會(huì)打印日志)
User user2 = userService.getUserById(1L);
assertEquals(user1.getId(), user2.getId());
}
}查看 Redis 緩存:
# 連接 Redis
redis-cli
# 查看所有 Key
127.0.0.1:6379> KEYS app:*
1) "app:users::1"
2) "app:usersByName::alice"
3) "app:userList"
# 查看緩存內(nèi)容
127.0.0.1:6379> GET "app:users::1"
"{\"@class\":\"com.example.demo.entity.User\",\"id\":1,\"username\":\"alice\",\"email\":\"alice@example.com\",\"createTime\":[\"java.time.LocalDateTime\",\"2024-01-01T10:00:00\"],\"updateTime\":[\"java.time.LocalDateTime\",\"2024-01-01T10:00:00\"]}"
# 查看 TTL(剩余過(guò)期時(shí)間)
127.0.0.1:6379> TTL "app:users::1"
(integer) 3599 # 還剩 3599 秒(約 1 小時(shí))7. 高級(jí)特性
7.1 緩存失效策略
主動(dòng)失效策略
| 策略 | 實(shí)現(xiàn)方式 | 適用場(chǎng)景 |
|---|---|---|
| TTL 過(guò)期 | entryTtl(Duration) | 時(shí)效性數(shù)據(jù) |
| LRU 淘汰 | Redis maxmemory-policy | 內(nèi)存受限 |
| 手動(dòng)清除 | @CacheEvict | 數(shù)據(jù)更新后 |
Redis 淘汰策略配置
redis.conf:
maxmemory 2gb maxmemory-policy allkeys-lru # LRU 算法淘汰任意 Key
常用策略:
volatile-lru:僅淘汰設(shè)置了過(guò)期時(shí)間的 Keyallkeys-lru:淘汰所有 Key(推薦)volatile-ttl:淘汰即將過(guò)期的 Keynoeviction:不淘汰,寫(xiě)入失敗
7.2 SpEL 表達(dá)式應(yīng)用
常用 SpEL 表達(dá)式:
| 表達(dá)式 | 說(shuō)明 | 示例 |
|---|---|---|
#參數(shù)名 | 方法參數(shù) | key = "#id" |
#p0, #p1 | 參數(shù)索引 | key = "#p0" |
#result | 方法返回值 | unless = "#result == null" |
#root.methodName | 方法名 | key = "#root.methodName + #id" |
#root.targetClass | 目標(biāo)類(lèi) | - |
復(fù)雜示例:
@Cacheable(
cacheNames = "users",
key = "#root.targetClass.simpleName + ':' + #root.methodName + ':' + #id",
condition = "#id > 0 && #includeDeleted == false",
unless = "#result == null || #result.id == null"
)
public User getUserById(Long id, boolean includeDeleted) {
return userRepository.findById(id).orElse(null);
}
7.3 條件緩存(condition/unless)
condition(緩存前置條件)
// 僅緩存 ID > 0 的用戶(hù)
@Cacheable(cacheNames = "users", key = "#id", condition = "#id > 0")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
unless(緩存排除條件)
// 不緩存 null 結(jié)果和管理員用戶(hù)
@Cacheable(
cacheNames = "users",
key = "#id",
unless = "#result == null || #result.role == 'ADMIN'"
)
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
兩者區(qū)別:
| 屬性 | 執(zhí)行時(shí)機(jī) | 適用場(chǎng)景 |
|---|---|---|
condition | 方法執(zhí)行前 | 根據(jù)參數(shù)決定是否緩存 |
unless | 方法執(zhí)行后 | 根據(jù)返回值決定是否緩存 |
7.4 自定義 CacheResolver
場(chǎng)景:動(dòng)態(tài)選擇緩存名稱(chēng)。
import org.springframework.cache.interceptor.CacheResolver;
import org.springframework.cache.interceptor.CacheOperationInvocationContext;
import org.springframework.stereotype.Component;
@Component("dynamicCacheResolver")
public class DynamicCacheResolver implements CacheResolver {
@Autowired
private CacheManager cacheManager;
@Override
public Collection<? extends Cache> resolveCaches(
CacheOperationInvocationContext<?> context) {
// 根據(jù)參數(shù)動(dòng)態(tài)選擇緩存名稱(chēng)
Object[] args = context.getArgs();
String cacheName = (boolean) args[1] ? "vipUsers" : "normalUsers";
return Collections.singletonList(cacheManager.getCache(cacheName));
}
}使用自定義 Resolver:
@Cacheable(cacheResolver = "dynamicCacheResolver", key = "#id")
public User getUserById(Long id, boolean isVip) {
return userRepository.findById(id).orElse(null);
}
8. 生產(chǎn)實(shí)踐與優(yōu)化
8.1 緩存穿透/擊穿/雪崩的防護(hù)
1?? 緩存穿透(查詢(xún)不存在的數(shù)據(jù))
問(wèn)題:惡意請(qǐng)求不存在的 Key,每次都打到數(shù)據(jù)庫(kù)。
解決方案:
// 方案 1:緩存空值
@Cacheable(cacheNames = "users", key = "#id")
public User getUserById(Long id) {
User user = userRepository.findById(id).orElse(null);
return user != null ? user : new User(); // 返回空對(duì)象而非 null
}
// 方案 2:布隆過(guò)濾器(需要單獨(dú)實(shí)現(xiàn))
@Autowired
private BloomFilter<Long> userIdBloomFilter;
public User getUserById(Long id) {
if (!userIdBloomFilter.mightContain(id)) {
return null; // 直接返回,不查數(shù)據(jù)庫(kù)
}
return userRepository.findById(id).orElse(null);
}配置緩存空值:
spring:
cache:
redis:
cache-null-values: true # 允許緩存 null
2?? 緩存擊穿(熱點(diǎn) Key 過(guò)期)
問(wèn)題:熱點(diǎn)數(shù)據(jù)過(guò)期瞬間,大量請(qǐng)求同時(shí)打到數(shù)據(jù)庫(kù)。
解決方案:使用 sync = true 啟用同步鎖。
@Cacheable(
cacheNames = "hotUsers",
key = "#id",
sync = true // ? 只有一個(gè)線程查詢(xún)數(shù)據(jù)庫(kù)
)
public User getHotUser(Long id) {
log.info("查詢(xún)熱點(diǎn)用戶(hù): {}", id);
return userRepository.findById(id).orElse(null);
}
原理:
請(qǐng)求1 → 未命中 → 獲取鎖 → 查詢(xún)DB → 寫(xiě)緩存 → 釋放鎖 請(qǐng)求2 → 未命中 → 等待鎖 ────────────────────> 讀緩存 請(qǐng)求3 → 未命中 → 等待鎖 ────────────────────> 讀緩存
3?? 緩存雪崩(大量 Key 同時(shí)過(guò)期)
問(wèn)題:大量緩存同時(shí)失效,數(shù)據(jù)庫(kù)瞬間壓力劇增。
解決方案:設(shè)置隨機(jī)過(guò)期時(shí)間。
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10 + new Random().nextInt(5))); // 10-15 分鐘隨機(jī)
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}對(duì)比表:
| 問(wèn)題 | 原因 | 解決方案 |
|---|---|---|
| 緩存穿透 | 查詢(xún)不存在的數(shù)據(jù) | 緩存空值 / 布隆過(guò)濾器 |
| 緩存擊穿 | 熱點(diǎn) Key 過(guò)期 | sync = true |
| 緩存雪崩 | 大量 Key 同時(shí)過(guò)期 | 隨機(jī) TTL |
8.2 性能監(jiān)控與指標(biāo)
啟用 Redis 監(jiān)控
application.yml:
management:
endpoints:
web:
exposure:
include: health,info,metrics,caches
metrics:
enable:
cache: true查看緩存指標(biāo)
訪問(wèn)端點(diǎn):
# 查看緩存統(tǒng)計(jì) curl http://localhost:8080/actuator/caches # 查看緩存命中率 curl http://localhost:8080/actuator/metrics/cache.gets?tag=result:hit curl http://localhost:8080/actuator/metrics/cache.gets?tag=result:miss
自定義監(jiān)控
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.stereotype.Component;
@Component
public class CacheMetrics {
@Autowired
private MeterRegistry meterRegistry;
public void recordCacheHit(String cacheName) {
meterRegistry.counter("cache.hit", "cache", cacheName).increment();
}
public void recordCacheMiss(String cacheName) {
meterRegistry.counter("cache.miss", "cache", cacheName).increment();
}
}8.3 常見(jiàn)問(wèn)題排查
問(wèn)題 1:緩存未生效
現(xiàn)象:每次調(diào)用都執(zhí)行方法。
排查步驟:
- 確認(rèn)
@EnableCaching是否添加 - 檢查方法是否被 Spring 管理(不能是 private)
- 確認(rèn)方法調(diào)用是否通過(guò)代理(同類(lèi)調(diào)用會(huì)失效)
錯(cuò)誤示例:
@Service
public class UserService {
// ? 同類(lèi)調(diào)用,緩存失效
public void businessMethod() {
getUserById(1L); // 直接調(diào)用,不走代理
}
@Cacheable(cacheNames = "users", key = "#id")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
}正確做法:
@Service
public class UserService {
@Autowired
private UserService self; // 注入自己
public void businessMethod() {
self.getUserById(1L); // 通過(guò)代理調(diào)用
}
}問(wèn)題 2:序列化異常
現(xiàn)象:
SerializationException: Could not read JSON: Unrecognized field...
解決方案:
// 實(shí)體類(lèi)添加無(wú)參構(gòu)造器
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
// ...
}
問(wèn)題 3:緩存不過(guò)期
現(xiàn)象:數(shù)據(jù)一直不更新。
排查:
# 查看 Key 的 TTL redis-cli TTL "app:users::1" (integer) -1 # -1 表示永不過(guò)期
解決:檢查配置是否正確設(shè)置 TTL。
8.4 最佳實(shí)踐建議
? 推薦做法
- 合理設(shè)置 TTL:根據(jù)數(shù)據(jù)更新頻率設(shè)置過(guò)期時(shí)間
- 使用 JSON 序列化:可讀性好,便于調(diào)試
- 啟用
sync = true:防止緩存擊穿 - 緩存空值:防止緩存穿透
- 監(jiān)控命中率:及時(shí)發(fā)現(xiàn)緩存效率問(wèn)題
? 避免做法
- 緩存大對(duì)象:?jiǎn)蝹€(gè) Value 不超過(guò) 1MB
- 緩存頻繁變化的數(shù)據(jù):如實(shí)時(shí)庫(kù)存
- 同類(lèi)調(diào)用:會(huì)導(dǎo)致緩存失效
- 過(guò)度依賴(lài)緩存:數(shù)據(jù)庫(kù)是最終數(shù)據(jù)源
性能優(yōu)化建議
| 優(yōu)化點(diǎn) | 建議 | 收益 |
|---|---|---|
| 連接池 | 使用 Lettuce + 連接池 | 減少連接開(kāi)銷(xiāo) |
| 序列化 | 使用 JSON 代替 JDK | 減少 50% 空間 |
| 批量操作 | 使用 Pipeline | 提升 10 倍吞吐 |
| 本地緩存 | Caffeine + Redis 二級(jí)緩存 | 減少網(wǎng)絡(luò)開(kāi)銷(xiāo) |
9. 對(duì)比與擴(kuò)展
9.1 與 EhCache/Caffeine 對(duì)比
| 特性 | Redis | EhCache | Caffeine |
|---|---|---|---|
| 部署模式 | 獨(dú)立服務(wù) | 嵌入式 | 嵌入式 |
| 數(shù)據(jù)共享 | ? 多實(shí)例共享 | ? 單實(shí)例 | ? 單實(shí)例 |
| 持久化 | ? 支持 | ?? 有限 | ? |
| 性能 | 網(wǎng)絡(luò)延遲(1-5ms) | 極快(納秒級(jí)) | 極快(納秒級(jí)) |
| 內(nèi)存管理 | 獨(dú)立進(jìn)程 | 占用 JVM 堆 | 占用 JVM 堆 |
| 適用場(chǎng)景 | 分布式系統(tǒng) | 單體應(yīng)用 | 單體應(yīng)用 |
選擇建議:
- 分布式系統(tǒng) → Redis
- 單體應(yīng)用 + 小數(shù)據(jù)量 → Caffeine
- 需要持久化 + 單機(jī) → EhCache
9.2 多級(jí)緩存架構(gòu)
架構(gòu)設(shè)計(jì):
請(qǐng)求 → Caffeine (本地緩存) → Redis (分布式緩存) → 數(shù)據(jù)庫(kù)
↑ 命中返回 ↑ 命中返回 ↑ 未命中查詢(xún)
實(shí)現(xiàn)示例:
@Configuration
public class MultiLevelCacheConfig {
@Bean
@Primary
public CacheManager cacheManager(
CacheManager redisCacheManager,
CacheManager caffeineCacheManager) {
return new CompositeCacheManager(
caffeineCacheManager, // 優(yōu)先查詢(xún)本地緩存
redisCacheManager // 本地未命中再查 Redis
);
}
}9.3 進(jìn)階閱讀資源
官方文檔
推薦書(shū)籍
- 《Redis 設(shè)計(jì)與實(shí)現(xiàn)》- 黃健宏
- 《Spring Boot 實(shí)戰(zhàn)》- Craig Walls
開(kāi)源項(xiàng)目
?? 總結(jié)
核心要點(diǎn)回顧
- Spring Cache 是抽象層,通過(guò)注解實(shí)現(xiàn)聲明式緩存
- Redis 是分布式緩存的首選,適合微服務(wù)架構(gòu)
- 核心注解:
@Cacheable(查詢(xún))、@CachePut(更新)、@CacheEvict(清除) - 序列化推薦 Jackson,兼顧性能和可讀性
- 防護(hù)三大問(wèn)題:穿透(緩存空值)、擊穿(
sync=true)、雪崩(隨機(jī)TTL)
快速檢查清單
? 添加依賴(lài)(spring-boot-starter-data-redis + cache) ? 配置 Redis 連接(application.yml) ? 啟用緩存注解(@EnableCaching) ? 配置序列化器(Jackson JSON) ? 設(shè)置 TTL 過(guò)期時(shí)間 ? 添加監(jiān)控指標(biāo)(Actuator) ? 處理緩存穿透/擊穿/雪崩
最后提醒:緩存不是銀彈,合理使用才能發(fā)揮最大價(jià)值。始終以數(shù)據(jù)一致性為第一原則!
到此這篇關(guān)于Spring Cache + Redis 聲明式緩存指南的文章就介紹到這了,更多相關(guān)Spring Cache Redis聲明式緩存內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot使用Cache集成Redis做緩存的保姆級(jí)教程
- SpringBoot集成Redis及SpringCache緩存管理示例詳解
- 使用Spring Cache和Redis實(shí)現(xiàn)查詢(xún)數(shù)據(jù)緩存
- 詳解Spring Cache使用Redisson分布式鎖解決緩存擊穿問(wèn)題
- Spring?Cache+Redis緩存數(shù)據(jù)的實(shí)現(xiàn)示例
- Java SpringCache+Redis緩存數(shù)據(jù)詳解
- 詳解SpringBoot2.0的@Cacheable(Redis)緩存失效時(shí)間解決方案
- SpringBoot+SpringCache實(shí)現(xiàn)兩級(jí)緩存(Redis+Caffeine)
相關(guān)文章
mybatis框架xml下trim中的prefix與suffix等標(biāo)簽的用法
這篇文章主要介紹了mybatis框架xml下trim中的prefix與suffix等標(biāo)簽的用法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
spring AOP代理執(zhí)行@EnableAspectJAutoProxy的exposeProxy屬性詳解
這篇文章主要為大家介紹了spring AOP代理執(zhí)行@EnableAspectJAutoProxy的exposeProxy屬性詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
詳解MyBatis的XML實(shí)現(xiàn)方法(附帶注解方式實(shí)現(xiàn))
這篇文章主要詳細(xì)介紹了MyBatis的XML實(shí)現(xiàn)方法(附帶注解方式實(shí)現(xiàn)),文中通過(guò)代碼示例給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-05-05
如何使用 Spring Boot 3.3 和 JdbcTemplate
本文介紹了如何在Spring Boot 3.3項(xiàng)目中使用 JdbcTemplate操作 MySQL數(shù)據(jù)庫(kù),通過(guò)RowMapper接口,我們將查詢(xún)結(jié)果映射為User對(duì)象,感興趣的朋友跟隨小編一起看看吧2025-06-06

