Redis 缓存笔记(最新现状)
本项目把 Redis 同时用于三类用途:① Spring Cache 声明式缓存(商品、分类)、② RedisTemplate 手动 TTL 存储(验证码)、③ 高并发原子操作(秒杀 Lua、分布式锁)。本文按”基础设施 → 各类用法 → 缓存三大问题 → 一致性治理 → 本次完善记录”整理,均对应当前代码。
历史:项目曾保留”本地 Map → RedisTemplate 手写 List → @Cacheable”三层演时代码(
selectCategoryTree1、selectById1、selectProductNameList),现已清理/重构,分类树也统一改成 Spring Cache。文末有演进说明。
一、项目里 Redis 的用法总览(当前)
| 用法 | 场景 | 位置 | 方式 |
|---|---|---|---|
| ① 声明式缓存 | 商品详情 | ProductServiceImpl.selectById |
@Cacheable(sync=true) |
| ① 声明式缓存 | 分类树 | CategoryServiceImpl.selectCategoryTree |
@Cacheable(categoryCache,'tree',sync=true) |
| ① 缓存淘汰 | 商品增/改/上下架/删/库存变动 | ProductServiceImpl 多个方法 | @CacheEvict |
| ① 缓存淘汰 | 分类增/改/删 | CategoryServiceImpl | @CacheEvict(categoryCache,'tree') |
| ② TTL 临时存储 | 图形验证码 | CaptchaController / 登录校验 |
RedisTemplate set 带 60s 过期 |
| ③ 原子操作 | 秒杀预减库存/一人一单 | seckill.lua + StringRedisTemplate |
Lua 原子脚本 |
| ③ 结果轮询 | 秒杀排队结果 | SeckillServiceImpl / Listener |
String key + TTL |
| ③ 分布式锁 | 下单/支付/加购 | Redisson | 见《分布式锁笔记.md》 |
缓存/连接配置集中在 mall-common,各服务依赖它,因此所有微服务共用同一套 Redis 与序列化配置。
二、基础设施
1. 依赖(父 pom)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId> <!-- @Cacheable 能力 -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId> <!-- RedisTemplate,底层默认 Lettuce -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId> <!-- Spring Cache 基于 AOP 代理 -->
</dependency>
2. 连接配置(各服务 application.yml)
spring:
data:
redis:
database: 0
host: 127.0.0.1
port: 6379
password: 123456
time-to-live: 3600000 # 缓存默认 TTL,毫秒 = 1 小时
cache-null-values: true # 缓存 null,防穿透
cache:
type: redis
3. 两个配置类(mall-common)
RedisConfig:自定义RedisTemplate,key 用StringRedisSerializer、value 用Jackson2JsonRedisSerializer(enableDefaultTyping(NON_FINAL)带@class类型信息),让 redis-cli 里能看到可读 JSON 而非 JDK 二进制。MyCacheConfig:配 Spring Cache 的RedisCacheConfiguration,key 字符串、valueGenericJackson2JsonRedisSerializer(自带类型信息),并把 yml 的 TTL、cache-null-values 生效;类上@EnableCaching开启注解缓存。
两套 value 序列化器类型标记方式不同,同一个 Redis key 不要混用 RedisTemplate 和 Spring Cache 两种写法。
三、用法①:商品缓存(Spring Cache)
mall-product/.../impl/ProductServiceImpl.java:
// 查:命中直接返回不查库;未命中查库并回填,sync=true 防击穿
@Cacheable(value = "productCache", key = "#id", sync = true)
@Override
public Product selectById(Long id) {
return productMapper.selectById(id);
}
// 删:淘汰该商品详情缓存
@CacheEvict(value = "productCache", key = "#id")
public void delectById(Long id) { productMapper.deleteById(id); }
// 新增/修改/上下架:更新后淘汰详情缓存(修复了原先 updateById 不清缓存的脏数据问题)
@CacheEvict(value = "productCache", key = "#product.id")
public void updateProduct(Product product) { productMapper.updateById(product); }
// 批量删除:无法逐一枚举 id,清空整个商品缓存
@CacheEvict(value = "productCache", allEntries = true)
public void deleteBatch(List<Long> ids) { productMapper.deleteBatchIds(ids); }
// 库存扣减/回补(秒杀链路调用):库存变了也要淘汰详情缓存
@CacheEvict(value = "productCache", key = "#id")
public boolean deductStock(Long id, Integer count) { return productMapper.deductStock(id, count) > 0; }
@CacheEvict(value = "productCache", key = "#id")
public boolean addStock(Long id, Integer count) { return productMapper.addStock(id, count) > 0; }
实际 Redis key 形如 productCache::1,TTL 1 小时。控制器 PUT /product/{id}、PUT /product/{id}/status/{status}、批量删除都已改走这些带淘汰的方法,不再直接调 MyBatis-Plus 的继承方法而漏清缓存。
四、用法①:分类树缓存(已统一为 Spring Cache)
分类树是典型”读多写少”(首页每次加载都要,后台偶尔改一次),缓存收益极高。当前实现 mall-category/.../impl/CategoryServiceImpl.java:
// 整棵树缓存到一个固定 key,sync=true 单实例内防击穿,TTL 1 小时兜底
@Cacheable(value = "categoryCache", key = "'tree'", sync = true)
@Override
public List<CategoryVO> selectCategoryTree() {
// 查全部分类(按 sort 升序) → BeanUtils 拷贝 → buildTree 组父子树
}
// 增/改/删都淘汰这一份树缓存
@CacheEvict(value = "categoryCache", key = "'tree'")
public void add(Category category) { categoryMapper.insert(category); }
@CacheEvict(value = "categoryCache", key = "'tree'")
public void updateCategory(Category category) { categoryMapper.updateById(category); }
@CacheEvict(value = "categoryCache", key = "'tree'")
public void deleteCategoryById(Long id) { categoryMapper.deleteById(id); }
实际 key 为 categoryCache::tree。相比旧实现(Redis List 存储、无 TTL、只在 add 删缓存、还残留一个 JVM HashMap),现在:统一注解、有 TTL、改/删也正确失效、去掉了多实例不一致的本地缓存。
五、用法②:验证码(RedisTemplate + TTL 临时存储)
严格说这不是”缓存”,而是利用 TTL 的一次性临时存储,免写定时清理任务:
// CaptchaController:生成验证码,存 60 秒
String uuid = UUID.randomUUID().toString().replace("-", "");
redisTemplate.opsForValue().set("captcha:" + uuid, captcha, 60, TimeUnit.SECONDS);
// 登录校验(AdminController):get 不到 = 已过期;校验通过立即删除保证一次性
String code = (String) redisTemplate.opsForValue().get("captcha:" + uuid);
if (!StringUtils.hasText(code)) return Result.error("验证码已过期");
redisTemplate.delete("captcha:" + uuid);
详见《验证码笔记.md》。
六、用法③:秒杀——Redis 不止是缓存
秒杀里 Redis 承担高并发挡洪峰,详见《秒杀功能实现详解.md》:
- String 库存键
seckill:{id}:stock+ Set 一人一单键seckill:{id}:users,预热时写入、带 TTL; - Lua 脚本把”判重复 + 判库存 + 扣减 + 记录用户”做成原子操作,返回 1 成功 / 0 售罄 / 2 重复,绝大部分流量在 Redis 就被挡下,不碰数据库;
- 结果键
seckill:result:{orderNo}= 0 排队 / 1 成功 / 2 失败,供前端轮询,带 TTL 自动清理; - 失败补偿
restoreStock:INCR回库存 +SREM解除一人一单。
这里用 StringRedisTemplate(key/value 都是字符串),与商品的 Spring Cache 缓存互不干扰(key 前缀不同)。
七、缓存三大经典问题的应对
| 问题 | 含义 | 本项目对策 |
|---|---|---|
| 穿透 | 查大量不存在的 id,全部打到 DB | cache-null-values:true 缓存 null(商品详情) |
| 击穿 | 热点 key 过期瞬间并发压 DB | @Cacheable(sync=true):同 key 只放一个线程回源 |
| 雪崩 | 大量 key 同时过期 | 统一 1h TTL + 写入时间天然错开;量小无碍,生产可加随机 TTL 抖动 |
| 一致性 | 改了库缓存仍旧 | 所有写操作 @CacheEvict,”先更新库、再删缓存”(Cache-Aside) |
策略是删缓存而不是更缓存:并发下更新缓存容易产生旧值覆盖新值,删除则让下次读时按需重建,更安全。
八、本次缓存一致性治理记录(曾是真实 bug,现已修复)
完善前的问题与处理:
- ~~商品修改/上下架走继承的
updateById,不清productCache,最长脏 1 小时~~ → 新增updateProduct带@CacheEvict(key=#product.id),控制器改调它。 - ~~批量删除
removeByIds不清缓存~~ → 新增deleteBatch带allEntries=true。 - ~~分类 update/delete 不清缓存(只 add 清),且用 Redis List 无 TTL~~ → 分类整体重写为 Spring Cache,增改删统一
@CacheEvict。 - ~~死代码
selectProductNameList()标 @Cacheable 却 return null;selectById1/selectCategoryTree1手写缓存~~ → 已删除,避免误导和脏 key。
仍需知晓的边界:
- Spring Cache 的
@CacheEvict默认在方法成功返回后执行,并非严格等事务提交后。低频后台写足够;强一致可配TransactionAwareCacheManagerPostProcessor让淘汰延迟到事务提交后。 - 旧版本残留的 Redis key(
categoryVOTree、productNameList::all、product1这类)不会自动消失,可手动DEL/SCARD清理;新 key 前缀是productCache::、categoryCache::。 enableDefaultTyping已弃用且有多态反序列化攻击面,生产建议activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ...)并约束可反序列化类型。
九、演进路线(教学价值,代码已收敛到最终形态)
本地 HashMap 缓存(单机、多实例不共享、重启即失、无 TTL)
│ why Redis:集中式、多服务共享、可设过期
▼
RedisTemplate 手写 Cache-Aside(灵活但样板代码多、易漏删缓存)
│ why 注解:统一、声明式、少出错
▼
Spring Cache @Cacheable/@CacheEvict(商品、分类当前形态)
│
秒杀等超高并发场景再进一步:Redis Lua 原子脚本(无锁化)