Redis缓存踩坑记
前言
这个其实应该属于分布式改造剧集中的一集(第一集见前面博客:http://www.cnblogs.com/Kidezyq/p/8748961.html),本来按照顺序来的话,不会这么快发布这篇博客。但是,因为这个坑让我浪费太多时间。这个情形和一年前我在另一个项目中试图优化mybatis时简直完全一致,即使拿出了源码来debug还是解决不了这个问题,网上搜索的方法全部尝试了一遍还是不行。足足浪费了两三天的时间,说想吐血一点都不为过...... 鉴于再次被坑的这么惨,这里先拿出来和大家说道说道,也算是对自己这几天努力的总结。
爱情来的太快就像龙卷风
为什么会用redis做缓存呢?刚开始我的分布式改造方案只是改进了Ehcache,增加了不同节点之间的同步特性。结果呢,在评审的时候,大家一致决定要引入Redis。当时的感觉真的就像这首龙卷风,终于可以在项目中研究新的技术。要说redis是啥怎么用,我其实还是有一定了解的(再怎么说都是买了两本书看)。但是一直苦于项目中用不到,看完就忘 。现在终于觉得英雄有用武之地了,竟然让我使用redis。嘿嘿嘿......
依葫芦画瓢
依葫芦画瓢是学习的最基本也是最难的方法。有的人只画出了形,有的人却在画形的过程中悟出了神。好吧,既然第一次在公司项目中使用redis,那我就百度下别人的使用方法。大致的配置如下:
<!-- redis缓存配置 -->
<!-- Jedis线程池 -->
<bean id="jedisCachePoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="1000" />
<property name="minIdle" value="0" />
<property name="maxTotal" value="1000" />
<property name="testOnBorrow" value="true" />
</bean>
<bean id="jedisShardInfo" class="redis.clients.jedis.JedisShardInfo">
<constructor-arg index="0" value="${redis.host}" />
<constructor-arg index="1" value="${redis.port}" type="int" />
<property name="password" value="${redis.password}"></property>
</bean>
<!-- Redis连接 -->
<bean id="jedisConnectionFactory"
class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="shardInfo" ref="jedisShardInfo"/>
<property name="poolConfig" ref="jedisCachePoolConfig"/>
</bean>
<!-- 缓存序列化方式 -->
<bean id="stringSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer" />
<bean id="jsonSerializer" class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer">
</bean>
<!-- redis数据库操作模板 -->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory" />
<property name="keySerializer" ref="stringSerializer" />
<property name="valueSerializer" ref="jsonSerializer" />
<property name="hashKeySerializer" ref="stringSerializer" />
<property name="hashValueSerializer" ref="jsonSerializer" />
</bean>
<!-- redis缓存管理器 -->
<bean id="cacheManager" class="org.springframework.data.redis.cache.RedisCacheManager">
<constructor-arg index="0" ref="redisTemplate" />
<property name="defaultExpiration" value="600" />
</bean>
本来以为可能启动会报各种错,然后需要我一一去解决。实际上没有报任何错,好像太顺利了。
山雨欲来风满楼
验证了下登录还有我自己写的有@Cacheable
注解的方法似乎没什么问题,本以为就可以愉快地使用Redis作缓存了。事实证明我还是Too Young Too Naive。就在我信心满满,准备测试验证主流程缓存使用情况的时候,意料之中地报错了,也就是这个错,拉开了我的采坑填坑之路......
坑1
不多废话了,直接给出报错的信息:
Caused by: com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException)(through reference chain:....
基本报错的情况就是和上面一致的,不同的可能就在后面的reference chain。这个报错倒是直接往百度上一搜一堆答案,但基本都不是我想要的。网上的答案基本都是和这个链接保持一致的http://hw1287789687.iteye.com/blog/2255940,并且举的都是Student的例子 虽然这个跟我遇到的完全不同,不过也给我找到问题指了一条路。基本原因可以断定是由于属性定义的类型和get方法返回的类型不一致。好吧,那就来看对应的Pojo。报错的Pojo的定义如下:
public class BankInfo {
private Integer bankCode;
@JsonSerialize(using = IdToNameJsonSerializable.class)
@TypeClass(typeClass = TypeConstants.BANK_CODE)
public Integer getBankCode() {
return this.bankCode;
}
}
报错信息中的referece chain就是这个BankInfo['bankCode']。初看这个属性的定义类型和get方法的返回值类型完全是一致的,那么为什么还是会报错呢?原因就在于get方法上面的注解,其中@JsonSerialize
注解是jackson自带的,下面的注解是项目自定义的。在我们项目中其实就是希望通过这两个注解将bankCode直接转换成对应的银行名称,直接给界面展示。而这个银行名称必然是字符串了,与属性bankCode的类型不符。好了原因找到了,剩下的就是看如何去掉对Pojo上面注解的解释执行了。
通过网上搜索资料后得知,jackson底层的序列化和反序列化使用的是ObjectMapper
,而ObjectMapper
在初始化之后可以设置各种各样的属性,通过查看源码发现有一个MapperFeature.USE_ANNOTATIONS
属性,定义如下:
/**
* Feature that determines whether annotation introspection
* is used for configuration; if enabled, configured
* {@link AnnotationIntrospector} will be used: if disabled,
* no annotations are considered.
*<p>
* Feature is enabled by default.
*/
USE_ANNOTATIONS(true),
于是我定义了一个自己的ObjectMapper对象实例,大致如下:
public class MyObjectMapper extends ObjectMapper {
private static final long serialVersionUID = 1L;
public CleObjectMapper() {
super();
// 去掉各种类似@JsonSerialize注解的解析
this.configure(MapperFeature.USE_ANNOTATIONS, false);
// 只针对非空的值进行序列化(这个是为了减少json序列化之后所占用的空间)
this.setSerializationInclusion(Include.NON_NULL);
}
}
并且修改xml中jsonSerializer
的定义如下:
<bean id="myObjectMapper" class="com.rampage.cache.customized.MyObjectMapper"></bean>
<bean id="jsonSerializer" class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer">
<constructor-arg name="mapper" ref="myObjectMapper"></constructor-arg>
</bean>
重启后试下了下,终于不报前面那个空指针错误了
坑2:
前面的问题解决后,序列化存入redis好像是没什么问题。然后,当我继续验证的时候发现又报了另种类型的错:
java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.rampage.model.BankInfo
而且这种错都是一大片一大片的,基本上所有类型都报了这个无法通过HashMap强转得到......
这......怎么从Redis反序列化出来的时候所有对象都变成了LinkedHashMap。这个坑耗费了我将近两天时间。一点点debug class文件还是没有任何进展。最后没辙,只有找以前的同事和我一起试下。最终我们两试了一下午,终于给试出来了。原因参照https://blog.csdn.net/pengguojun117/article/details/17339867。因为我定义的MyObjectMapper没有配置DefaultTyping
属性,jackson将使用简单的数据绑定具体的java类型,其中Object就会在反序列化的时候变成LinkedHashMap......再回过头来看下xml中的json序列化实现类GenericJackson2JsonRedisSerializer
源码:
public GenericJackson2JsonRedisSerializer(String classPropertyTypeName) {
this(new ObjectMapper());
this.mapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(classPropertyTypeName)));
if (StringUtils.hasText(classPropertyTypeName))
this.mapper.enableDefaultTypingAsProperty(ObjectMapper.DefaultTyping.NON_FINAL, classPropertyTypeName);
else
this.mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
}
特别需要注意this.mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
其中属性值的定义如下:
/**
* Method for enabling automatic inclusion of type information, needed
* for proper deserialization of polymorphic types (unless types
* have been annotated with {@link com.fasterxml.jackson.annotation.JsonTypeInfo}).
*<P>
* NOTE: use of <code>JsonTypeInfo.As#EXTERNAL_PROPERTY</code> <b>NOT SUPPORTED</b>;
* and attempts of do so will throw an {@link IllegalArgumentException} to make
* this limitation explicit.
*
* @param applicability Defines kinds of types for which additional type information
* is added; see {@link DefaultTyping} for more information.
*/
public ObjectMapper enableDefaultTyping(DefaultTyping applicability, JsonTypeInfo.As includeAs)
{
/* 18-Sep-2014, tatu: Let's add explicit check to ensure no one tries to
* use "As.EXTERNAL_PROPERTY", since that will not work (with 2.5+)
*/
if (includeAs == JsonTypeInfo.As.EXTERNAL_PROPERTY) {
throw new IllegalArgumentException("Can not use includeAs of "+includeAs);
}
TypeResolverBuilder<?> typer = new DefaultTypeResolverBuilder(applicability);
// we'll always use full class name, when using defaulting
typer = typer.init(JsonTypeInfo.Id.CLASS, null);
typer = typer.inclusion(includeAs);
return setDefaultTyping(typer);
}
/**
* Value that means that default typing will be used for
* all non-final types, with exception of small number of
* "natural" types (String, Boolean, Integer, Double), which
* can be correctly inferred from JSON; as well as for
* all arrays of non-final types.
*<p>
* Since 2.4, this does NOT apply to {@link TreeNode} and its subtypes.
*/
NON_FINAL
整个方法的意思就是在序列化的时候会将类型信息一起作为属性的一部分序列化,在反序列化的时候会根据对应的类型信息进行转换。最终我修改MyOjectMapper
如下:
public class CleObjectMapper extends ObjectMapper {
private static final long serialVersionUID = 1L;
public CleObjectMapper() {
super();
// 去掉各种@JsonSerialize注解的解析
this.configure(MapperFeature.USE_ANNOTATIONS, false);
// 只针对非空的值进行序列化
this.setSerializationInclusion(Include.NON_NULL);
// 将类型序列化到属性json字符串中
this.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY);
}
}
替换之后原来LinkedHashMap转各种对象的错误神奇地消失了~~
坑3:
解决完上面两个问题了之后,基本流程是不是可以完全跑通了呢?希望如此吧......
于是我替换修改的class文件,重新启动开始验证。美好的愿望又被一个报错给打破。具体报错信息如下:
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Unrecognized field "bankName"
at [Source: [B@38176916; line: 1, column: 444] (through reference chain: com.rampage.model.BankInfo["bankName"]);
有了前面两个填坑经验之后,我知道肯定先要看下对应的Pojo源码。由于这个报错是在序列化的时候报的,所以应该是get方法存在问题:
public class BankInfo {
private String bankNameCode;
public String getBankName() {
return this.bankNameCode;
}
}
可以看到,getBankName
并不是返回bankName
属性,实际上BankInfo
对象根本没有bankName
属性 。聪明的人不会在同一个地方绊倒三次. 我知道这个肯定又有一个属性设置忽略这种特殊情况报错。最终结合源码和链接https://blog.csdn.net/kobejayandy/article/details/45869861找到属性DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
:
/**
* Feature that determines whether encountering of unknown
* properties (ones that do not map to a property, and there is
* no "any setter" or handler that can handle it)
* should result in a failure (by throwing a
* {@link JsonMappingException}) or not.
* This setting only takes effect after all other handling
* methods for unknown properties have been tried, and
* property remains unhandled.
*<p>
* Feature is enabled by default (meaning that a
* {@link JsonMappingException} will be thrown if an unknown property
* is encountered).
*/
FAIL_ON_UNKNOWN_PROPERTIES(true),
将这个属性设置成false应该就可以解决报错了。最终MyObjectMapper
被修改成了这样:
public class CleObjectMapper extends ObjectMapper {
private static final long serialVersionUID = 1L;
public CleObjectMapper() {
super();
// 去掉各种@JsonSerialize注解的解析
this.configure(MapperFeature.USE_ANNOTATIONS, false);
// 只针对非空的值进行序列化
this.setSerializationInclusion(Include.NON_NULL);
// 将类型序列化到属性json字符串中
this.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY);
// 对于找不到匹配属性的时候忽略报错
this.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 不包含任何属性的bean也不报错
this.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
}
}
这下基本流程终于终于可以跑通了~ Happy ~~~~~~
坑4
本来以为基本流程跑通了之后就大功告成了。事实证明,永远都要去验证程序的异常情况。最终我再验证异常情况的时候,发现竟然又报了个空指针异常。严格地讲这个异常不是因为Redis缓存导致的问题。而是缓存使用方式不对导致的:就是因为以前项目的缓存使用的是Ehcache,所以直接可以往缓存中添加对象,甚至是Spring管理的对象。Redis缓存填了各种坑之后也可以愉快地往缓存中添加对象,但是必须注意是无法缓存Spring管理的对象的(Redis数据库才不会关心对象被不被Spring管理)。如果缓存Spring管理的对象,那么再从缓存取出来后,原来Spring注入的属性都不存在...... 这个空指针就是因为这个问题导致的。 还好机智的我花了不到一分钟就想到了原因迅速解决了。终于可以愉快地使用Redis + Cacheable注解了。
总结
这次填坑真的是耗费了我很长时间,完全打乱了我各种计划。甚至导致我一段时间不想干任何事,只是觉得好烦,又浪费了这么多时间.......
当然还是有收获的,具体来说有以下几点:
黎明前最黑暗,成功前最绝望!
- Jackson与ObjectMapper: 基本上Jackson导致的序列化和反序列化问题在无法改动源代码,都是可以通过调整ObjectMapper的相关属性来解决的,遇到问题的时候需要仔细分析具体应该如何改动默认属性
- Redis缓存也不是完全没有劣势的: 刚开始的时候觉得Redis作缓存一定比Ehcache高大上,只有优势没有劣势。事实证明并不是:Redis是Key、Value类型的,没法直接存储对象,必须序列化之后存入。Redis无法缓存Spring管理的对象。Redis缓存获取是需要反序列化以及数据IO操作的,效率肯定不及Ehcache,所以才有利用Redis和Ehcache实现多级缓存的实现。总之一句话,新的技术不一定表示是好的技术,而且新的技术可能遇到各种不适用当前历史遗留代码的各种问题。
- 架构设计的重要性: 各种挖坑填坑之后,我突然觉得:如果项目一开始就引入Redis作缓存,那么很多不规范的写法在开发的时候就会暴露出问题,自然可以规范大家使用缓存的方式。而这种后期引入新的框架,可能由于各种老代码百花齐放的各种写法,出现各种蛋疼问题。后续不仅要解决问题还要兼容丑陋的老代码。这个时间和人力成本是一开始设计好的很多很多倍......还让人特别不爽!