spring-data-redis 自定义注解扩展实现时效设置
2017-06-13 08:19
656 查看
前言
spring目前在@Cacheable和@CacheEvict等注解上不支持缓存时效设置,只允许通过配置文件设置全局时效。这样就很不方便设定时间。比如系统参数和业务数据的时效是不一样的,这给程序开发造成很大的困扰。不得已,本人实现了类似Sping的三个注解。以下是具体实现。工程下载>>>
实现
1、注解类的实现package com.example.spring.boot.redis.annotation; import java.lang.annotation.*; /** * Author: 王俊超 * Date: 2017-06-10 05:56 * All Rights Reserved !!! */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface RedisCacheEvict { /** * 缓存名称 * * @return */ String cacheName(); /** * 缓存key * * @return */ String key(); }
package com.example.spring.boot.redis.annotation; import java.lang.annotation.*; import java.util.concurrent.TimeUnit; /** * Author: 王俊超 * Date: 2017-06-10 05:56 * All Rights Reserved !!! */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface RedisCacheGet { /** * 缓存名称 * * @return */ String cacheName(); /** * 缓存key * * @return */ String key(); /** * 缓存过期时间 * * @return */ int expire() default 0; /** * 缓存的时间单位 * * @return */ TimeUnit timeUnit(); }
package com.example.spring.boot.redis.annotation; import java.lang.annotation.*; import java.util.concurrent.TimeUnit; /** * Author: 王俊超 * Date: 2017-06-10 05:56 * All Rights Reserved !!! */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface RedisCachePut { /** * 缓存名称 * * @return */ String cacheName(); /** * 缓存key * * @return */ String key(); /** * 缓存过期时间 * * @return */ int expire() default 0; /** * 缓存的时间单位 * * @return */ TimeUnit timeUnit(); }
2、切面代码实现
package com.example.spring.boot.redis.aspect; import com.example.spring.boot.redis.annotation.RedisCacheEvict; import com.example.spring.boot.redis.annotation.RedisCacheGet; import com.example.spring.boot.redis.annotation.RedisCachePut; import com.example.spring.boot.redis.common.RedisClient; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.util.StringUtils; import java.lang.reflect.Method; /** * 缓存处理时间切面 * * Author: 王俊超 * Date: 2017-06-10 06:17 * All Rights Reserved !!! */ @Aspect public class RedisCacheAspect { private final static char DOT = '.'; private final static char SHARP = '#'; private RedisClient redisClient; public RedisClient getRedisClient() { return redisClient; } public void setRedisClient(RedisClient redisClient) { this.redisClient = redisClient; } /** * 对象入缓存 * @param pjp * @param cachePut * @return * @throws Throwable */ @Around("@annotation(cachePut)") public Object cachePut(ProceedingJoinPoint pjp, RedisCachePut cachePut) throws Throwable { Object keyObject = getCacheKey(pjp, cachePut.key()); Object result = pjp.proceed(); // 不为空才保存数据 if (result != null && keyObject != null){ redisClient.set(cachePut.cacheName(), keyObject, result, cachePut.expire()); } return result; } /** * 优先从缓存中取对象 * * @param pjp * @param cacheGet * @return * @throws Throwable */ @Around("@annotation(cacheGet)") public Object cacheGet(ProceedingJoinPoint pjp, RedisCacheGet cacheGet) throws Throwable { Object keyObject = getCacheKey(pjp, cacheGet.key()); Object result = redisClient.get(cacheGet.cacheName(), keyObject); // 如果从缓存中没有取到数据,就从调用方法获取数据 if (result == null) { result = pjp.proceed(); // 方法的返回值不是void类型,就要将结果入缓存 Class clz = getAdvicedMethod(pjp).getReturnType(); if (clz != Void.class) { redisClient.set(cacheGet.cacheName(), keyObject, result, cacheGet.expire()); } } return result; } /** * 删除缓存 * * @param pjp * @param cacheEvict * @return * @throws Throwable */ @Around("@annotation(cacheEvict)") public Object cacheEvict(ProceedingJoinPoint pjp, RedisCacheEvict cacheEvict) throws Throwable { Object keyObject = getCacheKey(pjp, cacheEvict.key()); redisClient.del(cacheEvict.cacheName(), keyObject); return pjp.proceed(); } /** * 获取缓存的key对象 * * @param pjp * @param key * @return * @throws Exception */ private Object getCacheKey(ProceedingJoinPoint pjp, String key) throws Exception { // 以#开头 if (key.length() > 0 && key.charAt(0) == SHARP) { // 去掉# key = key.substring(1); // 将key分割成属性和参数名,第一个“.”之前是参数名,之后是属性名称 int dotIdx = key.indexOf(DOT); String argName = key; if (dotIdx > 0) { argName = key.substring(0, dotIdx); key = key.substring(dotIdx + 1); // 剩下的属性 } // 取参数值 Object argVal = getArg(pjp, argName); // 获取参数的属性值 Object objectKey = argVal; if (dotIdx > 0) { objectKey = getObjectKey(argVal, key); } return objectKey; } else { // 不是以#开头的就以其值作为参数key return key; } } /** * 获取参数对象 * * @param pjp 连接点 * @param parameterName 参数名称 * @return */ private Object getArg(ProceedingJoinPoint pjp, String parameterName) throws NoSuchMethodException { Method method = getAdvicedMethod(pjp); ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); String[] parameterNames = parameterNameDiscoverer.getParameterNames(method); if (parameterNames != null) { int idx = 0; for (String name : parameterNames) { if (name.equals(parameterName)) { return pjp.getArgs()[idx]; } idx++; } } throw new IllegalArgumentException("no such parameter name: [" + parameterName + "] in method: " + method); } /** * 获取拦截的方法 * * @param pjp 连接点 * @return * @throws NoSuchMethodException */ private Method getAdvicedMethod(ProceedingJoinPoint pjp) throws NoSuchMethodException { Signature sig = pjp.getSignature(); MethodSignature msig = null; if (!(sig instanceof MethodSignature)) { throw new IllegalArgumentException("annotation can only use to method."); } msig = (MethodSignature) sig; Object target = pjp.getTarget(); return target.getClass().getMethod(msig.getName(), msig.getParameterTypes()); } /** * 获取从object上获取key所对应的属性对象 * 例如:key: country.province.city.town * 就相当于调用:object.getCountry().getProvince().getCity.getTown() * * @param object * @param key * @return * @throws Exception */ private Object getObjectKey(Object object, String key) throws Exception { // 如果key已经是空了就直接返回 if (StringUtils.isEmpty(key)) { return object; } int dotIdx = key.indexOf(DOT); // 形如key=aa.bb**的情况 if (dotIdx > 0) { // 取第一个属性值 String propertyName = key.substring(0, dotIdx); // 取剩下的key key = key.substring(dotIdx + 1); Object property = getProperty(object, getterMethod(propertyName)); return getObjectKey(property, key); } else { // key=aa return getProperty(object, getterMethod(key)); } } /** * 获取name的getter方法名称 * * @param name * @return */ private String getterMethod(String name) { return "get" + Character.toUpperCase(name.charAt(0)) + name.substring(1); } /** * 调用obj对象上的getterMethodName * * @param obj * @param getterMethodName * @return * @throws Exception */ private Object getProperty(Object obj, String getterMethodName) throws Exception { return obj.getClass().getMethod(getterMethodName).invoke(obj); } }
3、工具类实现
package com.example.spring.boot.redis.common; import com.esotericsoftware.kryo.Kryo; import com.esotericsoftware.kryo.io.Input; import com.esotericsoftware.kryo.io.Output; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; /** * 序列化和反序列化工具 * * Author: 王俊超 * Date: 2017-05-31 21:43 * All Rights Reserved !!! */ public class KryoRedisSerializer<T> implements RedisSerializer<T> { private Kryo kryo = new Kryo(); @Override public byte[] serialize(T t) throws SerializationException { byte[] buffer = new byte[2048]; Output output = new Output(buffer); kryo.writeClassAndObject(output, t); byte[] result = output.toBytes(); output.close(); return result; } @Override public T deserialize(byte[] bytes) throws SerializationException { Input input = new Input(bytes); @SuppressWarnings("unchecked") T t = (T) kryo.readClassAndObject(input); input.close(); return t; } }
package com.example.spring.boot.redis.common; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; import java.util.Set; /** * Redis客户端实现工具 * * Author: 王俊超 * Date: 2017-06-04 19:57 * All Rights Reserved !!! */ public class RedisClientImpl implements RedisClient { private RedisTemplate<Object, Object> redisTemplate; private RedisSerializer<Object> keySerializer; private RedisSerializer<Object> valSerializer; public RedisTemplate<Object, Object> getRedisTemplate() { return redisTemplate; } public void setRedisTemplate(RedisTemplate<Object, Object> redisTemplate) { this.redisTemplate = redisTemplate; } public RedisSerializer<Object> getKeySerializer() { return keySerializer; } public void setKeySerializer(RedisSerializer<Object> keySerializer) { this.keySerializer = keySerializer; } public RedisSerializer<Object> getValSerializer() { return valSerializer; } public void setValSerializer(RedisSerializer<Object> valSerializer) { this.valSerializer = valSerializer; } ///////////////////////////////////// /** * 获取最终的key * * @param cacheName * @param key * @return */ private byte[] getRealKey(Object cacheName, Object key) { byte[] b1 = keySerializer.serialize(cacheName); byte[] b2 = keySerializer.serialize(key); byte[] result = new byte[b1.length + b2.length]; System.arraycopy(b1, 0, result, 0, b1.length); System.arraycopy(b2, 0, result, b1.length, b2.length); return result; } /** * 获取真实key * @param cacheName * @param key * @return */ private byte[] getRealKey(byte[] cacheName, Object key) { byte[] b2 = keySerializer.serialize(key); byte[] result = new byte[cacheName.length + b2.length]; System.arraycopy(cacheName, 0, result, 0, cacheName.length); System.arraycopy(b2, 0, result, cacheName.length, b2.length); return result; } @Override public long del(Object cacheName, Object... keys) { return redisTemplate.execute(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection connection) throws DataAccessException { byte[] b1 = keySerializer.serialize(cacheName); long result = 0; for (Object o : keys) { result += connection.del(getRealKey(b1, o)); } return result; } }); } @Override public void set(byte[] key, byte[] value, long liveTime) { redisTemplate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { connection.set(key, value); if (liveTime > 0) { connection.expire(key, liveTime); } return true; } }); } @Override public void set(Object cacheName, Object key, Object value, long liveTime) { this.set(getRealKey(cacheName, key), valSerializer.serialize(value), liveTime); } @Override public void set(Object cacheName, Object key, Object value) { this.set(cacheName, key, value, 0L); } @Override public void set(byte[] key, byte[] value) { this.set(key, value, 0L); } /** * 获取redis value (String) * * @param key * @return */ @Override public <T> T get(byte[] key) { return redisTemplate.execute(new RedisCallback<T>() { @Override public T doInRedis(RedisConnection connection) throws DataAccessException { byte[] result = connection.get(key); Object obj = null; if (result != null) { obj = valSerializer.deserialize(result); } return (T) obj; } }); } @Override public <T> T get(Object cacheName, Object key) { return this.get(getRealKey(cacheName, key)); } @Override public Set keys(byte[] pattern) { return redisTemplate.execute(new RedisCallback<Set>() { @Override public Set doInRedis(RedisConnection connection) throws DataAccessException { return connection.keys(pattern); } }); } @Override public Set keys(String pattern) { return this.keys(keySerializer.serialize(pattern)); } @Override public boolean exists(byte[] key) { return redisTemplate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { return connection.exists(key); } }); } @Override public boolean exists(Object cacheName, Object key) { return this.exists(getRealKey(cacheName, key)); } @Override public boolean flushDb() { return redisTemplate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { connection.flushDb(); return true; } }); } @Override public long dbSize() { return redisTemplate.execute(new RedisCallback<Long>() { public Long doInRedis(RedisConnection connection) throws DataAccessException { return connection.dbSize(); } }); } @Override public String ping() { return redisTemplate.execute(new RedisCallback<String>() { public String doInRedis(RedisConnection connection) throws DataAccessException { return connection.ping(); } }); } }
4、应用配置
package com.example.spring.boot.redis; import com.example.spring.boot.redis.aspect.RedisCacheAspect; import com.example.spring.boot.redis.common.KryoRedisSerializer; import com.example.spring.boot.redis.common.RedisClient; import com.example.spring.boot.redis.common.RedisClientImpl; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.data.redis.connection.RedisClusterConfiguration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import java.net.UnknownHostException; import java.util.Arrays; import java.util.List; /** * Author: 王俊超 * Date: 2017-05-07 10:02 * All Rights Reserved !!! */ @Configuration public class AppConfig { /** * Redis连接工厂 * * @return */ @Primary @Bean("redisConnectionFactory") public RedisConnectionFactory redisConnectionFactory() { // Redis集群地址 List<String> clusterNodes = Arrays.asList("192.168.241.150:7110", "192.168.241.150:7111", "192.168.241.150:7112", "192.168.241.150:7113", "192.168.241.150:7114", "192.168.241.150:7115", "192.168.241.150:7116", "192.168.241.150:7117", "192.168.241.150:7118", "192.168.241.150:7119" ); // 获取Redis集群配置信息 RedisClusterConfiguration rcf = new RedisClusterConfiguration(clusterNodes); return new JedisConnectionFactory(rcf); } /** * 创建redis模板 * * @param factory * @return * @throws UnknownHostException */ @Primary @Bean("redisTemplate") public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) throws UnknownHostException { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 可根据需要设置 // // redis value使用的序列化器 // template.setValueSerializer(new KryoRedisSerializer<>()); // template.setHashKeySerializer(new KryoRedisSerializer<>()); // // redis key使用的序列化器 // template.setKeySerializer(new KryoRedisSerializer<>()); // template.setHashValueSerializer(new KryoRedisSerializer<>()); template.afterPropertiesSet(); return template; } /** * 返回redis客户端 * * @param redisTemplate * @return */ @Bean public RedisClient redisClient(RedisTemplate<Object, Object> redisTemplate) { RedisClientImpl redisClient = new RedisClientImpl(); redisClient.setRedisTemplate(redisTemplate); KryoRedisSerializer<Object> serializer = new KryoRedisSerializer<>(); redisClient.setKeySerializer(serializer); redisClient.setValSerializer(serializer); return redisClient; } /** * redis缓存的切面 * @param redisClient * @return */ @Bean public RedisCacheAspect redisCacheAspect(RedisClient redisClient) { RedisCacheAspect aspect = new RedisCacheAspect(); aspect.setRedisClient(redisClient); return aspect; } }
5、应用的启动
package com.example.spring.boot.redis; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.transaction.annotation.EnableTransactionManagement; /** * Author: 王俊超 * Date: 2017-05-07 10:04 * All Rights Reserved !!! */ @SpringBootApplication @EnableTransactionManagement @EnableCaching @EnableAutoConfiguration //@EnableAspectJAutoProxy public class AppRunner { public static void main(String[] args) { ConfigurableApplicationContext ctx = SpringApplication.run(AppRunner.class, args); } }
6、配置文件
server.context-path=/redis/cache # 开启AOP spring.aop.auto=true
测试
1、测试数据package com.example.spring.boot.redis.entity; /** * 县 * Author: 王俊超 * Date: 2017-06-12 20:06 * All Rights Reserved !!! */ public class City { private long id; private String name; private Province province; public City() { } public City(long id, String name, Province province) { this.id = id; this.name = name; this.province = province; } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Province getProvince() { return province; } public void setProvince(Province province) { this.province = province; } }
package com.example.spring.boot.redis.entity; /** * 国家 * Author: 王俊超 * Date: 2017-06-12 20:06 * All Rights Reserved !!! */ public class Country { private long id; private String name; public Country() { } public Country(long id, String name) { this.id = id; this.name = name; } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
package com.example.spring.boot.redis.entity; /** * 省 * Author: 王俊超 * Date: 2017-06-12 20:06 * All Rights Reserved !!! */ public class Province { private long id; private String name; private Country country; public Province() { } public Province(long id, String name, Country country) { this.id = id; this.name = name; this.country = country; } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Country getCountry() { return country; } public void setCountry(Country country) { this.country = country; } }
package com.example.spring.boot.redis.entity; /** * 镇 * Author: 王俊超 * Date: 2017-06-12 20:06 * All Rights Reserved !!! */ public class Town { private long id; private String name; private City city; public Town() { } public Town(long id, String name, City city) { this.id = id; this.name = name; this.city = city; } public long getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public City getCity() { return city; } public void setCity(City city) { this.city = city; } }
package com.example.spring.boot.redis; import com.example.spring.boot.redis.annotation.RedisCacheEvict; import com.example.spring.boot.redis.annotation.RedisCacheGet; import com.example.spring.boot.redis.annotation.RedisCachePut; import com.example.spring.boot.redis.entity.City; import com.example.spring.boot.redis.entity.Country; import com.example.spring.boot.redis.entity.Province; import com.example.spring.boot.redis.entity.Town; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; /** * Author: 王俊超 * Date: 2017-06-10 06:26 * All Rights Reserved !!! */ @Component public class TestData { public final static Country COUNTRY = new Country(111111, "CHINA"); public final static Province PROVINCE = new Province(222222, "GuangZhou", COUNTRY); public final static City CITY = new City(333333, "ShenZhen", PROVINCE); public final static Town TOWN = new Town(444444, "Where", CITY); public final static String LOCATION = "location"; @RedisCachePut(cacheName = LOCATION, key = "#town.city.province.country.id", expire = 60, timeUnit = TimeUnit.SECONDS) public Country createCountry(Town town) { return town.getCity().getProvince().getCountry(); } @RedisCacheGet(cacheName = LOCATION, key = "#id", expire = 60, timeUnit = TimeUnit.SECONDS) public Country getCountry(long id) { return COUNTRY; } @RedisCacheEvict(cacheName = LOCATION, key = "#id") public void deleteCountry(long id) { // 清除缓存 } }
2、测试用例
import com.example.spring.boot.redis.AppRunner; import com.example.spring.boot.redis.TestData; import com.example.spring.boot.redis.common.KryoRedisSerializer; import com.example.spring.boot.redis.common.RedisClient; import com.example.spring.boot.redis.entity.Country; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; /** * Author: 王俊超 * Date: 2017-06-12 21:10 * All Rights Reserved !!! */ @RunWith(SpringRunner.class) @SpringBootTest( classes = AppRunner.class) public class TestRunner { @Autowired RedisClient redisClient; @Autowired TestData testData; /** * 测试序列化,反序列化 */ @Test public void testSerialize() { Country c1 = testData.createCountry(TestData.TOWN); KryoRedisSerializer<Object> serializer = new KryoRedisSerializer<>(); byte[] b1 = serializer.serialize(c1); Country c2 = (Country) serializer.deserialize(b1); Assert.assertNotNull(c2); Assert.assertEquals(c1.getId(), c2.getId()); Assert.assertEquals(c1.getName(), c2.getName()); } @Test public void testCachePut() { // 先清理缓存 redisClient.del(TestData.LOCATION, TestData.COUNTRY.getId()); Country c = redisClient.get(TestData.LOCATION, TestData.COUNTRY.getId()); Assert.assertNull(c); // 创建一个国家 Country c1 = testData.createCountry(TestData.TOWN); // 直接从缓存中取数据,说明数据已经入缓存 Country c2 = redisClient.get(TestData.LOCATION, TestData.COUNTRY.getId()); Assert.assertNotNull(c2); Assert.assertEquals(c1.getId(), c2.getId()); Assert.assertEquals(c1.getName(), c2.getName()); } @Test public void testCacheGet() { // 先清理缓存 redisClient.del(TestData.LOCATION, TestData.COUNTRY.getId()); Country c = redisClient.get(TestData.LOCATION, TestData.COUNTRY.getId()); Assert.assertNull(c); // 取数据 Country c1 = testData.getCountry(TestData.COUNTRY.getId()); // 从缓存中取 Country c2 = redisClient.get(TestData.LOCATION, TestData.COUNTRY.getId()); Assert.assertNotNull(c2); Assert.assertEquals(c1.getId(), c2.getId()); Assert.assertEquals(c1.getName(), c2.getName()); } @Test public void testCacheEvict() { // 创建一个国家 Country c = testData.createCountry(TestData.TOWN); redisClient.del(TestData.LOCATION, TestData.COUNTRY.getId()); c = redisClient.get(TestData.LOCATION, TestData.COUNTRY.getId()); Assert.assertNull(c); } }
相关文章推荐
- [SpringMVC+redis]自定义aop注解实现控制器访问次数限制
- 使用Spring AOP结合自定义Java注解实现动态数据源设置
- Spring Cache+Redis实现自定义注解缓存
- Spring-Data-Redis-Repository中以自定义class作为id的实现
- 深入理解Spring Redis的使用 (六)、用Spring Aop 实现注解Dao层的自动Spring Redis缓存
- spring-data-redis使用自定义序列化数据 使用 protobuf
- redis实现 spring-redis-data初学习
- SpringAOP拦截Controller,Service实现日志管理(自定义注解的方式)
- springdata redis实现的简单demo
- redis实现spring-redis-data的入门实例
- 利用redis(spring-data-redis)锁的功能来实现定时器的分布式
- java SpringAOP拦截Controller,Service实现日志管理(自定义注解的方式)
- SpringAOP拦截Controller,Service实现日志管理(自定义注解的方式)
- redis实现 spring-redis-data初学习二 进阶,存取对象
- java SpringAOP拦截Controller,Service实现日志管理(自定义注解的方式)
- 通过Spring实现对自定义注解属性进行资源注入
- Spring AOP + 自定义注解实现Session的验证
- redis实现 spring-redis-data初学习二 进阶,存取对象
- 使用Spring Data +Redis实现缓存
- spring data redis 集群(sentinel实现)和simple spring memcached分布式初使用