您的位置:首页 > 数据库 > Redis

SpringBoot 支持 redis 多数据库或redis多服务 自由切换路由,saas多租户支持.

2018-03-22 00:00 1906 查看
摘要: 使用SpringBoot+SpringCloud实现微服务架构下的SaaS多租户实现,每个租户独享数据库,独享redis服务,及存储空间。

公司产品XXX使用SpringBoot+SpringCloud实现微服务架构下的SaaS多租户实现,每个租户的创建都会:

1. 创建一个独享的database schema

2. 使用独享redis database

3. 使用独立的文件存储空间

----------

与多租户共享单个数据库的相比,这样可以不侵犯各业务类微服务的代码及数据库表结构,租户的控制有统一的微服务进行管控。下面列举了需要解决的几个技术点及解决方案:

微服务之间的租户信息传递

技术方案:

租户信息(id, db_schema, redis_db_index)保存在并设置到threadlocal上,微服务在feign请求其他微服务时,断路器拦截器将租户信息包装到token中传递到其他微服务;微服务被feign调用时,断路器拦截器获取http header中的token,并获取到租户信息,并设置到threadlocal。threadlocal中的租户信息将用到数据库切换和redis数据库切换。

租户数据库切换

技术方案:

实现springboot的AbstractRoutingDataSource, 实现方法determineCurrentLookupKey(), 方法中获取到threadlocal中的db_schema,并根据需要创建对应的datasource。(这个网上比较多,就不重复了。关于activiti的数据库多租户切换实现不太一样,但是大致原理相同)

使用springboot的cache注解前提下,实现租户redis动态切换(此次分享的重点,网上没啥材料)

失败的技术方案:

使用一个redistemplate,通过aop在有cache注解的方法执行前,修改template的connectionfactory。这个方案有多线程问题,单线程调用没问题,多线程会出现数据乱窜,具体原因就不多废话了。

技术方案:

效仿springboot的AbstractRoutingDataSource创建一个AbstractRoutingRedisTemplate, 然后通过abstract determineCurrentLookupKey由下游决定于redistemplate的选取。

package james.wu;

import java.io.Closeable;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.BoundGeoOperations;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.BoundListOperations;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.BoundZSetOperations;
import org.springframework.data.redis.core.BulkMapper;
import org.springframework.data.redis.core.ClusterOperations;
import org.springframework.data.redis.core.GeoOperations;
import org.s
7fe0
pringframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.HyperLogLogOperations;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.core.query.SortQuery;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.core.types.RedisClientInfo;
import org.springframework.data.redis.serializer.RedisSerializer;

/**
*
* @author James Wu 2018-3-22
*
*	Abstract {@link org.springframework.data.redis.core.RedisTemplate} implementation that routes {@link #getConnectionFactory()}
* calls to one of various target RedisTemplate based on a lookup key. The latter is usually
* (but not necessarily) determined through some thread-bound transaction context.
*
*/

public abstract class AbstractRoutingRedisTemplate<K,V> extends RedisTemplate<K, V>{

private Map<Integer, RedisTemplate<K, V>> redisTemplates = new HashMap<>();

@Override
public RedisConnectionFactory getConnectionFactory() {
return this.determineTargetRedisTemplate().getConnectionFactory();
}

@Override
public <T> T execute(RedisCallback<T> action) {
return this.determineTargetRedisTemplate().execute(action);
}

@Override
public <T> T execute(SessionCallback<T> session) {
return this.determineTargetRedisTemplate().execute(session);
}

@Override
public List<Object> executePipelined(RedisCallback<?> action) {
return this.determineTargetRedisTemplate().executePipelined(action);
}

@Override
public List<Object> executePipelined(RedisCallback<?> action, RedisSerializer<?> resultSerializer) {
return this.determineTargetRedisTemplate().executePipelined(action, resultSerializer);
}

@Override
public List<Object> executePipelined(SessionCallback<?> session) {
return this.determineTargetRedisTemplate().executePipelined(session);
}

@Override
public List<Object> executePipelined(SessionCallback<?> session, RedisSerializer<?> resultSerializer) {
return this.determineTargetRedisTemplate().executePipelined(session, resultSerializer);
}

@Override
public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {
return this.determineTargetRedisTemplate().execute(script, keys, args);
}

@Override
public <T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer,
List<K> keys, Object... args) {
return this.determineTargetRedisTemplate().execute(script, argsSerializer, resultSerializer, keys, args);
}

