多租户分缓存处理
多租户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 实现方式 懒汉式 在第一次调用单例对象时创建该对象,这样可以避免不必要的资源浪费 饿汉式 在程序启动时就创建单例对象…...
龙虎榜——20250610
上证指数放量收阴线,个股多数下跌,盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型,指数短线有调整的需求,大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的:御银股份、雄帝科技 驱动…...
国防科技大学计算机基础课程笔记02信息编码
1.机内码和国标码 国标码就是我们非常熟悉的这个GB2312,但是因为都是16进制,因此这个了16进制的数据既可以翻译成为这个机器码,也可以翻译成为这个国标码,所以这个时候很容易会出现这个歧义的情况; 因此,我们的这个国…...
vscode里如何用git
打开vs终端执行如下: 1 初始化 Git 仓库(如果尚未初始化) git init 2 添加文件到 Git 仓库 git add . 3 使用 git commit 命令来提交你的更改。确保在提交时加上一个有用的消息。 git commit -m "备注信息" 4 …...
Leetcode 3577. Count the Number of Computer Unlocking Permutations
Leetcode 3577. Count the Number of Computer Unlocking Permutations 1. 解题思路2. 代码实现 题目链接:3577. Count the Number of Computer Unlocking Permutations 1. 解题思路 这一题其实就是一个脑筋急转弯,要想要能够将所有的电脑解锁&#x…...
Mac软件卸载指南,简单易懂!
刚和Adobe分手,它却总在Library里给你写"回忆录"?卸载的Final Cut Pro像电子幽灵般阴魂不散?总是会有残留文件,别慌!这份Mac软件卸载指南,将用最硬核的方式教你"数字分手术"࿰…...
【开发技术】.Net使用FFmpeg视频特定帧上绘制内容
目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法,当前调用一个医疗行业的AI识别算法后返回…...
ip子接口配置及删除
配置永久生效的子接口,2个IP 都可以登录你这一台服务器。重启不失效。 永久的 [应用] vi /etc/sysconfig/network-scripts/ifcfg-eth0修改文件内内容 TYPE"Ethernet" BOOTPROTO"none" NAME"eth0" DEVICE"eth0" ONBOOT&q…...
【Java学习笔记】BigInteger 和 BigDecimal 类
BigInteger 和 BigDecimal 类 二者共有的常见方法 方法功能add加subtract减multiply乘divide除 注意点:传参类型必须是类对象 一、BigInteger 1. 作用:适合保存比较大的整型数 2. 使用说明 创建BigInteger对象 传入字符串 3. 代码示例 import j…...
代码随想录刷题day30
1、零钱兑换II 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。 假设每一种面额的硬币有无限个。 题目数据保证结果符合 32 位带…...
力扣热题100 k个一组反转链表题解
题目: 代码: func reverseKGroup(head *ListNode, k int) *ListNode {cur : headfor i : 0; i < k; i {if cur nil {return head}cur cur.Next}newHead : reverse(head, cur)head.Next reverseKGroup(cur, k)return newHead }func reverse(start, end *ListNode) *ListN…...
