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的选取。
然后创建DynamicRedisTemplate集成AbstractRoutingDataSource,实现determineCurrentLookupKey()方法从threadlocal中获取lookupkey,及 createRedisTemplateOnMissing()方法实现当找不到template时创建。
从代码中可以看出,我们目前不同租户是使用同一redis服务上的不同dbindex,但是这个方案完全可以支持不同的redis服务。
但是问题又来了,我们希望一部分缓存是租户独享,但是有些service的缓存是多租户共享的,例如共享缓存放在redis的dbindex 0中。方法是,通过spring aop类TenantAwareRedisTemplateAop在方法执行前后,将redis db index设置到threadlocal上。如何标识service是使用共享缓存,则通过创建一个类型注解@UseSharedCacheDbConfig,在类名上配置即可。不配置的则租户路由。上代码:
ThreadContext是保存用户信息及租户信息的类,这里就不分享了。
最后,如何通过@configuration来配置dynamicredistemplate就比较简单了
3. 租户文件存储的空间控制
技术方案:这个比较简单,实现方式百花齐放:)不献丑了
----------
来几句谦虚的话吧:代码写的不好,欢迎各位大神拍砖。共同进步,互通有无。
公司产品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. 租户文件存储的空间控制
技术方案:这个比较简单,实现方式百花齐放:)不献丑了
----------
来几句谦虚的话吧:代码写的不好,欢迎各位大神拍砖。共同进步,互通有无。
相关文章推荐
- spring boot 切换redis数据库
- 企业分布式微服务云SpringCloud SpringBoot mybatis (十一)Spring Boot中使用Redis数据库
- Spring Cloud Spring Boot mybatis分布式微服务云架构(二十一)使用Redis数据库(1)
- 企业分布式微服务云SpringCloud SpringBoot mybatis (十一)Spring Boot中使用Redis数据库
- SpringCloud SpringBoot mybatis 分布式微服务(十二)Spring Boot中使用Redis数据库
- Spring Cloud Spring Boot mybatis分布式微服务云架构(二十二)使用Redis数据库(2)
- SpringCloud SpringBoot mybatis 分布式微服务(十六)Spring Boot中使用Flyway来管理数据库版本
- spring boot中关于redis 保存数据的序列化(数据库中的乱码问题)
- SpringBoot整合mybatis、shiro、redis实现基于数据库的细粒度动态权限管理系统实例(转)
- spring+mybatis 多数据源切换,动态数据源增长,saas多租户模式方案
- STS创建Spring Boot项目实战(Rest接口、数据库、用户认证、分布式Token JWT、Redis操作、日志和统一异常处理)
- SpringBoot整合mybatis、shiro、redis实现基于数据库的细粒度动态权限管理系统实例
- SpringBoot整合redis哨兵主从服务
- spring boot 整合 redis,使用@Cacheable,@CacheEvict,@CachePut,jedisPool操作redis数据库
- SpringBoot对非关系型数据库NoSql的支持
- 【Spring Boot && Spring Cloud系列】在spring-data-Redis中如何使用切换库
- Spring Boot中使用Redis数据库
- springBoot微服务框架pom.xml内容(支持jsp)
- 数据库分库分表(sharding)(五) 一种支持自由规划无须数据迁移和修改路由代码的Sharding扩容方案
- STS创建Spring Boot项目实战(Rest接口、数据库、用户认证、分布式Token JWT、Redis操作、日志和统一异常处理)