批量缓存模版

批量缓存模版

缓存通常有两种使用方式,一种是Cache-Aside,一种是cache-through。也就是旁路缓存和缓存即数据源。

一般一种用于读,另一种用于读写。参考后台服务架构高性能设计之道。

最典型的Cache-Aside的样例:

//读操作
data = Cache.get(key);
if(data == NULL)
{
    data = SoR.load(key);
    Cache.set(key, data);
}
return data;

比如Spring-Cache就是Cache-Aside,只需要写上load逻辑,加上注解,就可以实现Cache-Aside缓存的效果了。

但是这种缓存存在一个缺陷,如果我们需要获取一批用户信息,碰巧用户的缓存全都失效了(也就是缓存雪崩),就需要去数据库中全部拉出来,那么这样一个本来性能很高的循环,就等同于全部查了数据库,缓存一点儿作用都没了。

批量缓存查询

举个栗子🌰:

  1. 批量请求数据

    • 这里的上方绿色矩形框表示一批需要查询的数据项(例如用户ID 1, 2, 3, 4)。
  2. 批量从缓存获取数据

    • 请求首先进入缓存层(例如 Redis),通过批量 GET 操作尝试获取所有请求的数据。
    • 如果缓存中存在对应数据,则会返回相应的结果;如果缓存中缺少部分数据(例如在缓存中没有 3),那么这些数据就会被标记为“未命中”。
    • 图中左侧绿色矩形框 1, 2, 4 表示缓存中命中的数据部分。
  3. 批量从数据库加载缺失的数据

    • 对于缓存中未命中的数据(例如 3),系统会通过批量 LOAD 操作从数据库加载。
    • 这个步骤不仅可以填充当前请求所需的数据,还可以将这些数据存入缓存中,以便后续请求可以直接从缓存中获取,减少对数据库的访问。
  4. 将结果返回给调用方

    • 最终返回完整的数据结果(包括从缓存获取的数据和从数据库加载的数据),完成整个批量查询的流程。
/**
 * 获取用户信息,盘路缓存模式
 */
public Map<Long, User> getUserInfoBatch(Set<Long> uids) {
    //批量组装key
    List<String> keys = uids.stream().map(a -> RedisKey.getKey(RedisKey.USER_INFO_STRING, a)).collect(Collectors.toList());
    //批量get
    List<User> mget = RedisUtils.mget(keys, User.class);
    Map<Long, User> map = mget.stream().filter(Objects::nonNull).collect(Collectors.toMap(User::getId, Function.identity()));
    //发现差集——还需要load更新的uid
    List<Long> needLoadUidList = uids.stream().filter(a -> !map.containsKey(a)).collect(Collectors.toList());
    if (CollUtil.isNotEmpty(needLoadUidList)) {
        //批量load
        List<User> needLoadUserList = userDao.listByIds(needLoadUidList);
        Map<String, User> redisMap = needLoadUserList.stream().collect(Collectors.toMap(a -> RedisKey.getKey(RedisKey.USER_INFO_STRING, a.getId()), Function.identity()));
        RedisUtils.mset(redisMap, 5 * 60);
        //加载回redis
        map.putAll(needLoadUserList.stream().collect(Collectors.toMap(User::getId, Function.identity())));
    }
    return map;
}

对这段代码进行抽象,找到可以进行复用的地方

黄色代表可复用的流程,红色代表需要根据不同的数据进行单独的实现。

首先定义一个顶层的抽象接口

public interface BatchCache<IN, OUT> {
    /**
     * 获取单个
     */
    OUT get(IN req);

    /**
     * 获取批量
     */
    Map<IN, OUT> getBatch(List<IN> req);

    /**
     * 修改删除单个
     */
    void delete(IN req);

    /**
     * 修改删除多个
     */
    void deleteBatch(List<IN> req);
}

再确定骨架

/**
 * Description: Redis String类型的批量缓存框架
 * 这是一个抽象类,用于实现基于Redis的批量缓存框架。
 * 该框架提供了缓存获取、批量获取、删除、批量删除的功能。
 */
public abstract class AbstractRedisStringCache<IN, OUT> implements BatchCache<IN, OUT> {

    private Class<OUT> outClass; // OUT类型的Class对象,用于反射操作,方便从Redis读取数据

    /**
     * 构造方法
     * 利用反射机制获取泛型OUT的具体类型。
     * 这样在Redis操作时可以知道OUT类型,用于数据转换。
     */
    protected AbstractRedisStringCache() {
        ParameterizedType genericSuperclass = (ParameterizedType) this.getClass().getGenericSuperclass();
        this.outClass = (Class<OUT>) genericSuperclass.getActualTypeArguments()[1];
    }

