MyBatis-Plus多数据源dynamic-datasource解决多数据源Redis Key 重复问题


前言

在使用ynamic-datasource 因为多个租户共用一个系统,但是每个租户设置的缓存资源必须是隔离的,不然就会造成redis 缓存数据混乱的情况,如 租户1 设置key为name value 为张三的缓存,其他的租户因为缓存key 也是相同的,造成多租户缓存的数据没有做到隔离性。


一、Redis Key 解决思路:

既然redis 通过key 存储和获取缓存数据,那么只需要将key 进行处理,既可以为key追加数据源的标识即可解决。

二、Redis 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 做自定义的操作处理。

2.1 自定义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);
    }
}

2.2 增加redis key的序列化:

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 不同租户标识的追加,从而解决不同租户缓存数据的隔离。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值