Data Manager
DM的主要职责:1)分页管理 DB 文件,并进行缓存
2)管理日志文件,保证在发生错误时可以根据日志进行恢复
3) 抽象 DB 文件为 DataItem 供上层模块使用,并提供缓存。
- DB是用来读写数据库中的文件并进行包装,供上层Manager使用。同时需要将数据进行缓存
- 关于缓存策略的选择:如果选择
LRU
策略:当缓存满了之后,缓存会驱逐一个资源,但是这个资源的驱逐是不可控的。也就是说,我们无法确定当前驱逐的资源是否还有上层模块在使用或者被修改。这个时候如果我们的上层模块恰好想要将该资源强制刷回数据源会发现在缓存中找不到该数据。这个时候如果我们不做任何操作,而这个数据又被上层模块修改了,就会导致修改无效。如果我们重新将其写入缓存,但是缓存已满,会导致抖动问题。因此作者这里采用了引用计数缓存框架。该框架需要上层模块不使用某个资源时release(key)
掉这个资源。我们有一个计数器记录了上层模块对资源的引用次数,当次数归零时会驱逐这个资源。当缓存满时不会自动驱逐,而是报错OOM
。
具体实现
- 作者在这里写了一个抽象类,里面定义了两个抽象方法
getForCache(long key)
和releaseForCache(T obj)
。分别用于将资源写入缓存和将资源写回数据源 除此之外,需要以下变量:private HashMap<long, T> cache;
这个用来存放缓存中的实际数据。private HashMap<Long, Integer> references
用来保存使用了资源的上层模块个数,当某个资源k
对应的v
为0时,需要将其写回缓存。private HashMap<Long, Boolean> getting
这个变量用于保存某个资源是否正在被其他进程获取。 - 对于
get(key)
方法:具体实现流程为:轮询所需资源是否被其他进程占用->检查该资源是否位于缓存中->如果没有则去数据源中尝试将数据写入缓存。
protected T get(long key) throws Exception {
// TODO: 是否可以先判断cache中是否有资源, 如果没有直接去数据源中获取而不是等待?
while (true) {
lock.lock();
if (getting.containsKey(key)) {
// 如果正在被获取,则需要等待一段时间之后重新请求获取该资源
lock.unlock();
// TODO: 尝试使用lock.Condition
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
continue; }
continue; // 直接进入下一次循环
}
// 轮到该进程使用资源, 首先检查该资源是否在缓存中
if (cache.containsKey(key)) {
// 缓存中包含该资源
T obj = cache.get(key);
references.put(key, references.get(key) + 1); // 更新资源引用次数
lock.unlock();
return obj;
}
// 缓存中没用该资源
// 首先检查是否达到最大缓存个数
if (maxResource > 0 && cnt == maxResource) {
lock.unlock();
throw Error.CacheFullException; // 缓存已满
}
cnt ++;
getting.put(key, true);
lock.unlock();
break; }
T obj = null;
try {
obj = getForCache(key);
} catch (Exception e) {
// 如果我们无法从数据源中获取数据
lock.lock();
cnt --;
getting.remove(key);
lock.unlock();
throw e;
}
lock.lock();
getting.remove(key);
cache.put(key, obj);
references.put(key, 1); // 由于是第一次从从资源中将数据放入缓存,所以值为1
lock.unlock();
return obj;
}
release(key)
:释放资源。需要先判断引用是否归零。
protected void release(long key) {
lock.lock();
try {
Integer ref = references.get(key) - 1; // 获取引用次数
if (ref == 0) {
// 所有占用都已释放
T obj = cache.get(key); // 获取缓存中的数据
releaseForCache(obj); // 写回数据源
references.remove(key); // 移除引用
cache.remove(key);
cnt --;
} else {
// 还有占用
references.put(key, ref);
}
} finally {
lock.unlock();
}
}
- 同时我们还需要一个强制将所有缓存写回数据源的方法
close
protected void close() {
lock.lock();
try {
Set<Long> keys = cache.keySet(); // 获取cache中的所有key
keys.forEach(key -> {
T obj = cache.get(key);
releaseForCache(obj);
cache.remove(key);
references.remove(key);
});
} finally {
lock.unlock();
}
}