springboot redis多租户切换统一切换key前缀

根据请求切换租户,修改redis前缀,不修改原来的代码,以最小改动实现,支持并发。

ThreadLocal 保存当前请求的数据以及租户相关信息,利用 Filter 拦截请求,判断配置当前请求的租户类型,保存设置到RequestContext中。

1.RequestContext 是当前线程文本类,存储当前线程中的一些变量以及租户信息,

TenantType是枚举类,定义租户信息,读者自行定义,这里不提供了。

public class RequestContext {

   private static final String TENANT_TYPE = "TENANT_TYPE";

   private Map values = new HashMap();
   
   private static final ThreadLocal LOCAL = new ThreadLocal() {
      @Override
      protected RequestContext initialValue() {
         return new RequestContext();
      }
   };

   public static RequestContext getContext() {
       return LOCAL.get();
   }
   
   public static void clearContext() {
       LOCAL.remove();
       LOCAL.set(new RequestContext());
   }
   
   public Object get(String key) {
        return values.get(key);
    }
   
   public void remove(String key) {
      values.remove(key);
   }
   
   public RequestContext set(String key, Object value) {
        if (value == null) {
            values.remove(key);
        } else {
            values.put(key, value);
        }
        return this;
    }

   /**
    * 设置数据源
    * @param value
    * @return
    */
   public RequestContext setDataSource(String value) {
      if (value == null) {
         values.remove(TENANT_TYPE);
      } else {
         values.put(TENANT_TYPE, value);
      }
      return this;
   }

   /**
    * 设置数据源
    * @param value
    * @return
    */
   public RequestContext setTenantType(String value) {
      if (value == null) {
         values.remove(TENANT_TYPE);
      } else {
         values.put(TENANT_TYPE, value);
      }
      return this;
   }

   /**
    * Get current DataSource
    *
    * @return data source name
    */
   public String getTenantType() {
      return (String) values.get(TENANT_TYPE);
   }

   /**
    * Get current DataSource
    *
    * @return data source name
    */
   public String getRedisPre() {
      if(StringUtils.isBlank(this.getTenantType())){
         throw new IllegalArgumentException("当前请求未找到租户类型");
      }
      return TenantType.getRedisPre(this.getTenantType());
   }

   /**
    * Get current DataSource
    *
    * @return data source name
    */
   public String getDataSource() {
      return (String) values.get(TENANT_TYPE);
   }

   /**
    * Clear current DataSource
    *
    * @return data source name
    */
   public void clearDataSource() {
      remove(TENANT_TYPE);
   }

}

 2.利用 Filter 过滤器在请求进入后,根据请求域名来判断租户(每个租户的域名请求是不同的),并且把租户保存到 RequestContext 中,供当前后续代码获取。

public class SwitchTenantFilter implements Filter {
   /** 日志 */
   private static Logger logger = LoggerFactory.getLogger(SwitchTenantFilter.class);

   @Override
   public void doFilter(ServletRequest req, ServletResponse resp,
         FilterChain filterChain) throws IOException, ServletException {
      HttpServletRequest request = (HttpServletRequest) req;
      HttpServletResponse response = (HttpServletResponse) resp;

      String host = request.getHeader("host");

      // 租户1访问
      if(host.contains("test1")){
         RequestContext.getContext().setDataSource("test1");
      }

      // 租户2访问
      if(host.contains("test2")){
         RequestContext.getContext().setDataSource("test2");
      }

      String dataSource = RequestContext.getContext().getDataSource();
      if(StringUtils.isBlank(dataSource)){
         logger.error("访问host:{},切换数据源失败!!!", host);
         filterChain.doFilter(request, response);
         return;
      }
      logger.info("访问host:{},切换数据源成功,数据源key:{}", host, RequestContext.getContext().getDataSource());
      filterChain.doFilter(request, response);
      // 清除 ThreadLocal 变量
   }
   
