黑马点评学习笔记07(缓存工具封装)

前言

用Redis完成登录界面后,我们实现了点评项目的店铺查询功能为并基于Redia解决了缓存穿透,缓存雪崩,缓存击穿等一系列Java并发 问题,可以发现我们的代码会不会太长太麻烦了,每一个查询功能都要有这么长的代码真的很麻烦,那我们可以封装起来,用的时候直接调用。

🤪先来看看要封装的方法是什么?

🙌🙌🙌方法1与方法3配合起来,负责常见的普通的缓存,用于解决缓存穿透 ;方法2与方法4结合,负责热点缓存,用于解决缓存击穿

了解封装的方法,有利于我们读懂Java底层的源代码,其实逻辑都是一样的,只是传参,其中用到了Java泛型

来看看什么是 Java 泛型🙌🙌🙌🙌🙌🙌?

✅ 定义

泛型(Generics) 是 Java 5 引入的一种机制,允许在定义类、接口或方法时使用类型参数(Type Parameter),从而在编译期提供类型安全检查,避免运行时类型转换错误。

✅ 基本语法

泛型类:class Box { ... }

泛型方法: T get(T item) { ... }

通配符:List<?>、List<? extends Number>、List<? super Integer>

来看看封装好的工具类的方法:

java 复制代码
package com.hmdp.utils;

