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

您所在的位置:网站首页 泛型缓存的缺点 装饰模式、泛型、序列化重构Caffeine解决缓存不一致的问题

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

2024-01-09 21:22| 来源: 网络整理| 查看: 265

一、前言

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

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

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

二、问题发生场景

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

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

实现方式:

缓存实体类本身封装成不可变类对象,类和属性全部用final修饰,不提供能改变属性的方法,属性的值只能在对象创建过程中设置 缓存获取API返回缓存实体后,重新创建一个新对象,对于基本类型的属性可以用set方法简单设置,注意对象类型的属性每一层都要重新创建,特繁琐,然后set,稍不小心就会出现浅拷贝问题,所有这里要不建议用BeanUtils等工具类 使用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中缓存查询方法做增强处理(主要是对缓存对象进行深拷贝),对目标类的某些方法进行增强的实现方式:

直接在目标类的方法上修改,这里Caffeine是三方库,根本没法改,硬要改的话,只能是改它的源代码重新生成jar包,这个包会成为野包,个人感觉此法不妥,这么做了面向对象的开闭原则(对扩展开放,对修改关闭) Spring AOP,这样额外引入了Spring框架,而且Caffeine中涉及查询方法太多,可能需要定义特别多的切点,比较麻烦 装饰模式,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


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3