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

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);}
}

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

  1. 在获得完整的缓存 key 的时候,我们其实对于缓存的 cacheName 做了验证,参见上代码块 21 行,不允许自己定义缓存的 cacheName,统一在枚举类中定义。
  2. 因为 tair 的资源有点不好申请,这里使用的 redis 作为缓存的工具,结合 spring-boot-starter-data-redis 作为操作的 API。
  3. 应对缓存穿透问题,这里使用的是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 接口的简化写法。
基于以上的方式,我们操作缓存就变得更加容易了。

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

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

相关文章

C#——Array类详情

Array类 Array 类是 C# 中所有数组的基类&#xff0c;其中提供了一系列用来处理数组的操作&#xff0c;例如对数组元素进行排序、搜索数组中指定的元素等。 Array 类中的属性 Array 类中提供了一系列属性&#xff0c;通过这些属性可以获取数组的各种信息。 属性 描述 IsF…

深入理解Spring Boot中的容器与依赖注入

深入理解Spring Boot中的容器与依赖注入 大家好&#xff0c;我是微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 在现代Java应用程序开发中&#xff0c;Spring Boot已经成为了一个非常流行的框架&#xff0c;它通过其强大的…

将缓冲文件写到磁盘中的命令sync

将缓冲文件写到磁盘中的命令sync There is no nutrition in the blog content. After reading it, you will not only suffer from malnutrition, but also impotence. The blog content is all parallel goods. Those who are worried about being cheated should leave quick…

C语言调试技巧

叠甲&#xff1a;以下文章主要是依靠我的实际编码学习中总结出来的经验之谈&#xff0c;求逻辑自洽&#xff0c;不能百分百保证正确&#xff0c;有错误、未定义、不合适的内容请尽情指出&#xff01; 文章目录 1.bug 是什么2.debug 是什么3.两种模式的区别4.IDE 调试介绍4.1.模…

RedHat运维-LinuxSELinux基础3-用布尔值调整SELinux政策

1. 获取所有的布尔值SELinux政策的命令是_____________________________&#xff1b; 2. 获取所有的布尔值SELinux政策的命令是_____________________________&#xff1b; 3. 获取所有的布尔值SELinux政策的命令是_____________________________&#xff1b; 4. 获取httpd_ena…

嵌入式c语言5——位运算符

<<与>>是c语言中两个移位运算符&#xff0c;分别有乘以2与除以2的意义 位运算符还包括&#xff0c;与&&#xff0c;或|&#xff0c;均进行按位操作 同时&#xff0c;还可以进行取反以及异或操作

synchronized和ReentrantLock

ReentrantLock ReentrantLock fairLock new ReentrantLock(true);// 这里是演示创建公平锁&#xff0c;一般情况不需要。 fairLock.lock(); try {// do something } finally {fairLock.unlock(); }

商家店铺电商小程序模板源码

橙色通用的商家入驻&#xff0c;商户商家&#xff0c;商家店铺&#xff0c;购物商城&#xff0c;商家购物平台app小程序网页模板。包含&#xff1a;商家主页、优先商家、商品详情、购物车、结算订单、个人中心、优惠券、会员卡、地址管理等功能页面。 商家店铺电商小程序模板源…

Spark SQL----内置函数Csv Functions Misc Functions

Spark SQL----内置函数Csv Functions& Misc Functions Csv Functions 例子&#xff1a; -- from_csv SELECT from_csv(1, 0.8, a INT, b DOUBLE); ---------------- |from_csv(1, 0.8)| ---------------- | {1, 0.8}| ----------------SELECT from_csv(26/08/2015,…

《妃梦千年》第二十六章:风暴前夕

第二十六章&#xff1a;风暴前夕 经过一系列的斗争&#xff0c;宫中的局势暂时得到了稳定。然而&#xff0c;林清婉知道&#xff0c;这一切只是表面的平静&#xff0c;敌人不会轻易放弃&#xff0c;他们必定会再次发动进攻。为了迎接即将到来的挑战&#xff0c;林清婉决定进一…

