咖啡因缓存 - 单值的许多键
Caffeine cache - many keys to single value
我有带有 Key->Value 映射的 Caffeine 缓存。 Key
接口有多个实现,具有不同的 equals
方法。为了从基于 someOtherVal
的缓存中删除值,我不得不使用像 cache.asMap().keySet().removeIf(comp::isSame)
这样超级慢的代码。
对于缓存中这种多键到单值的映射,还有其他解决方案吗?我想到的一件事是有 2 个缓存实例,一个带有 Cache<Key, String>
,另一个带有 Cache<someOtherVal, Key>
,每当我想删除一个值时,我都会使用另一个缓存找到 Key。
那么唯一的问题是如何让这两个缓存保持同步?是否已有解决方案?
import java.time.Duration;
import java.util.Objects;
import java.util.UUID;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.base.Stopwatch;
public class Removal {
private static final int MAX = 1_000_000;
interface Key{
String getSomeOtherVal();
default boolean isSame(Key k){
return Objects.equals(k.getSomeOtherVal(),getSomeOtherVal());
}
}
static class KeyImpl implements Key{
int id;
String someOtherVal;
public KeyImpl(int id, String someOtherVal) {
this.id = id;
this.someOtherVal = someOtherVal;
}
public int getId() {
return id;
}
@Override
public String getSomeOtherVal() {
return someOtherVal;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
KeyImpl key = (KeyImpl)o;
return id == key.id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
Cache<Key, String> cache = Caffeine.newBuilder().build();
public static void main(String[] args) {
Removal s = new Removal();
s.fill();
Duration sRem = s.slowRemovalFirst100();
Duration fRem = s.fastRemoval100To200();
System.out.println("Slow removal in " + sRem);
System.out.println("Fast removal in " + fRem);
}
private Duration slowRemovalFirst100(){
Stopwatch sw = Stopwatch.createStarted();
for(int i=0; i<100; i++){
Key comp = new KeyImpl(i, String.valueOf(i));
cache.asMap().keySet().removeIf(comp::isSame); //Finds a key by some other property and then removes it (SLOW)
//System.out.println("Removed " + i);
}
return sw.stop().elapsed();
}
private Duration fastRemoval100To200(){
Stopwatch sw = Stopwatch.createStarted();
for(int i=100; i<200; i++){
Key comp = new KeyImpl(i, String.valueOf(i));
cache.invalidate(comp); //Uses direct access to map by key (FAST)
//System.out.println("Removed " + i);
}
return sw.stop().elapsed();
}
private void fill(){
for(int i=0; i<MAX; i++){
cache.put(new KeyImpl(i, String.valueOf(i)), UUID.randomUUID().toString());
}
}
}
运行 此代码在我的机器上的结果:
Slow removal in PT2.807105177S
Fast removal in PT0.000126183S
哪里可以看出这么大的区别...
好的,我设法解决了这个问题:
public class IndexedCache<K,V> implements Cache<K,V> {
@Delegate
private Cache<K, V> cache;
private Map<Class<?>, Map<Object, Set<K>>> indexes;
private IndexedCache(Builder<K, V> bldr){
this.indexes = bldr.indexes;
cache = bldr.caf.build();
}
public <R> void invalidateAllWithIndex(Class<R> clazz, R value) {
cache.invalidateAll(indexes.get(clazz).getOrDefault(value, new HashSet<>()));
}
public static class Builder<K, V>{
Map<Class<?>, Function<K, ?>> functions = new HashMap<>();
Map<Class<?>, Map<Object, Set<K>>> indexes = new ConcurrentHashMap<>();
Caffeine<K,V> caf;
public <R> Builder<K,V> withIndex(Class<R> clazz, Function<K, R> function){
functions.put(clazz, function);
indexes.put(clazz, new ConcurrentHashMap<>());
return this;
}
public IndexedCache<K, V> buildFromCaffeine(Caffeine<Object, Object> caffeine) {
caf = caffeine.writer(new CacheWriter<K, V>() {
@Override
public void write( K k, V v) {
for(Map.Entry<Class<?>, Map<Object, Set<K>>> indexesEntry : indexes.entrySet()){
indexesEntry.getValue().computeIfAbsent(functions.get(indexesEntry.getKey()).apply(k), (ky)-> new HashSet<>())
.add(k);
}
}
@Override
public void delete( K k, V v, RemovalCause removalCause) {
for(Map.Entry<Class<?>, Map<Object, Set<K>>> indexesEntry : indexes.entrySet()){
indexesEntry.getValue().remove(functions.get(indexesEntry.getKey()).apply(k));
}
}
});
return new IndexedCache<>(this);
}
}
}
这是用例:
@AllArgsConstructor
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
static class CompositeKey{
@EqualsAndHashCode.Include
Integer k1;
String k2;
Long k3;
}
public static void main(String[] args) {
Caffeine<Object, Object> cfein = Caffeine.newBuilder().softValues().maximumSize(200_000);
IndexedCache<CompositeKey, String> cache = new IndexedCache.Builder<CompositeKey, String>()
.withIndex(Long.class, ck -> ck.getK3())
.withIndex(String.class, ck -> ck.getK2())
.buildFromCaffeine(cfein);
for(int i=0; i<100; i++){
cache.put(new CompositeKey(i, String.valueOf(i), Long.valueOf(i)), "sdfsdf");
}
for(int i=0; i<10; i++){
//use equals method of CompositeKey to do equals comp.
cache.invalidate(new CompositeKey(i, String.valueOf(i), Long.valueOf(i)));
}
for(int i=10; i<20; i++){
//use Long index
cache.invalidateAllWithIndex(Long.class, Long.valueOf(i));
}
for(int i=20; i<30; i++){
//use String index
cache.invalidateAllWithIndex(String.class, String.valueOf(i));
}
int y = 4;
}
这里是 link 我的讨论:https://github.com/ben-manes/caffeine/issues/279
我有带有 Key->Value 映射的 Caffeine 缓存。 Key
接口有多个实现,具有不同的 equals
方法。为了从基于 someOtherVal
的缓存中删除值,我不得不使用像 cache.asMap().keySet().removeIf(comp::isSame)
这样超级慢的代码。
对于缓存中这种多键到单值的映射,还有其他解决方案吗?我想到的一件事是有 2 个缓存实例,一个带有 Cache<Key, String>
,另一个带有 Cache<someOtherVal, Key>
,每当我想删除一个值时,我都会使用另一个缓存找到 Key。
那么唯一的问题是如何让这两个缓存保持同步?是否已有解决方案?
import java.time.Duration;
import java.util.Objects;
import java.util.UUID;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.base.Stopwatch;
public class Removal {
private static final int MAX = 1_000_000;
interface Key{
String getSomeOtherVal();
default boolean isSame(Key k){
return Objects.equals(k.getSomeOtherVal(),getSomeOtherVal());
}
}
static class KeyImpl implements Key{
int id;
String someOtherVal;
public KeyImpl(int id, String someOtherVal) {
this.id = id;
this.someOtherVal = someOtherVal;
}
public int getId() {
return id;
}
@Override
public String getSomeOtherVal() {
return someOtherVal;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
KeyImpl key = (KeyImpl)o;
return id == key.id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
Cache<Key, String> cache = Caffeine.newBuilder().build();
public static void main(String[] args) {
Removal s = new Removal();
s.fill();
Duration sRem = s.slowRemovalFirst100();
Duration fRem = s.fastRemoval100To200();
System.out.println("Slow removal in " + sRem);
System.out.println("Fast removal in " + fRem);
}
private Duration slowRemovalFirst100(){
Stopwatch sw = Stopwatch.createStarted();
for(int i=0; i<100; i++){
Key comp = new KeyImpl(i, String.valueOf(i));
cache.asMap().keySet().removeIf(comp::isSame); //Finds a key by some other property and then removes it (SLOW)
//System.out.println("Removed " + i);
}
return sw.stop().elapsed();
}
private Duration fastRemoval100To200(){
Stopwatch sw = Stopwatch.createStarted();
for(int i=100; i<200; i++){
Key comp = new KeyImpl(i, String.valueOf(i));
cache.invalidate(comp); //Uses direct access to map by key (FAST)
//System.out.println("Removed " + i);
}
return sw.stop().elapsed();
}
private void fill(){
for(int i=0; i<MAX; i++){
cache.put(new KeyImpl(i, String.valueOf(i)), UUID.randomUUID().toString());
}
}
}
运行 此代码在我的机器上的结果:
Slow removal in PT2.807105177S
Fast removal in PT0.000126183S
哪里可以看出这么大的区别...
好的,我设法解决了这个问题:
public class IndexedCache<K,V> implements Cache<K,V> {
@Delegate
private Cache<K, V> cache;
private Map<Class<?>, Map<Object, Set<K>>> indexes;
private IndexedCache(Builder<K, V> bldr){
this.indexes = bldr.indexes;
cache = bldr.caf.build();
}
public <R> void invalidateAllWithIndex(Class<R> clazz, R value) {
cache.invalidateAll(indexes.get(clazz).getOrDefault(value, new HashSet<>()));
}
public static class Builder<K, V>{
Map<Class<?>, Function<K, ?>> functions = new HashMap<>();
Map<Class<?>, Map<Object, Set<K>>> indexes = new ConcurrentHashMap<>();
Caffeine<K,V> caf;
public <R> Builder<K,V> withIndex(Class<R> clazz, Function<K, R> function){
functions.put(clazz, function);
indexes.put(clazz, new ConcurrentHashMap<>());
return this;
}
public IndexedCache<K, V> buildFromCaffeine(Caffeine<Object, Object> caffeine) {
caf = caffeine.writer(new CacheWriter<K, V>() {
@Override
public void write( K k, V v) {
for(Map.Entry<Class<?>, Map<Object, Set<K>>> indexesEntry : indexes.entrySet()){
indexesEntry.getValue().computeIfAbsent(functions.get(indexesEntry.getKey()).apply(k), (ky)-> new HashSet<>())
.add(k);
}
}
@Override
public void delete( K k, V v, RemovalCause removalCause) {
for(Map.Entry<Class<?>, Map<Object, Set<K>>> indexesEntry : indexes.entrySet()){
indexesEntry.getValue().remove(functions.get(indexesEntry.getKey()).apply(k));
}
}
});
return new IndexedCache<>(this);
}
}
}
这是用例:
@AllArgsConstructor
@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
static class CompositeKey{
@EqualsAndHashCode.Include
Integer k1;
String k2;
Long k3;
}
public static void main(String[] args) {
Caffeine<Object, Object> cfein = Caffeine.newBuilder().softValues().maximumSize(200_000);
IndexedCache<CompositeKey, String> cache = new IndexedCache.Builder<CompositeKey, String>()
.withIndex(Long.class, ck -> ck.getK3())
.withIndex(String.class, ck -> ck.getK2())
.buildFromCaffeine(cfein);
for(int i=0; i<100; i++){
cache.put(new CompositeKey(i, String.valueOf(i), Long.valueOf(i)), "sdfsdf");
}
for(int i=0; i<10; i++){
//use equals method of CompositeKey to do equals comp.
cache.invalidate(new CompositeKey(i, String.valueOf(i), Long.valueOf(i)));
}
for(int i=10; i<20; i++){
//use Long index
cache.invalidateAllWithIndex(Long.class, Long.valueOf(i));
}
for(int i=20; i<30; i++){
//use String index
cache.invalidateAllWithIndex(String.class, String.valueOf(i));
}
int y = 4;
}
这里是 link 我的讨论:https://github.com/ben-manes/caffeine/issues/279