装饰模式、泛型、序列化重构Caffeine解决缓存不一致的问题
一、前言
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<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中缓存查询方法做增强处理(主要是对缓存对象进行深拷贝),对目标类的某些方法进行增强的实现方式:
- 直接在目标类的方法上修改,这里Caffeine是三方库,根本没法改,硬要改的话,只能是改它的源代码重新生成jar包,这个包会成为野包,个人感觉此法不妥,这么做了面向对象的开闭原则(对扩展开放,对修改关闭)
- Spring AOP,这样额外引入了Spring框架,而且Caffeine中涉及查询方法太多,可能需要定义特别多的切点,比较麻烦
- 装饰模式,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是缓存的拷贝,类似的调用逻辑不需要做任何的修改,自动实现了增强效果
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;
}
}