标签: 序列化

一次序列化与反序列化引发的BUG排查

BUG现场

这是一份若干年前的历史代码,当时的同学写这份代码的时候设计思路是这样的,我复现这份BUG现场的代码如下

要点1 有一个用来传输数据的DTO,大致如下

@Data
public class CategoryBrandBu implements Serializable {

    protected static final Map<String,String> BRAND_RELATION_MAP = new HashMap<>();
    public Map<String,String> getRelationMap(){
        return BRAND_RELATION_MAP;
    }
    private Long id;
    private String brandCode;
    private String brandName;

    public String getBrandName() {
        if (StringUtils.isBlank(brandCode)){
            return StringUtils.EMPTY;
        }
        return BRAND_RELATION_MAP.getOrDefault(brandCode,StringUtils.EMPTY);
    }
}

DTO中声明了一个静态常量BRAND_RELATION_MAP的HashMap,用来保存某个不常更新的表中的映射信息,表中的数据基本只有几十条,所以放在这用来获取品牌名称

要点2. 再另外起一个定时job,定时来刷新这个HashMap中的映射关系数据,代码大意如下

@Configuration
public class ScheduledConfig {

    @Scheduled(fixedDelay = 5000)
    public void updateMap(){
        Random random = new Random();
        int i = random.nextInt(1000);
        System.out.println("updated: "+i);
        CategoryBrandBu dto = new CategoryBrandBu();
        dto.getRelationMap().put("ABC", String.valueOf(i));
    }
}

其中代码先new CategoryBrandBu()getRelationMap(),再put的操作虽然看起来比较挫,不过嗯、至少确实还是有用的(原来的代码就是这样),这个不是重点,另外fixedDelay = 5000是我特地调整的5秒刷新一次。

要点3. 接下来一个至关重要的东西,当前Application中提供了对外服务RPC(某自研RPC框架)接口用于查询相关数据,其中有一个接口的返回值中就用到了当前涉及的对象CategoryBrandBu。

RPC基础服务类中将请求参数序列化之后,发送到目标Application中。目标Application接收到请求之后,将请求信息解析之后调用对应bean实例的对应方法,且同时反序列化对应的请求参数为对应方法的java参数对象。这里这序列化和反序列化中用的fastjson(能用,不过也不是很高明的样子)。

BUG表现情况

我们的@Scheduled定时任务无论怎样刷新CategoryBrandBu类中静态成员变量Map的信息,外部应用实例(B)通过RPC访问过来的之后得到的结果永远都是第一次初始化之后得到Map中的值(其实也不是第一次初始化的值,后面我再说到)

举个栗子来说明下,首先当我们的Application(A)启动之后,当前静态成员变量上Map被初始化成了如下

(A)->AAA
(B)->BBB
(C)->CCC

此时外部的应用实例(B)通过RPC访问到当前Application(A)的对应接口之后,得到了这个DTO序列化的结果,其中包含已经被序列化了的BRAND_RELATION_MAP,而我们对应外部应用实例(B)的系统也恰巧用了同样的DTO的java类文件,于是在外部应用实例(B)接收到序列化的返回结果同时,也接收到了序列化的BRAND_RELATION_MAP,外部应用实例(B)对数据进行反序列化,同时也对外部应用实例(B)中的CategoryBrandBu类中的静态成员变量BRAND_RELATION_MAP进行赋值。

Continue reading

基于AOP的Redis缓存注解功能开发设计[3],缓存更新、删除、过期

前文

在完成了前面两篇文章(基于AOP的Redis缓存注解功能开发设计[2],JexlEngine自定义缓存Key)中的基础功能的开发之后,我们可以再进一步完成缓存更新、缓存删除、缓存过期这几个接口。从而适应在具体的业务开发过程中遇到的各种对缓存数据操作的需求。

因为前面对具体的Redis的缓存的操作和实现逻辑已经做了详细说明,所以在这三个功能注解的开发过程中只做简单的逻辑阐述,不在细究每一个方法的具体含义

缓存更新

缓存更新的意思就是,当我们在更新数据库中的某条数据之后,我们需要另外也把Redis中作为缓存的数据也同步时更新下,这里其实就会涉及到我们之前提到的缓存一致性问题,这个话题要讲的话可以展开来讲很长一段,我们不在这里做过多讨论,可以自行再去搜索一番。

我在这里需要提供一个新的注解@RedisPut,和PointCut来实现我们的功能即可,代码直接加到原来的切面类中。切面类中需要实现的逻辑就是根据方法执行的结果,直接更新Redis中的缓存数据,和之前@RedisCache的区别就是这里不需要去Redis中判断是否已经存在缓存数据。

Continue reading

基于AOP的Redis缓存注解功能开发设计[2],JexlEngine自定义缓存Key

那么在经历了前面两片文章《基于AOP的Redis缓存注解功能开发设计》和《JexlEngine表达式引擎的简单使用》之后,我们便可以将JexlEngine表达式引擎在AOP缓存中使用起来,通过表达式引擎的定义来自定义每个缓存的Key或者每组类型的缓存的Key,从而达到在不同的代码逻辑中增删改有相同组别或者类型关联的缓存数据的需求

如何指定生成Key规则

在之前的Redis缓存注解一文中,我们定义的注解中有个Key变量

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisCache {
    String value();

    String key() default "";
}

原本我们在使用的时候,value()用来指定在Redis中的Key,而hashKey则用注解作用在的方法的所有参数组合之后的Array来担当。现在我们把注解中的key()用上,用来填写对应的hashKey的生成规则,填写的内容则是一个JexlEngine的表达式。下面,写个Demo。

@RedisCache(value = "dict_cache", key = "paramStr+'_'+paramInteger+'_'+chars[0]+'_'+paramInt+'_'+paramDto.getCodeParam()")
public Result<DictionaryInfo> cacheParamExample(String paramStr, Integer paramInteger, ParamDto paramDto, char[] chars, int paramInt, Integer integer, double v){
    DictionaryInfo dictionaryInfo = new DictionaryInfo();
    dictionaryInfo.setDictName("DIC");
    dictionaryInfo.setDictCode("CODE");
    return new Result<>(dictionaryInfo);
}

表达式写得有点夸张,不过主要是做个展示,这里也不会用到太复杂的JexlEngine表达式,其根本在于用几个给予的对象拼接一个字符串,不会涉及到JexlEngine提供的各种复杂的功能。这边更新下之前文章中切面类RedisCacheAspect的代码

Continue reading

基于AOP的Redis缓存注解功能开发设计

在完成的前面的《Redis结合AOP实现限流注解的开发》之后,文中提到了一个配置管理或者字典管理的功能模块,作为一个对外提供快速查询获取配置信息的功能模块,其数据信息本身可以保存的Mysql数据库中。但如果每次查询都要从Mysql中获取数据则又非常影响接口效率和速度,故需要将对应信息在Redis中做一份缓存,帮助快速查询获取。

基础前置

需要的基础前置知识包括《Redis结合AOP实现限流注解的开发》AOP相关,《Java对象序列化常用操作-Protostuff》Java对象的序列化和反序列化,可以前往对应文章查看。

基础思路是这样的

首先我们需要定一个作用于方法上的注解比如@RedisCache(题外话,Spring自身也也有个类似功能的注解@Cacheable,回头可以再开一篇写下相关的使用方法,本文着重是自己开发一套这样的功能)。之后通过一个RedisCacheAspect类,使用@Around注解实现对添加了@RedisCache注解的方法的代理,并将方法的入参,执行结果序列化之后保存到Redis的一个Hash表中去。当下次请求过来的时候,先根据请求参数去Redis的Hash表中查询一下,如果存在,则将查到的信息反序列化为所要查询的对象直接返回给方法调用者。

代码实现

首先是注解类,我们需要新建一个这样的RedisCache注解类

/**
 * @author CheungQ
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisCache {
    String value();

    String key() default "";
}

之后,我们在自行实现一个切面类,用来定义切面,并实现切面上的功能。使用@Around注解,并在@RedisCache注解作用的方法前查询Redis中是否有缓存,如果有,则返回结果,如果没有则执行@RedisCache作用的方法,得到方法执行结果,并将结果缓存到Redis,待下次请求的时候查询获取。基本逻辑就是这样,具体的可以看下代码中的实现,这里用到了上面提到的前几篇文章中的相关技术细节。代码如下

Continue reading

Java对象序列化常用操作-Protostuff

在《使用Objenesis实例化java对象》中我们提到了创建一个对象的示例的场景中,有这么一条是将序列化之后的对象重新反序列化为Java对象,那么本篇将就在这一点上,介绍说明下使用Protostuff对Java对象进行序列化和反序列化操作首先。

引子

关于为何要序列化、如何序列化可以参看下https://www.cnblogs.com/wugongzi/p/14345859.html,这里不做额外说明,需要了解到的是,我们一般提到的java对象序列化的方案大致可分为如下几种

  • Java自身提供的序列化功能,需要实现Serializable接口或者Externalizable接口,并使用java的IOStream进行序列化和反序列化的操作
  • 使用第三方库进行JSON或者XML格式的序列化,XML序列化的好处在于可读性好,方便阅读和调试。但是序列化以后的 字节码文件比较大,而且效率不高,适应于对性能不高,而且QPS较低的企业级内部系统之间的数据交换的场景。相对于XML来说,JSON的字节流较小,而且可读性也非常好,应用也非常普遍的。常用的有Jackson、阿里的Fastjson、谷歌的Gson等
  • 除此之外,Protobuf也是一个非常广泛应用的选择,Protobuf是Google的一种数据交换格式,它独立于语言、独立于平台。Protobu解析性能比较高,序列化以后数据量相对较少,适合应用在对象的持久化场景中。但是不同于前面提到的几个方法,要使用 Protobuf 会相对略麻烦些,他有自己的语法,自己的编译器

和Protobuf类似、Protostuff也是谷歌的产品,它是基于Protobuf发展而来的,相对于Protobuf提供了更多的功能和更简易的用法。其中,protostuff-runtime实现了无需预编译对Java Bean进行protobuf序列化/反序列化的能力。protostuff-runtime的局限是序列化前需预先传入schema,反序列化不负责对象的创建只负责复制,因此必须提供默认构造函数。在性能上,Protostuff不输原生的Protobuf,甚至有反超之势

Continue reading