@Override
public <T extends Closeable> T executeWithStickyConnection(RedisCallback<T> callback) {
return this.determineTargetRedisTemplate().executeWithStickyConnection(callback);
}

@Override
public Boolean hasKey(K key) {
return this.determineTargetRedisTemplate().hasKey(key);
}

@Override
public void delete(K key) {
this.determineTargetRedisTemplate().delete(key);
}

@Override
public void delete(Collection<K> keys) {
this.determineTargetRedisTemplate().delete(keys);
}

@Override
public DataType type(K key) {
return this.determineTargetRedisTemplate().type(key);
}

@Override
public Set<K> keys(K pattern) {
return this.determineTargetRedisTemplate().keys(pattern);
}

@Override
public K randomKey() {
return this.determineTargetRedisTemplate().randomKey();
}

@Override
public void rename(K oldKey, K newKey) {
this.determineTargetRedisTemplate().rename(oldKey, newKey);

}

@Override
public Boolean renameIfAbsent(K oldKey, K newKey) {
return this.determineTargetRedisTemplate().renameIfAbsent(oldKey, newKey);
}

@Override
public Boolean expire(K key, long timeout, TimeUnit unit) {
return this.determineTargetRedisTemplate().expire(key, timeout, unit);
}

@Override
public Boolean expireAt(K key, Date date) {
return this.determineTargetRedisTemplate().expireAt(key, date);
}

@Override
public Boolean persist(K key) {
return this.determineTargetRedisTemplate().persist(key);
}

@Override
public Boolean move(K key, int dbIndex) {
return this.determineTargetRedisTemplate().move(key, dbIndex);
}

@Override
public byte[] dump(K key) {
return this.determineTargetRedisTemplate().dump(key);
}

@Override
public void restore(K key, byte[] value, long timeToLive, TimeUnit unit) {
this.determineTargetRedisTemplate().restore(key, value, timeToLive, unit);
}

@Override
public Long getExpire(K key) {
return this.determineTargetRedisTemplate().getExpire(key);
}

@Override
public Long getExpire(K key, TimeUnit timeUnit) {
return this.determineTargetRedisTemplate().getExpire(key, timeUnit);
}

@Override
public List<V> sort(SortQuery<K> query) {
return this.determineTargetRedisTemplate().sort(query);
}

@Override
public <T> List<T> sort(SortQuery<K> query, RedisSerializer<T> resultSerializer) {
return this.determineTargetRedisTemplate().sort(query, resultSerializer);
}

@Override
public <T> List<T> sort(SortQuery<K> query, BulkMapper<T, V> bulkMapper) {
return this.determineTargetRedisTemplate().sort(query, bulkMapper);
}

@Override
public <T, S> List<T> sort(SortQuery<K> query, BulkMapper<T, S> bulkMapper, RedisSerializer<S> resultSerializer) {
return this.determineTargetRedisTemplate().sort(query, bulkMapper, resultSerializer);
}

@Override
public Long sort(SortQuery<K> query, K storeKey) {
return this.determineTargetRedisTemplate().sort(query, storeKey);
}

@Override
public void watch(K key) {
this.determineTargetRedisTemplate().watch(key);
}

@Override
public void watch(Collection<K> keys) {
this.determineTargetRedisTemplate().watch(keys);
}

@Override
public void unwatch() {
this.determineTargetRedisTemplate().unwatch();
}

@Override
public void multi() {
this.determineTargetRedisTemplate().multi();
}

@Override
public void discard() {
this.determineTargetRedisTemplate().discard();
}

@Override
public List<Object> exec() {
return this.determineTargetRedisTemplate().exec();
}

