多租户分缓存处理
多租户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 实现方式 懒汉式 在第一次调用单例对象时创建该对象,这样可以避免不必要的资源浪费 饿汉式 在程序启动时就创建单例对象…...
STM32+rt-thread判断是否联网
一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...

深入理解JavaScript设计模式之单例模式
目录 什么是单例模式为什么需要单例模式常见应用场景包括 单例模式实现透明单例模式实现不透明单例模式用代理实现单例模式javaScript中的单例模式使用命名空间使用闭包封装私有变量 惰性单例通用的惰性单例 结语 什么是单例模式 单例模式(Singleton Pattern&#…...
质量体系的重要
质量体系是为确保产品、服务或过程质量满足规定要求,由相互关联的要素构成的有机整体。其核心内容可归纳为以下五个方面: 🏛️ 一、组织架构与职责 质量体系明确组织内各部门、岗位的职责与权限,形成层级清晰的管理网络…...

CocosCreator 之 JavaScript/TypeScript和Java的相互交互
引擎版本: 3.8.1 语言: JavaScript/TypeScript、C、Java 环境:Window 参考:Java原生反射机制 您好,我是鹤九日! 回顾 在上篇文章中:CocosCreator Android项目接入UnityAds 广告SDK。 我们简单讲…...
HTML前端开发:JavaScript 常用事件详解
作为前端开发的核心,JavaScript 事件是用户与网页交互的基础。以下是常见事件的详细说明和用法示例: 1. onclick - 点击事件 当元素被单击时触发(左键点击) button.onclick function() {alert("按钮被点击了!&…...
.Net Framework 4/C# 关键字(非常用,持续更新...)
一、is 关键字 is 关键字用于检查对象是否于给定类型兼容,如果兼容将返回 true,如果不兼容则返回 false,在进行类型转换前,可以先使用 is 关键字判断对象是否与指定类型兼容,如果兼容才进行转换,这样的转换是安全的。 例如有:首先创建一个字符串对象,然后将字符串对象隐…...
laravel8+vue3.0+element-plus搭建方法
创建 laravel8 项目 composer create-project --prefer-dist laravel/laravel laravel8 8.* 安装 laravel/ui composer require laravel/ui 修改 package.json 文件 "devDependencies": {"vue/compiler-sfc": "^3.0.7","axios": …...

Python 实现 Web 静态服务器(HTTP 协议)
目录 一、在本地启动 HTTP 服务器1. Windows 下安装 node.js1)下载安装包2)配置环境变量3)安装镜像4)node.js 的常用命令 2. 安装 http-server 服务3. 使用 http-server 开启服务1)使用 http-server2)详解 …...

c++第七天 继承与派生2
这一篇文章主要内容是 派生类构造函数与析构函数 在派生类中重写基类成员 以及多继承 第一部分:派生类构造函数与析构函数 当创建一个派生类对象时,基类成员是如何初始化的? 1.当派生类对象创建的时候,基类成员的初始化顺序 …...
Linux系统部署KES
1、安装准备 1.版本说明V008R006C009B0014 V008:是version产品的大版本。 R006:是release产品特性版本。 C009:是通用版 B0014:是build开发过程中的构建版本2.硬件要求 #安全版和企业版 内存:1GB 以上 硬盘…...