   @Override
   public void init(FilterConfig filterConfig) throws ServletException {
      // TODO Auto-generated method stub
      
   }

   @Override
   public void destroy() {
      // TODO Auto-generated method stub
      
   }

}

3.在Configuration类中实例化这个Filter  Bean,配置拦截

//  ===SwitchTenantFilter  切换租户,在WebAccessLogFilter之前加载===
    @Bean
    public FilterRegistrationBean switchDataSourceFilterRegistrationBean() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        SwitchTenantFilter switchTenantFilter = new SwitchTenantFilter();
        registration.setFilter(switchTenantFilter);

        registration.addUrlPatterns("/*");
        registration.setName("switchDataSourceFilter");
        registration.setOrder(12);
        registration.setDispatcherTypes(DispatcherType.REQUEST);
        return registration;
    }
// ===SwitchTenantFilter===

--------------------------------------------------------------------------------------------------------------------

以下是简单的源码跟踪,不需要的可以跳过:

springboot 使用 RedisTemplate 操作redis

redisTemplate.opsForValue().set(key, val); // 设置数据
redisTemplate.opsForValue().get(key); // 获取数据
redisTemplate.delete(key); //删除数据
等等....

 跟踪源码可以发现,RedisTemplate 在操作key和value时,会进行序列化。

AbstractOperations:

springboot redis多租户切换统一切换key前缀_第1张图片

springboot redis多租户切换统一切换key前缀_第2张图片

 这里扩展点就出来了,我们可以重写RedisTemplate  的 getKeySerializer 方法,来根据当前租户切换 keySerializer ,来达到修改前缀的目的。

-----------------------------------------------------------------------------------------------------------------------

以下是重写的RedisTemplate:

public class DynamicRedisTemplate extends RedisTemplate {

    public static final String SEPARATE=":";

    private RedisSerializer stringSerializer = RedisSerializer.string();