@Override
public List<Object> exec(RedisSerializer<?> valueSerializer) {
return this.determineTargetRedisTemplate().exec(valueSerializer);
}

@Override
public List<RedisClientInfo> getClientList() {
return this.determineTargetRedisTemplate().getClientList();
}

@Override
public void killClient(String host, int port) {
this.determineTargetRedisTemplate().killClient(host, port);
}

@Override
public void slaveOf(String host, int port) {
this.determineTargetRedisTemplate().slaveOf(host, port);
}

@Override
public void slaveOfNoOne() {
this.determineTargetRedisTemplate().slaveOfNoOne();
}

@Override
public void convertAndSend(String destination, Object message) {
this.determineTargetRedisTemplate().convertAndSend(destination, message);
}

@Override
public ValueOperations<K, V> opsForValue() {
return this.determineTargetRedisTemplate().opsForValue();
}

@Override
public BoundValueOperations<K, V> boundValueOps(K key) {
return this.determineTargetRedisTemplate().boundValueOps(key);
}

@Override
public ListOperations<K, V> opsForList() {
return this.determineTargetRedisTemplate().opsForList();
}

@Override
public BoundListOperations<K, V> boundListOps(K key) {
return this.determineTargetRedisTemplate().boundListOps(key);
}

@Override
public SetOperations<K, V> opsForSet() {
return this.determineTargetRedisTemplate().opsForSet();
}

@Override
public BoundSetOperations<K, V> boundSetOps(K key) {
return this.determineTargetRedisTemplate().boundSetOps(key);
}

@Override
public ZSetOperations<K, V> opsForZSet() {
return this.determineTargetRedisTemplate().opsForZSet();
}

@Override
public HyperLogLogOperations<K, V> opsForHyperLogLog() {
return this.determineTargetRedisTemplate().opsForHyperLogLog();
}

@Override
public BoundZSetOperations<K, V> boundZSetOps(K key) {
return this.determineTargetRedisTemplate().boundZSetOps(key);
}

@Override
public <HK, HV> HashOperations<K, HK, HV> opsForHash() {
return this.determineTargetRedisTemplate().opsForHash();
}

@Override
public <HK, HV> BoundHashOperations<K, HK, HV> boundHashOps(K key) {
return this.determineTargetRedisTemplate().boundHashOps(key);
}

@Override
public GeoOperations<K, V> opsForGeo() {
return this.determineTargetRedisTemplate().opsForGeo();
}

@Override
public BoundGeoOperations<K, V> boundGeoOps(K key) {
return this.determineTargetRedisTemplate().boundGeoOps(key);
}

@Override
public ClusterOperations<K, V> opsForCluster() {
return this.determineTargetRedisTemplate().opsForCluster();
}

@Override
public RedisSerializer<?> getKeySerializer() {
return this.determineTargetRedisTemplate().getKeySerializer();
}

@Override
public RedisSerializer<?> getValueSerializer() {
return this.determineTargetRedisTemplate().getValueSerializer();
}

@Override
public RedisSerializer<?> getHashKeySerializer() {
return this.determineTargetRedisTemplate().getHashKeySerializer();
}

@Override
public RedisSerializer<?> getHashValueSerializer() {
return this.determineTargetRedisTemplate().getHashValueSerializer();
}

protected RedisTemplate<K, V> determineTargetRedisTemplate() {
Integer lookupKey = determineCurrentLookupKey();
RedisTemplate<K, V> template = this.redisTemplates.get(lookupKey);
if (template == null) {
template = this.createRedisTemplateOnMissing(lookupKey);
this.redisTemplates.put(lookupKey, template);
}
return template;
}

protected abstract RedisTemplate<K,V> createRedisTemplateOnMissing(Integer lookupKey);

protected abstract Integer determineCurrentLookupKey();
}

然后创建DynamicRedisTemplate集成AbstractRoutingDataSource,实现determineCurrentLookupKey()方法从threadlocal中获取lookupkey,及 createRedisTemplateOnMissing()方法实现当找不到template时创建。

package james.wu;

