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 作缓存, 那么很多不规范的写法在开发的时候就会暴露出问题, 自然可以规范大家使用缓存的方式. 而这种后期引入新的框架, 可能由于各种老代码百花齐放的各种写法, 出现各种蛋疼问题. 后续不仅要解决问题还要兼容丑陋的老代码. 这个时间和人力成本是一开始设计好的很多很多倍...... 还让人特别不爽!
来源: https://www.cnblogs.com/Kidezyq/p/8942111.html