Created
November 17, 2019 21:36
-
-
Save quells/91d9be652562dd12cf6060668d78d076 to your computer and use it in GitHub Desktop.
Very simple in-memory thread safe cache
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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