设想高并发下的一种场景:假设我们将name=aty存放到缓存中,并设置的有过期时间。当缓存过期后,恰好有10个客户端发起请求,需要读取name的值。使用Guava Cache可以保证只让一个线程去加载数据(比如从数据库中),而其他线程则等待这个线程的返回结果。这样就能避免大量用户请求穿透缓存。
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.CountDownLatch; import java.util.concurrent.TimeUnit; public class Main { // 1s无访问则缓存过期, 每次加载一个key需要耗时2s private static LoadingCache<String, String> cache = CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.SECONDS) .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { System.out.println("begin to query db..."); Thread.sleep(2000); System.out.println("success to query db..."); return UUID.randomUUID().toString(); } }); 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("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(); } }
上面输出结果可以看到:只有一个线程去数据库中加载数据,其他线程都在等待(每个线程都耗时2s)。使用Guava确实可以做到:对于同一个key,无论有多少请求,都只会允许一个线程去加载数据。
但是也有一个很致命的缺陷:上面8个线程中,有一个线程实际去加载数据,其余7个线程都被阻塞了。如果能做到,当一个线程去加载数据,其余线程发现这个数据正在加载中,那么直接读取老的数据,这样就不会阻塞了。既然是缓存,读取旧一点数据也没有多大问题,却可以提高系统吞度量。