上一篇文章"Guava Cache特性:對於同一個key,只讓一個請求回源load數據,其他線程阻塞等待結果"提到:如果緩存過期,恰好有多個線程讀取同一個key的值,那麼guava只允許一個線程去加載數據,其餘線程阻塞。這雖然可以防止大量請求穿透緩存,但是效率低下。使用refreshAfterWrite可以做到:只阻塞加載數據的線程,其餘線程返回舊數據。
package net.aty.guava;
import com.google.common.base.Stopwatch;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class Main {
// 模擬一個需要耗時2s的數據庫查詢任務
private static Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("begin to mock query db...");
Thread.sleep(2000);
System.out.println("success to mock query db...");
return UUID.randomUUID().toString();
}
};
// 1s後刷新緩存
private static LoadingCache<String, String> cache = CacheBuilder.newBuilder().refreshAfterWrite(1, TimeUnit.SECONDS)
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
return callable.call();
}
});
private static CountDownLatch latch = new CountDownLatch(1);
public static void main(String[] args) throws Exception {
// 手動添加一條緩存數據,睡眠1.5s讓其過期
cache.put("name", "aty");
Thread.sleep(1500);
for (int i = 0; i < 8; i++) {
startThread(i);
}
// 讓線程運行
latch.countDown();
}
private static void startThread(int id) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "...begin");
latch.await();
Stopwatch watch = Stopwatch.createStarted();
System.out.println(Thread.currentThread().getName() + "...value..." + cache.get("name"));
watch.stop();
System.out.println(Thread.currentThread().getName() + "...finish,cost time=" + watch.elapsed(TimeUnit.SECONDS));
} catch (Exception e) {
e.printStackTrace();
}
}
});
t.setName("Thread-" + id);
t.start();
}
}
通過輸出結果可以看出:當緩存數據過期的時候,真正去加載數據的線程會阻塞一段時間,其餘線程立馬返回過期的值,顯然這種處理方式更符合實際的使用場景。
有一點需要注意:我們手動向緩存中添加了一條數據,並讓其過期。如果沒有這行代碼,程序執行結果如下。
由於緩存沒有數據,導致一個線程去加載數據的時候,別的線程都阻塞了(因爲沒有舊值可以返回)。所以一般系統啓動的時候,我們需要將數據預先加載到緩存,不然就會出現這種情況。
還有一個問題不爽:真正加載數據的那個線程一定會阻塞,我們希望這個加載過程是異步的。這樣就可以讓所有線程立馬返回舊值,在後臺刷新緩存數據。refreshAfterWrite默認的刷新是同步的,會在調用者的線程中執行。我們可以改造成異步的,實現CacheLoader.reload()。
package net.aty.guava;
import com.google.common.base.Stopwatch;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class Main {
// 模擬一個需要耗時2s的數據庫查詢任務
private static Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("begin to mock query db...");
Thread.sleep(2000);
System.out.println("success to mock query db...");
return UUID.randomUUID().toString();
}
};
// guava線程池,用來產生ListenableFuture
private static ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10));
private static LoadingCache<String, String> cache = CacheBuilder.newBuilder().refreshAfterWrite(1, TimeUnit.SECONDS)
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
return callable.call();
}
@Override
public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
System.out.println("......後臺線程池異步刷新:" + key);
return service.submit(callable);
}
});
private static CountDownLatch latch = new CountDownLatch(1);
public static void main(String[] args) throws Exception {
cache.put("name", "aty");
Thread.sleep(1500);
for (int i = 0; i < 8; i++) {
startThread(i);
}
// 讓線程運行
latch.countDown();
}
private static void startThread(int id) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "...begin");
latch.await();
Stopwatch watch = Stopwatch.createStarted();
System.out.println(Thread.currentThread().getName() + "...value..." + cache.get("name"));
watch.stop();
System.out.println(Thread.currentThread().getName() + "...finish,cost time=" + watch.elapsed(TimeUnit.SECONDS));
} catch (Exception e) {
e.printStackTrace();
}
}
});
t.setName("Thread-" + id);
t.start();
}
}