分享大厂对于缓存操作的封装
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);/** 缓存操作 */@Autowiredprivate 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;}@Overridepublic <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;}@Overridepublic <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;}@Overridepublic void delete(String cacheName, String key) {redisTemplate.opsForValue().getOperations().delete(getCacheKey(cacheName, key));}@Overridepublic 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);}
}
这里就是接口的具体实现。需要注意:
- 在获得完整的缓存 key 的时候,我们其实对于缓存的 cacheName 做了验证,参见上代码块 21 行,不允许自己定义缓存的 cacheName,统一在枚举类中定义。
- 因为 tair 的资源有点不好申请,这里使用的 redis 作为缓存的工具,结合 spring-boot-starter-data-redis 作为操作的 API。
- 应对缓存穿透问题,这里使用的是Semaphore 信号量。
别的就没什么好说的,现在我们来测试一下我们的封装是否管用。
测试代码
设置缓存
测试用定义的枚举类创建缓存:
@Testvoid set() {cacheService.set(CacheEnum.USER_INFO.getCacheName(), "10001", getFakeUser("10001"));}
是没问题的,不用枚举类创建:
@Testvoid testSetSelfDefinedCacheName() {cacheService.set("user", "10001", getFakeUser("10001"));}
直接异常出来了:
java.lang.IllegalArgumentException: 需要使用CacheEnum枚举创建缓存
读取缓存
读取缓存,我的 API 中分为两种情况:直接读取,没有就算了;读取缓存,没有的话再从 DB 中拿。对应的单测如下:
@Testvoid testGet() {UserEntity user = cacheService.get(CacheEnum.USER_INFO.getCacheName(), "10001",UserEntity.class);log.info("user: {}", user);}@Testvoid testGetWithCacheLoader() {UserEntity user = cacheService.get(CacheEnum.USER_INFO.getCacheName(), "10001",UserEntity.class, new CacheLoader<UserEntity>() {@Overridepublic UserEntity load(String key) {return getFakeUser("10001");}});log.info("user: {}", user);}@Testvoid testGetWithSimpledCacheLoader() {UserEntity user = cacheService.get(CacheEnum.USER_INFO.getCacheName(), "10001",UserEntity.class, key -> getFakeUser(key));log.info("user: {}", user);}
第三种就是对于 lambda 接口的简化写法。
基于以上的方式,我们操作缓存就变得更加容易了。
相关文章:
分享大厂对于缓存操作的封装
hello,伙伴们好久不见,我是shigen。发现有两周没有更新我的文章了。也是因为最近比较忙,基本是993了。 缓存大家再熟悉不过了,几乎是现在任何系统的标配,并引申出来很多的问题:缓存穿透、缓存击穿、缓存雪崩…...
冯诺依曼体系结构与操作系统(Linux)
文章目录 前言冯诺依曼体系结构(硬件)操作系统(软件)总结 前言 冯诺依曼体系结构(硬件) 上图就是冯诺依曼体系结构图,主要包括输入设备,输出设备,存储器,运算…...
开源六轴协作机械臂myCobot280实现交互式乘法!让学习充满乐趣
本文经作者Fumitaka Kimizuka 授权我们翻译和转载。 原文链接:myCobotに「頷き」「首振り」「首傾げ」をしてもらう 🤖 - みかづきブログ・カスタム 引言 Fumitaka Kimizuka 创造了一个乘法表系统,帮助他的女儿享受学习乘法表的乐趣。她可以…...
[C++][CMake][嵌套的CMake]详细讲解
目录 0.前言 & 准备1.节点关系2.添加子目录3.解决问题1.根目录2.calc目录3.sort目录4.calc_test目录5.sort_test 4.注意 0.前言 & 准备 如果项目很大,或者项目中有很多的源码目录,在通过CMake管理项目的时候如果只使用一个CMakeLists.txt&#…...
尚品汇-(十三)
(1)查询sku列表 在ManageService 中添加 /*** SKU分页列表* param pageParam* return*/ IPage<SkuInfo> getPage(Page<SkuInfo> pageParam);接口实现类 Override public IPage<SkuInfo> getPage(Page<SkuInfo> pageParam) {Qu…...
python小练习04
三国演义词频统计与词云图绘制 import jieba import wordcloud def analysis():txt open("三国演义.txt",r,encodingutf-8).read()words jieba.lcut(txt)#精确模式counts {}for word in words:if len(word) 1:continueelif word "诸葛亮" or word &q…...
小试牛刀-Solana合约账户详解
目录 一.Solana 三.账户详解 3.1 程序账户 3.2 系统所有账户 3.3 程序派生账户(PDA) 3.4 Token账户 四、相关学习文档 五、在线编辑器 Welcome to Code Blocks blog 本篇文章主要介绍了 [Solana合约账户详解] ❤博主广交技术好友,喜欢文章的可以关注一下❤ …...
Spring Boot+Vue项目从零入手
Spring BootVue项目从零入手 一、前期准备 在搭建spring bootvue项目前,我们首先要准备好开发环境,所需相关环境和软件如下: 1、node.js 检测安装成功的方法:node -v 2、vue 检测安装成功的方法:vue -V 3、Visu…...
Vue+Xterm.js+WebSocket+JSch实现Web Shell终端
一、需求 在系统中使用Web Shell连接集群的登录节点 二、实现 前端使用Vue,WebSocket实现前后端通信,后端使用JSch ssh通讯包。 1. 前端核心代码 <template><div class"shell-container"><div id"shell"/>&l…...
用 adb 来模拟手机插上电源和拔掉电源的情形
实用的 ADB 命令 要模拟手机从 USB 充电器上拔掉的情形,你可以使用: adb shell dumpsys battery set usb 0或者,如果你使用的是 Android 6.0 或更高版本的设备,你可以使用: adb shell dumpsys battery unplug要重新…...
【SPIE独立出版】第四届智能交通系统与智慧城市国际学术会议(ITSSC 2024)
第四届智能交通系统与智慧城市国际学术会议(ITSSC 2024)将于2024年8月23-25日在中国西安举行。本次会议主要围绕智能交通、交通新能源、无人驾驶、智慧城市、智能家居、智能生活等研究领域展开讨论, 旨在为该研究领域的专家学者们提供一个分享…...
【Unity数据交互】如何Unity中读取Ecxel中的数据
👨💻个人主页:元宇宙-秩沅 👨💻 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 👨💻 本文由 秩沅 原创 👨💻 专栏交流🧧&…...
基于深度学习LightWeight的人体姿态检测跌倒系统源码
一. LightWeight概述 light weight openpose是openpose的简化版本,使用了openpose的大体流程。 Light weight openpose和openpose的区别是: a 前者使用的是Mobilenet V1(到conv5_5),后者使用的是Vgg19(前10…...
SpringBoot 生产实践:没有父 starter 的打包问题
文章目录 前言一、搜索引擎二、Chat GPT三、官方文档四、小结推荐阅读 前言 今天刚准备写点文章,需要 SpringBoot 项目来演示效果。一时心血来潮,没有采用传统的方式(即通过引入 spring-boot-starter-parent 父工程的方式)。 &l…...
IDEA配Git
目录 前言 1.创建Git仓库,获得可提交渠道 2.选择本地提交的项目名 3.配置远程仓库的地址 4.新增远程仓库地址 5.开始进行commit操作 6.push由于邮箱问题被拒绝的解决方法: 后记 前言 以下操作都是基于你已经下载了Git的前提下进行的,…...
51单片机STC89C52RC——14.1 直流电机调速
目录 目的/效果 1:电机转速同步LED呼吸灯 2 通过独立按键 控制直流电机转速。 一,STC单片机模块 二,直流电机 2.1 简介 2.2 驱动电路 2.2.1 大功率器件直接驱动 2.2.2 H桥驱动 正转 反转 2.2.3 ULN2003D 引脚、电路 2.3 PWM&…...
AI对于高考和IT行业的深远影响
目录 AI对IT行业的冲击及深远影响1. 工作自动化2. 新的就业机会3. 行业融合4. 技术升级和创新5. 数据的重要性 IT行业的冬天要持续多久?大学的软件开发类专业是否还值得报考?其他问题IT行业是否都是加班严重?35岁后就业困难是否普遍现象&…...
C语言下的文件详解
主要内容 文件概述文件指针文件的打开与关闭文件的读写 文件 把输入和输出的数据以文件的形式保存在计算机的外存储器上,可以确保数据能随时使用,避免反复输入和读取数据 文件概述 文件是指一组相关数据的有序集合 文件是存储数据的基本单位&#…...
Oracle PL / SQL块结构
在PL / SQL中,最小的有意义的代码分组被称为块。 块代码为变量声明和异常处理提供执行和作用域边界。 PL / SQL允许您创建匿名块和命名块。 命名块可以是包,过程,函数,触发器或对象类型。 PL / SQL是SQL的过程语言扩展&#x…...
MySQL的安装和启动
安装 版本 1,社区版:免费,不提供任何技术支持 2,商业版:可以试用30天,官方提供技术支持下载 1,下载地址:https://dev.mysql.com/downloads/mysql/ 2,安装:傻…...
如何快速批量下载高质量歌词:ZonyLrcToolsX跨平台终极解决方案
如何快速批量下载高质量歌词:ZonyLrcToolsX跨平台终极解决方案 【免费下载链接】ZonyLrcToolsX ZonyLrcToolsX 是一个能够方便地下载歌词的小软件。 项目地址: https://gitcode.com/gh_mirrors/zo/ZonyLrcToolsX 还在为本地音乐库缺少歌词而烦恼吗࿱…...
Postgresql基础实践教程(八)
⭐️⭐️⭐️⭐️⭐️ 完整数据详见 练习数据免费 ⭐️⭐️⭐️⭐️⭐️ 六十九、查找会员ID 27的向上推荐链 问题 查找会员ID 27的向上推荐链:即推荐该会员的人,以及推荐那个人的人,依此类推。返回会员ID、名字和姓氏。按会员ID降序排列。…...
YOLOv8晶圆体缺识别检测系统(项目源码+YOLO数据集+模型权重+UI界面+python+深度学习+环境配置)
摘要 晶圆制造过程中的缺陷检测是保证芯片良率的关键环节。本文基于YOLOv8目标检测算法,构建了一套针对晶圆表面9类典型缺陷的自动检测系统。所识别的缺陷类型包括:Center、Donut、Edge-Loc、Edge-Ring、Loc、Near-full、None、Random、Scratch。模型在…...
企业云盘签章技术方案:从数字签名原理到工程落地
背景 电子签章在企业云盘中的落地,不只是一个"上传盖章图片"的功能实现。本质上,它是一套涉及数字签名、PKI基础设施、文档完整性校验的综合性技术方案。本文从技术选型角度,说清楚企业云盘内置签章需要解决哪些问题、主流实现方案…...
Log4Shell漏洞深度解析:Spring Boot日志注入原理与四层修复方案
1. 这个漏洞不是“远程执行代码”那么简单——它是一次对Java生态信任链的系统性击穿Log4j CVE-2021-44228,业内常简称为“Log4Shell”,2021年12月爆发时,我正在给一家金融客户的Spring Boot微服务集群做灰度发布前的安全加固。凌晨三点收到告…...
DIY智能USB充电器:基于电流检测与双稳态继电器的零功耗节能方案
1. 项目概述:打造一款智能、节能的USB手机充电器作为一名电子爱好者,我经常折腾各种电源项目。市面上很多手机充电器,包括一些原装货,都存在一个通病:手机充满电后,充电器依然插在插座上,内部电…...
基于PIC32单片机实现Android USB音频转SPDIF输出的DIY方案
1. 项目概述:为Android设备打造一个高保真SPDIF音频接口作为一名长期折腾嵌入式音频和家庭影院的玩家,我经常遇到一个痛点:手头那些性能不错的Android手机或平板,其内置的3.5mm耳机孔或者USB-C口的音频输出质量,在连接…...
使用Taotoken CLI工具一键配置多开发环境下的统一模型接入点
🚀 告别海外账号与网络限制!稳定直连全球优质大模型,限时半价接入中。 👉 点击领取海量免费额度 使用Taotoken CLI工具一键配置多开发环境下的统一模型接入点 在团队协作或管理多个AI应用项目时,一个常见的痛点是每个…...
MaxEnt建模总失败?别急着换数据,先检查ArcGIS裁剪栅格这1个像素的坑
MaxEnt建模失败?ArcGIS栅格裁剪的1像素陷阱与精准修复指南当你花费数小时整理好WorldClim气候数据、本地DEM高程和物种分布数据,满心期待地点击MaxEnt的运行按钮时,屏幕上突然跳出"Error projecting, two layers have different geograp…...
每日一书㉗ | 刻意练习:为什么有些人努力一辈子还是平庸?
“本文来自「乐想屋」公众号,系列更新[每日一书],每次5分钟,帮你把书读薄,把知识用活”先问你一个问题。你身边有没有这样的人:入行时间比你短,但能力已经甩你好几条街。他们好像没有特别刻苦,但…...
