Skip to content

Instantly share code, notes, and snippets.

@quells
Created November 17, 2019 21:36
Show Gist options
  • Save quells/91d9be652562dd12cf6060668d78d076 to your computer and use it in GitHub Desktop.
Save quells/91d9be652562dd12cf6060668d78d076 to your computer and use it in GitHub Desktop.
Very simple in-memory thread safe cache
package me.kaiwells.cache;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.commons.lang3.SerializationUtils;
import org.springframework.stereotype.Component;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Component
public class Cache {
private static final Cache INSTANCE = new Cache();
private final ConcurrentHashMap<Serializable, Value> cache;
/**
* 1 => Maximum parallel access
* Long.MAX_VALUE => Serial access
*/
private final Long parallelismThreshold;
private final ConcurrentHashMap<Serializable, AtomicLong> hits;
private final ConcurrentHashMap<Serializable, AtomicLong> misses;
@Data
@AllArgsConstructor
private static class Value implements Serializable {
private Instant eol;
private Serializable value;
}
private Cache() {
cache = new ConcurrentHashMap<>();
parallelismThreshold = Long.MAX_VALUE;
hits = new ConcurrentHashMap<>();
misses = new ConcurrentHashMap<>();
}
private Optional<Serializable> _put(Serializable key, Serializable value, Long ttl) {
Instant now = Instant.now();
Value newValue = new Value(now.plusSeconds(ttl), value);
return Optional.ofNullable(cache.put(key, newValue))
.filter(oldValue -> oldValue.getEol().isAfter(now))
.map(Value::getValue);
}
/**
* Upsert the value for a key. If a value was already present and has not expired, it is returned.
*
* @param key Key used to retrieve object later.
* @param value Object stored for key.
* @param ttl Time for object to live in seconds.
* @return Object previously stored for key, if any and not expired.
*/
public static Optional<Serializable> put(@NotNull Serializable key, @NotNull Serializable value, @NotNull Long ttl) {
return INSTANCE._put(key, value, ttl);
}
private Optional<Serializable> _get(Serializable key) {
Value v = cache.get(key);
if (v == null) {
Optional.ofNullable(misses.get(key))
.orElseGet(() -> {
AtomicLong m = new AtomicLong();
misses.put(key, m);
return m;
})
.incrementAndGet();
return Optional.empty();
}
Instant now = Instant.now();
if (!v.getEol().isAfter(now)) {
Optional.ofNullable(misses.get(key))
.orElseGet(() -> {
AtomicLong m = new AtomicLong();
misses.put(key, m);
return m;
})
.incrementAndGet();
cache.remove(key);
return Optional.empty();
}
Optional.ofNullable(hits.get(key))
.orElseGet(() -> {
AtomicLong h = new AtomicLong();
hits.put(key, h);
return h;
})
.incrementAndGet();
return Optional.of(v.getValue());
}
/**
* Get the current value for a key, if it exists and has not expired.
*
* If it has expired, it is removed.
*/
public static Optional<Serializable> get(@NotNull Serializable key) {
return INSTANCE._get(key);
}
@Data
@AllArgsConstructor
public static final class Snapshot implements Serializable {
private Map<Serializable, Long> hits;
private Map<Serializable, Long> misses;
private Map<Serializable, Value> cache;
}
/**
* Get a snapshot of the current state of live objects in the cache as well as historical hit/miss counts.
*/
public static Snapshot getSnapshot() {
Map<Serializable, Value> snapshot = new HashMap<>();
Instant now = Instant.now();
INSTANCE.cache.forEachEntry(INSTANCE.parallelismThreshold, entry -> {
if (entry.getValue().getEol().isAfter(now)) {
snapshot.put(
SerializationUtils.clone(entry.getKey()),
SerializationUtils.clone(entry.getValue()));
}
});
Map<Serializable, Long> hits = new HashMap<>();
INSTANCE.hits.forEach((key, count) -> hits.put(key, count.get()));
Map<Serializable, Long> misses = new HashMap<>();
INSTANCE.misses.forEach((key, count) -> misses.put(key, count.get()));
return new Snapshot(hits, misses, snapshot);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment