扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
Caffeine是一个高性能的 Java 缓存库,底层数据存储采用ConcurrentHashMap
成都创新互联公司主要从事网站设计制作、做网站、网页设计、企业做网站、公司建网站等业务。立足成都服务阳春,10余年网站建设经验,价格优惠、服务专业,欢迎来电咨询建站服务:028-86922220
优点:因为Caffeine面向JDK8,在jdk8中ConcurrentHashMap增加了红黑树,在hash冲突严重时也能有良好的读性能。多线程环境中,不同的key可以并发写,相同的key会加锁,天然的解决了缓存击穿问题和缓存雪崩问题。
缺点:因为底层数据结构是ConcurrentHashMap,所有不能作为分布式缓存,同时如果使用不当,会带来数据不一致的问题
本文主要内容是探讨Caffeine使用不当时,数据一致性安全问题
如下图所示,问题的根源在于A能直接拿到缓存地址的引用,然后通过引用就能随意修改引用指向的缓存对象,要解决这个问题,可以在步骤三这里,将缓存对象深拷贝后的副本的引用返回给客户端,这样客户端对缓存的任何操作,改变的只是副本,缓存本身只能由维护者来更新
实现方式:
因为我们的项目中已经在大量使用Caffeine,方案1和2需要对大量的实体类和缓存获取接口进行大量的改造,工作量巨大,后面采用方案3
后面需要使用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中缓存查询方法做增强处理(主要是对缓存对象进行深拷贝),对目标类的某些方法进行增强的实现方式:
装饰模式类图:
改造前,缓存初始化及缓存查询的实现代码
//缓存初始化
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 extends K> 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;
}
}
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流