成都网站建设设计

将想法与焦点和您一起共享

装饰模式、泛型、序列化重构Caffeine解决缓存不一致的问题

一、前言

Caffeine是一个高性能的 Java 缓存库,底层数据存储采用ConcurrentHashMap

成都创新互联公司主要从事网站设计制作、做网站、网页设计、企业做网站、公司建网站等业务。立足成都服务阳春,10余年网站建设经验,价格优惠、服务专业,欢迎来电咨询建站服务:028-86922220

  • 优点:因为Caffeine面向JDK8,在jdk8中ConcurrentHashMap增加了红黑树,在hash冲突严重时也能有良好的读性能。多线程环境中,不同的key可以并发写,相同的key会加锁,天然的解决了缓存击穿问题和缓存雪崩问题。

  • 缺点:因为底层数据结构是ConcurrentHashMap,所有不能作为分布式缓存,同时如果使用不当,会带来数据不一致的问题
    本文主要内容是探讨Caffeine使用不当时,数据一致性安全问题

二、问题发生场景

如下图所示,问题的根源在于A能直接拿到缓存地址的引用,然后通过引用就能随意修改引用指向的缓存对象,要解决这个问题,可以在步骤三这里,将缓存对象深拷贝后的副本的引用返回给客户端,这样客户端对缓存的任何操作,改变的只是副本,缓存本身只能由维护者来更新

三、如何进行对象的深拷贝

实现方式:

  1. 缓存实体类本身封装成不可变类对象,类和属性全部用final修饰,不提供能改变属性的方法,属性的值只能在对象创建过程中设置
  2. 缓存获取API返回缓存实体后,重新创建一个新对象,对于基本类型的属性可以用set方法简单设置,注意对象类型的属性每一层都要重新创建,特繁琐,然后set,稍不小心就会出现浅拷贝问题,所有这里要不建议用BeanUtils等工具类
  3. 使用Jackson将缓存对象先系列化为Json字符串,然后将Json字符串反序列化为对象,这里一定能得到一个安全的深拷贝对象

因为我们的项目中已经在大量使用Caffeine,方案1和2需要对大量的实体类和缓存获取接口进行大量的改造,工作量巨大,后面采用方案3

四、怎么获取泛型T的Type

后面需要使用Jackson对泛型V进行反序列化,需要用到泛型V的Type属性,如果是普通对象用class,比如User.class,这里只有泛型无法知道class,之所以采用泛型,是因为缓存是面向任意数据类型的,定义泛型是为了更强的通用性,下面是获取泛型T的Typede代码,参考了Jackson的TypeReference类的源码

//拷贝了的缓存对象,原来缓存操作类的装饰者
public abstract class CopiedCache implements Cache, LoadingCache {

	/**
     * 被装饰的缓存实例
     */
    private final Cache cache;

    /**
     * 泛型V的类型,反序列化时会用到
     */
    protected final Type vType;

    public CopiedCache(Cache cache) {
        this.cache = cache;
        //获取泛型V的类型,参考Jackson的TypeReference类的源码
        Type superClass = getClass().getGenericSuperclass();
        if (superClass instanceof Class) { // sanity check, should never happen
            throw new IllegalArgumentException("Internal error: TypeReference constructed without actual type information");
        }
        //获取泛型参数列表,下标0表示K,1表示V,后面的反序列化只用到了V的Type
        vType = ((ParameterizedType) superClass).getActualTypeArguments()[1];
        
        /**
         * json字符串反序列化对象:
         * ObjectMapper om = new ObjectMapper(new JsonFactory());
         *
         * 普通对象
         * OM.readValue(json, XXX.class);
         *
         * 泛型对象
         * OM.readValue(json, OM.getTypeFactory().constructType(vType));
         */
    }
    
    /**
     *其余部分省略,这里主要说明如何获取泛型的class
     * 
     * 1.这个类最初设计时不是抽象类,创建对象的代码:
     *      LoadingCache userCache = new CopiedCache<>(cache);
     * 如果这么做,构造方法中使用类似TypeReference的方法,无法获取泛型V的class
     *
     * 2.经测试分析,在泛型类中,只能通过子类来获取泛型类型,为了强制使用此类,CopiedCache设计成了泛型,初始化时可以用
     * 匿名内部类来代替子类,创建该类对象的代码:
     *      LoadingCache userCache = new CopiedCache(cache) {};
     *
     */
    
}