import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import redis.clients.jedis.JedisPoolConfig;

/**
*
* @author James Wu 2018-3-22
*
*	switch Redis template based on contextLocal's dbIndex
*
*/

public class DynamicRedisTemplate<K, V> extends AbstractRoutingRedisTemplate<K, V>{
private final static Logger logger = LoggerFactory.getLogger(DynamicRedisTemplate.class);

private static final ThreadLocal<Integer> contextLocal = new ThreadLocal<Integer>();

public static void setCurrentDbIndex(Integer dbIndex){
contextLocal.set(dbIndex);
}

public static Integer getCurrentDbIndex(){
return contextLocal.get();
}

public static void remove(){
contextLocal.remove();
}

@Autowired
private Environment env;

@Override
protected Integer determineCurrentLookupKey() {
Integer dbIndex;
if(DynamicRedisTemplate.getCurrentDbIndex()==null){
dbIndex =  0;
}else{
dbIndex = DynamicRedisTemplate.getCurrentDbIndex();
}
return dbIndex;
}

@Override
protected RedisTemplate<K, V> createRedisTemplateOnMissing(Integer lookupKey) {
RedisTemplate<K, V> template = new RedisTemplate<K, V>();
template.setConnectionFactory(this.createConnectionFactory(lookupKey));
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.afterPropertiesSet();
logger.debug("【redis路由器】create template for database:"+lookupKey);
return template;
}

private RedisClusterConfiguration getClusterConfiguration() {
Map<String, Object> source = new HashMap<String, Object>();
String clusterNodes = env.getProperty("spring.redis.cluster.nodes");
source.put("spring.redis.cluster.nodes", clusterNodes);
return new RedisClusterConfiguration(new MapPropertySource("RedisClusterConfiguration", source));
}

private JedisConnectionFactory createConnectionFactory(Integer database){
JedisConnectionFactory redisConnectionFactory = null;
String clusterEnable = env.getProperty("spring.redis.cluster.enable");
if(clusterEnable !=null && clusterEnable.equals("true")){
redisConnectionFactory = new JedisConnectionFactory(getClusterConfiguration());
redisConnectionFactory.setDatabase(database);
String clusterPassword = env.getProperty("spring.redis.cluster.password");
redisConnectionFactory.setPassword(clusterPassword);
}
else{
redisConnectionFactory = new JedisConnectionFactory();
redisConnectionFactory.setDatabase(database);
String host = env.getProperty("spring.redis.host");
redisConnectionFactory.setHostName(host);
String port = env.getProperty("spring.redis.port");
redisConnectionFactory.setPort(Integer.parseInt(port));
String password = env.getProperty("spring.redis.password");
redisConnectionFactory.setPassword(password);
}

JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(8);
jedisPoolConfig.setMaxWaitMillis(1);
jedisPoolConfig.setMinIdle(0);
redisConnectionFactory.setPoolConfig(jedisPoolConfig);
redisConnectionFactory.afterPropertiesSet();
return redisConnectionFactory;
}

}

从代码中可以看出,我们目前不同租户是使用同一redis服务上的不同dbindex,但是这个方案完全可以支持不同的redis服务。

但是问题又来了,我们希望一部分缓存是租户独享,但是有些service的缓存是多租户共享的,例如共享缓存放在redis的dbindex 0中。方法是,通过spring aop类TenantAwareRedisTemplateAop在方法执行前后,将redis db index设置到threadlocal上。如何标识service是使用共享缓存,则通过创建一个类型注解@UseSharedCacheDbConfig,在类名上配置即可。不配置的则租户路由。上代码:

package james.wu;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import james.wu.ThreadContext;

/**
*
* @author James Wu
*
*	determine redis template dbindex on method and tenant
*
*/

