分享大厂对于缓存操作的封装

news2024/11/28 0:56:09

hello,伙伴们好久不见,我是shigen。发现有两周没有更新我的文章了。也是因为最近比较忙,基本是993了。

缓存大家再熟悉不过了,几乎是现在任何系统的标配,并引申出来很多的问题:缓存穿透、缓存击穿、缓存雪崩…哎,作为天天敲业务代码的人,哪有时间天天考虑这么多的破事。直接封装一个东西,我们直接拿来就用岂不是美哉。看了项目组的代码,我也忍不住 diy 了,对于增删就算了,就是 get set 的 API 调用,修改?直接删了重新添加吧,哪有先查缓存再去修改保存的。难点就在于缓存的查询,要不缓存的穿透、击穿、雪崩会诞生对吧。
我们先看下缓存的逻辑:

是的,其实就是这么简单,剩下的就是考虑一下缓存穿透问题,最常见的处理方式就是加锁。这里我采用的是信号量 Semaphore。
好的,现在展示我的代码,代码结构如下:

.
├── CacheEnum.java									-- 缓存枚举
├── CacheLoader.java								-- 缓存加载接口
├── CacheService.java								-- 缓存服务
└── CacheServiceImpl.java 					-- 缓存服务实现类

1 directory, 4 files

ok,现在我们一一讲解下:

CacheEnum

主要的代码:

public enum CacheEnum {
                       /** 用户token缓存 */
                       USER_TOKEN("USER_TOKEN", 60, "用户token"),
                       /** 用户信息缓存 */
                       USER_INFO("USER_INFO", 60, "用户信息"),;

    /** 缓存前缀 */
    private final String  cacheName;
    /** 缓存过期时间 */
    private final Integer expire;
    /** 缓存描述 */
    private final String  desc;

其他的就是 get/set 方法,这里不做展示。主要解决的痛点就是缓存过期时间的统一管理、缓存名称的统一管理。

CacheService

这里边就是定义了缓存操作的接口:

public interface CacheService {
    
    /**
     * 获取缓存
     * @param cacheName 缓存名称
     * @param key 缓存key
     * @param type 缓存类型
     * @return 缓存值
     * @param <T> 缓存类型
     */
    <T> T get(String cacheName, String key, Class<T> type);

    /**
     * 获取缓存
     * @param cacheName 缓存名称
     * @param key 缓存key
     * @param type 缓存类型
     * @param loader 缓存加载器
     * @return 缓存值
     * @param <T> 缓存类型
     */
    <T> T get(String cacheName, String key, Class<T> type, CacheLoader<T> loader);

    /**
     * 删除缓存
     * @param cacheName 缓存名称
     * @param key 缓存key
     */
    void delete(String cacheName, String key);

    /**
     * 设置缓存
     * @param cacheName 缓存名称
     * @param key 缓存key
     * @param value 缓存值
     */
    void set(String cacheName, String key, Object value);
}    

其实就是一些增删查的方法。只不过这里我们更加关注的是:缓存的名称,缓存的 key,缓存对象,缓存对象的类型。
在 22 行这里用到了CacheLoader 接口,其实就是处理缓存对象在缓存中拿不到的问题,它的定义也很简单:

@FunctionalInterface
public interface CacheLoader<V> {
    /**
     * 加载缓存
     * @param key 缓存key
     * @return 缓存值
     */
    V load(String key);
}

就一个方法,直接使用上 lambda 表达式,下边的测试类会讲到。

CacheServiceImpl

@Slf4j
@Service
public class CacheServiceImpl implements CacheService {

    /** Semaphore */
    private static final Semaphore        CACHE_LOCK = new Semaphore(100);

    /** 缓存操作 */
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 获取缓存key
     * @param cacheName 缓存名称
     * @param key 缓存key
     * @return 缓存key
     */
    private String getCacheKey(String cacheName, String key) {
        Assert.isTrue(StrUtil.isNotBlank(cacheName), "cacheName不能为空");
        Assert.isTrue(StrUtil.isNotBlank(key), "key不能为空");
        Assert.isTrue(CacheEnum.getByCacheName(cacheName) != null, "需要使用CacheEnum枚举创建缓存");
        return cacheName + ":" + key;
    }

