苍穹外卖-DAY07

一、redis缓存

  • 设置缓存

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    @GetMapping("/list")
    @ApiOperation("根据分类id查询菜品")
    public Result<List<DishVO>> list(Long categoryId) {

    //构造redis中key
    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);//查询起售中的菜品
    list = dishService.listWithFlavor(dish);

    //不存在设置数据返回
    redisTemplate.opsForValue().set(Key,list);

    return Result.success(list);
    }
  • 删除缓存

    1
    2
    3
    4
    5
    6
    private void deleteCache(String patten){
    Set keys = redisTemplate.keys(patten);

    redisTemplate.delete(keys);

    }

二、SpringCache

  • 具体实现流程

    image-20260205195312900

  • 具体方法

    image-20260205184759084

1.@CachePut

1
2
3
4
5
6
// 修改完数据库后,自动把最新的 Dish 对象塞回 Redis,覆盖旧数据
@CachePut(value = "dishCache", key = "#dish.id")
public Dish updateDish(Dish dish) {
dishMapper.update(dish);
return dish; // 这个返回值会被存入 Redis
}

SPEL 表达式

Spring Expression Language (SpEL) 允许在注解中通过字符串的形式,动态获取方法参数或返回值的属性

Redis 缓存注解(@Cacheable, @CachePut)中,我们用它来定义 Key

  1. 获取参数属性:
    • #参数名.属性名:例如 #dish.id (推荐,语义清晰)
    • #p0.id#a0.id:表示第1个参数的 id 属性(p=param, a=arg)。
  2. 获取返回值(仅限 @CachePut):
    • #result:代表方法执行后的返回值。
    • 场景:新增数据时,ID 可能是数据库自动生成的,参数里没有 ID。这时必须用 #result.id 才能拿到 ID 存入缓存。
  3. 拼接字符串:
    • 单引号包裹字符串:'user_' + #id
  4. 具体如下
image-20260205191647922

redis 中key的树形存储方式

Redis 本质上是一个 Key-Value 数据库,并没有真正的“文件夹”概念。但是,所有的 Redis 可视化管理工具(如 Another Redis Desktop Manager, RDM)都遵循一个约定:

约定:使用冒号 : 作为分隔符来模拟目录结构

image-20260205192437372

2.@Cacheable

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class DishService {

// 假设 id = 10,Key 解析为 "dish_10"
// 第一次调用:执行方法查库,存入 Redis
// 第二次调用:发现 Redis 有 "dish_10",直接返回,不再打印 "查询数据库..."
@Cacheable(value = "dishCache", key = "'dish_' + #id")
public Dish getDishById(Long id) {
System.out.println("查询数据库..."); // 只有第一次会打印
return dishMapper.selectById(id);
}
}

执行流程

当你调用一个被它标记的方法时,Spring 会按以下步骤操作:

  1. 先查缓存:根据定义的 Key,去 Redis(或其他缓存)里找有没有数据。
  2. 如果命中:
    • 直接返回缓存里的数据。
    • 完全跳过你的 Java 方法体(里面的 SQL 语句压根不会执行)。
  3. 如果未命中:
    • 执行你的 Java 方法(去查数据库)。
    • 拿到返回值。
    • 自动写入缓存(为下一次请求做准备)。
    • 返回数据给调用者。

表达式限制

@CachePut 不同:

  • @CachePut:方法肯定会执行,所以它的 Key 可以使用 #result(返回值)来生成。
  • @Cacheable:因为要先查缓存,还没执行方法呢,哪里来的返回值?
    • 结论:@Cacheablekey 属性中不能使用 #result,只能使用参数(如 #id, #p0)。

底层原理

在 Controller 里注入 DishService 时:

1
2
@Autowired
private DishService dishService;

Spring 容器并没有直接把 DishService 原始对象,而是给了你一个 代理对象(Proxy)。这个代理对象“包裹”了你的原始对象。

Spring 在运行时动态生成的代理类大概长这样(简化版):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class DishServiceProxy extends DishService {

private DishService target; // 指向你的原始对象

@Override
public Dish getDishById(Long id) {
// 1. 【切面逻辑】先查 Redis
Dish cache = redis.get("dish_" + id);
if (cache != null) {
return cache; // 有缓存,直接返回!不调用 target
}

// 2. 【原始逻辑】缓存没有,才调用你的原始对象
Dish result = target.getDishById(id);

// 3. 【切面逻辑】把结果存入 Redis
redis.set("dish_" + id, result);

return result;
}

@Override
public void methodA() {
// methodA 没有注解,直接透传
target.methodA();
}
}

3.@CacheEvict

根据 ID 删除某一条或多条数据。

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class DishService {

// 假设 id=10,这行代码会把 Redis 中 key 为 "dish_10" 的数据删掉
@CacheEvict(value = "dishCache", key = "'dish_' + #id")
public void deleteDish(Long id) {
// 1. 先删数据库
dishMapper.deleteById(id);

// 2. 方法执行成功后,Spring 会自动去 Redis 删掉对应的 Key
}
}
1
2
3
4
5
6
7
// 只要执行这个方法,dishCache 下的所有 Key 全部被清空!
// 相当于 Redis 命令:DEL dishCache::* (逻辑上)
@CacheEvict(value = "dishCache", allEntries = true)
public void reloadAllDishes() {
// 即使这里什么都不做,缓存也会被清空
System.out.println("缓存已清空,下次查询将走数据库");
}

底层原理依旧是通过动态代理实现