2

I have Caffeine cache with Key->Value mapping. There are multiple implementations of Key interface with different equals methods. In order to delete value from cache based on someOtherVal, I had to use code like cache.asMap().keySet().removeIf(comp::isSame) which is super slow.

Is there any other solution for this kind of many keys to single value mapping in cache? One thing that comes to my mind is to have 2 Cache instances, one with Cache<Key, String> and other with Cache<someOtherVal, Key>, and whenever I want to delete a value I locate Key using this other cache.

Then only question is how to keep this 2 caches in sync? Are there already solutions for this?

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());
        }
    }
}

Result of running this code on my machine:

Slow removal in PT2.807105177S
Fast removal in PT0.000126183S

where you can see such a big difference...

Bojan Vukasovic
  • 2,054
  • 22
  • 43

1 Answers1

3

Ok, I managed to solve this:

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);
        }
    }

}

and this is use-case:

@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;

    }

here is link to discussion I had: https://github.com/ben-manes/caffeine/issues/279

Bojan Vukasovic
  • 2,054
  • 22
  • 43