分布式改造剧集之Redis缓存踩坑记

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: Redis缓存踩坑记​前言​ 这个其实应该属于分布式改造剧集中的一集(第一集见前面博客:http://www.cnblogs.com/Kidezyq/p/8748961.html),本来按照顺序来的话,不会这么快发布这篇博客。

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作缓存,那么很多不规范的写法在开发的时候就会暴露出问题,自然可以规范大家使用缓存的方式。而这种后期引入新的框架,可能由于各种老代码百花齐放的各种写法,出现各种蛋疼问题。后续不仅要解决问题还要兼容丑陋的老代码。这个时间和人力成本是一开始设计好的很多很多倍......还让人特别不爽!
黎明前最黑暗,成功前最绝望!
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1天前
|
存储 缓存 NoSQL
解决Redis缓存数据类型丢失问题
解决Redis缓存数据类型丢失问题
109 85
|
1月前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
1月前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构
|
18天前
|
存储 NoSQL Java
使用lock4j-redis-template-spring-boot-starter实现redis分布式锁
通过使用 `lock4j-redis-template-spring-boot-starter`,我们可以轻松实现 Redis 分布式锁,从而解决分布式系统中多个实例并发访问共享资源的问题。合理配置和使用分布式锁,可以有效提高系统的稳定性和数据的一致性。希望本文对你在实际项目中使用 Redis 分布式锁有所帮助。
47 5
|
21天前
|
NoSQL Java 数据处理
基于Redis海量数据场景分布式ID架构实践
【11月更文挑战第30天】在现代分布式系统中,生成全局唯一的ID是一个常见且重要的需求。在微服务架构中,各个服务可能需要生成唯一标识符,如用户ID、订单ID等。传统的自增ID已经无法满足在集群环境下保持唯一性的要求,而分布式ID解决方案能够确保即使在多个实例间也能生成全局唯一的标识符。本文将深入探讨如何利用Redis实现分布式ID生成,并通过Java语言展示多个示例,同时分析每个实践方案的优缺点。
39 8
|
1月前
|
NoSQL Redis
Redis分布式锁如何实现 ?
Redis分布式锁通过SETNX指令实现,确保仅在键不存在时设置值。此机制用于控制多个线程对共享资源的访问,避免并发冲突。然而,实际应用中需解决死锁、锁超时、归一化、可重入及阻塞等问题,以确保系统的稳定性和可靠性。解决方案包括设置锁超时、引入Watch Dog机制、使用ThreadLocal绑定加解锁操作、实现计数器支持可重入锁以及采用自旋锁思想处理阻塞请求。
57 16
|
1月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
40 5
|
2月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(一)
数据的存储--Redis缓存存储(一)
100 1
|
2月前
|
存储 缓存 NoSQL
数据的存储--Redis缓存存储(二)
数据的存储--Redis缓存存储(二)
52 2
数据的存储--Redis缓存存储(二)
|
2月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
78 6
下一篇
DataWorks