реализация неблокирующего кэша Java - PullRequest
0 голосов
/ 04 июня 2019

(обратите внимание, я не могу использовать внешние библиотеки для кэша).

--- это можно сделать с помощью потокового API?---

мне нужно реализовать кеш, у него есть 1 свойство ключа:

Если у кеша запрашивается ключ, которого он не содержит, он должен извлечь данные, используяпредоставленная извне функция, которая считывает данные из другого источника (базы данных или аналогичного).

Я начал создавать базовый код скелета:

public interface ICache<K,V> {
}

interface IDataSource<K,V> {
   void put(K key, V value);
   V get(K key);
}


public class Cache<K,V> implements ICache<K,V> {
   Map<K,V> cache = new HashMap<>();
   IDataSource<K,V> dataSource;


public Cache(IDataSource<K,V> dataSrc) {
    dataSource = dataSrc;
}

//may it change to a future? how it can be done?
public V getAsync(K key) {   
    if (cache.containsKey(key)) {
        return cache.get(key);
    }
    else {
        //do some async op
    }
}
}

Вы можете посоветовать?

Как вы думаете, нужно ли больше функций?

1 Ответ

2 голосов
/ 04 июня 2019

На самом деле то, что вы пишете, является ленивым оценщиком.Вы предоставляете Supplier для значения, не вычисляя его в первый раз.В тот момент, когда кто-то запрашивает ваше значение, вы вычисляете его и возвращаете его, запоминая (кэшируя) его для будущего использования.

Посмотрите на класс Lazy Вавра, он делает именно это (но для одного значения)).Вы можете взять некоторые идеи из того, что это делает, а также некоторые дополнительные служебные методы, такие как проверка того, было ли оно уже вычислено.

https://github.com/vavr-io/vavr/blob/master/vavr/src/main/java/io/vavr/Lazy.java

Другой вариант - просто использовать ConcurrentHashMap.Он предоставляет методы для безопасного (атомарного) обновления значений, если их нет на карте.

Если вы хотите, чтобы они были асинхронными, вам нужно ввести некоторые ExecutorService или использовать CompletableFuture (с вашим собственным * 1015).* или пул потоков по умолчанию, который используется параллельными потоками и т. д.).Например:

public class Cache<K,V> implements ICache<K,V> {
   Map<K,V> cache = new ConcurrentHashMap<>();
   IDataSource<K,V> dataSource;

   public Cache(IDataSource<K,V> dataSrc) {
     dataSource = dataSrc;
   }

   // async non-blocking call
   public CompletableFuture<V> getAsync(K key) {   
     return CompletableFuture.supplyAsync(() -> get(key));
   }

   // blocking call
   public V get(K key) {   
     //computeIfAbsent is atomic and threadsafe, in case multiple CompletableFutures try this in parallel
     return cache.computeIfAbsent(key, (k) -> dataSource.get(k));
   }
}

Если вы также хотите иметь асинхронное прямое обновление кеша и источника данных, вы можете сделать что-то вроде:

  public CompletableFuture<Void> putAsync(K key, V value) {
    return  CompletableFuture.runAsync(() -> {
      synchronized (cache) {
        dataSource.put(key, value);
        cache.put(key, value);
      }
    }
  }

Хотя, честно говоря, я бы не использовал 2 точки входа для обновленияdataSource (кеш и источник данных напрямую).Также трудно сделать этот поток полностью безопасным, не имея synchronized (который блокирует параллельное кэширование, которое полностью исключает параллельное выполнение, даже если ключ другой).

...