Browse Source

fix cache

Looly 4 years ago
parent
commit
a8add399c2

+ 1 - 0
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)

+ 3 - 1
hutool-cache/src/main/java/cn/hutool/cache/Cache.java

@@ -83,7 +83,7 @@ public interface Cache<K, V> extends Iterable<V>, Serializable {
 	 * <p>
 	 * 调用此方法时,会检查上次调用时间,如果与当前时间差值大于超时时间返回{@code null},否则返回值。
 	 * <p>
-	 * 每次调用此方法会刷新最后访问时间,也就是说会重新计算超时时间。
+	 * 每次调用此方法会可选是否刷新最后访问时间,{@code true}表示会重新计算超时时间。
 	 *
 	 * @param key                键
 	 * @param isUpdateLastAccess 是否更新最后访问时间,即重新计算超时时间。
@@ -96,6 +96,8 @@ public interface Cache<K, V> extends Iterable<V>, Serializable {
 	 * 从缓存中获得对象,当对象不在缓存中或已经过期返回{@code null}
 	 * <p>
 	 * 调用此方法时,会检查上次调用时间,如果与当前时间差值大于超时时间返回{@code null},否则返回值。
+	 * <p>
+	 * 每次调用此方法会可选是否刷新最后访问时间,{@code true}表示会重新计算超时时间。
 	 *
 	 * @param key                键
 	 * @param isUpdateLastAccess 是否更新最后访问时间,即重新计算超时时间。

+ 26 - 20
hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java

@@ -30,6 +30,9 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
 
 	protected Map<K, CacheObj<K, V>> 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<K, V> implements Cache<K, V> {
 	protected boolean existCustomTimeout;
 
 	/**
-	 * 命中数
+	 * 命中数,即命中缓存计数
 	 */
 	protected AtomicLong hitCount = new AtomicLong();
 	/**
-	 * 丢失数
+	 * 丢失数,即未命中缓存计数
 	 */
 	protected AtomicLong missCount = new AtomicLong();
 
@@ -143,8 +146,8 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
 	public V get(K key, boolean isUpdateLastAccess, Func0<V> 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<K, V> implements Cache<K, V> {
 					}
 					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<K, V> implements Cache<K, V> {
 	@Override
 	public V get(K key, boolean isUpdateLastAccess) {
 		// 尝试读取缓存,使用乐观读锁
-		long stamp = lock.readLock();
-		try {
-			// 不存在或已移除
-			final CacheObj<K, V> co = cacheMap.get(key);
-			if (null == co) {
-				missCount.getAndIncrement();
-				return null;
+		long stamp = lock.tryOptimisticRead();
+		CacheObj<K, V> 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;
 	}

+ 6 - 6
hutool-cache/src/main/java/cn/hutool/cache/impl/LFUCache.java

@@ -9,7 +9,7 @@ import java.util.Iterator;
  * 使用率是通过访问次数计算的。<br>
  * 当缓存满时清理过期对象。<br>
  * 清理后依旧满的情况下清除最少访问(访问计数最小)的对象并将其他对象的访问数减去这个最小访问数,以便新对象进入后可以公平计数。
- * 
+ *
  * @author Looly,jodd
  *
  * @param <K> 键类型
@@ -20,7 +20,7 @@ public class LFUCache<K, V> extends AbstractCache<K, V> {
 
 	/**
 	 * 构造
-	 * 
+	 *
 	 * @param capacity 容量
 	 */
 	public LFUCache(int capacity) {
@@ -29,7 +29,7 @@ public class LFUCache<K, V> extends AbstractCache<K, V> {
 
 	/**
 	 * 构造
-	 * 
+	 *
 	 * @param capacity 容量
 	 * @param timeout 过期时长
 	 */
@@ -37,7 +37,7 @@ public class LFUCache<K, V> extends AbstractCache<K, V> {
 		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<K, V> extends AbstractCache<K, V> {
 	/**
 	 * 清理过期对象。<br>
 	 * 清理后依旧满的情况下清除最少访问(访问计数最小)的对象并将其他对象的访问数减去这个最小访问数,以便新对象进入后可以公平计数。
-	 * 
+	 *
 	 * @return 清理个数
 	 */
 	@Override
@@ -89,7 +89,7 @@ public class LFUCache<K, V> extends AbstractCache<K, V> {
 				}
 			}
 		}
-		
+
 		return count;
 	}
 }

+ 1 - 1
hutool-cache/src/main/java/cn/hutool/cache/impl/LRUCache.java

@@ -40,7 +40,7 @@ public class LRUCache<K, V> extends AbstractCache<K, V> {
 
 		this.capacity = capacity;
 		this.timeout = timeout;
-		
+
 		//链表key按照访问顺序排序,调用get方法后,会将这次访问的元素移至头部
 		cacheMap = new FixedLinkedHashMap<>(capacity);
 	}

+ 5 - 5
hutool-cache/src/main/java/cn/hutool/cache/impl/TimedCache.java

@@ -10,7 +10,7 @@ import java.util.concurrent.ScheduledFuture;
 /**
  * 定时缓存<br>
  * 此缓存没有容量限制,对象只有在过期后才会被移除
- * 
+ *
  * @author Looly
  *
  * @param <K> 键类型
@@ -24,7 +24,7 @@ public class TimedCache<K, V> extends AbstractCache<K, V> {
 
 	/**
 	 * 构造
-	 * 
+	 *
 	 * @param timeout 超时(过期)时长,单位毫秒
 	 */
 	public TimedCache(long timeout) {
@@ -33,7 +33,7 @@ public class TimedCache<K, V> extends AbstractCache<K, V> {
 
 	/**
 	 * 构造
-	 * 
+	 *
 	 * @param timeout 过期时长
 	 * @param map 存储缓存对象的map
 	 */
@@ -46,7 +46,7 @@ public class TimedCache<K, V> extends AbstractCache<K, V> {
 	// ---------------------------------------------------------------- prune
 	/**
 	 * 清理过期对象
-	 * 
+	 *
 	 * @return 清理数
 	 */
 	@Override
@@ -68,7 +68,7 @@ public class TimedCache<K, V> extends AbstractCache<K, V> {
 	// ---------------------------------------------------------------- auto prune
 	/**
 	 * 定时清理
-	 * 
+	 *
 	 * @param delay 间隔时长,单位毫秒
 	 */
 	public void schedulePrune(long delay) {

+ 2 - 2
hutool-cache/src/main/java/cn/hutool/cache/impl/WeakCache.java

@@ -6,7 +6,7 @@ import java.util.WeakHashMap;
  * 弱引用缓存<br>
  * 对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的丢弃,这就使该键成为可终止的,被终止,然后被回收。<br>
  * 丢弃某个键时,其条目从映射中有效地移除。<br>
- * 
+ *
  * @author Looly
  *
  * @param <K> 键
@@ -18,7 +18,7 @@ public class WeakCache<K, V> extends TimedCache<K, V>{
 	private static final long serialVersionUID = 1L;
 
 	public WeakCache(long timeout) {
-		super(timeout, new WeakHashMap<K, CacheObj<K, V>>());
+		super(timeout, new WeakHashMap<>());
 	}
 
 }