com.github.netty.core.util.ExpiryLRUMap Maven / Gradle / Ivy
package com.github.netty.core.util;
import java.lang.ref.Reference;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.*;
/**
* 定时过期Map 会自动过期删除
*
* 支持项 : 1.定时过期(过期事件通知) {@link #setOnExpiryConsumer(Consumer)} {@link #onExpiry(Node)}
* 2. LRU淘汰机制(淘汰事件通知) {@link #setOnEvictionConsumer(Consumer)} {@link #onEviction(Node)}
* 3. Map操作
* 4. 并发操作(线程安全) {@link ConcurrentMap}
* 5. gc回收 Reference(Weak,Soft,strong). {@link #ExpiryLRUMap(int, long, long, ConcurrentLinkedHashMap.Weigher, Class)}
* 6. 统计功能(miss, hit) {@link #getHitCount()} {@link #getMissCount()}
* 7. null值替换, 防止缓存击穿 {@link #setReplaceNullValueFlag(boolean)} {@link #NULL} if(data == ExpiryLRUMap.NULL)
*
* 常用场景 : localCache
*
* @author wangzihao
*/
public class ExpiryLRUMap extends AbstractMap implements ConcurrentMap {
public static final Object NULL = new Object() {
@Override
public String toString() {
return "ExpiryLRUMap.NULL";
}
};
private static final ConcurrentSkipListSet NO_EXPIRY_NODES = new ConcurrentSkipListSet<>((o1, o2) -> {
if (o1 == o2) {
return 0;
}
long x = o1.getExpiryTimestamp();
long y = o2.getExpiryTimestamp();
return x <= y ? -1 : 1;
});
private static final BlockingQueue> EXPIRY_NOTIFY_QUEUE = new LinkedBlockingQueue<>();
private static final Set> INSTANCE_SET = Collections.newSetFromMap(new WeakHashMap<>());
private static volatile ScheduledFuture> SCHEDULED_FUTURE;
private final transient LongAdder missCount = new LongAdder();
private final transient LongAdder hitCount = new LongAdder();
private final ConcurrentLinkedHashMap> map;
private long defaultExpiryTime;
/**
* null值替换, 防止缓存提击穿. 需要设置成true后, 取值后需要判断是否 data == ExpiryLRUMap.NULL
*/
private boolean replaceNullValueFlag = false;
private transient Collection values;
private transient EntrySet entrySet;
/**
* 超过时间的 过期通知
*/
private transient volatile Consumer> onExpiryConsumer = this::onExpiry;
/**
* 超过上限的 淘汰通知
*/
private transient volatile Consumer> onEvictionConsumer = this::onEviction;
/**
* 用户主动删除 删除通知
*/
private transient volatile Consumer> onRemoveConsumer = this::onRemove;
/**
* 默认永不过期 (相当于普通的 ConcurrentMap)
*/
public ExpiryLRUMap() {
this(Long.MAX_VALUE);
}
public ExpiryLRUMap(long defaultExpiryTime) {
this(256, Long.MAX_VALUE, defaultExpiryTime, null);
}
public ExpiryLRUMap(int initialCapacity, long maxCacheSize, long defaultExpiryTime, ConcurrentLinkedHashMap.Weigher> weigher) {
this(initialCapacity, maxCacheSize, defaultExpiryTime, weigher, null);
}
/**
* @param initialCapacity initialCapacity
* @param maxCacheSize maxCacheSize
* @param defaultExpiryTime defaultExpiryTime
* @param weigher weigher
* @param referenceType null is FinalReference.
* else if {@link java.lang.ref.WeakReference}
* else if {@link java.lang.ref.SoftReference}
*/
public ExpiryLRUMap(int initialCapacity, long maxCacheSize, long defaultExpiryTime, ConcurrentLinkedHashMap.Weigher> weigher, Class extends Reference> referenceType) {
this.defaultExpiryTime = defaultExpiryTime < 0 ? -1 : defaultExpiryTime;
this.map = new ConcurrentLinkedHashMap.Builder>()
.initialCapacity(initialCapacity)
.maximumWeightedCapacity(maxCacheSize)
.referenceType(referenceType)
.weigher(weigher == null ? ConcurrentLinkedHashMap.Weighers.singleton() : weigher)
.listener((key, value) -> {
Consumer> onEvictionConsumer = ExpiryLRUMap.this.onEvictionConsumer;
if (onEvictionConsumer != null) {
onEvictionConsumer.accept(value);
}
})
// .catchup()
.build();
//init static block. thread scheduled.
synchronized (INSTANCE_SET) {
INSTANCE_SET.add(this);
if (SCHEDULED_FUTURE == null) {
SCHEDULED_FUTURE = ExpiresScan.scheduleWithFixedDelay();
}
}
}
public static Set getInstanceSet() {
return Collections.unmodifiableSet(INSTANCE_SET);
}
public static BlockingQueue> getExpiryNotifyQueue() {
return EXPIRY_NOTIFY_QUEUE;
}
public static boolean isExpiry(Node node) {
if (node.expiryTimestamp == Long.MAX_VALUE) {
return false;
}
long currentTime = System.currentTimeMillis();
return currentTime > node.expiryTimestamp;
}
private static void localVarTest() throws InterruptedException {
ExpiryLRUMap lruMap = new ExpiryLRUMap();
lruMap.put("data3", "1233", -1);
lruMap.put("data", "123", 5000);
System.out.println("初始化 new ExpiryLRUMap() set = " + ExpiryLRUMap.INSTANCE_SET);
while (true) {
Object aa = lruMap.get("data");
System.out.println("data = " + aa);
Thread.sleep(1000);
if (aa == null) {
return;
}
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("set = " + ExpiryLRUMap.INSTANCE_SET);
// localVarTest();
System.out.println("gc 前 set = " + ExpiryLRUMap.INSTANCE_SET);
System.gc();
System.out.println("gc 后 set = " + ExpiryLRUMap.INSTANCE_SET);
int i = 0;
ExpiryLRUMap expiryLRUMap = new ExpiryLRUMap<>(1, 3333,
Integer.MAX_VALUE, null
);
for (int j = 0; j < 100; j++) {
expiryLRUMap.put(j + "", j);
}
for (int j = 1000; j > 0; j--) {
expiryLRUMap.put(j + "", j);
}
System.out.println("expiryLRUMap = " + expiryLRUMap);
expiryLRUMap.setOnEvictionConsumer(node -> {
long expiry = node.getExpiryTimestamp() - node.getCreateTimestamp();
long timeout = System.currentTimeMillis() - node.getCreateTimestamp();
System.out.println("eviction event. expiry = " + expiry + ", timeout=" + timeout);
});
expiryLRUMap.setOnExpiryConsumer(node -> {
long expiry = node.getExpiryTimestamp() - node.getCreateTimestamp();
long timeout = System.currentTimeMillis() - node.getCreateTimestamp();
System.out.println("expiry event. expiry = " + expiry + ", timeout=" + timeout);
});
while (i++ < 3) {
expiryLRUMap.put(i + "", i, 1000);
Set> set = ExpiryLRUMap.INSTANCE_SET;
System.out.println("set = " + ExpiryLRUMap.INSTANCE_SET);
}
System.gc();
}
public long weightedSize() {
return map.weightedSize();
}
public long getMaxCacheSize() {
return map.capacity();
}
public void setMaxCacheSize(long maxCacheSize) {
this.map.setCapacity(maxCacheSize);
}
public Consumer> getOnEvictionConsumer() {
return onEvictionConsumer;
}
public void setOnEvictionConsumer(Consumer> onEvictionConsumer) {
this.onEvictionConsumer = onEvictionConsumer;
}
public long getMissCount() {
return missCount.sum();
}
public long getHitCount() {
return hitCount.sum();
}
public Consumer> getOnExpiryConsumer() {
return onExpiryConsumer;
}
public void setOnExpiryConsumer(Consumer> onExpiryConsumer) {
this.onExpiryConsumer = onExpiryConsumer;
}
public void setOnRemoveConsumer(Consumer> onRemoveConsumer) {
this.onRemoveConsumer = onRemoveConsumer;
}
public boolean isReplaceNullValueFlag() {
return replaceNullValueFlag;
}
public void setReplaceNullValueFlag(boolean replaceNullValueFlag) {
this.replaceNullValueFlag = replaceNullValueFlag;
}
@Override
public V put(K key, V value) {
return put(key, value, defaultExpiryTime);
}
/**
* @param key key
* @param value value
* @param timeout 键值对有效期 毫秒(Long.MAX_VALUE 表示永不过期)
* @return 旧值
*/
public V put(K key, V value, long timeout) {
if (value == null) {
value = (V) NULL;
}
Node old = map.get(key);
Node node = new Node<>(timeout, key, value, this);
if (old != null) {
synchronized (old) {
old.covered = true;
map.put(key, node);
return old.getData();
}
} else {
map.put(key, node);
return null;
}
}
@Override
public boolean containsKey(Object key) {
return map.containsKey(key);
}
@Override
public int size() {
return map.size();
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public boolean containsValue(Object value) {
for (Node node : map.values()) {
if (Objects.equals(node.getData(), value)) {
return true;
}
}
return false;
}
@Override
public V remove(Object key) {
Node old = map.remove(key);
if (old == null) {
return null;
} else {
notifyRemove(old);
return old.getData();
}
}
public void notifyRemove(Node node) {
Consumer> onRemoveConsumer = this.onRemoveConsumer;
if (onRemoveConsumer != null) {
onRemoveConsumer.accept(node);
}
}
public V atomicGet(K key, Supplier supplier) {
/* todo atomicGet */
Node old = map.get(key);
if (old == null) {
missCount.increment();
V value = supplier.get();
put(key, value);
return value;
} else {
hitCount.increment();
return old.getData();
}
}
@Override
public V get(Object key) {
Node old = map.get(key);
if (old == null) {
missCount.increment();
return null;
} else {
hitCount.increment();
return old.getDataIfExpiry();
}
}
@Override
public Set keySet() {
return map.keySet();
}
@Override
public void clear() {
map.clear();
}
@Override
public Collection values() {
Collection vs = values;
if (vs == null) {
vs = values = new Values(map.values());
}
return vs;
}
@Override
public Set> entrySet() {
Set> es = entrySet;
if (entrySet == null) {
es = entrySet = new EntrySet(map.entrySet());
}
return es;
}
@Override
public V getOrDefault(Object key, V defaultValue) {
V v;
return ((v = get(key)) != null) ? v : defaultValue;
}
@Override
public void forEach(BiConsumer super K, ? super V> action) {
Objects.requireNonNull(action);
for (Entry entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch (IllegalStateException ise) {
// this usually means the entry is no longer in the map.
continue;
}
action.accept(k, v);
}
}
@Override
public boolean remove(Object key, Object value) {
Node old = map.get(key);
if (old != null && Objects.equals(old.getData(), value)) {
map.remove(key, old);
notifyRemove(old);
return true;
}
return false;
}
@Override
public V replace(K key, V newValue) {
Node old = map.get(key);
if (old != null) {
map.put(key, new Node<>(old.expiryTime, key, newValue, this));
return old.getData();
}
return null;
}
@Override
public boolean replace(K key, V oldValue, V newValue) {
Node old = map.get(key);
if (old != null && Objects.equals(old.getData(), oldValue)) {
map.put(key, new Node<>(old.expiryTime, key, newValue, this));
return true;
}
return false;
}
@Override
public void replaceAll(BiFunction super K, ? super V, ? extends V> function) {
Objects.requireNonNull(function);
for (Entry> entry : map.entrySet()) {
Node old = entry.getValue();
K key = entry.getKey();
V value = old.getData();
V newValue = function.apply(key, value);
entry.setValue(new Node<>(old.expiryTime, key, newValue, this));
}
}
@Override
public V computeIfAbsent(K key, Function super K, ? extends V> mappingFunction) {
Objects.requireNonNull(mappingFunction);
V v, newValue;
return ((v = get(key)) == null &&
(newValue = mappingFunction.apply(key)) != null &&
(v = putIfAbsent(key, newValue)) == null) ? newValue : v;
}
@Override
public V computeIfPresent(K key, BiFunction super K, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
V oldValue;
while ((oldValue = get(key)) != null) {
V newValue = remappingFunction.apply(key, oldValue);
if (newValue != null) {
if (replace(key, oldValue, newValue)) {
return newValue;
}
} else if (remove(key, oldValue)) {
return null;
}
}
return oldValue;
}
@Override
public V compute(K key, BiFunction super K, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
V oldValue = get(key);
for (; ; ) {
V newValue = remappingFunction.apply(key, oldValue);
if (newValue == null) {
// delete mapping
if (oldValue != null || containsKey(key)) {
// something to remove
if (remove(key, oldValue)) {
// removed the old value as expected
return null;
}
// some other value replaced old value. try again.
oldValue = get(key);
} else {
// nothing to do. Leave things as they were.
return null;
}
} else {
// add or replace old mapping
if (oldValue != null) {
// replace
if (replace(key, oldValue, newValue)) {
// replaced as expected.
return newValue;
}
// some other value replaced old value. try again.
oldValue = get(key);
} else {
// add (replace if oldValue was null)
if ((oldValue = putIfAbsent(key, newValue)) == null) {
// replaced
return newValue;
}
// some other value replaced old value. try again.
}
}
}
}
@Override
public V merge(K key, V value, BiFunction super V, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
Objects.requireNonNull(value);
V oldValue = get(key);
for (; ; ) {
if (oldValue != null) {
V newValue = remappingFunction.apply(oldValue, value);
if (newValue != null) {
if (replace(key, oldValue, newValue)) {
return newValue;
}
} else if (remove(key, oldValue)) {
return null;
}
oldValue = get(key);
} else {
if ((oldValue = putIfAbsent(key, value)) == null) {
return value;
}
}
}
}
@Override
public V putIfAbsent(K key, V value) {
V v = get(key);
if (v == null) {
v = put(key, value);
}
return v;
}
public long getDefaultExpiryTime() {
return defaultExpiryTime;
}
public void setDefaultExpiryTime(long defaultExpiryTime) {
this.defaultExpiryTime = defaultExpiryTime;
}
@Override
public String toString() {
synchronized (map) {
Iterator>> i = map.entrySet().iterator();
if (!i.hasNext()) {
return "{}";
}
long currentTime = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
sb.append('{');
for (; ; ) {
Entry> e = i.next();
K key = e.getKey();
Node node = e.getValue();
V value = node.getDataIfExpiry();
long timeout = node.expiryTimestamp;
sb.append(key == this ? "(this Map)" : key);
sb.append('=');
sb.append(value == this ? "(this Map)" : value);
sb.append('|');
sb.append((timeout - currentTime) / 1000);
sb.append("/s");
if (!i.hasNext()) {
return sb.append('}').toString();
}
sb.append(',').append(' ');
}
}
}
public void onExpiry(Node node) {
}
public void onEviction(Node node) {
}
public void onRemove(Node node) {
}
@Override
public boolean equals(Object o) {
return this == o;
}
@Override
public int hashCode() {
return System.identityHashCode(this);
}
public static class Node {
private final ExpiryLRUMap expiryLRUMap;
private final long createTimestamp = System.currentTimeMillis();
private final long expiryTimestamp;
private final long expiryTime;
private final VALUE data;
private final KEY key;
/**
* 是否被put方法覆盖
*/
private volatile boolean covered = false;
Node(long timeout, KEY key, VALUE value, ExpiryLRUMap expiryLRUMap) {
long expiryTimestamp;
this.expiryTime = timeout;
if (timeout == Long.MAX_VALUE || timeout < 0) {
expiryTimestamp = Long.MAX_VALUE;
} else {
expiryTimestamp = System.currentTimeMillis() + timeout;
//如果算数溢出
if (expiryTimestamp < 0) {
expiryTimestamp = Long.MAX_VALUE;
}
}
this.expiryTimestamp = expiryTimestamp;
this.key = key;
this.data = value;
this.expiryLRUMap = expiryLRUMap;
if (expiryLRUMap != null && expiryTimestamp != Long.MAX_VALUE) {
NO_EXPIRY_NODES.add(this);
}
}
public boolean isCovered() {
return covered;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public boolean equals(Object obj) {
return super.equals(obj);
}
public ExpiryLRUMap getExpiryLRUMap() {
return expiryLRUMap;
}
public long getExpiryTime() {
return expiryTime;
}
public VALUE getDataIfExpiry() {
if (isExpiry()) {
return null;
} else {
return getData();
}
}
public VALUE getData() {
if (expiryLRUMap.isReplaceNullValueFlag()) {
return this.data;
} else {
return this.data == NULL ? null : this.data;
}
}
public KEY getKey() {
return key;
}
public long getExpiryTimestamp() {
return expiryTimestamp;
}
public long getCreateTimestamp() {
return createTimestamp;
}
public boolean isExpiry() {
return ExpiryLRUMap.isExpiry(this);
}
@Override
public String toString() {
VALUE data = getData();
return data == null ? "null" : data.toString();
}
}
public static class ExpiresNotify extends Thread {
@Override
public void run() {
while (true) {
ExpiryLRUMap.Node node;
try {
node = EXPIRY_NOTIFY_QUEUE.take();
} catch (InterruptedException e) {
return;
}
Consumer consumer = node.expiryLRUMap.onExpiryConsumer;
if (consumer != null) {
try {
consumer.accept(node);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
public static class ExpiresScan implements Runnable {
public static final ExpiresNotify NOTIFY_INSTANCE = new ExpiresNotify();
static final ScheduledExecutorService SCHEDULED = Executors.newScheduledThreadPool(1, runnable -> {
Thread thread = new Thread(runnable);
thread.setDaemon(true);
thread.setName("ExpiryLRUMap-ExpiresScan-" + thread.getId());
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
});
private static final ExpiresScan INSTANCE = new ExpiresScan();
static {
NOTIFY_INSTANCE.setDaemon(true);
NOTIFY_INSTANCE.setName("ExpiryLRUMap-ExpiresNotify-" + NOTIFY_INSTANCE.getId());
NOTIFY_INSTANCE.start();
}
private final Consumer> removeConsumer = EXPIRY_NOTIFY_QUEUE::offer;
static long getScheduleInterval() {
String intervalMillisecond = System.getProperty("ExpiryLRUMap-ExpiresScan.interval");
long intervalLong = 100;
if (intervalMillisecond != null && !intervalMillisecond.isEmpty()) {
try {
intervalLong = Long.parseLong(intervalMillisecond);
} catch (Exception e) {
//skip
}
}
return intervalLong;
}
public static ScheduledFuture> scheduleWithFixedDelay() {
long intervalLong = getScheduleInterval();
return SCHEDULED.scheduleWithFixedDelay(INSTANCE, intervalLong, intervalLong, TimeUnit.MILLISECONDS);
}
@Override
public void run() {
if (INSTANCE_SET.isEmpty()) {
synchronized (INSTANCE_SET) {
if (INSTANCE_SET.isEmpty()) {
NO_EXPIRY_NODES.clear();
ScheduledFuture> scheduledFuture = SCHEDULED_FUTURE;
scheduledFuture.cancel(false);
SCHEDULED_FUTURE = null;
}
}
return;
}
if (NO_EXPIRY_NODES.isEmpty()) {
return;
}
Node now = new Node<>(0, null, null, null);
NavigableSet expiryNodes = NO_EXPIRY_NODES.headSet(now);
if (expiryNodes.isEmpty()) {
return;
}
Iterator iterator = expiryNodes.iterator();
while (iterator.hasNext()) {
Node expiryRemoveNode = iterator.next();
synchronized (expiryRemoveNode) {
boolean remove = expiryRemoveNode.getExpiryLRUMap().map.remove(expiryRemoveNode.getKey(), expiryRemoveNode);
if (!remove && !expiryRemoveNode.covered) {
continue;
}
}
try {
iterator.remove();
removeConsumer.accept(expiryRemoveNode);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
class Values extends AbstractCollection {
private final Collection> values;
Values(Collection> values) {
this.values = values;
}
@Override
public Iterator iterator() {
return new Iterator() {
private Iterator> iterator = values.iterator();
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public V next() {
return iterator.next().getDataIfExpiry();
}
@Override
public void remove() {
iterator.remove();
}
};
}
@Override
public int size() {
return values.size();
}
}
class EntrySet extends AbstractSet> {
private final Set>> entries;
EntrySet(Set>> entries) {
this.entries = entries;
}
@Override
public Iterator> iterator() {
return new Iterator>() {
private Iterator>> iterator = entries.iterator();
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public Entry next() {
Entry> next = iterator.next();
K key = next.getKey();
return new Entry() {
@Override
public K getKey() {
return key;
}
@Override
public V getValue() {
return next.getValue().getDataIfExpiry();
}
@Override
public V setValue(V value) {
Node old = next.getValue();
if (old == null) {
return null;
} else {
next.setValue(new Node<>(old.expiryTime, key, value, ExpiryLRUMap.this));
return old.getData();
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Map.Entry)) {
return false;
}
Entry node = (Entry) o;
return Objects.equals(getKey(), node.getKey()) &&
Objects.equals(getValue(), node.getValue());
}
@Override
public int hashCode() {
return Objects.hash(getKey(), getValue());
}
};
}
@Override
public void remove() {
iterator.remove();
}
};
}
@Override
public int size() {
return entries.size();
}
}
}