    @Override
    public <T> T get(String cacheName, String key, Class<T> type) {
        Object value = redisTemplate.opsForValue().get(getCacheKey(cacheName, key));
        if (value != null) {
            return type.cast(value);
        }
        return null;
    }

    @Override
    public <T> T get(String cacheName, String key, Class<T> type, CacheLoader<T> cacheLoader) {
        try {
            // 获取锁, 防止缓存击穿
            CACHE_LOCK.acquire();
            String cacheKey = getCacheKey(cacheName, key);
            Object value = redisTemplate.opsForValue().get(cacheKey);
            if (value != null) {
                return type.cast(value);
            }
            value = cacheLoader.load(cacheKey);
            if (value != null) {
                this.set(cacheName, key, value);
                return type.cast(value);
            }
        } catch (InterruptedException e) {
            log.warn("获取锁失败");
        } finally {
            CACHE_LOCK.release();
        }
        return null;
    }

    @Override
    public void delete(String cacheName, String key) {
        redisTemplate.opsForValue().getOperations().delete(getCacheKey(cacheName, key));
    }

    @Override
    public void set(String cacheName, String key, Object value) {
        String cacheKey = getCacheKey(cacheName, key);
        CacheEnum cacheEnum = CacheEnum.getByCacheName(cacheName);
        Assert.isTrue(cacheEnum != null, "需要使用CacheEnum枚举创建缓存");
        redisTemplate.opsForValue().set(cacheKey, value, cacheEnum.getExpire(), TimeUnit.SECONDS);
    }
}

这里就是接口的具体实现。需要注意:

  1. 在获得完整的缓存 key 的时候,我们其实对于缓存的 cacheName 做了验证,参见上代码块 21 行,不允许自己定义缓存的 cacheName,统一在枚举类中定义。
  2. 因为 tair 的资源有点不好申请,这里使用的 redis 作为缓存的工具,结合 spring-boot-starter-data-redis 作为操作的 API。
  3. 应对缓存穿透问题,这里使用的是Semaphore 信号量。

别的就没什么好说的,现在我们来测试一下我们的封装是否管用。

测试代码

设置缓存

测试用定义的枚举类创建缓存:

    @Test
    void set() {
        cacheService.set(CacheEnum.USER_INFO.getCacheName(), "10001", getFakeUser("10001"));
    }

是没问题的,不用枚举类创建:

    @Test
    void testSetSelfDefinedCacheName() {
        cacheService.set("user", "10001", getFakeUser("10001"));
    }

直接异常出来了:

java.lang.IllegalArgumentException: 需要使用CacheEnum枚举创建缓存
读取缓存

读取缓存,我的 API 中分为两种情况:直接读取,没有就算了;读取缓存,没有的话再从 DB 中拿。对应的单测如下:

    @Test
    void testGet() {
        UserEntity user = cacheService.get(CacheEnum.USER_INFO.getCacheName(), "10001",
            UserEntity.class);
        log.info("user: {}", user);
    }

    @Test
    void testGetWithCacheLoader() {
        UserEntity user = cacheService.get(CacheEnum.USER_INFO.getCacheName(), "10001",
            UserEntity.class, new CacheLoader<UserEntity>() {
                @Override
                public UserEntity load(String key) {
                    return getFakeUser("10001");
                }
            });
        log.info("user: {}", user);
    }