@ConditionalOnProperty("spring.redis.host")
@Component
@Aspect
@Order(1)
public class TenantAwareRedisTemplateAop {

private final static Logger logger = LoggerFactory.getLogger(TenantAwareRedisTemplateAop.class);

@Pointcut(value="!@within(com.anchora.sre.common.conf.redis.UseSharedCacheDbConfig)"
+ " && ( @annotation(org.springframework.cache.annotation.Cacheable) "
+ " ||  @annotation(org.springframework.cache.annotation.CacheEvict) "
+ " ||  @annotation(org.springframework.cache.annotation.CachePut) )")
public void multiTenantRedisConfig() {

}

@Pointcut(value="@within(com.anchora.sre.common.conf.redis.UseSharedCacheDbConfig)"
+ " && ( @annotation(org.springframework.cache.annotation.Cacheable) "
+ " ||  @annotation(org.springframework.cache.annotation.CacheEvict) "
+ " ||  @annotation(org.springframework.cache.annotation.CachePut) )")
public void defaultRedisConfig() {

}

@After(value = "defaultRedisConfig()")
public void afterDefaultRedisCacheMethod(JoinPoint joinPoint){
DynamicRedisTemplate.remove();
}

@Before(value = "defaultRedisConfig()")
public void beforeDefaultRedisConfig(JoinPoint joinPoint){
DynamicRedisTemplate.setCurrentDbIndex(0);
logger.debug("【redis路由器】database 0;method:"+joinPoint.toString());
}

@After(value = "multiTenantRedisConfig()")
public void afterTenantAwareRedisCacheMethod(JoinPoint joinPoint){
DynamicRedisTemplate.remove();
}

@Before(value = "multiTenantRedisConfig()")
public void beforeTenantAwareRedisCacheMethod(JoinPoint joinPoint){
if(ThreadContext.getContext().getTenant()==null){
logger.error("【redis路由器】error:缓存路由无法选取对应redis database,因为线程上没有租户信息, method:"+ joinPoint.toString()+" ");
throw new RuntimeException("error:缓存路由无法选取对应redis database,因为线程上没有租户信息");
}else{
DynamicRedisTemplate.setCurrentDbIndex(ThreadContext.getContext().getTenant().getRedis_db());
logger.debug("【redis路由器】database "+ThreadContext.getContext().getTenant().getRedis_db()+";method:"+joinPoint.toString());
}
}
}

ThreadContext是保存用户信息及租户信息的类,这里就不分享了。

package james.wu;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
*
*
* 使用缓存,加入此注解,使用默认的redis db
* @author jameswu
*
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UseSharedCacheDbConfig {
String value() default "";
}

最后,如何通过@configuration来配置dynamicredistemplate就比较简单了

package james.wu;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisTemplate;

@ConditionalOnProperty("spring.redis.host")
@EnableCaching
@Configuration
public class RedisConfiguration {

private String cache_name = "";

private String expiration = "";

private long defaultExpiration = 7200;

public class Cache{
public int index;
public String name;
public long expiration;
}

@Bean
public RedisTemplate<String, String> redisTemplate() {
DynamicRedisTemplate<String, String> template = new DynamicRedisTemplate<String, String>();
return template;
}

@Bean
public CacheManager cacheManager(RedisTemplate<String, String> redisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
List<String> cacheNames=new ArrayList<String>();
Map<String,Long> cacheExpirations=new HashMap<String,Long>(cacheNames.size(),1);
String[] exps=expiration.split(",");
Cache c=new Cache();
Optional.ofNullable(cache_name)
.ifPresent(cname -> {
c.index=0;
Arrays.asList(cname.split(","))
.forEach(name -> {
if(name!=null && !name.equals("")){
cacheNames.add(name);
c.index=c.index++;
if(exps[c.index]!=null &&  !exps[c.index].equals("")){
cacheExpirations.put(name, Long.valueOf(exps[c.index]));
}
}
});
});
cacheManager.setCacheNames(cacheNames);
cacheManager.setDefaultExpiration(defaultExpiration);
cacheManager.setExpires(cacheExpirations);
return cacheManager;
}

}


3. 租户文件存储的空间控制

技术方案:这个比较简单,实现方式百花齐放:)不献丑了

----------

来几句谦虚的话吧:代码写的不好,欢迎各位大神拍砖。共同进步,互通有无。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