首页 > 其他分享 >装饰者模式、深拷贝、泛型序列化解决Caffeine中的缓存一致性问题

装饰者模式、深拷贝、泛型序列化解决Caffeine中的缓存一致性问题

时间:2022-09-22 00:14:21浏览次数:66  
标签:CopiedCache 缓存 cache Caffeine 泛型 序列化

一、前言

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

  • 优点:因为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<K, V> implements Cache<K, V>, LoadingCache<K, V> {

	/**
     * 被装饰的缓存实例
     */
    private final Cache<K, V> cache;

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

    public CopiedCache(Cache<K, V> 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<Long, UserDetails> userCache = new CopiedCache<>(cache);
     * 如果这么做,构造方法中使用类似TypeReference的方法,无法获取泛型V的class
     *
     * 2.经测试分析,在泛型类中,只能通过子类来获取泛型类型,为了强制使用此类,CopiedCache设计成了泛型,初始化时可以用
     * 匿名内部类来代替子类,创建该类对象的代码:
     *      LoadingCache<Long, UserDetails> userCache = new CopiedCache<Long, UserDetails>(cache) {};
     *
     */
    
}

五、装饰模式

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

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

装饰模式类图:

六、关键代码

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

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

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

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

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

//这里userDetails是缓存的拷贝,userCache.get调用自动实现了增强效果
UserDetails userDetails = userCache.get(userId);

装饰类的代码

@ThreadSafe
public abstract class CopiedCache<K, V> implements Cache<K, V>, LoadingCache<K, V> {

    /**
     * 被装饰的缓存实例
     */
    private final Cache<K, V> cache;

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

    public CopiedCache(Cache<K, V> 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<K, V> getAll(@Nonnull Iterable<? extends K> keys) {
        //委托给装饰类去调用
        Map<K, V> 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<K,V>
     */
    private Map<K,V> copyMap(Map<K,V> map) {
        Map copiedMap = new LinkedHashMap();
        Maps.each(map, (k, v) -> copiedMap.put(k, copy(v)));
        return copiedMap;
    }
}

标签:CopiedCache,缓存,cache,Caffeine,泛型,序列化
From: https://www.cnblogs.com/huanongying/p/16717718.html

相关文章

  • form表单内容序列化的两种方法
    form表单内容序列化form表单自带两种方法serialize()方法和serialize()方法1.serialize()方法描述:序列化表单内容为字符串(不包括文件),用于Ajax请求。格式:vardata=......
  • C# .net 对外接口返回json字符串序列化
     UserBankCarduserBankCard=newUserBankCard(){BankCard=bankCard,UserID=userID,RealName......
  • Python爬取任意城市肯德基门店信息(json数据反序列化、提取数据、写入CSV)
    本案关键内容点:json数据反序列化、提取数据、写入CSV创建csv,写入表头数据,脚本同目录下会创建名称为book的csv文件,且第一行插入表头内容 importcsvf=open('book.cs......
  • 集合.泛型
    Java泛型是JDK1.5中引入的一个新特性,其本质是参数化类型,把类型作为参数传递常见形式有泛型类、泛型接口、泛型方法语法:<T,...>T称为类型占位符,表示一种引用类型......
  • C# 泛型和泛型约束
    C#泛型和泛型约束使用C#创建泛型类,泛型类是使用C#开发OOP应用程序时必须具备的结构。通过创建单个泛型类,您将能够处理一个类中可能有10个不同类中的重复代码.......
  • flask升序降序,分页,序列化器
    classUserInfoView(Resource):#@marshal_with(field)defget(self):#daming=User('daming',40,None)#xiaoming=User('xiaoming',16,damin......
  • CVE-2017-12149 JBoss JMXInvokerServlet 反序列化漏洞
    一、漏洞概述     2017年8月30日,厂商Redhat发布了一个JBOSSAS5.x的反序列化远程代码执行漏洞通告。该漏洞位于JBoss的HttpInvoker组件中的ReadOnlyAccessFil......
  • C#教程 - 泛型(Generic Types)
    更新记录转载请注明出处:https://www.cnblogs.com/cqpanda/p/16690994.html2022年9月18日发布。2022年9月10日从笔记迁移到博客。泛型(GenericTypes)说明一般情况下......
  • Java 序列化
    Java序列化当一个对象需要持久化(存储)或者传输的时候,就用到了序列化。对象可以转换成字节序列,该字节序列包括这个对象的数据和类型信息也包括存储在对象中数据的类型。将......
  • SpringBoot + Caffeine实现本地缓存(内存缓存)
    1.Caffeine简介  Caffeine是一个基于Java8开发的提供了近乎最佳命中率的高性能的缓存库。借鉴GoogleGuava和ConcurrentLinkedHashMap的经验,实现内存缓存。  缓存和......