Cache-Aside Pattern
一. 背景和问题
缓存已经成为了几乎所有应用系统的必备要素。使用缓存可以有效提高系统的读性能,相比于直接读取数据库,吞吐量有了很大的提高。但是,在实际生产环境中,很难保证缓存与数据库中数据的完全一致。程序应采取某种策略,尽可能地保证缓存中的数据是最新的,并且可以检测到缓存中数据失效,并提供相应的解决方案。
简单来说,Cache-Aside Pattern的提出是为了尽可能地解决缓存与数据库的数据不一致问题。
二. 解决方案
大多数的商用缓存系统都提供了下面的功能:
- 访问数据时,首先尝试从缓存中获取。如果缓存命中,则直接返回。
- 如果缓存未命中,则查询数据库。
- 将从数据库中查询到的结果放入缓存中,并返回。
- 缓存中任何数据的更新,都会自动同步到数据库。
如果所使用的缓存没有提供这些功能,则需要应用系统自己去实现,实现时就可以基于Cache-Aside Pattern。
三. Cache-Aside Pattern
Cache-Aside Pattern分为读操作和写操作两种。
-
读操作
原理如下图:
流程:
-
首先从缓存中查询数据,如果缓存命中则直接返回。
-
缓存未命中,则去数据库中读取。
-
将从数据库中读取的结果的副本放入到缓存中,并返回。
-
写操作
流程:
- 首先更新数据库。
- 然后删除缓存中的数据。
四. 一些思考
-
为什么是删除缓存,而不是更新缓存?主要基于以下两点考量:
- 数据更新后,可能不会有大量的访问。如果每次更新数据后都更新缓存,可能会造成大量不必要的计算开销。因此,这里采用一种lazy的思想,每次更新数据时仅仅是删除缓存,只有在真正读请求到来时才进行缓存的更新。
- 在高并发场景下,并发地更新缓存可能会造成缓存可数据库中数据不一致的问题。
-
写操作的流程十分关键!一定要先更新数据库,再删除缓存。如果先删除缓存,就会存在一个很小的窗口期,使得客户端查询时无法命中缓存,而去读数据库,然而此时数据库中的数据还未更新,就会从数据库中加载到旧的数据并放入缓存中,最终导致缓存数据被污染。
-
缓存的过期策略
许多缓存系统都会对缓存数据设置一定的过期策略。使用Cache-Aside Pattern时,一定要合理地设置过期策略。如果过期时间太短,可能导致大量请求涌入数据库。相反,如果过期时间太长,有可能导致缓存中数据的大量失效。使用缓存的一个原则,就是尽量缓存那些相对静态的、频繁被读取的数据。
-
**Cache-Aside Pattern并无法完全保证数据库和缓存的数据一致性。**当某条数据被修改时,在数据库中会立即更新,但是缓存中的更新会在下次读取数据时才会发生,
五. 应用场景
-
适用场景:
- 应用程序所使用的缓存系统并没有提供前文所述的缓存系统的那些功能。
- 加载资源的需求是不可预测的。该模式使得系统可以按需加载数据,而不需提前预设哪些数据可能需要被获取。
-
不适用场景:
所缓存的数据集是静态的。
六. 使用示例
/**
* @Author: ZhangShenao
* @Date: 2019/5/15 11:07
* @Description:演示Cache-Aside Pattern
*/
@Service
public class CacheAsidePatternService {
@Autowired
private CacheService cacheService;
@Autowired
private DataDao dataDao;
//读操作
public Data getData(String key) {
//1. 读缓存,如果命中则直接返回
Data data = cacheService.loadDataFromCache(key);
if (data != null) {
return data;
}
//2. 缓存未命中,读数据库
data = dataDao.loadDataFromDB(key);
//3. 将读取到的数据放入缓存
cacheService.putData(key,data);
return data;
}
//写操作
public void updateData(String key,Data data){
//1. 更新数据库
dataDao.updateData(key, data);
//2. 删除缓存
cacheService.evictData(key);
}
}