    @Test
    void testGetWithSimpledCacheLoader() {
        UserEntity user = cacheService.get(CacheEnum.USER_INFO.getCacheName(), "10001",
            UserEntity.class, key -> getFakeUser(key));
        log.info("user: {}", user);
    }

第三种就是对于 lambda 接口的简化写法。
基于以上的方式,我们操作缓存就变得更加容易了。

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

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

相关文章

markdown导入其它markdown文件

相应语法 [toc] # 测试不同功能 <p> 这是类似超链接的管理方式 </p>[链接测试0](./part0/uml_base.md)[链接测试1](./part1/math_base.md)---<p> 这是直接导入文件的管理方式 </p>import "./part0/uml_base.md"---import "./part1/mat…

arm64架构kvm情景分析 - type1型和type2型虚拟机管理器

内核版本&#xff1a;linux-v5.9 架构&#xff1a;arm64 1 type1型和type2虚拟机管理器 在arm64架构中&#xff0c;共有EL3到EL0四个异常级别&#xff0c;EL3异常级别最高。通常操作系统&#xff08;如linux&#xff09;运行在EL1&#xff0c;应用程序运行在EL0&#xff0c;EL…

S32DS S32 Design Studio for S32 Platform 3.5 代码显示行号与空白符

介绍 NXP S32DS&#xff0c;全称 S32 Design Studio&#xff0c;s32 系列芯片默认使用 S32 Design Studio for S32 Platform 作为 IDE 集成开发环境&#xff0c;当前版本 S32 Design Studio for S32 Platform 3.5&#xff0c;IDE 可以简称 s32DS 使用 S32DS&#xff0c;可以认…

python操作SQLite3数据库进行增删改查

python操作SQLite3数据库进行增删改查 1、创建SQLite3数据库 可以通过Navicat图形化软件来创建: 2、创建表 利用Navicat图形化软件来创建: 存储在 SQLite 数据库中的每个值(或是由数据库引擎所操作的值)都有一个以下的存储类型: NULL. 值是空值。 INTEGER. 值是有符…

JavaWeb开发基础7个Web术语解析

7个Web术语 Website: static vs dynamic HTTP HTTP Requests GET vs POST Servlet Container Server: Web vs Application Content Type Website: static vs dynamic 网站内容包括文本、图片、音频、视频&#xff0c;通过URL来访问。网站分为静态网站和动态网站。 静态网…

LLMs之gptpdf:gptpdf的简介、安装和使用方法、案例应用之详细攻略

LLMs之gptpdf&#xff1a;gptpdf的简介、安装和使用方法、案例应用之详细攻略 目录 gptpdf的简介 1、处理流程 第一步&#xff0c;使用 PyMuPDF 库&#xff0c;对 PDF 进行解析出所有非文本区域&#xff0c;并做好标记&#xff0c;比如: 第二步&#xff0c;使用视觉大模型&…

【数智化人物展】天云数据CEO雷涛:大模型连接数据库 为数智化提供高价值数据...

雷涛 本文由天云数据CEO雷涛投递并参与由数据猿联合上海大数据联盟共同推出的《2024中国数智化转型升级先锋人物》榜单/奖项评选。 大数据产业创新服务媒体 ——聚焦数据 改变商业 这几天&#xff0c;奥特曼讲SQL数据库和大模型结合起来会产生什么样的化学变化引起行业关注。为…

13.SQL注入-宽字节

SQL注入-宽字节 含义&#xff1a; MySQL是用的PHP语言&#xff0c;然后PHP有addslashes()等函数&#xff0c;这类函数会自动过滤 ’ ‘’ null 等这些敏感字符&#xff0c;将它们转义成’ ‘’ \null&#xff1b;然后宽字节字符集比如GBK它会自动把两个字节的字符识别为一个汉…

frp技术

说明&#xff1a;frp&#xff08;https://github.com/fatedier/frp&#xff09; 是一个专注于内网穿透的高性能的反向代理应用&#xff0c;支持 TCP、UDP、HTTP、HTTPS 等多种协议&#xff0c;且支持 P2P 通信。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露…

19.JWT

1►JWT博客推荐 阮老师讲得很好了&#xff0c;网址如下&#xff1a; http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html 2►ry是怎么践行JWT的呢&#xff1f; 问题一&#xff1a;不登录的时候有token吗&#xff1f; 答&#xff1a;没有&#xff0c;所…

中标麒麟 RAC 19c 部署(Openssh免密BUG解决方案)

部署环境&#xff1a; 主机一主机二host ip192.168.80.46192.168.80.47vip 192.168.80.48192.168.80.49private ip192.168.10.10192.168.10.11storage ip192.168.20.33192.168.20.34主机名rac19c1rac19c2 需要上传的软件包&#xff1a; 一.虚拟机配置 选择中标麒麟IOS文件&am…

关于 element-ui el-cascader 数据回显问题的解决方案

前言 这两天在使用 el-cascader 控件时&#xff0c;后端日期的数据如“2023-05-06”前端需要按照“年-月-日”的形式分割成三级联动&#xff0c;因为数据库保存的是完整的日期&#xff0c;前端数据回显时需要对后端返回的数据进行处理。 问题再现 联动下拉框的数据如下&#x…

昇思25天学习打卡营第十四天|Pix2Pix实现图像转换

训练营进入第十四天&#xff0c;今天学的内容是Pix2Pix图像转换&#xff0c;记录一下学习内容&#xff1a; Pix2Pix概述 Pix2Pix是基于条件生成对抗网络&#xff08;cGAN, Condition Generative Adversarial Networks &#xff09;实现的一种深度学习图像转换模型&#xff0c…

C语言 -- 扫雷游戏

C语言 – 扫雷游戏 游戏规则&#xff1a; 给定一个棋盘&#xff0c;玩家需要排查出所有隐藏的雷&#xff0c;也就是选择出所有不是雷的地方。 玩家选择位置&#xff0c;若此处有雷&#xff0c;玩家被炸死&#xff0c;游戏结束&#xff1b; 若此处无雷&#xff0c;此处提示周围一…

【SOLID原则前端中的应用】里氏替换原则(Liskov Substitution Principle,LSP)- vue3示例

里氏替换原则&#xff08;Liskov Substitution Principle&#xff0c;LSP&#xff09;规定&#xff0c;子类对象必须能够替换父类对象&#xff0c;并且程序的行为保持不变。 在Vue 3中&#xff0c;这意味着我们在创建可替换的组件时&#xff0c;应该确保子组件能够完全替代父组…

Spring源码十五:Bean的加载

上一篇我们通过Spring源码十四&#xff1a;Spring生命周期介绍了refresh的最后两个方法&#xff0c;至此通过前面大概十篇左右的篇幅介绍完了Spring容器初始化&#xff0c;接下来&#xff0c;将进入Spring另外一个模块Bean相关的知识点。 在Spring框架中&#xff0c;Bean加载过…

动态白色小幽灵404网站源码

动态白色小幽灵404网站源码&#xff0c;页面时单页HTML源码&#xff0c;将代码放到空白的html里面&#xff0c;鼠标双击html即可查看效果&#xff0c;或者上传到服务器&#xff0c;错误页重定向这个界面即可&#xff0c;喜欢的朋友可以拿去使用 <!DOCTYPE html> <ht…

联想小新14Pro,误删了一个注册表,怎么办?

&#x1f3c6;本文收录于「Bug调优」专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&&…

uniapp报错--app.json: 在项目根目录未找到 app.json

【问题】 刚创建好的uni-app项目&#xff0c;运行微信小程序控制台报错如下&#xff1a; 【解决方案】 1. 程序根目录打开project.config.json文件 2. 配置miniprogramRoot&#xff0c;指定小程序代码的根目录 我的小程序代码编译后的工程文件目录为&#xff1a;dist/dev/mp…

Java常见面试题汇总带答案

本文分为十九个模块,分别是: Java 基础、容器、多线程、反射、对象拷贝、Java Web 、异常、网 络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、MyBatis、RabbitMQ、 Kafka、Zookeeper、MySQL、Redis、JVM 等等… JDK 和 JRE 有什么区别? JDK:Jav…