在使用ynamic-datasource 因为多个租户共用一个系统,但是每个租户设置的缓存资源必须是隔离的,不然就会造成redis 缓存数据混乱的情况,如 租户1 设置key为name value 为张三的缓存,其他的租户因为缓存key 也是相同的,造成多租户缓存的数据没有做到隔离性。
既然redis 通过key 存储和获取缓存数据,那么只需要将key 进行处理,既可以为key追加数据源的标识即可解决。
在springboot 中redis 会通过 RedisAutoConfiguration 自动装配 两个redisTemplate 和 stringRedisTemplate
,然后让我们直接在项目中使用;
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.boot.autoconfigure.data.redis;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
@AutoConfiguration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
通过代码可以知晓,这两个bean 的加载条件都是 ConditionalOnMissingBean,所以我们可以通过在项目中自己定义相同的bean 从而完成bean覆盖,在我们自定义的bean 中我们就可以对redis 的序列化key
做自定义的操作处理。
CustomerStringRedisSerializer 代码如下(示例):
import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class CustomerStringRedisSerializer implements RedisSerializer<String> {
private final Charset charset;
public static final CustomerStringRedisSerializer US_ASCII;
public static final CustomerStringRedisSerializer ISO_8859_1;
public static final CustomerStringRedisSerializer UTF_8;
public CustomerStringRedisSerializer() {
this(StandardCharsets.UTF_8);
}
public CustomerStringRedisSerializer(Charset charset) {
Assert.notNull(charset, "Charset must not be null!");
this.charset = charset;
}
public String deserialize(@Nullable byte[] bytes) {
return bytes == null ? null : new String(bytes, this.charset);
}
// 重要方法,在这里对key 的序列化完成自定义处理 ,追加数据源标识
public byte[] serialize(@Nullable String string) {
if (notRedisDbKey(string)) {
// 可以自定义过滤掉某些不需要追加 数据源标识的key
return string == null ? null : string.getBytes(this.charset);
}
try {
if (hasDbKey(string)) {
// 自定义方法,判断如果key 已经有了数据源标识则直接序列化并返回
return string == null ? null : string.getBytes(this.charset);
}
// 获取当前的数据源标识
String dbKey = DynamicDataSourceContextHolder.peek();
if (StringUtils.hasText(dbKey) && !"master".equals(dbKey)) {
// 如果感觉 数据源标识比较敏感 ,可以做一次转换处理
string = string + ":" + transtorDbKey(dbKey);
} else {
// 如果当前没有数据源 则直接追加 一个自己想定义的标识
string = string + ":master";
}
return string == null ? null : string.getBytes(this.charset);
} catch (Throwable ex) {
log.debug("get redis dbkey not dbkey to choice");
// 如果获取数据源过程出错了,这里可以直接抛出异常,或者按自己业务处理
return string.getBytes(this.charset);
}
}
private String transtorDbKey(String dbKey) {
// do some thing
return dbKey;
}
private boolean notRedisDbKey(String string) {
// OTHER KEY
return false;
}
private boolean hasDbKey(String string) {
return false;
}
public Class<?> getTargetType() {
return String.class;
}
static {
US_ASCII = new CustomerStringRedisSerializer(StandardCharsets.US_ASCII);
ISO_8859_1 = new CustomerStringRedisSerializer(StandardCharsets.ISO_8859_1);
UTF_8 = new CustomerStringRedisSerializer(StandardCharsets.UTF_8);
}
}
在定义好 redis key 的序列化之后,后面我们将 定义好的 CustomerStringRedisSerializer 填充到redisTemplate 和 stringRedisTemplate
,这样我们在对redis 进行set 和get 时,传入的key 就会在增加租户标识后在进行序列化。
我们定义的 CustomerStringRedisSerializer 是参考 redis StringRedisSerializer 序列化进行的改造:
StringRedisSerializer 代码如下(示例):
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.data.redis.serializer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
public class StringRedisSerializer implements RedisSerializer<String> {
private final Charset charset;
public static final StringRedisSerializer US_ASCII;
public static final StringRedisSerializer ISO_8859_1;
public static final StringRedisSerializer UTF_8;
public StringRedisSerializer() {
this(StandardCharsets.UTF_8);
}
public StringRedisSerializer(Charset charset) {
Assert.notNull(charset, "Charset must not be null!");
this.charset = charset;
}
public String deserialize(@Nullable byte[] bytes) {
return bytes == null ? null : new String(bytes, this.charset);
}
public byte[] serialize(@Nullable String string) {
return string == null ? null : string.getBytes(this.charset);
}
public Class<?> getTargetType() {
return String.class;
}
static {
US_ASCII = new StringRedisSerializer(StandardCharsets.US_ASCII);
ISO_8859_1 = new StringRedisSerializer(StandardCharsets.ISO_8859_1);
UTF_8 = new StringRedisSerializer(StandardCharsets.UTF_8);
}
}
RedisConfig 示例代码
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* redis配置
*
* @author ruoyi
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
serializer.setObjectMapper(mapper);
// 使用CustomerStringRedisSerializer 来序列化和反序列化redis的key值
CustomerStringRedisSerializer keySerializer = new CustomerStringRedisSerializer();
// template.setKeySerializer(new StringRedisSerializer());
template.setKeySerializer(keySerializer);
template.setValueSerializer(serializer);
// Hash的key也采用CustomerStringRedisSerializer 的序列化方式
// template.setHashKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(keySerializer);
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
// 使用CustomerStringRedisSerializer 来序列化和反序列化redis的key值
CustomerStringRedisSerializer keySerializer = new CustomerStringRedisSerializer();
template.setKeySerializer(keySerializer);
template.setHashKeySerializer(keySerializer);
template.afterPropertiesSet();
return template;
}
}
因为redis 的hash 数据内层还有一个key 所以我们同时对 key 和 hashKey 都进行下处理;
我们通过自定义 redis key 的序列化类,完成对redis key 不同租户标识的追加,从而解决不同租户缓存数据的隔离。