Redis 配置与使用笔记(思途商城)
本项目把 Redis 同时用在:Spring Cache 声明式缓存(
@Cacheable,商品与分类)、RedisTemplate 手动读写(验证码 TTL)、以及秒杀的 Lua 原子操作和 Redisson 分布式锁。本文聚焦”依赖 → 连接配置 → Java 配置 → 序列化”,业务用法与最新现状见《Redis缓存笔记.md》。
一、总览:Redis 在本项目的用途(当前现状)
| 场景 | 模块 | 方式 |
|---|---|---|
| 商品详情缓存 | mall-product | @Cacheable(productCache:#id, sync=true) |
| 分类树缓存 | mall-category | @Cacheable(categoryCache:'tree', sync=true)(已从手写 List 改为注解) |
| 商品/分类写操作 | mall-product / mall-category | @CacheEvict 淘汰缓存 |
| 图形验证码 | mall-service | RedisTemplate set + 60s TTL |
| 秒杀库存/一人一单/结果 | mall-order | StringRedisTemplate + Lua 原子脚本 |
| 分布式锁 | mall-common | Redisson(见《分布式锁笔记.md》) |
缓存配置与两个配置类都放在 mall-common,各服务依赖它并扫描 com.situ.mall.config,因此每个微服务都用同一套 Redis 配置。
历史代码
selectProductNameList()(return null 的死缓存)、手写缓存selectById1()、分类的 Redis List 缓存与本地 HashMap 均已在重构中删除/替换,本文旧版示例不再适用。
二、依赖配置
这些依赖定义在父 POM pom.xml,被子模块继承:
<!-- Spring Cache 抽象(提供 @Cacheable 等注解能力) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Spring Data Redis(提供 RedisTemplate / RedisConnectionFactory) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 底层客户端 Jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<!-- AOP(Spring Cache 基于 AOP 代理实现) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
说明:
spring-boot-starter-data-redis默认用 Lettuce 客户端;这里显式引入了 Jedis,属于教学常用的组合(jedis 2.9.0 版本较老,实战建议用更高版本或直接用默认 Lettuce)。
三、连接配置(各服务 application.yml)
每个微服务的 application.yml 都有这段(以 mall-product 为例):
spring:
data:
redis:
database: 0 # 使用 0 号库
host: 127.0.0.1
port: 6379
password: 123456
time-to-live: 3600000 # 缓存默认过期时间,单位毫秒(= 1 小时)
cache-null-values: true # 缓存空值,防止缓存穿透
cache:
type: redis # 指定缓存管理器用 Redis 实现
关键两点:
spring.cache.type: redis—— 让 Spring Cache 用 Redis 作为底层缓存,而不是默认的 ConcurrentMap。time-to-live/cache-null-values会被下面的MyCacheConfig读取并生效。
四、Java 配置类(mall-common)
4.1 RedisConfig:自定义 RedisTemplate 的序列化
// mall-common/.../config/RedisConfig.java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
ObjectMapper om = new ObjectMapper();
// 让 Jackson 能序列化对象的全部字段(含 private)
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 记录类型信息,反序列化时能还原成原本的类
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer(om, Object.class);
template.setConnectionFactory(factory);
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
// key / hashKey 用字符串序列化,可读性好
template.setKeySerializer(redisSerializer);
template.setHashKeySerializer(redisSerializer);
// value / hashValue 用 JSON 序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
}
为什么要自定义? Spring Boot 默认的 RedisTemplate 用 JDK 序列化,存进 Redis 的是不可读的二进制。这里改成 key 用字符串、value 用 JSON,Redis 里能用 GET key 直接看到可读 JSON。
4.2 MyCacheConfig:Spring Cache 的 Redis 配置
// mall-common/.../config/MyCacheConfig.java
@EnableConfigurationProperties(CacheProperties.class) // 绑定 spring.cache.* 配置
@Configuration
@EnableCaching // 开启声明式缓存
public class MyCacheConfig {
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// key 用字符串,value 用 JSON(GenericJackson2Json 会自动带上 @class 类型信息)
config = config.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// 把 application.yml 里 spring.cache.redis.* 的配置全部生效
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive()); // 过期时间
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues(); // 是否缓存空值
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
这个类做了两件事:把 RedisCacheConfiguration 的序列化改成 字符串 key + JSON value,并把 yml 里的 time-to-live、cache-null-values 等配置应用上去。
4.3 各服务开启缓存
每个启动类都加了 @EnableCaching(MyCacheConfig 上也有,二者任一生效即可):
// AdminApplication / ProductApplication / CategoryApplication / ServiceApplication
@EnableCaching
@SpringBootApplication
public class ProductApplication { ... }
五、用法一:Spring Cache 注解(声明式)
代表是 mall-product 的 ProductServiceImpl 与 mall-category 的 CategoryServiceImpl:方法上加注解,查自动读缓存、写后自动淘汰,业务零侵入。
// 商品详情:命中不查库,未命中查库回填,sync=true 防击穿
@Cacheable(value = "productCache", key = "#id", sync = true)
public Product selectById(Long id) { return productMapper.selectById(id); }
// 写操作一律淘汰缓存(修复前 update 走继承方法不清缓存,会脏 1 小时)
@CacheEvict(value = "productCache", key = "#id")
public void delectById(Long id) { productMapper.deleteById(id); }
@CacheEvict(value = "productCache", key = "#product.id")
public void updateProduct(Product product) { productMapper.updateById(product); }
@CacheEvict(value = "productCache", allEntries = true) // 批量删除,清空整个分区
public void deleteBatch(List<Long> ids) { productMapper.deleteBatchIds(ids); }
// 分类树:整棵树缓存到固定 key,增改删统一淘汰
@Cacheable(value = "categoryCache", key = "'tree'", sync = true)
public List<CategoryVO> selectCategoryTree() { /* 查库 → 组树 */ }
@CacheEvict(value = "categoryCache", key = "'tree'")
public void add / updateCategory / deleteCategoryById(...) { ... }
要点:
value= 缓存名(成为 Redis key 前缀,如productCache::1、categoryCache::tree)。key用 SpEL:#id取参数,'tree'/'all'是固定字符串(要加引号)。@Cacheable命中则不执行方法体;@CacheEvict方法成功后删缓存;allEntries=true清空整个分区。sync = true:并发时对同一 key 加锁防击穿。- 注解基于 AOP 代理,同类自调用不生效,必须从另一个 Bean 调进来。
六、用法二:RedisTemplate / StringRedisTemplate 手动操作
需要自定义结构、TTL 或原子操作时直接注入模板。当前项目里 Spring Cache 之外的手动用法主要是两类。
6.1 验证码:String + TTL(mall-service)
// 生成:写 60 秒过期
redisTemplate.opsForValue().set("captcha:" + uuid, captcha, 60, TimeUnit.SECONDS);
// 校验:get 不到即过期;通过后立即 delete 保证一次性
6.2 秒杀:String/Set + Lua 原子脚本(mall-order)
// 预热:库存写 String,一人一单用 Set,活动信息写 String(JSON),带 TTL
stringRedisTemplate.opsForValue().set(SeckillConstant.stockKey(id), String.valueOf(stock));
stringRedisTemplate.delete(SeckillConstant.usersKey(id));
// 抢购:整段 Lua 原子执行,返回 1成功/0售罄/2重复
Long ret = stringRedisTemplate.execute(seckillScript,
List.of(stockKey, usersKey), String.valueOf(userId));
// 结果轮询:seckill:result:{orderNo} = 0排队/1成功/2失败,带 TTL
详见《秒杀功能实现详解.md》。分布式锁(Redisson)底层也是 Redis,见《分布式锁笔记.md》。
说明:分类树、商品详情这些”读缓存”场景已统一改用 Spring Cache 注解,不再手写 RedisTemplate;旧的
opsForList().rightPushAll("categoryVOTree", ...)和selectById1已删除。
七、序列化机制小结
| 场景 | key 序列化 | value 序列化 |
|---|---|---|
RedisTemplate(RedisConfig) |
StringRedisSerializer |
Jackson2JsonRedisSerializer(带 @class 类型信息) |
| Spring Cache(MyCacheConfig) | StringRedisSerializer |
GenericJackson2JsonRedisSerializer |
两者都是 key 字符串 + value JSON,因此 Redis 里存的是可读 JSON。value 序列化都带类型信息,反序列化时能还原成原本的 Java 类型(如 Product、List<CategoryVO>)。
注意:两种 value 序列化器的类型标记方式略有差别(前者靠
enableDefaultTyping,后者是 GenericJackson2Json 自带),同一 key 不要混用两种写入方式,否则反序列化可能失败。
八、Redis key 与 TTL 一览(当前)
| 来源 | Redis key 形式 | TTL |
|---|---|---|
| 商品详情 Spring Cache | productCache::1 |
全局 time-to-live = 1h |
| 分类树 Spring Cache | categoryCache::tree |
1h |
| 验证码 | captcha:{uuid} |
60s |
| 秒杀库存 | seckill:{id}:stock |
活动结束时间 |
| 秒杀一人一单 | seckill:{id}:users |
活动结束时间 |
| 秒杀结果 | seckill:result:{orderNo} |
排队 10min / 终态 1h |
| 分布式锁 | lock:order:create:{userId} 等 |
leaseTime 到期自动释放 |
九、注意事项 / 常见坑
- 缓存一致性:写操作一定要淘汰缓存。商品的增改/上下架/删/库存变动、分类的增改删现在都已带
@CacheEvict(本次完善修复了原先 update/delete 走继承方法漏清缓存的脏数据问题)。策略是”先更新库、再删缓存”。 - 序列化要统一:
RedisTemplate与 Spring Cache 用了两套 value 序列化器,别对同一个 key 混用两种写法。 @EnableCaching别漏:注解式缓存靠它开启;漏了则@Cacheable完全不生效(但代码不报错,最难查)。@Cacheable的自调用失效:同一个类内部方法互相调用不走代理,缓存注解不生效。- 缓存与手写存储分开看:商品/分类读缓存已统一用 Spring Cache;验证码、秒杀这类需要 TTL/原子性/自定义数据结构的才手写 RedisTemplate。不要把整份快照当 List 逐个
rightPushAll(旧分类树写法),存 JSON 用opsForValue().set(key, obj, ttl)更直观。 - 连接信息:默认连本机
127.0.0.1:6379、密码123456、db 0。若 Redis 未启动或密码不对,服务启动/首次用缓存即报连接异常。 - jedis 2.9.0 版本偏老,仅用于教学;生产建议升级或直接用 Spring Boot 默认的 Lettuce。
- 遗留字段:
mall-service的UploadController注入了RedisTemplate但当前未使用(早期图片跟踪设计移除后残留),可删除以免误解。