公司产品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.springframework.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<?> resultSerial