diff --git a/orm/src/main/java/com/zfoo/orm/util/LazyCache.java b/orm/src/main/java/com/zfoo/orm/util/LazyCache.java new file mode 100644 index 00000000..5caee407 --- /dev/null +++ b/orm/src/main/java/com/zfoo/orm/util/LazyCache.java @@ -0,0 +1,122 @@ +package com.zfoo.orm.util; + +import com.zfoo.scheduler.util.TimeUtils; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiConsumer; + +/** + * lazy detect the expiration time + * + * @author godotg + */ +public class LazyCache { + + private static class ValueCache { + public volatile V value; + public volatile long expireTime; + } + + + private AtomicLong expireCheckTimeAtomic; + private volatile long minExpireTime; + private long expireAfterAccessMillis; + private long expireCheckInterval; + private int maximumSize; + private ConcurrentMap> cacheMap; + private BiConsumer removeCallback = (k, v) -> { + }; + + public LazyCache(int maximumSize, long expireAfterAccessMillis, long expireCheckIntervalMillis, BiConsumer removeCallback) { + this.maximumSize = maximumSize; + this.expireAfterAccessMillis = expireAfterAccessMillis; + this.expireCheckInterval = expireCheckIntervalMillis; + this.expireCheckTimeAtomic = new AtomicLong(TimeUtils.now() + expireCheckInterval); + this.cacheMap = new ConcurrentHashMap<>(Math.max(maximumSize / 16, 512)); + if (removeCallback != null) { + this.removeCallback = removeCallback; + } + } + + /** + * If the cache previously contained a value associated with the key, the old value is replaced by the new value. + */ + public void put(K key, V value) { + var valueCache = new ValueCache(); + valueCache.value = value; + valueCache.expireTime = TimeUtils.now(); + cacheMap.put(key, valueCache); + checkMaximumSize(); + checkExpire(); + } + + public V get(K key) { + checkExpire(); + + var valueCache = cacheMap.get(key); + if (valueCache == null) { + return null; + } + if (valueCache.expireTime < TimeUtils.now()) { + remove(key); + return null; + } + valueCache.expireTime = TimeUtils.now() + expireAfterAccessMillis; + return valueCache.value; + } + + + public void remove(K key) { + if (key == null) { + return; + } + var valueCache = cacheMap.remove(key); + if (valueCache != null) { + removeCallback.accept(key, valueCache.value); + } + } + + + // ----------------------------------------------------------------------------------------------------------------- + private void checkMaximumSize() { + if (cacheMap.size() <= maximumSize) { + return; + } + K minKey = null; + var minTimestamp = Long.MAX_VALUE; + for (var entry : cacheMap.entrySet()) { + if (entry.getValue().expireTime < minTimestamp) { + minKey = entry.getKey(); + minTimestamp = entry.getValue().expireTime; + } + } + this.minExpireTime = minTimestamp; + remove(minKey); + checkMaximumSize(); + } + + private void checkExpire() { + var now = TimeUtils.now(); + var expireCheckTime = expireCheckTimeAtomic.get(); + if (now > expireCheckTime) { + if (expireCheckTimeAtomic.compareAndSet(expireCheckTime, now + expireCheckInterval)) { + if (now > this.minExpireTime) { + var minTimestamp = Long.MAX_VALUE; + for (var entry : cacheMap.entrySet()) { + var expireTime = entry.getValue().expireTime; + if (expireTime < now) { + remove(entry.getKey()); + } + if (expireTime < minTimestamp) { + minTimestamp = expireTime; + } + } + this.minExpireTime = minTimestamp; + } + } + } + } + +} diff --git a/orm/src/test/java/com/zfoo/orm/cache/LazyCacheTest.java b/orm/src/test/java/com/zfoo/orm/cache/LazyCacheTest.java new file mode 100644 index 00000000..aecc8b45 --- /dev/null +++ b/orm/src/test/java/com/zfoo/orm/cache/LazyCacheTest.java @@ -0,0 +1,67 @@ +package com.zfoo.orm.cache; + +import com.zfoo.orm.util.LazyCache; +import com.zfoo.protocol.util.StringUtils; +import com.zfoo.protocol.util.ThreadUtils; +import com.zfoo.scheduler.util.TimeUtils; +import org.junit.Ignore; +import org.junit.Test; + +import java.util.function.BiConsumer; + +/** + * @author godotg + */ +@Ignore +public class LazyCacheTest { + + private static final BiConsumer myRemoveCallback = new BiConsumer() { + @Override + public void accept(Integer key, String value) { + System.out.println(StringUtils.format("remove key:[{}] value:[{}]", key, value)); + } + }; + + @Test + public void putTest() { + var lazyCache = new LazyCache(3, 10 * TimeUtils.MILLIS_PER_SECOND, 5 * TimeUtils.MILLIS_PER_SECOND, myRemoveCallback); + + lazyCache.put(1, "a"); + lazyCache.put(2, "b"); + ThreadUtils.sleep(1000); + lazyCache.put(3, "c"); + ThreadUtils.sleep(1000); + lazyCache.put(4, "d"); + ThreadUtils.sleep(1000); + lazyCache.put(5, "e"); + } + + @Test + public void expireTest() { + var lazyCache = new LazyCache(10, 10 * TimeUtils.MILLIS_PER_SECOND, 5 * TimeUtils.MILLIS_PER_SECOND, myRemoveCallback); + + lazyCache.put(1, "a"); + lazyCache.put(2, "b"); + lazyCache.put(3, "c"); + lazyCache.put(4, "d"); + lazyCache.put(5, "e"); + ThreadUtils.sleep(11 * TimeUtils.MILLIS_PER_SECOND); + System.out.println(lazyCache.get(1)); + System.out.println(lazyCache.get(2)); + System.out.println(lazyCache.get(3)); + System.out.println(lazyCache.get(4)); + System.out.println(lazyCache.get(5)); + } + + @Test + public void batchTest() { + var lazyCache = new LazyCache(1_0000, 10 * TimeUtils.MILLIS_PER_SECOND, 5 * TimeUtils.MILLIS_PER_SECOND, myRemoveCallback); + + for (int i = 0; i < 1000_0000; i++) { + lazyCache.put(i, String.valueOf(i)); + if (i % 1_0000 == 0) { + ThreadUtils.sleep(5 * TimeUtils.MILLIS_PER_SECOND); + } + } + } +}