目录
[1. 工作流程](#1. 工作流程)
[2. 三大注解](#2. 三大注解)
[3. TTL 配置](#3. TTL 配置)
[Spring Boot 示例代码](#Spring Boot 示例代码)
[1. 启用缓存(主类)](#1. 启用缓存(主类))
[2. Redis 缓存配置(TTL + JSON 序列化)](#2. Redis 缓存配置(TTL + JSON 序列化))
[3. 模拟商品服务(Service)](#3. 模拟商品服务(Service))
[4. 控制器(Controller)](#4. 控制器(Controller))
Cache-Aside(旁路缓存模式)
Cache 即缓存,Aside有在旁边的含义 。在这种模式下,应用程序在数据读取和写入操作时,并非直接与缓存交互,缓存像是处于应用程序数据操作的旁路位置,应用程序主要操作数据库,同时兼顾对缓存的维护,因此得名旁路缓存模式。
1. 工作流程
| 阶段 | 读操作(查询) | 写操作(更新/删除) |
|---|---|---|
| 第一步 | 先查缓存 | 先写数据库 |
| 第二步 | 缓存没命中就查数据库 | 再删缓存(而不是更新缓存) |
| 第三步 | 把查询结果写入缓存 | 下次查询再自动回填 |
优点:数据一致性高、实现简单。
注意:写时要删除缓存,不要直接更新缓存,否则容易产生脏数据。
2. 三大注解
| 注解 | 作用 | 时机 |
|---|---|---|
| @Cacheable | 查询时自动查缓存→回源→回写 | 读 |
| @CacheEvict | 删除缓存(支持 key/全部) | 写 |
| @CachePut | 更新缓存但不影响数据库 | 写(少用) |
回源指从数据库重新取数据
回写指把刚从数据库查到的数据写回缓存,让后续的查询可以直接从缓存命中,避免再次访问数据库
@CachePut 少用的核心原因:数据不一致风险
@CachePut 的理念是方法执行后必定更新缓存
但业务逻辑中,方法执行成功 ≠ 数据库更新成功
比如方法执行了,但数据库事务回滚、数据库连接失败、乐观锁冲突等
结果缓存更新了,但数据库没更新,导致数据永久不一致
大部分更新操作需要保证数据库和缓存同时成功
3. TTL 配置
在配置类中设置:
bash
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)); // 每个缓存项存活 10 分钟
防止缓存爆炸或脏数据长期存在
defaultCacheConfig()
名字里的 default = 默认
CacheConfig = 缓存配置对象
含义是创建一个默认的缓存配置(默认序列化、无过期时间)
然后我们可以在它的基础上再定制(比如加 TTL、加序列化器等)
entryTtl(...)entry = 每个缓存条目(每个 key)
TTL = Time To Live(生存时间)
含义是为每个缓存条目设置过期时间
这里是说每个缓存项活多久
Duration 是 Java 8 的时间类,用来表示一段持续时间ofMinutes(10) 表示 10 分钟的时长对象
所以这句返回一个代表10分钟的 Duration 实例
Spring Boot 示例代码
1. 启用缓存(主类)
java
@SpringBootApplication
@EnableCaching
public class RedisApplication {
public static void main(String[] args) {
SpringApplication.run(RedisApplication.class, args);
}
}
2. Redis 缓存配置(TTL + JSON 序列化)
java
@Configuration
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
Jackson2JsonRedisSerializer<Object> serializer =
new Jackson2JsonRedisSerializer<>(Object.class);
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(serializer))
.entryTtl(Duration.ofMinutes(10));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}
3. 模拟商品服务(Service)
java
@Service
public class ProductService {
private static final Map<Long, Map<String, String>> DB = new HashMap<>();
static {
DB.put(1L, Map.of("id", "1", "name", "Phone", "price", "1999"));
DB.put(2L, Map.of("id", "2", "name", "Laptop", "price", "4999"));
}
@Cacheable(cacheNames = "prod", key = "#id")
public Map<String, String> findById(Long id) {
System.out.println("📦 查询数据库 id=" + id);
try { Thread.sleep(300); } catch (InterruptedException ignored) {}
return DB.get(id);
}
@CacheEvict(cacheNames = "prod", key = "#id")
public void updatePrice(Long id, int newPrice) {
System.out.println("🧾 更新价格并清除缓存 id=" + id);
Map<String, String> product = DB.get(id);
DB.put(id, Map.of(
"id", product.get("id"),
"name", product.get("name"),
"price", String.valueOf(newPrice)
));
}
}
4. 控制器(Controller)
java
@RestController
@RequestMapping("/prod")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/{id}")
public Map<String, String> get(@PathVariable Long id) {
return productService.findById(id);
}
@PostMapping("/{id}/price")
public String update(@PathVariable Long id, @RequestParam int price) {
productService.updatePrice(id, price);
return "OK";
}
}
验证步骤
1. 第一次访问:
GET http://localhost:8080/prod/1
控制台输出:📦 查询数据库 id=1
说明缓存写入成功
2. 第二次访问同一接口:
控制台没有查询数据库字样,则命中缓存
3. 更新:
POST http://localhost:8080/prod/1/price?price=1888
控制台输出清除缓存,再访问 /prod/1 会重新回源
**4.**10 分钟后 TTL 到期,再访问会自动回源刷新

