一、模块介绍
DM 管理数据库的 DB 文件和日志文件。它是上层模块和下层文件系统的中间层,向上提供数据的包装,向下将数据写入磁盘。DM 的主要职责有:1) 抽象 DB 文件为 DataItem 供上层模块使用,并提供缓存;2) 分页管理 DB 文件,并进行缓存;3)管理日志文件,保证在发生错误时可以根据日志进行恢复。
二、引用计数缓存淘汰策略
无论是向上传递数据还是向下写入数据,都需要缓存以提升操作效率。那么缓存淘汰策略使用 Least Recently Used (LRU) 还是引用计数呢?
在 LRU 策略中,资源驱逐不可控,上层模块无法感知。而在引用计数策略中,只有上层模块主动释放引用,缓存在确保没有模块在使用这个资源了,才会去驱逐资源。
如果使用LRU,设想这样一个场景:某个时刻缓存满了,缓存驱逐了一个资源,这时上层模块想要将某个资源强制刷回数据源,这个资源恰好是刚刚被驱逐的资源。那么上层模块就发现,这个数据在缓存里消失了,这时候就陷入了一种尴尬的境地:是否有必要做回源操作?
- 不回源。由于没法确定缓存被驱逐的时间,更没法确定被驱逐之后数据项是否被修改,这样是极其不安全的;
- 回源。如果数据项被驱逐时的数据和现在又是相同的,那就是一次无效回源(因为被驱逐时脏页会自动刷回磁盘);
- 放回缓存里,等下次被驱逐时回源。看起来解决了问题,但是此时缓存已经满了,这意味着你还需要驱逐一个资源才能放进去。这有可能会导致缓存抖动问题。
三、抽象类
1. 属性
用一个Map存储缓存中的资源号和其相应的数据,用一个Map存储缓存中的资源号和其被引用的次数,用一个Map存储缓存中的资源号和其是否正在被访问的标识符(用于多线程场景)。
private HashMap<Long, T> cache; // 资源及其相应的数据
private HashMap<Long, Integer> references; // 资源及其被引用的个数
private HashMap<Long, Boolean> getting; // 资源是否正在被获取
private int maxResource; // 缓存的最大缓存资源数
private int count = 0; // 缓存中元素的个数
private Lock lock;
2. 抽象方法
我们在程序中设置了一个抽象类,有两个抽象方法分别用于当资源在缓存不存在时获取资源,和当资源要缓存淘汰时将资源写回。这两个抽象方法留给子类去做具体实现。
protected abstract T getForCache(long key) throws Exception;
protected abstract void releaseForCache(T obj);
3. 从缓存中获取资源
抽象类中的普通方法用于描述子类中共有的方法,因为我们并不知道资源封装的具体类型,所以使用一个通配符 T。
当想要获取一个资源时通过死循环无限尝试,这里是需要加锁的。
如果发现资源正在被其他线程所访问(暗含资源就在缓存当中,把这个 if 放在下一个 if 前面可能是为了避免 if 嵌套,并与 continue 搭配使用),那么就 sleep 一段时间,再次尝试获取。
如果发现资源就在缓存当中(暗含资源没有被其他线程访问),首先给资源的引用标识加一,然后再返回资源。
如果上述两个 if 判断都为假,说明资源没有在缓存中。
如果发现缓存中的资源个数已经等于最大资源个数了,直接抛出异常终止程序。
如果缓存还没有满,那接下来就要从磁盘中获取资源并放入缓存中,首先 count++,然在 getting 中注册一下,表明这个资源正在被我使用。接下来调用抽象方法从磁盘中获取资源。如果出现异常,操作需要回滚。如果资源成功被获取,那么首先从getting中删除key,然后把资源放入缓存,然后把资源的引用数量设为1(为什么不设置为0呢?0表示不可能再被使用)。
protected T get(long key) throws Exception {
while(true) {
lock.lock();
if(getting.containsKey(key)) {
// 请求的资源正在被其他线程获取
lock.unlock();
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 && count == maxResource) {
lock.unlock();
throw Error.CacheFullException;
}
count ++;
getting.put(key, true);
lock.unlock();
break;
}
T obj = null;
try {
obj = getForCache(key);
} catch(Exception e) {
lock.lock();
count --;
getting.remove(key);
lock.unlock();
throw e;
}
lock.lock();
getting.remove(key);
cache.put(key, obj);
references.put(key, 1);
lock.unlock();
return obj;
}
4. 从缓存中释放资源
首先将资源的引用数减一,如果变成0了,就可以写回数据库,并从缓存中删除。如果不为0,就更新引用为减一后的值。
protected void release(long key) {
lock.lock();
try {
int ref = references.get(key)-1;
if(ref == 0) {
T obj = cache.get(key);
releaseForCache(obj);
references.remove(key);
cache.remove(key);
count --;
} else {
references.put(key, ref);
}
} finally {
lock.unlock();
}
}
5. 关闭缓存
关闭缓存并写回所有资源。
protected void close() {
lock.lock();
try {
Set<Long> keys = cache.keySet();
for (long key : keys) {
T obj = cache.get(key);
releaseForCache(obj);
references.remove(key);
cache.remove(key);
}
} finally {
lock.unlock();
}
}