黑马点评笔记

基础知识

  • Redis 是一个 key-value 的非关系型数据库,key 一般是 String 类型,而 value 类型多样

String 存单值,Hash 存对象,List 做队列,Set 去重,ZSet 排序。

  1. String(字符串)
    ➤ 最基本的 key-value,value 可以是字符串、数字或二进制数据。
    ✅ 例子:SET name “Alice” → 存一个值。
  2. Hash(哈希)
    ➤ 一个 key 对应多个字段(field)和值(value),像 Java 的 Map<String, String>。
    ✅ 例子:存用户信息:HSET user:1001 name “Alice” age “25”
  3. List(列表)
    ➤ 有序、可重复的字符串列表,支持从两端插入/弹出。
    ✅ 例子:消息队列、最新评论列表。
  4. Set(集合)
    ➤ 无序、不重复的字符串集合,支持交集、并集等操作。
    ✅ 例子:用户标签、好友去重。
  5. Sorted Set(有序集合 / ZSet)
    ➤ 每个元素关联一个分数(score),自动按分数排序。
    ✅ 例子:排行榜(按积分排序)、带权重的任务队列。

Jedis 使用

  1. 导入依赖

  2. 建立连接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private Jedis jedis;

    @BeforeEach
    void setUp() {
    // 建立连接
    jedis = new Jedis("192.168.56.10", 6379);
    // 设置密码
    jedis.auth("123");
    // 选择数据库
    jedis.select(0);
    }
  3. 使用 jedis

    1
    2
    3
    4
    5
    6
    void testString() {
    // 插入数据,方法名称就是redis命令名称
    String result = jedis.set("name", "张三");
    System.out.println(result);
    // 获取数据
    }
  4. 关闭连接,释放资源

    1
    2
    3
    4
    5
    void tearDown() {
    if (jedis != null) {
    jedis.close();
    }
    }

但是 jedis 是线程不安全的,所以一般使用连接池,连接池可以复用连接,避免频繁创建连接,提高性能

Spring Data Redis 使用

苍穹外卖用的就是这种 Redis 的 Java 客户端

  1. 引入 spring-boot-starter-data-redis 依赖
  2. 在 application.yml 中配置 Redis 连接信息
  3. 注入 RedisTemplate
  • 有自动序列化工具,指定序列化器之后,存储时他能将数据序列化为 JSON,取出时也能反序列化为对象

  • 但是这样 redis 里面就要存全类名,占用空间,所以推荐手动序列化和反序列化
    String json = mapper.writeValueAsString(object);序列化
    object = mapper.readValue(json, Object.class);反序列化

  • RedisTemplate 可以处理任意类型键值对,但是他用的 JDK 序列化器,会向 redis 里面存全类名,占用空间

  • StringRedisTemplate 专门处理字符串类型的键值对,用的字符串序列化器而任何对象又都能转成字符串,所以这个项目用的都是这个

项目导入

这个项目用 Java 8,也就是项目结构哪里改成 1.8 就能运行了

单表查询

用了 MybatisPlus 后单表查询非常简单,
query() 是 MP 提供的方法,查询的表名用注解写在实体类上,查询的实体类由当前 Service 的泛型决定

1
2
// 一致,根据手机号查询用户 select * from tb_user where phone = ?
User user = query().eq("phone", phone).one();

拦截器

和苍穹外卖一样,每次登录前对登录状态做校验

  1. utils 包下新建一个登录拦截器类,实现 HandlerInterceptor 接口 ctrl + i列出所有未实现的方法
  2. 实现前置拦截方法和后置拦截方法,将 session 的用户信息存入 threadlocal,每次请求都会创建一个线程,请求完归还线程
  • HttpServletRequest request 是由 Servlet 容器创建的,并且开发者不能直接调用或构造它
  • 只有在实现了像拦截器(HandlerInterceptor)这样的容器的特定接口后,框架才会自动把 request 对象“注入”到实现的里方法(如 preHandle)中供你使用。
  • 这是 Facade 模式的应用,容器通过 RequestFacade 向外暴露一个安全、简化的接口,隐藏内部复杂实现

session

  • 默认使用的是 Servlet 容器的 session 管理机制(如 tomcat)存在了 tomcat 的内存里,多台 tomcat 并不共享 session 存储空间,tomcat 会自动将 sessionid 写入 cookie
  • 引入 spring-sessino-data-redis 依赖并配置之后,session 数据就存在 redis 里面,每次用户登录后缓存他的 session,之后每次发出请求都从 redis 的 session 里面获取数据

redis 缓存验证码

  1. 之前配置了 SpringRedisTemplate 和 redis 的连接信息,所以现在直接像 Mapper 那样注入就行了,@Resource是根据类型注入@Autowired是根据名称注入,之后用 srringRedisTemplate.opsForValue()来操作 redis
  2. 发送验证码的时候,以手机号作为 key,验证码作为 value,设置过期时间
  3. 验证成功保存用户到 redis 的时候,以随机的 UUID 作为 key,然后 user 对象转成 userDTO,去掉敏感信息,userDTO 转成的 Map 作为 value,用 hash 类型用多个键值对存储用户的信息,
  4. 然后加一个 token 刷新拦截器,拦截所有请求,只要用户有操作就刷新 token 有效期,原先的登录拦截器就只做登录拦截,其余逻辑(存放用户信息到 ThreadLocal)放到 token 刷新拦截器里
    Another 的 redis 图形化界面,每个 key 的 ttl 显示在最上面,hashMap 里面有一列 ttl 都显示-1,不是真的,还是要看 map 在 redis 里 key 的 ttl