    /**
     * 抽象方法:获取缓存键
     * 子类需要实现,用于根据请求参数生成唯一的Redis缓存键。
     * @param req 请求参数
     * @return Redis键
     */
    protected abstract String getKey(IN req);

    /**
     * 抽象方法:获取缓存的过期时间
     * 子类需要实现,定义缓存的有效期。
     * @return 过期时间(以秒为单位)
     */
    protected abstract Long getExpireSeconds();

    /**
     * 抽象方法:批量加载数据
     * 当缓存中没有对应数据时,通过该方法从数据库或其他存储加载数据。
     * @param req 请求参数列表
     * @return 加载后的数据映射
     */
    protected abstract Map<IN, OUT> load(List<IN> req);

    /**
     * 单个数据的缓存获取方法
     * 使用getBatch方法来获取单个数据的缓存内容。
     * @param req 单个请求参数
     * @return 单个请求对应的OUT对象
     */
    @Override
    public OUT get(IN req) {
        return getBatch(Collections.singletonList(req)).get(req);
    }

    /**
     * 批量获取缓存数据的方法
     * 该方法尝试从缓存中批量获取请求数据,缺失的数据将从数据库加载并写入缓存。
     * @param req 请求参数列表
     * @return 返回请求参数和对应OUT对象的映射
     */
    @Override
    public Map<IN, OUT> getBatch(List<IN> req) {
        if (CollectionUtil.isEmpty(req)) { // 防御性编程,避免空请求列表的处理
            return new HashMap<>();
        }

        // 去重,避免重复的请求参数
        req = req.stream().distinct().collect(Collectors.toList());

        // 组装Redis键列表
        List<String> keys = req.stream().map(this::getKey).collect(Collectors.toList());

        // 批量从Redis获取缓存数据
        List<OUT> valueList = RedisUtils.mget(keys, outClass);

        // 计算差集,找出缓存中未命中的请求
        List<IN> loadReqs = new ArrayList<>();
        for (int i = 0; i < valueList.size(); i++) {
            if (Objects.isNull(valueList.get(i))) { // 如果某项数据未命中缓存,则添加到loadReqs
                loadReqs.add(req.get(i));
            }
        }

        Map<IN, OUT> load = new HashMap<>();
        // 如果有未命中缓存的数据,则从数据库加载并写入缓存
        if (CollectionUtil.isNotEmpty(loadReqs)) {
            load = load(loadReqs); // 批量从数据库加载数据
            Map<String, OUT> loadMap = load.entrySet().stream()
                    .map(a -> Pair.of(getKey(a.getKey()), a.getValue())) // 为每个数据生成对应的Redis键值对
                    .collect(Collectors.toMap(Pair::getFirst, Pair::getSecond));
            RedisUtils.mset(loadMap, getExpireSeconds()); // 批量写入Redis,设置过期时间
        }

        // 将缓存命中和重新加载的数据合并成最终结果
        Map<IN, OUT> resultMap = new HashMap<>();
        for (int i = 0; i < req.size(); i++) {
            IN in = req.get(i);
            OUT out = Optional.ofNullable(valueList.get(i)) // 优先使用缓存中的数据
                    .orElse(load.get(in)); // 如果缓存中没有,则使用加载的数据
            resultMap.put(in, out); // 将结果放入resultMap
        }
        return resultMap;
    }

    /**
     * 删除单个数据的缓存
     * 使用deleteBatch方法删除单个数据的缓存
     * @param req 单个请求参数
     */
    @Override
    public void delete(IN req) {
        deleteBatch(Collections.singletonList(req));
    }

    /**
     * 批量删除缓存数据的方法
     * @param req 请求参数列表
     */
    @Override
    public void deleteBatch(List<IN> req) {
        // 根据请求参数生成对应的Redis键列表
        List<String> keys = req.stream().map(this::getKey).collect(Collectors.toList());
        RedisUtils.del(keys); // 从Redis批量删除这些键
    }
}

具体的实现类

@Component
public class UserInfoCache extends AbstractRedisStringCache<Long, User> {
    @Autowired
    private UserDao userDao;

    @Override
    protected String getKey(Long uid) {
        return RedisKey.getKey(RedisKey.USER_INFO_STRING, uid);
    }

    @Override
    protected Long getExpireSeconds() {
        return 5 * 60L;
    }

