欢迎光临
我们一直在努力

【苍穹外卖 day05-2 | Redis在项目中的应用】

前言

苍穹外卖这个项目里,Redis 的作用很具体,主要落在两件事上:减少重复查询,以及 在多个端之间共享状态。

这一篇不讲安装和命令,重点只放在项目代码里的 Redis 用法。主线很明确:先接入 RedisTemplate,再看用户端菜品缓存,最后看店铺营业状态的共享读写。

Redis在这个项目里解决什么问题

先看业务场景。

苍穹外卖是前后端分离项目,用户端会频繁查询菜品列表,管理端会频繁调整店铺营业状态。这两类数据都有一个共同点:访问频率高,但每次都走数据库并不划算。

所以 Redis 在这里承担了两个职责:

  • 缓存用户端高频查询的数据
  • 保存需要多端共享的轻量状态

这样做之后,Redis 不再只是一个独立的知识点,而是直接参与了接口调用链。

Redis怎么接进 Spring Boot 项目

当前项目的 sky-server/pom.xml 已经引入了 Redis 相关依赖:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

这一步的作用很直接。Spring Boot 会基于这个 starter 自动完成 Redis 连接能力的装配,后面业务代码里就可以直接注入 RedisTemplate。

RedisTemplate 是操作 Redis 的核心模板类,字符串读写通常通过 opsForValue 完成。当前这个项目里的几处 Redis 用法,也正是沿着这条路线写的。

Redis配置是怎么落进项目的

当前项目把 Redis 配置拆成了公共结构和开发环境具体值两层。

公共结构在 application.yml:

spring:
profiles:
active: dev
data:
redis:
host: ${sky.redis.host}
port: ${sky.redis.port}
password: ${sky.redis.password}
database: ${sky.redis.database}

这里表达的是一条很清楚的取值链路:

  • 项目当前激活的是 dev 环境
  • Redis 的真实连接参数不直接写在这里
  • 真实值由 sky.redis.* 提供

开发环境里只要补上对应字段即可。博客里不放真实敏感值,保留结构就够了:

sky:
redis:
host: localhost
port: 6379
password: ******
database: 0

这种写法的好处是配置层次清晰。以后切环境时,业务代码不用动,优先改配置就可以。

为什么项目里还单独写了 RedisConfiguration

项目里还有一个 RedisConfiguration.java:

@Slf4j
@Configuration
public class RedisConfiguration {

@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnetionFactory){
log.info("开始创建redis模拟对象…");
RedisTemplate redisTemplate= new RedisTemplate();
//设置redis的连接工厂对象
redisTemplate.setConnectionFactory(redisConnetionFactory);
//设置key的序列化器为StringRedisSerializer
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}

这段配置的核心目的,是把 Redis 的 key 序列化器 改成 StringRedisSerializer。

RedisTemplate 默认序列化策略并不一定适合当前这种“直接用可读字符串作为 key”的场景。这个项目里 Redis key 写得都比较直白,比如:

  • dish_分类id
  • shop_status

如果 key 的序列化方式不直观,后面排查缓存时会很麻烦。现在把 key 统一成字符串序列化之后,无论是命令行查看,还是图形工具查看,都会更清楚。

第一处应用:用户端菜品查询缓存

Redis 在这个项目里的第一处典型应用,是用户端根据分类查询菜品。

对应代码在 user/DishController.java:

@GetMapping("/list")
@Operation(summary = "根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {

//构造redis中的key,规则:dish_分类id
String key="dish_"+categoryId;

//查询redis中是否存在菜品数据
List<DishVO>list=(List<DishVO>) redisTemplate.opsForValue().get(key);

//如果存在就直接返回,无需返回数据库
if(list!=null && list.size()>0){
return Result.success(list);
}

Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
//如果不存在查询数据库,把查询到的数据放到redis
list = dishService.listWithFlavor(dish);
redisTemplate.opsForValue().set(key,list);

return Result.success(list);
}

这段代码的逻辑很完整,已经是一个标准缓存读取流程。