redis 缓存更新策略

  1. 内存淘汰策略
    redis 自动就会使用,不用自己维护,内存不足时自动淘汰掉旧数据
    一致性差,维护成本无
  2. 超时剔除
    给缓存添加 TTL 时间,到期后自动删除
    一致性一般,维护成本低
  3. 主动更新
    自己编写逻辑,在修改数据库时更新缓存
    一致性高,维护成本高

主动更新策略

  1. 调用者更新数据库的时候更新缓存(推荐)
  2. 将缓存与数据库整合为一个服务,由服务来维护一致性,调用者调用就行,无需关心缓存一致性
  3. 调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证最终一致性
  • 删除缓存还是更新缓存,如果进行上百次更新但中间没有人访问,那就会出现大量无效的写操作,但是删除就不会有这种问题,而且两个线程同时更新也会有线程安全的问题
  • 怎么保证缓存和数据库操作的原子性,单体系统就放在一个事务里,分布式有分布式方案
  • 多线程是先删缓存再操作数据库,还是先操作数据库再删缓存。都可能有问题,但是先操作数据库再删缓存出问题的概率更小,因为更新数据库再删缓存中间的时间间隔更短。此时再加上超时剔除可以兜底

【总结】

  1. 低一致性需求:用 redis 自带的内存淘汰机制
  2. 高一致性需求:主动更新,并用超时剔除兜底
    • 读操作
      • 缓存命中直接返回
      • 缓存未命中则查询数据库,并写入缓存,设定超时时间
    • 写操作
      • 先写数据库,再删除缓存
      • 要确保数据库和缓存操作之间的原子性

缓存穿透

缓存穿透是指客户端请求的数据在缓存和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库

【解决方案】

  • 缓存空对象
    数据库中没查到就缓存一个空对象到 redis 中,之后的请求就不会到达数据库了
    简单,易于维护,但是有额外内存消耗(设置 TTL 来减少),可能存在短期不一致
  • 布隆过滤
    在客户端和 redis 中加一层布隆过滤器,不存在就拒绝,存在则放行
    但是他是有概率的,他说不存在是真的不存在,但是他说存在也有可能不存在然后可能缓存穿透
    空间占用少,但是实现复杂,存在误判可能

【预防方案】

  • 增加 id 的复杂度
  • 做好格式校验
  • 加强用户权限

缓存空对象的流程:

  1. 数据库查到用户不存在时,就缓存一个空对象到 redis 中
  2. 然后新增一个判断空值是null还是""的语句,判断当前是查到的是空对象还是 redis 当前没缓存

缓存雪崩

缓存雪崩是指同一时间段缓存的大量 key 同时失效或者 redis 宕机,导致大量请求都到达服务器,带来巨大压力

【解决方案】

  • 给不同的 key 的 TTL 添加随机值
  • 利用 redis 集群提高可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

缓存击穿

缓存击穿问题也叫热点 key 问题,就是一个被高并发访问并且缓存重建业务比较复杂的 key 突然失效了,无数的请求都会到达数据库带来巨大压力

  • 互斥锁:给重建缓存的逻辑加锁,让缓存重建的逻辑只有一个线程执行,其他线程等待,一会后重试
    保证一致性,简单,但是性能低,可能死锁
  • 逻辑过期:不设置 TTL 而是设置了一个字段来存储过期时间,之后查询缓存的时候多一个判断是否过期的逻辑,如果过期就获取互斥锁开启一个新线程来重建缓存完成后重置逻辑过期时间,当前和在此时访问的其他线程都直接返回旧数据
    性能好,但不保证一致性,有额外内存消耗,实现复杂

【简单互斥锁解决方案】
用 redis 的 setnx 命令设置一个 key,这个是只有 key 不存在是才能设置这个 key 的值,用这个 key 来标记当前互斥锁的状态

函数作为参数

  • 在封装解决缓存穿透的工具类的时候,没有缓存的时候要查数据库,而这个方法只有调用者知道,所以将其作为入参传入
  • 通过函数式接口(Functional Interface) 和 Lambda 表达式可以达到这种效果
    • 函数式接口:
    • Function<T, R>:接受一个参数,返回一个结果。
    • Consumer<T>:接受一个参数,无返回值。
    • Supplier<T>:无参数,返回一个结果。
    • Predicate<T>:接受一个参数,返回 boolean。
    • Runnable:无参数无返回值(常用于线程)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 先声明泛型列表,名字可以自定义
// 声明一个有参有返回值的Function作为参数,它的入参为ID,返回值为R
public <R, ID> R queryWithPassThrough(String keyPrefix, ID id,
Class<R> type,Function<ID, R> dbFallback,
Long time, TimeUnit unit) {
// 4.缓存不存在,根据id查询数据库
R r = dbFallback.apply(id);
}

// 调用者使用上面这个工具类
// 用 lambda 表达式作为函数
cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, id2 -> getById(id2), CACHE_SHOP_TTL, TimeUnit.MINUTES)
// 这种 lambda 表达式可以简写
id2 -> getById(id2)
this::getById // 直接传入本类的方法

黑马点评笔记
http://www.981928.xyz/2026/01/13/黑马点评笔记/
作者
981928
发布于
2026年1月13日
许可协议