分布式改造劇集之Redis緩存踩坑記


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作緩存,那么很多不規范的寫法在開發的時候就會暴露出問題,自然可以規范大家使用緩存的方式。而這種后期引入新的框架,可能由於各種老代碼百花齊放的各種寫法,出現各種蛋疼問題。后續不僅要解決問題還要兼容丑陋的老代碼。這個時間和人力成本是一開始設計好的很多很多倍......還讓人特別不爽!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM