这几天作的功能涉及到Redis缓存,踩了很多坑,这里记录下来。html
在SpringBoot中能够在properties
配置文件中配置spring.redis.*
相关属性,SpringBoot就会自动帮你建立相关Redis链接以及RedisTemplate相关对象。java
@Configuration @ConditionalOnClass({ JedisConnection.class, RedisOperations.class, Jedis.class }) @EnableConfigurationProperties(RedisProperties.class) public class RedisAutoConfiguration { // Redis链接的自动配置 @Configuration @ConditionalOnClass(GenericObjectPool.class) protected static class RedisConnectionConfiguration { ... } /** * RedisTemplate相关配置,SpringBoot会为咱们生成两个RedisTemplate */ @Configuration protected static class RedisConfiguration { @Bean @ConditionalOnMissingBean(name = "redisTemplate") public RedisTemplate<Object, Object> redisTemplate( RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>(); template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean(StringRedisTemplate.class) public StringRedisTemplate stringRedisTemplate( RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } } }
大多数状况下使用SpringBoot默认的配置便可。git
SpringBoot默认为咱们配置了两个RedisTemplate,其中StringRedisTemplate
继承自RedisTemplate<String,String>
。github
public class StringRedisTemplate extends RedisTemplate<String, String> { public StringRedisTemplate() { // StringRedisTemplate默认使用StringRedisSerializer进行序列化 RedisSerializer<String> stringSerializer = new StringRedisSerializer(); setKeySerializer(stringSerializer); setValueSerializer(stringSerializer); setHashKeySerializer(stringSerializer); setHashValueSerializer(stringSerializer); } public StringRedisTemplate(RedisConnectionFactory connectionFactory) { this(); setConnectionFactory(connectionFactory); afterPropertiesSet(); } protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) { return new DefaultStringRedisConnection(connection); } }
二者的区别:web
一、StringRedisTemplate使用StringRedisSerializer
进行序列化,而RedisTemplate默认使用JdkSerializationRedisSerializer
进行序列化。redis
二、StringRedisTemplate对RedisConnection
进行了一层包装。主要是由于RedisConnection的全部操做都是基于字节数组的,DefaultStringRedisConnection
会把全部的结果转成String,包装了StringRedisSerializer并对批量操做数据进行批量序列化和反序列化,具体能够参考SetConverter,ListConverter,MapConverter的实现。spring
Spring Data Redis为了适配各类Redis客户端实现,抽象了一个RedisConnection接口。json
事实上若是直接使用Jedis客户端,其实更方便,Jedis已经对String类型作了编解码处理。api
package redis.clients.jedis; public class Client extends BinaryClient implements Commands { ... public void hset(final String key, final String field, final String value) { hset(SafeEncoder.encode(key), SafeEncoder.encode(field), SafeEncoder.encode(value)); } public void hget(final String key, final String field) { hget(SafeEncoder.encode(key), SafeEncoder.encode(field)); } ... } // package redis.clients.util; public final class SafeEncoder { private SafeEncoder(){ throw new InstantiationError( "Must not instantiate this class" ); } public static byte[][] encodeMany(final String... strs) { byte[][] many = new byte[strs.length][]; for (int i = 0; i < strs.length; i++) { many[i] = encode(strs[i]); } return many; } public static byte[] encode(final String str) { try { if (str == null) { throw new JedisDataException("value sent to redis cannot be null"); } return str.getBytes(Protocol.CHARSET); } catch (UnsupportedEncodingException e) { throw new JedisException(e); } } public static String encode(final byte[] data) { try { return new String(data, Protocol.CHARSET); } catch (UnsupportedEncodingException e) { throw new JedisException(e); } } }
使用RedisTemplate很简单,由于SpringBoot已经为咱们建立了RedisTemplate和StringRedisTemplate,因此咱们直接在须要使用的Bean里面注入就行:数组
@Component public class Example { // 由于StringRedisTemplate继承自RedisTemplate<String, String> // 那么问题来了: // 这个地方注入的是StringRedisTemplate仍是普通的RedisTemplate呢 @Autowired private RedisTemplate<String, String> template; @PostConstruct public void init() { // 答案是:StringRedisTemplate System.out.println(template.getClass()); } // 只有明确声明RedisTemplate<Object, Object>才会注入普通的RedisTemplate @Autowired private RedisTemplate<Object, Object> redisTemplate; }
RedisTemplate按照Redis的命令分组为咱们提供了相应的操做视图:
@Component public class Example { @Autowired private StringRedisTemplate template; public void doSomething() { template.opsForList().leftPush("my-list", "value"); template.opsForSet().add("my-set", "member1", "member2"); ... BoundHashOperations<String, Object, Object> hashOps = template.boundHashOps("my-hash"); hashOps.put("name", "holmofy"); hashOps.put("age", "23"); hashOps.put("gender", "male"); } }
这种随用随调方式的弊端是每次调用opsForXxx()都会建立一个新的视图。
@Component public class Example { // 只能用jsr250的@Resource注解注入 @Resource(name="redisTemplate") private ListOperations<String, String> listOps; public void addLink(String userId, URL url) { listOps.leftPush(userId, url.toExternalForm()); } }
这个功能得益于PropertyEditorSupport,具体可参考该连接
由于StringRedisTemplate和RedisTemplate默认使用的序列化不同,因此在使用视图操做时要注意一些序列化方面的细节:
@Component public class Example { @Resource(name = "redisTemplate") private ValueOperations<String, Object> jdkSerializerValueOps; @Resource(name = "stringRedisTemplate") private ValueOperations<String, String> stringSerializerValueOps; @PostConstruct public void doSomething() { jdkSerializerValueOps.set("jdkNumber", 1); jdkSerializerValueOps.set("jdkString", "1"); stringSerializerValueOps.set("string", "1"); try { jdkSerializerValueOps.increment("jdkNumber"); //失败 } catch (Exception ignore) { } try { jdkSerializerValueOps.increment("jdkString"); //失败 } catch (Exception ignore) { } try { stringSerializerValueOps.increment("string"); //成功 } catch (Exception ignore) { } } }
通过不一样的序列化器保存到Redis中的内容是不同的,StringRedisTemplate直接转成字符串保存到Redis里面,但RedisTemplate默认使用JdkSerializer会将对象信息存储到Redis中。
JdkSerializer优缺点
优势:序列化存储了类型信息,因此反序列化能直接生成相应对象。
缺点:
一、Redis中存储的内容包括对象头信息,存储了过多的无用内容,浪费Redis内存。
二、Redis中的一些操做不能使用,好比自增自减。
StringRedisSerializer优缺点
优势:
一、使用方便,全部的操做都以字符串形式保存到Redis
二、占用Redis更小
缺点:全部操做只能以字符串形式执行。StringRedisTemplate的key,value等参数都必须是String类型,由于StringRedisSerializer只负责把String转换成byte[]。存储对象时,须要咱们手动序列化成字符串;相应地,取对象须要反序列化。
目前最新的SpringDataRedis 2.1.5版默认提供了6种序列化方案。
GenericJackson2JsonRedisSerializer
底层使用Jackson进行序列化并存入Redis。对于普通类型(如数值类型,字符串)能够正常反序列化回相应对象。
但若是存入对象时因为没有存入类信息,则没法反序列化。
不过GenericJackson2JsonRedisSerializer默认为咱们开启了Jackson的类型信息的存储:
public GenericJackson2JsonRedisSerializer(String classPropertyTypeName) { this(new ObjectMapper()); // 使用Jackson的类型功能嵌入反序列化所需的类型信息 // the type hint embedded for deserialization using the default typing feature. mapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(classPropertyTypeName))); if (StringUtils.hasText(classPropertyTypeName)) { mapper.enableDefaultTypingAsProperty(DefaultTyping.NON_FINAL, classPropertyTypeName); } else { mapper.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY); } }
因此当我存入一个对象时,它会把对象的类型信息也序列化存入Redis:
@Data @AllArgsConstructor @NoArgsConstructor private class Person { private String name; private int age; } //{\"@class\":\"com.example.demo.Person\",\"name\":\"Tom\",\"age\":10} jacksonSerializerValueOps.set("jsonObject", new Person("Tom", 10)); Object obj = jacksonSerializerValueOps.get("jsonObject"); System.out.println(obj.getClass()); // com.example.demo.Person
具体能够参考Jackson相关文档
Jackson2JsonRedisSerializer与GenericToStringSerializer
这两种序列化器是针对特定对象类型,前者用的是Jackson,后者用Spring的ConversionService。