五、装饰模式

使用装饰模式是为了对Caffeine中缓存查询方法做增强处理(主要是对缓存对象进行深拷贝),对目标类的某些方法进行增强的实现方式:

  1. 直接在目标类的方法上修改,这里Caffeine是三方库,根本没法改,硬要改的话,只能是改它的源代码重新生成jar包,这个包会成为野包,个人感觉此法不妥,这么做了面向对象的开闭原则(对扩展开放,对修改关闭)
  2. Spring AOP,这样额外引入了Spring框架,而且Caffeine中涉及查询方法太多,可能需要定义特别多的切点,比较麻烦
  3. 装饰模式,Cache和LoadingCache是Caffeine中的缓存操作接口,充当被装饰者的角色,CopiedCache充当装饰者,CopiedCache持有被装饰者的引用,对象创建时需传入被装饰者引用,同时实现了被装饰者的接口。通过继承重写需要增强的方法,对于不需要增强的方法,直接委托给被装饰者调用,最后可以依据里氏替换原则,只需将缓存操作接口的引用指向CopiedCache,原来缓存查询相关的代码不用作任何改变,极大降低耦合度

装饰模式类图:

六、代码实现

改造前,缓存初始化及缓存查询的实现代码

//缓存初始化
LoadingCache userCache = Caffeine.newBuilder()
                .maximumSize(1024L)
                .expireAfterWrite(Duration.ofMinutes(15))
                .build(delegate::getUserDetails);

//这里userDetails就是缓存,客户端拿到后执行userDetails.setXXX,将会破坏缓存一致性
UserDetails userDetails = userCache.get(userId);

改造后,缓存初始化及缓存查询的实现代码

//缓存初始化
LoadingCache cache = Caffeine.newBuilder()
                .maximumSize(1024L)
                .expireAfterWrite(Duration.ofMinutes(15))
                .build(delegate::getUserDetails);
//对原生的缓存类进行装饰,注意后面的小括号,CopiedCache是抽象类,这里创建了匿名子类,可以将泛型类型传给父类         
LoadingCache userCache = new CopiedCache(cache) {};

//这里userDetails是缓存的拷贝,类似的调用逻辑不需要做任何的修改,自动实现了增强效果
UserDetails userDetails = userCache.get(userId);

装饰类的代码

@ThreadSafe
public abstract class CopiedCache implements Cache, LoadingCache {

    /**
     * 被装饰的缓存实例
     */
    private final Cache cache;

    /**
     * 泛型V的类型,反序列化时会用到
     */
    protected final Type vType;

    public CopiedCache(Cache cache) {
        this.cache = cache;
        //获取泛型V的类型
        Type superClass = getClass().getGenericSuperclass();
        if (superClass instanceof Class) { // sanity check, should never happen
            throw new IllegalArgumentException("Internal error: TypeReference constructed without actual type information");
        }
        vType = ((ParameterizedType) superClass).getActualTypeArguments()[1];
    }

    @Nullable
    @Override
    public V get(@Nonnull K key) {
    	//委托装饰类去调用
        V v = (V) ((LoadingCache) cache).get(key);
        //下面的copy方法就是增强的逻辑
        return copy(v);
    }

    @Nonnull
    @Override
    public Map getAll(@Nonnull Iterable keys) {
        //委托给装饰类去调用
        Map map = ((LoadingCache) cache).getAll(keys);
        return copyMap(map);
    }
    
    @Override
    public void put(@Nonnull K key, @Nonnull V value) {
    	//不需要增强,直接委托给被装饰类来调用
        cache.put(key, value);
    }

    /**
     * 普通对象的深拷贝,实现方式:序列化+反序列化
     * @param v
     * @return V
     */
    private V copy(V v) {
        return JSON.parse(JSON.stringify(v), vType);
    }
    
    //这里省略了其他方法...

    /**
     * Map对象的深拷贝,实现方式:序列化+反序列化
     * @param map
     * @return java.util.Map
     */
    private Map copyMap(Map map) {
        Map copiedMap = new LinkedHashMap();
        Maps.each(map, (k, v) -> copiedMap.put(k, copy(v)));
        return copiedMap;
    }
}

当前标题:装饰模式、泛型、序列化重构Caffeine解决缓存不一致的问题
标题链接:http://chengdu.cdxwcx.cn/article/dscgiij.html