序
本文主要讲述下缓存的Cache Aside模式。
Cache Aside
有两个要点:
- 应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
- 更新是先更新数据库,成功后,让缓存失效.为什么不是写完数据库后更新缓存?主要是怕两个并发的写操作导致脏数据。
public V read(K key) {
V result = cache.getIfPresent(key);
if (result == null) {
result = readFromDatabase(key);
cache.put(key, result);
}
return result;
}
public void write(K key, V value) {
writeToDatabase(key, value);
cache.invalidate(key);
};
脏数据
一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。
这个case理论上会出现,不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。
maven
com.github.ben-manes.caffeine
caffeine
2.5.5
com.google.guava
guava
22.0
代码复现
这里使用代码复现一下这个脏数据场景。
- 读操作进来,发现没有cache,则触发loading,获取数据,尚未返回
- 写操作进来,更新数据源,invalidate缓存
- loading获取的旧数据返回,cache里头存的是脏数据
@Test
public void testCacheDirty() throws InterruptedException, ExecutionException {
AtomicReference db = new AtomicReference<>(1);
LoadingCache cache = CacheBuilder.newBuilder()
.build(
new CacheLoader() {
public Integer load(String key) throws InterruptedException {
LOGGER.info("loading reading from db ...");
Integer v = db.get();
LOGGER.info("loading read from db get:{}",v);
Thread.sleep(1000L); //这里1秒才返回,模拟引发脏缓存
LOGGER.info("loading Read from db return : {}",v);
return v;
}
}
);
Thread t2 = new Thread(() -> {
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
LOGGER.info("Writing to db ...");
db.set(2);
LOGGER.info("Wrote to db");
cache.invalidate("k");
LOGGER.info("Invalidated cached");
});
t2.start();
//这里在t2 invalidate 之前 先触发cache loading
//loading那里增加sleep,确保在invalidate之后,cache loading才返回
//此时返回的cache就是脏数据了
LOGGER.info("fire loading cache");
LOGGER.info("get from cache: {}",cache.get("k"));
t2.join();
for(int i=0;i<3;i++){
LOGGER.info("get from cache: {}",cache.get("k"));
}
}
输出
15:54:05.751 [main] INFO com.example.demo.CacheTest - fire loading cache
15:54:05.772 [main] INFO com.example.demo.CacheTest - loading reading from db ...
15:54:05.772 [main] INFO com.example.demo.CacheTest - loading read from db get:1
15:54:06.253 [Thread-1] INFO com.example.demo.CacheTest - Writing to db ...
15:54:06.253 [Thread-1] INFO com.example.demo.CacheTest - Wrote to db
15:54:06.253 [Thread-1] INFO com.example.demo.CacheTest - Invalidated cached
15:54:06.778 [main] INFO com.example.demo.CacheTest - loading Read from db return : 1
15:54:06.782 [main] INFO com.example.demo.CacheTest - get from cache: 1
15:54:06.782 [main] INFO com.example.demo.CacheTest - get from cache: 1
15:54:06.782 [main] INFO com.example.demo.CacheTest - get from cache: 1
15:54:06.782 [main] INFO com.example.demo.CacheTest - get from cache: 1
使用caffeine
@Test
public void testCacheDirty() throws InterruptedException, ExecutionException {
AtomicReference db = new AtomicReference<>(1);
com.github.benmanes.caffeine.cache.LoadingCache cache = Caffeine.newBuilder()
.build(key -> {
LOGGER.info("loading reading from db ...");
Integer v = db.get();
LOGGER.info("loading read from db get:{}",v);
Thread.sleep(1000L); //这里1秒才返回,模拟引发脏缓存
LOGGER.info("loading Read from db return : {}",v);
return v;
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
LOGGER.info("Writing to db ...");
db.set(2);
LOGGER.info("Wrote to db");
cache.invalidate("k");
LOGGER.info("Invalidated cached");
});
t2.start();
//这里在t2 invalidate 之前 先触发cache loading
//loading那里增加sleep,确保在invalidate之后,cache loading才返回
//此时返回的cache就是脏数据了
LOGGER.info("fire loading cache");
LOGGER.info("get from cache: {}",cache.get("k"));
t2.join();
for(int i=0;i<3;i++){
LOGGER.info("get from cache: {}",cache.get("k"));
}
}
输出
16:05:10.141 [main] INFO com.example.demo.CacheTest - fire loading cache
16:05:10.153 [main] INFO com.example.demo.CacheTest - loading reading from db ...
16:05:10.153 [main] INFO com.example.demo.CacheTest - loading read from db get:1
16:05:10.634 [Thread-1] INFO com.example.demo.CacheTest - Writing to db ...
16:05:10.635 [Thread-1] INFO com.example.demo.CacheTest - Wrote to db
16:05:11.172 [main] INFO com.example.demo.CacheTest - loading Read from db return : 1
16:05:11.172 [main] INFO com.example.demo.CacheTest - get from cache: 1
16:05:11.172 [Thread-1] INFO com.example.demo.CacheTest - Invalidated cached
16:05:11.172 [main] INFO com.example.demo.CacheTest - loading reading from db ...
16:05:11.172 [main] INFO com.example.demo.CacheTest - loading read from db get:2
16:05:12.177 [main] INFO com.example.demo.CacheTest - loading Read from db return : 2
16:05:12.177 [main] INFO com.example.demo.CacheTest - get from cache: 2
16:05:12.177 [main] INFO com.example.demo.CacheTest - get from cache: 2
16:05:12.177 [main] INFO com.example.demo.CacheTest - get from cache: 2
这里可以看到invalidate的时候,loading又重新触发了一次,然后脏数据就清除了
doc
- 缓存更新的套路
- caffeine: Java 8高性能缓存库包