Redis 配置与使用笔记(思途商城)

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 到期自动释放

九、注意事项 / 常见坑

  1. 缓存一致性:写操作一定要淘汰缓存。商品的增改/上下架/删/库存变动、分类的增改删现在都已带 @CacheEvict(本次完善修复了原先 update/delete 走继承方法漏清缓存的脏数据问题)。策略是”先更新库、再删缓存”。
  2. 序列化要统一:RedisTemplate 与 Spring Cache 用了两套 value 序列化器,别对同一个 key 混用两种写法。
  3. @EnableCaching 别漏:注解式缓存靠它开启;漏了则 @Cacheable 完全不生效(但代码不报错,最难查)。
  4. @Cacheable 的自调用失效:同一个类内部方法互相调用不走代理,缓存注解不生效。
  5. 缓存与手写存储分开看:商品/分类读缓存已统一用 Spring Cache;验证码、秒杀这类需要 TTL/原子性/自定义数据结构的才手写 RedisTemplate。不要把整份快照当 List 逐个 rightPushAll(旧分类树写法),存 JSON 用 opsForValue().set(key, obj, ttl) 更直观。
  6. 连接信息:默认连本机 127.0.0.1:6379、密码 123456、db 0。若 Redis 未启动或密码不对,服务启动/首次用缓存即报连接异常。
  7. jedis 2.9.0 版本偏老,仅用于教学;生产建议升级或直接用 Spring Boot 默认的 Lettuce。
  8. 遗留字段:mall-service 的 UploadController 注入了 RedisTemplate 但当前未使用(早期图片跟踪设计移除后残留),可删除以免误解。
上一篇
下一篇