它的执行顺序可以直接拆成四步:

  • 根据分类 id 拼出 Redis key,规则是 dish_分类id
  • 先去 Redis 查缓存
  • 如果缓存命中,直接返回
  • 如果缓存没命中,再查数据库,并把结果写回 Redis
  • 这里最关键的地方不是 set 和 get 本身,而是 先查缓存,再查数据库 这个顺序。

    这样写之后,用户端重复打开同一个分类时,不需要每次都重新走数据库查询。对于“用户频繁访问、数据变化不算特别频繁”的场景,这种方式很合适。

    这里为什么要按分类id做 key

    当前代码把 key 写成:

    String key="dish_"+categoryId;

    这说明缓存粒度不是“所有菜品一份大缓存”,而是 每个分类一份缓存。

    这种设计有几个好处:

    • key 结构简单,容易看懂
    • 不同分类互不影响
    • 后面清理缓存时更容易定位

    比如:

    • 分类 1 的缓存是 dish_1
    • 分类 2 的缓存是 dish_2

    只要分类 id 不一样,缓存就自然分开了。

    第二处应用:管理端操作后清理菜品缓存

    有缓存,就一定要考虑缓存什么时候失效。

    这个项目的处理方式不是做复杂的自动刷新,而是 在管理端改菜品数据后,主动清理相关缓存。

    对应代码在 admin/DishController.java。

    新增菜品后清缓存

    @PostMapping
    @Operation(summary = "新增菜品")
    public Result save(@RequestBody DishDTO dishDTO) {
    log.info("新增菜品:{}", dishDTO);
    dishService.saveWithFlavor(dishDTO);

    //清理缓存数据
    String key="dish_"+dishDTO.getCategoryId();
    cleanCache(key);
    return Result.success();
    }

    这段逻辑很清楚。新增菜品之后,当前分类下的缓存数据已经可能过期,所以直接把这个分类对应的缓存删掉。

    删除、修改、起售停售后清缓存

    当前控制器里还有几处类似处理:

    cleanCache("dish_*");

    出现的位置包括:

    • 批量删除菜品
    • 修改菜品
    • 修改菜品状态

    这几个操作都有一个共同点:影响范围不再局限于单个明确分类,或者当前实现里为了简单,直接把所有菜品相关缓存一起清掉。

    cleanCache 这个方法做了什么

    控制器里单独抽了一个方法:

    private void cleanCache(String pattern){
    Set keys=redisTemplate.keys(pattern);
    redisTemplate.delete(keys);
    }

    这段代码的意思很直接:

  • 先根据模式查 Redis key
  • 再把查到的 key 一次性删掉
  • 比如传入:

    cleanCache("dish_*");

    就表示把所有 dish_ 开头的缓存都删掉。

    这就是当前项目的缓存一致性策略:查询时先查 Redis,数据变化时主动删缓存。

    这套策略的优点是实现简单,适合教学项目,也适合当前这种业务复杂度不高的场景。

    第三处应用:店铺营业状态共享

    除了缓存菜品列表,Redis 在这个项目里还有一类很典型的用途,就是 多端共享状态。

    对应代码在:

    • admin/ShopController.java
    • user/ShopController.java

    这两边共用同一个 key:

    public static final String key="shop_status";

    管理端写入状态

    @PutMapping("/{status}")
    @Operation(summary = "设置店铺的营业状态")
    public Result sestatus(@PathVariable Integer status){
    log.info("设置店铺的营业状态为:{}", status ==1 ? "营业中":"打烊中");
    redisTemplate.opsForValue().set(key, status);
    return Result.success();
    }

    这里的逻辑很直接。管理端传入状态值,后端把这个状态写进 Redis。

    管理端和用户端读取状态

    管理端和用户端读取逻辑基本一致:

    Integer status=(Integer) redisTemplate.opsForValue().get(key);
    if (status == null) {
    status = StatusConstant.DISABLE;
    }
    return Result.success(status);

    这段代码说明了一件事:Redis 在这里不是拿来做复杂缓存,而是拿来保存一个 全局共享的小状态值。

    只要管理端改过一次营业状态,用户端查到的就是同一份数据。

    这里为什么要做空值兜底

    读取营业状态时,代码里都做了同样的处理:

    if (status == null) {
    status = StatusConstant.DISABLE;
    }

    原因很简单。

    Redis 里一开始不一定有 shop_status。如果项目刚启动,还没有人设置过营业状态,那么直接读取就可能得到空值。

    当前写法把空值兜底成 打烊状态,这样接口不会因为空指针出问题,业务含义也更稳妥。

    这个处理非常实用。它说明 Redis 读写不能只考虑“正常有值”的情况,还要考虑“第一次启动、缓存不存在”这种初始状态。

    这三处代码能总结出什么规律

    把菜品缓存和营业状态这两块放在一起看,可以很清楚地看到 Redis 在项目里常见的两种落点。

    第一种是 缓存查询结果:

    • 用户端先查 Redis
    • 没有再查数据库
    • 管理端改数据后主动删缓存

    第二种是 共享轻量状态:

    • 管理端负责写入状态
    • 多个端负责读取同一份状态
    • 读取时做好空值兜底

    这两类用法都没有追求复杂设计,但已经很贴近实际项目。

    这篇代码最值得记住的实现思路

    如果把这篇内容压成几句可迁移的经验,核心就是下面这几条。

    • 查询频繁、结果可复用的数据,适合先进 Redis
    • 数据一旦被管理端改动,就要同步考虑缓存失效
    • 多个端要共享的小状态,适合直接放 Redis
    • 读取 Redis 时要考虑 key 还不存在的情况

    这几条不是只对苍穹外卖有用,后面做别的 Spring Boot 项目时也一样能套进去。

    怎么验证这几处 Redis 应用

    如果要自己顺一遍这套流程,可以按这个顺序看。

  • 先启动 Redis,确认 6379 正常监听
  • 启动项目,确认 Redis 连接配置没问题
  • 调用户端菜品查询接口,观察第一次会查数据库
  • 再调同一分类查询接口,观察会直接走缓存
  • 调管理端新增、修改、删除菜品接口,确认缓存会被清掉
  • 调管理端修改营业状态接口,再调用户端查询营业状态接口,确认两端读到的是同一份状态
  • 只要这几步能走通,就说明 Redis 已经真正参与到了项目业务流程里。

    总结

    Redis 在这个项目里的价值,不是单独提供了一个中间件,而是直接优化了业务代码的执行路径。

    用户端菜品查询借助 Redis 减少了重复查库,管理端通过清理缓存保证了数据不会长期过期,店铺营业状态又通过 Redis 实现了多端共享。这样串起来之后,Redis 在项目里的定位就很清楚了:它既是缓存层,也是轻量状态存储层。

    赞(0)
    未经允许不得转载:171主机测评 » 【苍穹外卖 day05-2 | Redis在项目中的应用】
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址