多租户分缓存处理
多租户redis缓存分租户处理
那么数据库方面已经做到了拦截,但是缓存还是没有分租户,还是通通一个文件夹里,
想实现上图效果,global文件夹里存的是公共缓存。
首先,那么就要规定一个俗称,缓存名字带有global的为公共缓存,其余的为租户缓存
首先先改造springcache的缓存管理器,这个是走springcache的,也就是说走@Cacheable那些时会走这个地方,但走了这里就不会走后面的TenantKeyPrefixHandler
public class TenantSpringCacheManager extends PlusSpringCacheManager {public TenantSpringCacheManager() {}@Overridepublic Cache getCache(String name) {/*if (CacheUtils.isCommonCache(name)) {return super.getCache(name);}*/if (StringUtils.contains(name, GlobalConstants.GLOBAL_REDIS_KEY)) {return super.getCache(name);}String tenantId = TenantHelper.getTentInfo().getTenantId();if (StringUtils.startsWith(name, tenantId)) {// 如果存在则直接返回return super.getCache(name);}return super.getCache(tenantId + ":" + name);}}
继承类代码如下
/*** Copyright (c) 2013-2021 Nikita Koksharov** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*//*** A {@link org.springframework.cache.CacheManager} implementation* backed by Redisson instance.* <p>* 修改 RedissonSpringCacheManager 源码* 重写 cacheName 处理方法 支持多参数** @author Nikita Koksharov**/
@SuppressWarnings("unchecked")
public class PlusSpringCacheManager implements CacheManager {private boolean dynamic = true;private boolean allowNullValues = true;private boolean transactionAware = true;Map<String, CacheConfig> configMap = new ConcurrentHashMap<>();ConcurrentMap<String, Cache> instanceMap = new ConcurrentHashMap<>();/*** Creates CacheManager supplied by Redisson instance*/public PlusSpringCacheManager() {}/*** Defines possibility of storing {@code null} values.* <p>* Default is <code>true</code>** @param allowNullValues stores if <code>true</code>*/public void setAllowNullValues(boolean allowNullValues) {this.allowNullValues = allowNullValues;}/*** Defines if cache aware of Spring-managed transactions.* If {@code true} put/evict operations are executed only for successful transaction in after-commit phase.* <p>* Default is <code>false</code>** @param transactionAware cache is transaction aware if <code>true</code>*/public void setTransactionAware(boolean transactionAware) {this.transactionAware = transactionAware;}/*** Defines 'fixed' cache names.* A new cache instance will not be created in dynamic for non-defined names.* <p>* `null` parameter setups dynamic mode** @param names of caches*/public void setCacheNames(Collection<String> names) {if (names != null) {for (String name : names) {getCache(name);}dynamic = false;} else {dynamic = true;}}/*** Set cache config mapped by cache name** @param config object*/public void setConfig(Map<String, ? extends CacheConfig> config) {this.configMap = (Map<String, CacheConfig>) config;}protected CacheConfig createDefaultConfig() {return new CacheConfig();}@Overridepublic Cache getCache(String name) {Cache cache = instanceMap.get(name);if (cache != null) {return cache;}if (!dynamic) {return cache;}//去缓存配置Map里查找是否有该缓存 没有就添加一个配置CacheConfig config = configMap.get(name);if (config == null) {config = createDefaultConfig();configMap.put(name, config);}// 重写 cacheName 支持多参数// 重中之重 缓存配置信息 在缓存名中配置 以#号分割 入 sys_cache#时间(毫秒)可以写成xxs的形式#最大空闲时间#最大容量String[] array = StringUtils.delimitedListToStringArray(name, "#");name = array[0];if (array.length > 1) {config.setTTL(DurationStyle.detectAndParse(array[1]).toMillis());}if (array.length > 2) {config.setMaxIdleTime(DurationStyle.detectAndParse(array[2]).toMillis());}if (array.length > 3) {config.setMaxSize(Integer.parseInt(array[3]));}if (config.getMaxIdleTime() == 0 && config.getTTL() == 0 && config.getMaxSize() == 0) {return createMap(name, config);}return createMapCache(name, config);}private Cache createMap(String name, CacheConfig config) {RMap<Object, Object> map = RedisUtils.getClient().getMap(name);Cache cache = new RedissonCache(map, allowNullValues);if (transactionAware) {cache = new TransactionAwareCacheDecorator(cache);}Cache oldCache = instanceMap.putIfAbsent(name, cache);if (oldCache != null) {cache = oldCache;}return cache;}private Cache createMapCache(String name, CacheConfig config) {RMapCache<Object, Object> map = RedisUtils.getClient().getMapCache(name);Cache cache = new RedissonCache(map, config, allowNullValues);if (transactionAware) {cache = new TransactionAwareCacheDecorator(cache);}Cache oldCache = instanceMap.putIfAbsent(name, cache);if (oldCache != null) {cache = oldCache;} else {map.setMaxSize(config.getMaxSize());}return cache;}@Overridepublic Collection<String> getCacheNames() {return Collections.unmodifiableSet(configMap.keySet());}}
这里要提一点,假如redis中删除了对应的key值,那么此时geCache方法还是能获取对象的,不过此时的map为空map
删除前获取的值是有的:

删除后获取的对象还有,不过值就没有了
改完了springcache之后需要改redis的缓存前缀处理器,这个和上面的是两个不同的地方,这边是直接拿redis的操作会走这里,使用springcache后不会再走这边,代码如下
/*** 多租户redis缓存key前缀处理** @author Lion Li*/
public class TenantKeyPrefixHandler extends KeyPrefixHandler {public TenantKeyPrefixHandler(String keyPrefix) {super(keyPrefix);}/*** 增加前缀*/@Overridepublic String map(String name) {if (StrUtil.isBlank(name)) {return null;}/*if (CacheUtils.isCommonCache(name)) {return super.map(name);}*/if (StringUtils.contains(name, GlobalConstants.GLOBAL_REDIS_KEY)) {return super.map(name);}String tenantId = TenantHelper.getTentInfo().getTenantId();if (StringUtils.startsWith(name, tenantId)) {// 如果存在则直接返回return super.map(name);}return super.map(tenantId + ":" + name);}/*** 去除前缀*/@Overridepublic String unmap(String name) {String unmap = super.unmap(name);if (StrUtil.isBlank(unmap)) {return null;}/*if (CacheUtils.isCommonCache(unmap)) {return super.unmap(name);}*/if (StringUtils.contains(name, GlobalConstants.GLOBAL_REDIS_KEY)) {return super.unmap(name);}String tenantId = TenantHelper.getTentInfo().getTenantId();if (StringUtils.startsWith(unmap, tenantId)) {// 如果存在则删除return unmap.substring((tenantId + ":").length());}return unmap;}}
继承的类
/*** redis缓存key前缀处理** @author ye* @date 2022/7/14 17:44* @since 4.3.1*/
public class KeyPrefixHandler implements NameMapper {private final String keyPrefix;public KeyPrefixHandler(String keyPrefix) {//前缀为空 则返回空前缀this.keyPrefix = StringUtils.isBlank(keyPrefix) ? "" : keyPrefix + ":";}/*** 增加前缀*/@Overridepublic String map(String name) {if (StringUtils.isBlank(name)) {return null;}if (StringUtils.isNotBlank(keyPrefix) && !name.startsWith(keyPrefix)) {return keyPrefix + name;}return name;}/*** 去除前缀*/@Overridepublic String unmap(String name) {if (StringUtils.isBlank(name)) {return null;}if (StringUtils.isNotBlank(keyPrefix) && name.startsWith(keyPrefix)) {return name.substring(keyPrefix.length());}return name;}}
然后在redis配置类中添加上述的配置
@Slf4j
@Configuration
@EnableCaching
@EnableConfigurationProperties(RedissonProperties.class)
public class RedisConfig extends CachingConfigurerSupport {@Autowiredprivate RedissonProperties redissonProperties;@Autowiredprivate ObjectMapper objectMapper;@Beanpublic RedissonAutoConfigurationCustomizer redissonCustomizer() {return config -> {TenantKeyPrefixHandler nameMapper = new TenantKeyPrefixHandler(redissonProperties.getKeyPrefix());
...}})/*** 自定义缓存管理器 整合spring-cache*/@Beanpublic CacheManager cacheManager() {return new TenantSpringCacheManager();}
}
到这里几乎是可以完成了,但是还有个关键点,就是登录后从登录域里拿租户id,那么登录域也是从redis里面拿登录信息的,所以token不能放在缓存的租户文件夹里,只能放在全局文件夹里。本项目使用的是satoken
先自定义一个satokendao层,用于指定
/*** SaToken 认证数据持久层 适配多租户** @author Lion Li*/
public class TenantSaTokenDao extends PlusSaTokenDao {@Overridepublic String get(String key) {return super.get(GlobalConstants.GLOBAL_REDIS_KEY + key);}@Overridepublic void set(String key, String value, long timeout) {super.set(GlobalConstants.GLOBAL_REDIS_KEY + key, value, timeout);}/*** 修修改指定key-value键值对 (过期时间不变)*/@Overridepublic void update(String key, String value) {long expire = getTimeout(key);// -2 = 无此键if (expire == NOT_VALUE_EXPIRE) {return;}this.set(key, value, expire);}/*** 删除Value*/@Overridepublic void delete(String key) {super.delete(GlobalConstants.GLOBAL_REDIS_KEY + key);}/*** 获取Value的剩余存活时间 (单位: 秒)*/@Overridepublic long getTimeout(String key) {return super.getTimeout(GlobalConstants.GLOBAL_REDIS_KEY + key);}/*** 修改Value的剩余存活时间 (单位: 秒)*/@Overridepublic void updateTimeout(String key, long timeout) {// 判断是否想要设置为永久if (timeout == NEVER_EXPIRE) {long expire = getTimeout(key);if (expire == NEVER_EXPIRE) {// 如果其已经被设置为永久,则不作任何处理} else {// 如果尚未被设置为永久,那么再次set一次this.set(key, this.get(key), timeout);}return;}RedisUtils.expire(GlobalConstants.GLOBAL_REDIS_KEY + key, Duration.ofSeconds(timeout));}/*** 获取Object,如无返空*/@Overridepublic Object getObject(String key) {return super.getObject(GlobalConstants.GLOBAL_REDIS_KEY + key);}/*** 写入Object,并设定存活时间 (单位: 秒)*/@Overridepublic void setObject(String key, Object object, long timeout) {super.setObject(GlobalConstants.GLOBAL_REDIS_KEY + key, object, timeout);}/*** 更新Object (过期时间不变)*/@Overridepublic void updateObject(String key, Object object) {long expire = getObjectTimeout(key);// -2 = 无此键if (expire == NOT_VALUE_EXPIRE) {return;}this.setObject(key, object, expire);}/*** 删除Object*/@Overridepublic void deleteObject(String key) {super.deleteObject(GlobalConstants.GLOBAL_REDIS_KEY + key);}/*** 获取Object的剩余存活时间 (单位: 秒)*/@Overridepublic long getObjectTimeout(String key) {return super.getObjectTimeout(GlobalConstants.GLOBAL_REDIS_KEY + key);}/*** 修改Object的剩余存活时间 (单位: 秒)*/@Overridepublic void updateObjectTimeout(String key, long timeout) {// 判断是否想要设置为永久if (timeout == NEVER_EXPIRE) {long expire = getObjectTimeout(key);if (expire == NEVER_EXPIRE) {// 如果其已经被设置为永久,则不作任何处理} else {// 如果尚未被设置为永久,那么再次set一次this.setObject(key, this.getObject(key), timeout);}return;}RedisUtils.expire(GlobalConstants.GLOBAL_REDIS_KEY + key, Duration.ofSeconds(timeout));}/*** 搜索数据*/@Overridepublic List<String> searchData(String prefix, String keyword, int start, int size, boolean sortType) {return super.searchData(GlobalConstants.GLOBAL_REDIS_KEY + prefix, keyword, start, size, sortType);}
}
然后在配置类里控制反转
/*** 自定义dao层存储*/@Beanpublic SaTokenDao saTokenDao() {
// return new PlusSaTokenDao();return new TenantSaTokenDao();}
相关文章:
多租户分缓存处理
多租户redis缓存分租户处理 那么数据库方面已经做到了拦截,但是缓存还是没有分租户,还是通通一个文件夹里, 想实现上图效果,global文件夹里存的是公共缓存。 首先,那么就要规定一个俗称,缓存名字带有globa…...
RN输入框默认设置数字键盘
<TextInput keyboardType"numeric"/> keyboardType 决定弹出何种软键盘类型,譬如numeric(纯数字键盘)。 See screenshots of all the types here. 这些值在所有平台都可用: defaultnumber-paddecimal-padnume…...
计算机网络——应用层
文章目录 **1 网络应用模型****2 域名系统DNS****3 文件传输协议FTP****4 电子邮件****4.1 电子邮件系统的组成结构****4.2 电子邮件格式与MIME****4.3 SMTP和POP3** **5 万维网WWW****5.1 HTTP** 1 网络应用模型 客户/服务器模型 C/S 服务器服务于许多来自其他称为客户机的主…...
【C++】写一个函数实现系统时间与输入时间进行比较
目录 1 代码 2 运行结果 时间比较函数: 输入为字符串2023-7-28,将字符串分解为年、月、日信息。 获取系统时间2023-7-24,然后将输入时间和系统时间进行比较,输出比较结果。 1 代码 #include <ctime> #include<iostream> #include<vector> using names…...
uniapp 微信小程序 navigationBarBackgroundColor 标题栏颜色渐变
大体思路: 第一步:“navigationStyle”:“custom” 第二步: template内 重点:给view添加ref“top” 第三步:添加渐变色样式 1、pages.json {"path" : "pages/user/user","style" : …...
ffplay播放器剖析(7)----音视频暂停模块分析
文章目录 1. 暂停触发流程2. toggle_pause3. stream_toggle_pause 1. 暂停触发流程 1.通过SDL触发事件调用toggle_pause 2.toggle_pause调用stream_toggle_pause 3.stream_toggle_pause修改暂停变量 2. toggle_pause static void toggle_pause(VideoState *is) {stream_to…...
ceph-mon运行原理分析
一、流程:ceph-deploy部署ceph-mon组建集群 1.ceph-deploy部署ceph-mon的工作流程及首次启动 1)通过命令创建ceph-mon,命令为:ceph-deploy create mon keyring def mon(args):if args.subcommand create:mon_create(args)elif…...
听GPT 讲K8s源代码--pkg(八)
k8s项目中 pkg/kubelet/envvars,pkg/kubelet/events,pkg/kubelet/eviction,pkg/kubelet/images,pkg/kubelet/kubeletconfig这些目录都是 kubelet 组件的不同功能模块所在的代码目录。 pkg/kubelet/envvars 目录中包含了与容器运行…...
差速驱动机器人的车轮里程计模型
一、说明 车轮测程法是指使用旋转编码器(即连接到车轮电机以测量旋转的传感器)的测程法(即估计运动和位置)。这是轮式机器人和自动驾驶汽车定位的有用技术。 在本文中,我们将通过探索差速驱动机器人的车轮里程计模型来深入研究车轮里...
Pytorch个人学习记录总结 09
目录 损失函数与反向传播 L1Loss MSELOSS CrossEntropyLoss 损失函数与反向传播 所需的Loss计算函数都在torch.nn的LossFunctions中,官方网址是:torch.nn — PyTorch 2.0 documentation。举例了L1Loss、MSELoss、CrossEntropyLoss。 在这些Loss函数…...
代码随想录算法训练营day51 309.最佳买卖股票时机含冷冻期 714.买卖股票的最佳时机含手续费
题目链接309.最佳买卖股票时机含冷冻期 class Solution {public int maxProfit(int[] prices) {if (prices null || prices.length < 2) {return 0;}int[][] dp new int[prices.length][2];dp[0][0] -prices[0];dp[0][1] 0;dp[1][0] Math.max(dp[0][0], dp[0][1] - pr…...
做UI设计需要具备什么平面技能呢优漫动游
想要成为一名合格的UI设计师,那么需要学会的技能是非常多的,UI设计包含的知识点也比较多,那么具体做UI设计需要具备什么技能呢?来看看下面小编的详细介绍吧。 —、软件能力 一位好的ui设计师除了需要精通Photoshop.IlustratorDW.C4D等设…...
cass--单选不累加设置
打开软件,在空白处右击--选项--选择,如下: 完成后,点击确定按钮即可。...
线程结构——链表
C中的链表是一种非常常见的数据结构,它由一系列节点组成,每个节点包含一个数据元素和一个指向下一个节点的指针。 链表结构包括单向链表、双向链表和循环链表; 1.单向链表 单向链表由一系列节点组成,每个节点包含一个数据元素和…...
freeRTOS:基于(信号量+线程)的日志系统设计
1.日志的重要性 故障排查与调试:嵌入式系统通常运行在资源有限的环境中,故障排查和调试变得尤为复杂。日志系统可以记录系统在运行过程中的各种操作、状态和事件信息,方便开发人员追踪和定位问题所在。通过分析日志,可以快速找到故…...
数据可视化(1)
使用python带的matplotlib库进行简单的绘图。使用之前先进行安装,pip install matplotlib。如果安装了Anaconda,则无需安装matplotlib。 1.简单折线图 #绘制简单图表 import matplotlib.pyplot as plt plt.plot([1,2,3,4,5]) plt.show() import matplotlib.pyp…...
Llama 2: Open Foundation and Fine-Tuned Chat Models
文章目录 TL;DRIntroduction背景本文方案 实现方式预训练预训练数据训练细节训练硬件支持预训练碳足迹 微调SFTSFT 训练细节 RLHF人类偏好数据收集奖励模型迭代式微调(RLHF)拒绝采样(Rejection Sampling)PPO多轮一致性的系统消息&…...
BTY-DNS AMA回顾:致力于创建Web3领域中的去中心化身份(DID)
传统域名系统 (DNS) 是一个分层的分散信息存储,用于将用户在网络浏览器中输入可读名称(例如www.baidu.com)解析为IP地址,来访问互联网上的计算机。传统域名系统存在一些例如过于集中化管理、效率并不高等局限性问题。而去中心化域…...
【设计模式——学习笔记】23种设计模式——装饰器模式Decorator(原理讲解+应用场景介绍+案例介绍+Java代码实现)
生活案例 咖啡厅 咖啡定制案例 在咖啡厅中,有多种不同类型的咖啡,客户在预定了咖啡之后,还可以选择添加不同的调料来调整咖啡的口味,当客户点了咖啡添加了不同的调料,咖啡的价格需要做出相应的改变。 要求ÿ…...
《golang设计模式》第一部分·创建型模式-01-单例模式(Singleton)
文章目录 1. 概述1.1 目的1.2 实现方式 2. 代码示例2.1 设计2.2 代码 1. 概述 1.1 目的 保证类只有一个实例有方法能让外部访问到该实例 1.2 实现方式 懒汉式 在第一次调用单例对象时创建该对象,这样可以避免不必要的资源浪费 饿汉式 在程序启动时就创建单例对象…...
【WiFi帧结构】
文章目录 帧结构MAC头部管理帧 帧结构 Wi-Fi的帧分为三部分组成:MAC头部frame bodyFCS,其中MAC是固定格式的,frame body是可变长度。 MAC头部有frame control,duration,address1,address2,addre…...
大型活动交通拥堵治理的视觉算法应用
大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动(如演唱会、马拉松赛事、高考中考等)期间,城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例,暖城商圈曾因观众集中离场导致周边…...
vue3 字体颜色设置的多种方式
在Vue 3中设置字体颜色可以通过多种方式实现,这取决于你是想在组件内部直接设置,还是在CSS/SCSS/LESS等样式文件中定义。以下是几种常见的方法: 1. 内联样式 你可以直接在模板中使用style绑定来设置字体颜色。 <template><div :s…...
微信小程序 - 手机震动
一、界面 <button type"primary" bindtap"shortVibrate">短震动</button> <button type"primary" bindtap"longVibrate">长震动</button> 二、js逻辑代码 注:文档 https://developers.weixin.qq…...
ETLCloud可能遇到的问题有哪些?常见坑位解析
数据集成平台ETLCloud,主要用于支持数据的抽取(Extract)、转换(Transform)和加载(Load)过程。提供了一个简洁直观的界面,以便用户可以在不同的数据源之间轻松地进行数据迁移和转换。…...
【Web 进阶篇】优雅的接口设计:统一响应、全局异常处理与参数校验
系列回顾: 在上一篇中,我们成功地为应用集成了数据库,并使用 Spring Data JPA 实现了基本的 CRUD API。我们的应用现在能“记忆”数据了!但是,如果你仔细审视那些 API,会发现它们还很“粗糙”:有…...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...
06 Deep learning神经网络编程基础 激活函数 --吴恩达
深度学习激活函数详解 一、核心作用 引入非线性:使神经网络可学习复杂模式控制输出范围:如Sigmoid将输出限制在(0,1)梯度传递:影响反向传播的稳定性二、常见类型及数学表达 Sigmoid σ ( x ) = 1 1 +...
【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)
1.获取 authorizationCode: 2.利用 authorizationCode 获取 accessToken:文档中心 3.获取手机:文档中心 4.获取昵称头像:文档中心 首先创建 request 若要获取手机号,scope必填 phone,permissions 必填 …...
Java线上CPU飙高问题排查全指南
一、引言 在Java应用的线上运行环境中,CPU飙高是一个常见且棘手的性能问题。当系统出现CPU飙高时,通常会导致应用响应缓慢,甚至服务不可用,严重影响用户体验和业务运行。因此,掌握一套科学有效的CPU飙高问题排查方法&…...
