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作緩存,那麼很多不規範的寫法在開發的時候就會暴露出問題,自然可以規範大家使用緩存的方式。而這種後期引入新的架構,可能由于各種老代碼百花齊放的各種寫法,出現各種蛋疼問題。後續不僅要解決問題還要相容醜陋的老代碼。這個時間和人力成本是一開始設計好的很多很多倍......還讓人特别不爽!
黎明前最黑暗,成功前最絕望!