    @Override
    public RedisSerializer getKeySerializer() {
        RedisSerializer redisSerializer = super.getKeySerializer();
        if(StringUtils.isNotBlank(RequestContext.getContext().getRedisPre())){
            String cachePrefix = RequestContext.getContext().getRedisPre() + DynamicRedisTemplate.SEPARATE;
            redisSerializer = new KeyStringRedisSerializer(cachePrefix);
        }
        return redisSerializer;
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#delete(java.lang.Object)
     */
    @Override
    public Boolean delete(K key) {

        byte[] rawKey = rawKey(key);

        Long result = execute(connection -> connection.del(rawKey), true);

        return result != null && result.intValue() == 1;
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#delete(java.util.Collection)
     */
    @Override
    public Long delete(Collection keys) {

        if (CollectionUtils.isEmpty(keys)) {
            return 0L;
        }

        byte[][] rawKeys = rawKeys(keys);

        return execute(connection -> connection.del(rawKeys), true);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#unlink(java.lang.Object)
     */
    @Override
    public Boolean unlink(K key) {

        byte[] rawKey = rawKey(key);

        Long result = execute(connection -> connection.unlink(rawKey), true);

        return result != null && result.intValue() == 1;
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#unlink(java.util.Collection)
     */
    @Override
    public Long unlink(Collection keys) {

        if (CollectionUtils.isEmpty(keys)) {
            return 0L;
        }

        byte[][] rawKeys = rawKeys(keys);

        return execute(connection -> connection.unlink(rawKeys), true);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#countExistingKeys(java.util.Collection)
     */
    @Override
    public Long countExistingKeys(Collection keys) {

        Assert.notNull(keys, "Keys must not be null!");

        byte[][] rawKeys = rawKeys(keys);
        return execute(connection -> connection.exists(rawKeys), true);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#hasKey(java.lang.Object)
     */
    @Override
    public Boolean hasKey(K key) {

        byte[] rawKey = rawKey(key);

        return execute(connection -> connection.exists(rawKey), true);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#expire(java.lang.Object, long, java.util.concurrent.TimeUnit)
     */
    @Override
    public Boolean expire(K key, final long timeout, final TimeUnit unit) {

        byte[] rawKey = rawKey(key);
        long rawTimeout = TimeoutUtils.toMillis(timeout, unit);

        return execute(connection -> {
            try {
                return connection.pExpire(rawKey, rawTimeout);
            } catch (Exception e) {
                // Driver may not support pExpire or we may be running on Redis 2.4
                return connection.expire(rawKey, TimeoutUtils.toSeconds(timeout, unit));
            }
        }, true);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#expireAt(java.lang.Object, java.util.Date)
     */
    @Override
    public Boolean expireAt(K key, final Date date) {

        byte[] rawKey = rawKey(key);

        return execute(connection -> {
            try {
                return connection.pExpireAt(rawKey, date.getTime());
            } catch (Exception e) {
                return connection.expireAt(rawKey, date.getTime() / 1000);
            }
        }, true);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#getExpire(java.lang.Object)
     */
    @Override
    public Long getExpire(K key) {

        byte[] rawKey = rawKey(key);
        return execute(connection -> connection.ttl(rawKey), true);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#getExpire(java.lang.Object, java.util.concurrent.TimeUnit)
     */
    @Override
    public Long getExpire(K key, final TimeUnit timeUnit) {

        byte[] rawKey = rawKey(key);
        return execute(connection -> {
            try {
                return connection.pTtl(rawKey, timeUnit);
            } catch (Exception e) {
                // Driver may not support pTtl or we may be running on Redis 2.4
                return connection.ttl(rawKey, timeUnit);
            }
        }, true);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#keys(java.lang.Object)
     */
    @Override
    @SuppressWarnings("unchecked")
    public Set keys(K pattern) {

        byte[] rawKey = rawKey(pattern);
        Set rawKeys = execute(connection -> connection.keys(rawKey), true);

        return getKeySerializer() != null ? SerializationUtils.deserialize(rawKeys, getKeySerializer()) : (Set) rawKeys;
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#persist(java.lang.Object)
     */
    @Override
    public Boolean persist(K key) {

        byte[] rawKey = rawKey(key);
        return execute(connection -> connection.persist(rawKey), true);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#move(java.lang.Object, int)
     */
    @Override
    public Boolean move(K key, final int dbIndex) {

        byte[] rawKey = rawKey(key);
        return execute(connection -> connection.move(rawKey, dbIndex), true);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#randomKey()
     */
    @Override
    public K randomKey() {

        byte[] rawKey = execute(RedisKeyCommands::randomKey, true);
        return deserializeKey(rawKey);
    }

    @SuppressWarnings("unchecked")
    private K deserializeKey(byte[] value) {
        return getKeySerializer() != null ? (K) getKeySerializer().deserialize(value) : (K) value;
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#rename(java.lang.Object, java.lang.Object)
     */
    @Override
    public void rename(K oldKey, K newKey) {

        byte[] rawOldKey = rawKey(oldKey);
        byte[] rawNewKey = rawKey(newKey);

        execute(connection -> {
            connection.rename(rawOldKey, rawNewKey);
            return null;
        }, true);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#renameIfAbsent(java.lang.Object, java.lang.Object)
     */
    @Override
    public Boolean renameIfAbsent(K oldKey, K newKey) {

        byte[] rawOldKey = rawKey(oldKey);
        byte[] rawNewKey = rawKey(newKey);
        return execute(connection -> connection.renameNX(rawOldKey, rawNewKey), true);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#type(java.lang.Object)
     */
    @Override
    public DataType type(K key) {

        byte[] rawKey = rawKey(key);
        return execute(connection -> connection.type(rawKey), true);
    }

    /**
     * Executes the Redis dump command and returns the results. Redis uses a non-standard serialization mechanism and
     * includes checksum information, thus the raw bytes are returned as opposed to deserializing with valueSerializer.
     * Use the return value of dump as the value argument to restore
     *
     * @param key The key to dump
     * @return results The results of the dump operation
     */
    @Override
    public byte[] dump(K key) {

        byte[] rawKey = rawKey(key);
        return execute(connection -> connection.dump(rawKey), true);
    }

    /**
     * Executes the Redis restore command. The value passed in should be the exact serialized data returned from
     * {@link #dump(Object)}, since Redis uses a non-standard serialization mechanism.
     *
     * @param key The key to restore
     * @param value The value to restore, as returned by {@link #dump(Object)}
     * @param timeToLive An expiration for the restored key, or 0 for no expiration
     * @param unit The time unit for timeToLive
     * @param replace use {@literal true} to replace a potentially existing value instead of erroring.
     * @throws RedisSystemException if the key you are attempting to restore already exists and {@code replace} is set to
     *           {@literal false}.
     */
    @Override
    public void restore(K key, final byte[] value, long timeToLive, TimeUnit unit, boolean replace) {

        byte[] rawKey = rawKey(key);
        long rawTimeout = TimeoutUtils.toMillis(timeToLive, unit);

        execute(connection -> {
            connection.restore(rawKey, rawTimeout, value, replace);
            return null;
        }, true);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#watch(java.lang.Object)
     */
    @Override
    public void watch(K key) {

        byte[] rawKey = rawKey(key);

        execute(connection -> {
            connection.watch(rawKey);
            return null;
        }, true);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#watch(java.util.Collection)
     */
    @Override
    public void watch(Collection keys) {

        byte[][] rawKeys = rawKeys(keys);

        execute(connection -> {
            connection.watch(rawKeys);
            return null;
        }, true);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#sort(org.springframework.data.redis.core.query.SortQuery, org.springframework.data.redis.serializer.RedisSerializer)
     */
    @Override
    public  List sort(SortQuery query, @Nullable RedisSerializer resultSerializer) {

        byte[] rawKey = rawKey(query.getKey());
        SortParameters params = QueryUtils.convertQuery(query, stringSerializer);

        List vals = execute(connection -> connection.sort(rawKey, params), true);

        return SerializationUtils.deserialize(vals, resultSerializer);
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.data.redis.core.RedisOperations#sort(org.springframework.data.redis.core.query.SortQuery, java.lang.Object)
     */
    @Override
    public Long sort(SortQuery query, K storeKey) {

        byte[] rawStoreKey = rawKey(storeKey);
        byte[] rawKey = rawKey(query.getKey());
        SortParameters params = QueryUtils.convertQuery(query, stringSerializer);

        return execute(connection -> connection.sort(rawKey, params, rawStoreKey), true);
    }

    @SuppressWarnings("unchecked")
    private byte[] rawKey(Object key) {
        Assert.notNull(key, "non null key required");
        if (getKeySerializer() == null && key instanceof byte[]) {
            return (byte[]) key;
        }
        return getKeySerializer().serialize(key);
    }

    private byte[][] rawKeys(Collection keys) {
        final byte[][] rawKeys = new byte[keys.size()][];

        int i = 0;
        for (K key : keys) {
            rawKeys[i++] = rawKey(key);
        }

        return rawKeys;
    }
}

发现我们重写了 RedisTemplate里面的挺多方法没这事什么原因?

原因:这里RedisTemplate里面有个“坑”,不能只重写getKeySerializer方法。

因为调用 redisTemplate.delete 等RedisTemplate本身的方法时,获取keySerializer的方法 rawKey

是私有的,并且不是通过 getKeySerializer 方法获取的 keySerializer,而是直接使用RedisTemplate本身的变量,这就不得以把 RedisTemplate 一些操作方法都进行重写。

RedisTemplate:

springboot redis多租户切换统一切换key前缀_第3张图片

 springboot redis多租户切换统一切换key前缀_第4张图片

你可能感兴趣的:(redis,java,spring)