Redis 缓存模式与注解缓存
目录
Cache-Aside(旁路缓存模式)
1. 工作流程
2. 三大注解
3. TTL 配置
Spring Boot 示例代码
1. 启用缓存(主类)
2. Redis 缓存配置(TTL + JSON 序列化)
3. 模拟商品服务(Service)
4. 控制器(Controller)
Cache-Aside(旁路缓存模式)
Cache 即缓存,Aside有在旁边的含义 。在这种模式下,应用程序在数据读取和写入操作时,并非直接与缓存交互,缓存像是处于应用程序数据操作的旁路位置,应用程序主要操作数据库,同时兼顾对缓存的维护,因此得名旁路缓存模式。
1. 工作流程
阶段 | 读操作(查询) | 写操作(更新/删除) |
---|---|---|
第一步 | 先查缓存 | 先写数据库 |
第二步 | 缓存没命中就查数据库 | 再删缓存(而不是更新缓存) |
第三步 | 把查询结果写入缓存 | 下次查询再自动回填 |
优点:数据一致性高、实现简单。
注意:写时要删除缓存,不要直接更新缓存,否则容易产生脏数据。
2. 三大注解
注解 | 作用 | 时机 |
---|---|---|
@Cacheable | 查询时自动查缓存→回源→回写 | 读 |
@CacheEvict | 删除缓存(支持 key/全部) | 写 |
@CachePut | 更新缓存但不影响数据库 | 写(少用) |
回源指从数据库重新取数据
回写指把刚从数据库查到的数据写回缓存,让后续的查询可以直接从缓存命中,避免再次访问数据库
@CachePut 少用的核心原因:数据不一致风险
@CachePut 的理念是方法执行后必定更新缓存
但业务逻辑中,方法执行成功 ≠ 数据库更新成功
比如方法执行了,但数据库事务回滚、数据库连接失败、乐观锁冲突等
结果缓存更新了,但数据库没更新,导致数据永久不一致
大部分更新操作需要保证数据库和缓存同时成功
3. TTL 配置
在配置类中设置:
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. 启用缓存(主类)
@SpringBootApplication
@EnableCaching
public class RedisApplication {public static void main(String[] args) {SpringApplication.run(RedisApplication.class, args);}
}
2. Redis 缓存配置(TTL + JSON 序列化)
@Configuration
public class RedisCacheConfig {@Beanpublic 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)
@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)
@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 到期,再访问会自动回源刷新