根据请求切换租户,修改redis前缀,不修改原来的代码,以最小改动实现,支持并发。
ThreadLocal 保存当前请求的数据以及租户相关信息,利用 Filter 拦截请求,判断配置当前请求的租户类型,保存设置到RequestContext中。
1.RequestContext 是当前线程文本类,存储当前线程中的一些变量以及租户信息,
TenantType是枚举类,定义租户信息,读者自行定义,这里不提供了。
public class RequestContext { private static final String TENANT_TYPE = "TENANT_TYPE"; private Mapvalues = 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:
这里扩展点就出来了,我们可以重写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: