多租户分缓存处理
多租户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 实现方式 懒汉式 在第一次调用单例对象时创建该对象,这样可以避免不必要的资源浪费 饿汉式 在程序启动时就创建单例对象…...
铭豹扩展坞 USB转网口 突然无法识别解决方法
当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…...
树莓派超全系列教程文档--(62)使用rpicam-app通过网络流式传输视频
使用rpicam-app通过网络流式传输视频 使用 rpicam-app 通过网络流式传输视频UDPTCPRTSPlibavGStreamerRTPlibcamerasrc GStreamer 元素 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 使用 rpicam-app 通过网络流式传输视频 本节介绍来自 rpica…...
线程同步:确保多线程程序的安全与高效!
全文目录: 开篇语前序前言第一部分:线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分:synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分ÿ…...
大数据零基础学习day1之环境准备和大数据初步理解
学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 (1)设置网关 打开VMware虚拟机,点击编辑…...
cf2117E
原题链接:https://codeforces.com/contest/2117/problem/E 题目背景: 给定两个数组a,b,可以执行多次以下操作:选择 i (1 < i < n - 1),并设置 或,也可以在执行上述操作前执行一次删除任意 和 。求…...
DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI
前一阵子在百度 AI 开发者大会上,看到基于小智 AI DIY 玩具的演示,感觉有点意思,想着自己也来试试。 如果只是想烧录现成的固件,乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外,还提供了基于网页版的 ESP LA…...
Java 加密常用的各种算法及其选择
在数字化时代,数据安全至关重要,Java 作为广泛应用的编程语言,提供了丰富的加密算法来保障数据的保密性、完整性和真实性。了解这些常用加密算法及其适用场景,有助于开发者在不同的业务需求中做出正确的选择。 一、对称加密算法…...
鱼香ros docker配置镜像报错:https://registry-1.docker.io/v2/
使用鱼香ros一件安装docker时的https://registry-1.docker.io/v2/问题 一键安装指令 wget http://fishros.com/install -O fishros && . fishros出现问题:docker pull 失败 网络不同,需要使用镜像源 按照如下步骤操作 sudo vi /etc/docker/dae…...
高防服务器能够抵御哪些网络攻击呢?
高防服务器作为一种有着高度防御能力的服务器,可以帮助网站应对分布式拒绝服务攻击,有效识别和清理一些恶意的网络流量,为用户提供安全且稳定的网络环境,那么,高防服务器一般都可以抵御哪些网络攻击呢?下面…...
Spring AI与Spring Modulith核心技术解析
Spring AI核心架构解析 Spring AI(https://spring.io/projects/spring-ai)作为Spring生态中的AI集成框架,其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似,但特别为多语…...
