From a8add399c260bf5da447e7e3a86491467bc84d8e Mon Sep 17 00:00:00 2001 From: Looly Date: Sun, 24 Jan 2021 22:21:39 +0800 Subject: [PATCH] fix cache --- CHANGELOG.md | 1 + .../src/main/java/cn/hutool/cache/Cache.java | 4 +- .../cn/hutool/cache/impl/AbstractCache.java | 48 +++++++++++-------- .../java/cn/hutool/cache/impl/LFUCache.java | 12 ++--- .../java/cn/hutool/cache/impl/LRUCache.java | 2 +- .../java/cn/hutool/cache/impl/TimedCache.java | 10 ++-- .../java/cn/hutool/cache/impl/WeakCache.java | 4 +- 7 files changed, 45 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77e69aee5..f6a51fd13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * 【core 】 修改上传文件检查逻辑 * 【core 】 修正LocalDateTimeUtil.offset方法注释问题(issue#I2EEXC@Gitee) * 【extra 】 VelocityEngine的getRowEngine改为getRawEngine(issue#I2EGRG@Gitee) +* 【cache 】 缓存降低锁的粒度,提高并发能力(pr#1385@Github) ### Bug修复 * 【core 】 修复FileUtil.move以及PathUtil.copy等无法自动创建父目录的问题(issue#I2CKTI@Gitee) diff --git a/hutool-cache/src/main/java/cn/hutool/cache/Cache.java b/hutool-cache/src/main/java/cn/hutool/cache/Cache.java index d349ab76f..ec021402e 100644 --- a/hutool-cache/src/main/java/cn/hutool/cache/Cache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/Cache.java @@ -83,7 +83,7 @@ public interface Cache extends Iterable, Serializable { *

* 调用此方法时,会检查上次调用时间,如果与当前时间差值大于超时时间返回{@code null},否则返回值。 *

- * 每次调用此方法会刷新最后访问时间,也就是说会重新计算超时时间。 + * 每次调用此方法会可选是否刷新最后访问时间,{@code true}表示会重新计算超时时间。 * * @param key 键 * @param isUpdateLastAccess 是否更新最后访问时间,即重新计算超时时间。 @@ -96,6 +96,8 @@ public interface Cache extends Iterable, Serializable { * 从缓存中获得对象,当对象不在缓存中或已经过期返回{@code null} *

* 调用此方法时,会检查上次调用时间,如果与当前时间差值大于超时时间返回{@code null},否则返回值。 + *

+ * 每次调用此方法会可选是否刷新最后访问时间,{@code true}表示会重新计算超时时间。 * * @param key 键 * @param isUpdateLastAccess 是否更新最后访问时间,即重新计算超时时间。 diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java index a3359fddb..2e348380a 100644 --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java @@ -30,6 +30,9 @@ public abstract class AbstractCache implements Cache { protected Map> cacheMap; + // 乐观锁,此处使用乐观锁解决读多写少的场景 + // get时乐观读,再检查是否修改,修改则转入悲观读重新读一遍,可以有效解决在写时阻塞大量读操作的情况。 + // see: https://www.cnblogs.com/jiagoushijuzi/p/13721319.html private final StampedLock lock = new StampedLock(); /** @@ -52,11 +55,11 @@ public abstract class AbstractCache implements Cache { protected boolean existCustomTimeout; /** - * 命中数 + * 命中数,即命中缓存计数 */ protected AtomicLong hitCount = new AtomicLong(); /** - * 丢失数 + * 丢失数,即未命中缓存计数 */ protected AtomicLong missCount = new AtomicLong(); @@ -143,8 +146,8 @@ public abstract class AbstractCache implements Cache { public V get(K key, boolean isUpdateLastAccess, Func0 supplier) { V v = get(key, isUpdateLastAccess); if (null == v && null != supplier) { - //每个key单独获取一把锁,降低锁的粒度提高并发能力 - Lock keyLock = keyLockMap.computeIfAbsent(key, k -> new ReentrantLock()); + //每个key单独获取一把锁,降低锁的粒度提高并发能力,see pr#1385@Github + final Lock keyLock = keyLockMap.computeIfAbsent(key, k -> new ReentrantLock()); keyLock.lock(); try { // 双重检查锁 @@ -157,7 +160,7 @@ public abstract class AbstractCache implements Cache { } put(key, v, this.timeout); } else { - v = co.get(true); + v = co.get(isUpdateLastAccess); } } finally { keyLock.unlock(); @@ -170,25 +173,28 @@ public abstract class AbstractCache implements Cache { @Override public V get(K key, boolean isUpdateLastAccess) { // 尝试读取缓存,使用乐观读锁 - long stamp = lock.readLock(); - try { - // 不存在或已移除 - final CacheObj co = cacheMap.get(key); - if (null == co) { - missCount.getAndIncrement(); - return null; + long stamp = lock.tryOptimisticRead(); + CacheObj co = cacheMap.get(key); + if(false == lock.validate(stamp)){ + // 有写线程修改了此对象,悲观读 + stamp = lock.readLock(); + try { + co = cacheMap.get(key); + } finally { + lock.unlockRead(stamp); } - - // 命中 - if (false == co.isExpired()) { - hitCount.getAndIncrement(); - return co.get(isUpdateLastAccess); - } - } finally { - lock.unlockRead(stamp); } - // 过期 + // 未命中 + if (null == co) { + missCount.getAndIncrement(); + return null; + } else if (false == co.isExpired()) { + hitCount.getAndIncrement(); + return co.get(isUpdateLastAccess); + } + + // 过期,既不算命中也不算非命中 remove(key, true); return null; } diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/LFUCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/LFUCache.java index 67000be1b..32709ed6b 100644 --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/LFUCache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/LFUCache.java @@ -9,7 +9,7 @@ import java.util.Iterator; * 使用率是通过访问次数计算的。
* 当缓存满时清理过期对象。
* 清理后依旧满的情况下清除最少访问(访问计数最小)的对象并将其他对象的访问数减去这个最小访问数,以便新对象进入后可以公平计数。 - * + * * @author Looly,jodd * * @param 键类型 @@ -20,7 +20,7 @@ public class LFUCache extends AbstractCache { /** * 构造 - * + * * @param capacity 容量 */ public LFUCache(int capacity) { @@ -29,7 +29,7 @@ public class LFUCache extends AbstractCache { /** * 构造 - * + * * @param capacity 容量 * @param timeout 过期时长 */ @@ -37,7 +37,7 @@ public class LFUCache extends AbstractCache { if(Integer.MAX_VALUE == capacity) { capacity -= 1; } - + this.capacity = capacity; this.timeout = timeout; cacheMap = new HashMap<>(capacity + 1, 1.0f); @@ -48,7 +48,7 @@ public class LFUCache extends AbstractCache { /** * 清理过期对象。
* 清理后依旧满的情况下清除最少访问(访问计数最小)的对象并将其他对象的访问数减去这个最小访问数,以便新对象进入后可以公平计数。 - * + * * @return 清理个数 */ @Override @@ -89,7 +89,7 @@ public class LFUCache extends AbstractCache { } } } - + return count; } } diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/LRUCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/LRUCache.java index 9cc2d1b64..ed003b721 100644 --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/LRUCache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/LRUCache.java @@ -40,7 +40,7 @@ public class LRUCache extends AbstractCache { this.capacity = capacity; this.timeout = timeout; - + //链表key按照访问顺序排序,调用get方法后,会将这次访问的元素移至头部 cacheMap = new FixedLinkedHashMap<>(capacity); } diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/TimedCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/TimedCache.java index d5ed8ebcf..5e094875d 100644 --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/TimedCache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/TimedCache.java @@ -10,7 +10,7 @@ import java.util.concurrent.ScheduledFuture; /** * 定时缓存
* 此缓存没有容量限制,对象只有在过期后才会被移除 - * + * * @author Looly * * @param 键类型 @@ -24,7 +24,7 @@ public class TimedCache extends AbstractCache { /** * 构造 - * + * * @param timeout 超时(过期)时长,单位毫秒 */ public TimedCache(long timeout) { @@ -33,7 +33,7 @@ public class TimedCache extends AbstractCache { /** * 构造 - * + * * @param timeout 过期时长 * @param map 存储缓存对象的map */ @@ -46,7 +46,7 @@ public class TimedCache extends AbstractCache { // ---------------------------------------------------------------- prune /** * 清理过期对象 - * + * * @return 清理数 */ @Override @@ -68,7 +68,7 @@ public class TimedCache extends AbstractCache { // ---------------------------------------------------------------- auto prune /** * 定时清理 - * + * * @param delay 间隔时长,单位毫秒 */ public void schedulePrune(long delay) { diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/WeakCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/WeakCache.java index 848f30011..3a98bc601 100644 --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/WeakCache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/WeakCache.java @@ -6,7 +6,7 @@ import java.util.WeakHashMap; * 弱引用缓存
* 对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。
* 丢弃某个键时,其条目从映射中有效地移除。
- * + * * @author Looly * * @param 键 @@ -18,7 +18,7 @@ public class WeakCache extends TimedCache{ private static final long serialVersionUID = 1L; public WeakCache(long timeout) { - super(timeout, new WeakHashMap>()); + super(timeout, new WeakHashMap<>()); } }