import...
@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate ;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 将数据加入Redis,并设置有效期
     *
     * @param key
     * @param value
     * @param timeout
     * @param unit
     */
    public void set(String key, Object value, Long timeout, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), timeout, unit);
    }


    /**
     * 设置缓存和逻辑过期时间
     *
     * @param key  key前缀
     * @param value
     * @param time  有效期
     * @param unit  有效期的时间单位
     */
    public void setWithLogicalExpire(String key, Object value, Long time,  TimeUnit unit) {
        // 设置逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        // 写入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }


    /**
     * 根据id从缓存中查找数据(缓存参透)
     * @param keyPrefix Key前缀
     * @param id
     * @param type  返回数据类型
     * @param dbFallback 数据库查询方法
     * @param time  有效期
     * @param unit  有效期的时间单位
     * @return
     * @param <R>
     * @param <ID>
     */
    public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time,  TimeUnit unit) {
        String key = keyPrefix + id;
        //1.从Redis中查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);

        //2.存在,直接返回
        if (StrUtil.isNotBlank(json)) {
            return JSONUtil.toBean(json, type);
        }

        //判断是否是空值
        if (json == null) {
            //返回错误
            return null;
        }

        //3.不存在,根据id查询数据库
        R r = dbFallback.apply(id);

        //4.数据库不存在,返回错误
        if (r == null) {
            //将空值写入Redis
            stringRedisTemplate.opsForValue().set(key, "",CACHE_NULL_TTL , TimeUnit.MINUTES);
            return null;
        }

        //5.存在,写入Redis
        this.set(key, r, time, unit);
        //6.返回
        return r;
    }

    /*
   创建线程池
    */
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);


    /*
    缓存重建(逻辑过期)
     */

    public <R, ID> R queryWithLogicalExpire( String keyPrefix,ID id, Class<R> type, Function<ID, R> dbFallback, Long time,  TimeUnit unit) {
        String key = keyPrefix + id;
        //1.从Redis中查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);

        //2.判断是否存在
        if (StrUtil.isBlank(json)) {
            //3.存在,直接返回
            return null;
        }

        //4.命中,需要先将json反序列化为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        JSONObject data =  (JSONObject) redisData.getData();
        R r = JSONUtil.toBean(data, type);
        LocalDateTime expireTime = redisData.getExpireTime();
        //5.判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            //5.1 未过期,直接返回
            return r;
        }

        //5.2 处于过期时间,需要缓存重建
        //6.缓存重建
        //6.1 获取互斥锁
        String lock = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lock);

        //6.2 判断是否获取成功
        if (isLock) {
            //6.3 成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    //重建缓存
                    //1.查询数据库
                    R r1 = dbFallback.apply(id);
                    //2.写入Redis
                    this.setWithLogicalExpire(key, r1, time , unit);

                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unLock(lock);
                }
            });

        }

        //6.3 成功,返回结果

        //6.4 失败,返回错误

        //7.返回
        return r;
    }

    /*
    尝试获取锁
     */
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }
    /*
    释放锁
     */
    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }


}
java 复制代码
    /**
     * 根据id查询商铺数据
     *
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        // 调用解决缓存穿透的方法
//        Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class,
//                this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
//        if (Objects.isNull(shop)){
//            return Result.fail("店铺不存在");
//        }
        
        // 调用解决缓存击穿的方法
        Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class,
                this::getById, CACHE_SHOP_TTL, TimeUnit.SECONDS);
        if (Objects.isNull(shop)) {
            return Result.fail("店铺不存在");
        }
        
        return Result.ok(shop);
    }

在 CacheClient 类中,泛型主要出现在两个核心方法中:

java 复制代码
public <R, ID> R queryWithPassThrough(
    String keyPrefix, 
    ID id, 
    Class<R> type, 
    Function<ID, R> dbFallback, 
    Long time,  
    TimeUnit unit
)


public <R, ID> R queryWithLogicalExpire(
    String keyPrefix,
    ID id, 
    Class<R> type, 
    Function<ID, R> dbFallback, 
    Long time,  
    TimeUnit unit
)

1. <R, ID> 是什么?

这是方法级别的泛型声明,表示:

R:返回值的类型(Result),比如 Shop、User 等业务实体。

ID:主键的类型(Identifier),比如 Long、String、Integer。

2. 参数 Class type 的作用

  • 用于 反序列化时指定目标类型

  • 因为 Java 的泛型存在 类型擦除(Type Erasure),运行时无法直接知道 R 是什么类型。

  • 所以需要显式传入 Class(如 Shop.class)来告诉 JSONUtil.toBean():请把 JSON 转成 Shop 对象。

✅ ShopService类中的示例调用:

java 复制代码
// 调用解决缓存击穿的方法
        Shop shop = cacheClient.queryWithLogicalExpire(
        CACHE_SHOP_KEY, 
        id, 
        Shop.class,
        this::getById, 
        CACHE_SHOP_TTL, 
        TimeUnit.SECONDS
        );
     

3. 参数 Function<ID, R> dbFallback 的含义

  • 这是一个 函数式接口(Java 8+),表示"根据 ID 查询数据库"的逻辑。
  • Function<ID, R>:输入 ID,输出 R。
  • 使用 Lambda 表达式传入数据库查询逻辑,解耦缓存逻辑与具体业务。

本文是学习黑马程序员---黑马点评项目的课程笔记,小白啊!!!写的不好轻喷啊🤯如果觉得写的不好,点个赞吧🤪(批评是我写作的动力)

...。。。。。。。。。。。...

...。。。。。。。。。。。...

相关推荐
Yurko132 小时前
【C语言】选择结构和循环结构的进阶
c语言·开发语言·学习
范纹杉想快点毕业2 小时前
12个月嵌入式进阶计划ZYNQ 系列芯片嵌入式与硬件系统知识学习全计划(基于国内视频资源)
c语言·arm开发·单片机·嵌入式硬件·学习·fpga开发·音视频
大邳草民2 小时前
深入理解 Python 的“左闭右开”设计哲学
开发语言·笔记·python
im_AMBER3 小时前
React 12
前端·javascript·笔记·学习·react.js·前端框架
清钟沁桐3 小时前
mlir 编译器学习笔记之四 -- 调度
笔记·学习·mlir
lijun_xiao20093 小时前
elasticsearch学习笔记-02
笔记·学习·elasticsearch
wdfk_prog4 小时前
[Linux]学习笔记系列 -- [kernel][time]timer
linux·笔记·学习
Wilber的技术分享4 小时前
【大模型实战笔记 6】Prompt Engineering 提示词工程
人工智能·笔记·llm·prompt·大语言模型·提示词工程
JJJJ_iii4 小时前
【机器学习16】连续状态空间、深度Q网络DQN、经验回放、探索与利用
人工智能·笔记·python·机器学习·强化学习