    @Override
    protected Map<Long, User> load(List<Long> uidList) {
        List<User> needLoadUserList = userDao.listByIds(uidList);
        return needLoadUserList.stream().collect(Collectors.toMap(User::getId, Function.identity()));
    }
}

骨架通过实现类的getKey方法来获取到具体的Redis Key,然后实现接口的复用。

缓存雪崩

缓存雪崩的场景解决方案:

  1. 缓存失效时间分布随机
  2. 缓存预热,在系统启动阶段去mysql拉一次数据load到redis

缓存击穿

缓存击穿的场景解决方案:

空缓存+分布式锁

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/913234.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Vue3学习笔记(上)

Vue3学习笔记&#xff08;上&#xff09; Vue3的优势&#xff1a; 更容易维护&#xff1a; 组合式API更好的TypeScript支持 更快的速度&#xff1a; 重写diff算法模板编译优化更高效的组件初始化 更小的体积&#xff1a; 良好的TreeShaking按需引入 更优的数据响应式&#xf…

SPIRE: Semantic Prompt-Driven Image Restoration 论文阅读笔记

这是一篇港科大学生在google research 实习期间发在ECCV2024的语义引导生成式修复的文章&#xff0c;港科大陈启峰也挂了名字。从首页图看效果确实很惊艳&#xff0c;尤其是第三行能用文本调控修复结果牌上的字。不过看起来更倾向于生成&#xff0c;对原图内容并不是很复原&…

Knowledge Graph-Enhanced Large Language Models via Path Selection

研究背景 研究问题&#xff1a;这篇文章要解决的问题是大型语言模型&#xff08;LLMs&#xff09;在生成输出时存在的事实不准确性&#xff0c;即所谓的幻觉问题。尽管LLMs在各种实际应用中表现出色&#xff0c;但当遇到超出训练语料库范围的新知识时&#xff0c;它们通常会生…

常见计算机网络知识整理(未完,整理中。。。)

TCP和UDP区别 TCP是面向连接的协议&#xff0c;发送数据前要先建立连接&#xff1b;UDP是无连接的协议&#xff0c;发送数据前不需要建立连接&#xff0c;是没有可靠性&#xff1b; TCP只支持点对点通信&#xff0c;UDP支持一对一、一对多、多对一、多对多&#xff1b; TCP是…

HashMap(深入源码追踪)

一篇让你搞懂HashMap的几个最重要的知识点,往源码跟踪可以让我们很轻松应对所谓的一些八股面试题. 一. 属性解释 先来解释HashMap中重要的常量属性值 DEFAULT_INITIAL_CAPACITY : 默认初始化容量,也就是如果不指定初始化的Map存储容量大小,默认生成一个存储16个空间的Map集合…

MySQL中的事务与锁

目录 事务 InnoDB 和 ACID 模型 原⼦性的实现 持久性的实现 ​隔离性的实现 锁 隔离级别 ​多版本控制(MVCC) 事务 1.什么是事务? 事务是把⼀组SQL语句打包成为⼀个整体&#xff0c;在这组SQL的执⾏过程中&#xff0c;要么全部成功&#xff0c;要么全部失败&#…

C#开发基础:WPF和WinForms关于句柄使用的区别

1、前言 在 Windows 应用程序开发中&#xff0c;WPF&#xff08;Windows Presentation Foundation&#xff09;和 WinForms&#xff08;Windows Forms&#xff09;是两种常见的用户界面&#xff08;UI&#xff09;框架。它们各自有不同的架构和处理方式&#xff0c;其中一个显…

基于.NET开源、功能强大且灵活的工作流引擎框架

前言 工作流引擎框架在需要自动化处理复杂业务流程、提高工作效率和确保流程顺畅执行的场景中得到了广泛应用。今天大姚给大家推荐一款基于.NET开源、功能强大且灵活的工作流引擎框架&#xff1a;elsa-core。 框架介绍 elsa-core是一个.NET开源、免费&#xff08;MIT License…

.NET6中WPF项目添加System.Windows.Forms引用

.NET6中WPF项目添加System.Windows.Forms引用 .NET6的WPF自定义控件默认是不支持System.Windows.Forms引用的&#xff0c;需要添加这个引用方法如下&#xff1a; 1. 在项目浏览器中找到项目右击&#xff0c;选择编辑项目文件&#xff08;Edit Project File&#xff09;。 …

16.UE5拉怪机制,怪物攻击玩家,伤害源,修复原视频中的BUG

