批量缓存模版

news2024/11/13 17:55:25

批量缓存模版

缓存通常有两种使用方式,一种是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

缓存击穿

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

空缓存+分布式锁

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

亚信安全并购亚信科技交易正式完成

亚信安全与亚信科技联合宣布&#xff0c;亚信安全正式完成对亚信科技的控股权收购&#xff0c;由此&#xff0c;规模近百亿的中国最大的软件企业之一诞生&#xff01;双方将全面实现公司发展战略&#xff0c;以及优势能力与资源的深度融合&#xff0c;形成业界独有的“懂网、懂…

MybatisPlus入门(十)MybatisPlus-逻辑删除和多记录操作

一、Mybatis-Plus 多记录操作 按照主键删除多条记录 List<Long> ids Arrays.asList(new Long[]{2,3}) userDao.deleteBatchIds(ids); 示例代码如下: Testvoid testDelete(){//删除指定多条数据List<Long> list new ArrayList<>();list.add(14025513424818…

【css】overflow: hidden效果

1. 不添加overflow: hidden 1.1 效果 上面无圆角 1.2 代码 <template><view class"parent"><view class"child1">child1</view><view class"child2">child2</view></view></template><…

「QT」几何数据类 之 QPolygonF 浮点型多边形类

✨博客主页何曾参静谧的博客&#x1f4cc;文章专栏「QT」QT5程序设计&#x1f4da;全部专栏「VS」Visual Studio「C/C」C/C程序设计「UG/NX」BlockUI集合「Win」Windows程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「PK」Parasolid…

架构篇(04理解架构的演进)

目录 学习前言 一、架构演进 1. 初始阶段的网站架构 2. 应用服务和数据服务分离 3. 使用缓存改善网站性能 4. 使用应用服务器集群改善网站的并发处理能力 5. 数据库读写分离 6. 使用反向代理和CDN加上网站相应 7. 使用分布式文件系统和分布式数据库系统 8. 使用NoSQL和…

OpenCV基础05_GUI和PyMsql

目录 一、PySimpleGUI 1、布局和窗口 2、文本框组件 3、视频处理 4、图片处理 二、pymsql 1、数据库操作 2、数据采集 3、人脸识别 一、PySimpleGUI PySimpleGUI 是一个用于简化 GUI 编程的 Python 包&#xff0c;它封装了多种底层 GUI 框架&#xff08;如 tkinter、…

ModuleNotFoundError: No module named ‘_ssl‘ centos7中的Python报错

报错 ModuleNotFoundError: No module named ‘_ssl’ 解决步骤&#xff1a; 1.下载openssl wget https://www.openssl.org/source/openssl-3.0.7.tar.gz tar -zxvf openssl-3.0.7.tar.gz cd openssl-3.0.72.编译安装 ./config --prefix/usr/local/openssl make make install3…

外呼系统只需这 3 种功能,电销效率快速提升

在当今竞争激烈的商业环境中&#xff0c;电销团队面临着诸多挑战。如何提高电销效率&#xff0c;成为了企业关注的焦点。今天&#xff0c;小编就给大家介绍&#xff0c;沃创云三种外呼系统功能&#xff0c;让你的电销效率快速提升。 一、智能拨号功能 传统的电销方式中&#x…

18. Mouse 鼠标、KeyBoard 键盘和 Action 消息事件处理

在本节的例子中&#xff0c;会自定义很多UI控件实现不同的事件响应&#xff0c;如下图所示&#xff1a; IOKit 事件框架 事件流程 OS X的事件依赖 IOKit 框架&#xff0c;事件发生后首先会传递到IOKit框架中处理&#xff0c;然后通知Window Server服务层处理&#xff0c;由…

C# 实现对指定句柄的窗口进行键盘输入的实现

在C#中实现对指定句柄的窗口进行键盘操作&#xff0c;可以通过多种方式来实现。以下是一篇详细的指南&#xff0c;介绍如何在C#中实现这一功能。 1. 使用Windows API函数 在C#中&#xff0c;我们可以通过P/Invoke调用Windows API来实现对指定窗口的键盘操作。以下是一些关键的…

Spring Plugin与策略模式:打造动态可扩展的应用

目录 一、策略模式 二、Spring Plugin 2.1 Spring Plugin 实现策略模式开发 2.2 策略模式优缺点 三、Spring Plugin 原理 一、策略模式 策略模式是一种设计模式&#xff0c;它允许程序在运行中动态的选择不同的行为方式进行动态执行。策略模式的核心思想是将行为封装在一个个…

Word大珩助手:超大数字怎么读?35位数字?69位数字?

俄罗斯日前对谷歌开出了20000000000000000000000000000000000&#xff08;35位数字&#xff09;美元的罚款 这一数字远超全球GDP总和&#xff0c;消息一出很快就登上热搜。 面对这样一个庞大的数字&#xff0c;人们不禁好奇&#xff0c;这样的数字该如何读出来&#xff1f; …

asp.net文件防盗链

URLRewriter实现 可以参考下面的文章 代码 .net framework 新建asp.net framework的web项目&#xff0c;新建AntiTheftChainHandler using System.Web;namespace AntiTheftChainStu01.Handler {public class AntiTheftChainHandler : IHttpHandler{public bool IsReusable…

【含开题报告+文档+PPT+源码】基于SSM的蛋糕店销售管理系统的设计与实现

开题报告 在现代社会&#xff0c;蛋糕作为一种受欢迎的甜点&#xff0c;广泛应用于各种庆祝活动和节日。传统的蛋糕预订方式往往需要用户亲自到店面进行预订&#xff0c;预订流程繁琐&#xff0c;时间和地点限制也给用户带来了不便。随着智能手机和移动互联网的普及&#xff0…

政治经济学笔记

【拯救者】政治经济学速成&#xff08;基础习题&#xff09; 研究生产关系必须联系生产力和上层建筑 1.生产力与生产关系 生产力代表生产的物质内容&#xff0c;生产关系是生产的社会形式。生产力决定生产关系&#xff0c;生产关系对生产力具有 反作用 *其中的”反作用”指的是…

005.精读《B-Tree vs LSM-Tree》

文章目录 1. 引言&#xff1a;2. 精读2.1 性能指标2.2 B-tree2.3 LSM-tree2.4 性能对比 3. 写在最后 1. 引言&#xff1a; 在本期的技术深度解析中&#xff0c;我们将聚焦于数据领域的两个重要成员——B-Tree和LSM-Tree。这两种数据结构在数据管理系统中最为普遍且广泛采用的数…

关于 el-table 的合计行问题

目录 一.自定义合计行 二.合计行不展示&#xff0c;只有缩放/变大窗口或者F12弹出后台时才展示 三.合计行出现了表格滚动条下方 四.合计行整体样式的修改 五.合计行单元格样式修改 1.css 2.jsx方式 六.合计行单元格合并 一.自定义合计行 通过 show-summary 属性开启合计…

C++ | Leetcode C++题解之第554题砖墙

题目&#xff1a; 题解&#xff1a; class Solution { public:int leastBricks(vector<vector<int>>& wall) {unordered_map<int, int> cnt;for (auto& widths : wall) {int n widths.size();int sum 0;for (int i 0; i < n - 1; i) {sum wi…

如何使用 C# 编写一个修改文件时间属性的小工具?

下面是简鹿办公一个用 C# 编写的简单工具&#xff0c;它可以批量修改文件的创建时间、最后访问时间和最后修改时间。我们将使用 .NET Framework 或 .NET Core 来实现这个功能。 完整示例代码 1. 创建一个新的 C# 控制台应用程序 您可以使用 Visual Studio 或 .NET CLI 创建一个…

使用FTP与多个合作伙伴传文件,如何处理运维管理和数据安全问题

许多行业的企业使用FTP与外部客户、供应商等合作伙伴进行文件交换&#xff0c;如大型保险公司、研究所、IC设计企业、汽车制造厂商等。基于FTP可以满足企业与外部合作伙伴文件收发的基础需求&#xff0c;但在IT运维管理、数据安全保障及业务便利性上仍存在不同程度的缺陷和不足…