分布式改造剧集之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
相关文章
|
29天前
|
数据采集 存储 数据可视化
分布式爬虫框架Scrapy-Redis实战指南
本文介绍如何使用Scrapy-Redis构建分布式爬虫系统,采集携程平台上热门城市的酒店价格与评价信息。通过代理IP、Cookie和User-Agent设置规避反爬策略,实现高效数据抓取。结合价格动态趋势分析,助力酒店业优化市场策略、提升服务质量。技术架构涵盖Scrapy-Redis核心调度、代理中间件及数据解析存储,提供完整的技术路线图与代码示例。
分布式爬虫框架Scrapy-Redis实战指南
|
1月前
|
缓存 监控 NoSQL
Redis--缓存击穿、缓存穿透、缓存雪崩
缓存击穿、缓存穿透和缓存雪崩是Redis使用过程中可能遇到的常见问题。理解这些问题的成因并采取相应的解决措施,可以有效提升系统的稳定性和性能。在实际应用中,应根据具体场景,选择合适的解决方案,并持续监控和优化缓存策略,以应对不断变化的业务需求。
100 29
|
1月前
|
缓存 NoSQL Java
Redis应用—8.相关的缓存框架
本文介绍了Ehcache和Guava Cache两个缓存框架及其使用方法,以及如何自定义缓存。主要内容包括:Ehcache缓存框架、Guava Cache缓存框架、自定义缓存。总结:Ehcache适合用作本地缓存或与Redis结合使用,Guava Cache则提供了更灵活的缓存管理和更高的并发性能。自定义缓存可以根据具体需求选择不同的数据结构和引用类型来实现特定的缓存策略。
123 16
Redis应用—8.相关的缓存框架
|
21天前
|
人工智能 缓存 NoSQL
Redis 与 AI:从缓存到智能搜索的融合之路
Redis 已从传统缓存系统发展为强大的 AI 支持平台,其向量数据库功能和 RedisAI 模块为核心,支持高维向量存储、相似性搜索及模型服务。文章探讨了 Redis 在实时数据缓存、语义搜索与会话持久化中的应用场景,并通过代码案例展示了与 Spring Boot 的集成方式。总结来看,Redis 结合 AI 技术,为现代应用提供高效、灵活的解决方案。
|
1月前
|
NoSQL Redis
Redis分布式锁如何实现 ?
Redis分布式锁主要依靠一个SETNX指令实现的 , 这条命令的含义就是“SET if Not Exists”,即不存在的时候才会设置值。 只有在key不存在的情况下,将键key的值设置为value。如果key已经存在,则SETNX命令不做任何操作。 这个命令的返回值如下。 ● 命令在设置成功时返回1。 ● 命令在设置失败时返回0。 假设此时有线程A和线程B同时访问临界区代码,假设线程A首先执行了SETNX命令,并返回结果1,继续向下执行。而此时线程B再次执行SETNX命令时,返回的结果为0,则线程B不能继续向下执行。只有当线程A执行DELETE命令将设置的锁状态删除时,线程B才会成功执行S
|
1月前
|
存储 缓存 NoSQL
Redis缓存设计与性能优化
Redis缓存设计与性能优化涵盖缓存穿透、击穿、雪崩及热点key重建等问题。针对缓存穿透,可采用缓存空对象或布隆过滤器;缓存击穿通过随机设置过期时间避免集中失效;缓存雪崩需确保高可用性并使用限流熔断组件;热点key重建利用互斥锁防止大量线程同时操作。此外,开发规范强调键值设计、命令使用和客户端配置优化,如避免bigkey、合理使用批量操作和连接池管理。系统内核参数如vm.swappiness、vm.overcommit_memory及文件句柄数的优化也至关重要。慢查询日志帮助监控性能瓶颈。
71 9
|
1月前
|
消息中间件 缓存 NoSQL
缓存与数据库的一致性方案,Redis与Mysql一致性方案,大厂P8的终极方案(图解+秒懂+史上最全)
缓存与数据库的一致性方案,Redis与Mysql一致性方案,大厂P8的终极方案(图解+秒懂+史上最全)
|
缓存 NoSQL Java
为什么分布式一定要有redis?
1、为什么使用redis 分析:博主觉得在项目中使用redis,主要是从两个角度去考虑:性能和并发。当然,redis还具备可以做分布式锁等其他功能,但是如果只是为了分布式锁这些其他功能,完全还有其他中间件(如zookpeer等)代替,并不是非要使用redis。
1396 0
|
机器学习/深度学习 缓存 NoSQL
|
4月前
|
存储 缓存 NoSQL
解决Redis缓存数据类型丢失问题
解决Redis缓存数据类型丢失问题
229 85

热门文章

最新文章