2-18 拉怪机制&#xff0c;怪物攻击玩家、伤害源、黑板_哔哩哔哩_bilibili 目录 1.实行行为树实现拉怪机制 1.1行为树黑板 1.2获取施加伤害对象&#xff08;伤害源&#xff09; 2.修复原视频中&#xff0c;第二次攻击怪物后&#xff0c;怪物卡在原地不动的BUG 3.怪物攻击玩…

<项目代码>YOLOv8 草莓成熟识别<目标检测>

YOLOv8是一种单阶段&#xff08;one-stage&#xff09;检测算法&#xff0c;它将目标检测问题转化为一个回归问题&#xff0c;能够在一次前向传播过程中同时完成目标的分类和定位任务。相较于两阶段检测算法&#xff08;如Faster R-CNN&#xff09;&#xff0c;YOLOv8具有更高的…

Vue全栈开发旅游网项目(9)-用户登录/注册及主页页面开发

1.用户登录页面开发 1.查询vant组件 2.实现组件模板部分 3.模型层准备 4.数据上传 1.1 创建版权声明组件Copyright 新建文件&#xff1a;src\components\common\Copyright.vue <template><!-- 版权声明 --><div class"copyright">copyright xx…

后台管理系统窗体程序:文章管理 > 文章列表

目录 文章列表的的功能介绍&#xff1a; 1、进入页面 2、页面内的各种功能设计 &#xff08;1&#xff09;文章表格 &#xff08;2&#xff09;删除按钮 &#xff08;3&#xff09;编辑按钮 &#xff08;4&#xff09;发表文章按钮 &#xff08;5&#xff09;所有分类下拉框 &a…

【万字详解】如何在微信小程序的 Taro 框架中设置静态图片 assets/image 的 Base64 转换上限值

设置方法 mini 中提供了 imageUrlLoaderOption 和 postcss.url 。 其中&#xff1a; config.limit 和 imageUrlLoaderOption.limit 服务于 Taro 的 MiniWebpackModule.js &#xff0c; 值的写法要 &#xff08;&#xff09;KB * 1024。 config.maxSize 服务于 postcss-url 的…

基于STM32通过TM1637驱动4位数码管详细解析(可直接移植使用)

目录 1. 单位数码管概述 2. 对应编码 2.1 共阳数码管 2.2 共阴数码管 3. TM1637驱动数码管 3.1 工作原理 3.1.1 读键扫数据 3.1.2 显示器寄存器地址和显示模式 3.2 时序 3.2.1 指令数据传输过程&#xff08;读案件数据时序&#xff09; 3.2.2 写SRAM数据…

数字信号处理Python示例(11)生成非平稳正弦信号

文章目录 前言一、生成非平稳正弦信号的实验设计二、生成非平稳正弦信号的Python代码三、仿真结果及分析写在后面的话 前言 本文继续给出非平稳信号的Python示例&#xff0c;所给出的示例是非平稳正弦信号&#xff0c;在介绍了实验设计之后给出Python代码&#xff0c;最后给出…

Linux 系统结构

Linux系统一般有4个主要部分&#xff1a;内核、shell、文件系统和应用程序。内核、shell和文件系统一起形成了基本的操作系统结构&#xff0c;它们使得用户可以运行程序、管理文件并使用系统。 1. linux内核 内核是操作系统的核心&#xff0c;具有很多最基本功能&#xff0c;它…

网络安全之SQL初步注入

一.字符型 平台使用pikachu $name$_GET[name]; ​ $query"select id,email from member where username$name"; 用户输入的数据会被替换到SQL语句中的$name位置 查询1的时候&#xff0c;会展示username1的用户数据&#xff0c;可以测试是否有注入点&#xff08;闭…

【IEEE/EI会议】第八届先进电子材料、计算机与软件工程国际学术会议(AEMCSE 2025)

会议通知 会议时间&#xff1a;2025年4月25-27日 会议地点&#xff1a;中国南京 会议官网&#xff1a;www.aemcse.org 会议简介 第八届先进电子材料、计算机与软件工程国际学术会议&#xff08;AEMCSE 2025&#xff09;由南京信息工程大学主办&#xff0c;将于2025年4月25日…

华为海思招聘-芯片与器件设计工程师-模拟芯片方向- 机试题-真题套题题目——共8套(每套四十题)

华为海思招聘-芯片与器件设计工程师-模拟芯片方向- 机试题-真题套题题目分享——共九套&#xff08;每套四十题&#xff09; 岗位——芯片与器件设计工程师 岗位意向——模拟芯片 真题题目分享&#xff0c;完整题目&#xff0c;无答案&#xff08;共8套&#xff09; 实习岗位…