基于CentOS Stream 9平台搭建RabbitMQ3.13.4以及开机自启

1. erlang与RabbitMQ对应版本参考&#xff1a;https://www.rabbitmq.com/which-erlang.html 2. 安装erlang 官网&#xff1a;https://www.erlang.org/downloads GitHub: https://github.com/rabbitmq/erlang-rpm/releases 2.1 安装依赖&#xff1a; yum -y install gcc glib…

Nginx实战:nginx性能压测(ab)

在nginx的生产实践中,不管是服务上线,还是性能优化,都会遇到需要对nginx的性能压测,本文介绍一个简单的压测工具:ab命令 ab(Apache Bench)是一个常用的HTTP压力测试工具,可以用来测试Nginx的性能和压力。ab命令可以指定并发请求数、请求数、请求类型等参数,并输出测试…

显示近期用户登录情况的命令last

显示近期用户登录情况的命令last There is no nutrition in the blog content. After reading it, you will not only suffer from malnutrition, but also impotence. The blog content is all parallel goods. Those who are worried about being cheated should leave quick…

Spring Boot中集成DJL运行Python PyTorch模型:MNIST实战

Java Spring Boot 使用DJL 部署python训练的PyTorch模型&#xff08;MNIST&#xff09; Python 训练Pytorch模型Java Spring Boot使用DJL调用模型 pom.xml TranslatorCriteriaNDArray 创建NDArray(Tensor) 变更数据类型运算切片赋值翻转 PredictorMNIST实战 环境 Java 使用 …

算法力扣刷题总结篇 ——【四】

前言 栈和队列篇题目学习结束。 总结&#xff1a; 一、结构基础 stack类 &#xff08;1&#xff09;使用栈&#xff08;后入先出&#xff09;的结构时&#xff0c;用它。 &#xff08;2&#xff09;容器适配器&#xff08;container adaptor&#xff09;。本身不算容器&…

550kg级大载重长航时无人机直升机技术详解

550kg级大载重长航时无人机直升机&#xff0c;作为一种高性能的无人机系统&#xff0c;具备了多项先进的技术特点&#xff0c;以满足高海拔、高寒等复杂环境下的应用需求。这些无人机直升机通常具备高载重、长航时、强适应性、高可靠性和良好的任务拓展性。 设备由无人直升机平…

【面试八股文】java基础知识

引言 本文是java面试时的一些常见知识点总结归纳和一些拓展&#xff0c;笔者在学习这些内容时&#xff0c;特地整理记录下来&#xff0c;以供大家学习共勉。 一、数据类型 1.1 为什么要设计封装类&#xff0c;Integer和int区别是什么&#xff1f; 使用封装类的目的 对象化:…

数据结构——单链表专题

前言 本篇我们继续来介绍数据结构的知识——链表&#xff0c;这与顺序表是一个类别的知识&#xff0c;同样也非常重要&#xff0c;所以大家需要掌握好链表的内容&#xff0c;这对我们后面数据结构的学习也很重要&#xff0c;如果你对本篇的内容感兴趣&#xff0c;希望大佬可以…

使用RabbitMQ实现可靠的消息传递机制

使用RabbitMQ实现可靠的消息传递机制 大家好&#xff0c;我是微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01; 1. RabbitMQ简介 RabbitMQ是一个开源的消息代理软件&#xff0c;实现了高级消息队列协议&#xff08;AMQP&…

四大常见的排序算法JAVA

1. 冒泡排序 相邻的元素两两比较&#xff0c;大的放右边&#xff0c;小的放左边 第一轮比较完毕之后&#xff0c;最大值就已经确定&#xff0c;第二轮可以少循环一次&#xff0c;后面以此类推 如果数组中有n个数据&#xff0c;总共我们只要执行n-1轮的代码就可以 package Bu…