Java Cache-EHCache系列之AA-Tree实现溢出到磁盘的数据管理(2)

标签: java cache ehcache | 发表时间:2013-10-29 01:59 | 作者:DLevin
出处:http://www.blogjava.net/
在上一篇《 Java Cache-EHCache系列之AA-Tree实现溢出到磁盘的数据管理(1)》已经详细讲解了EHCache中在AATreeSet中对AA Tree算法的实现,并且指出EHCache是采用它作为空闲磁盘管理数据结构,本文主要关注于EHCache是如何采用AATreeSet类来管理空闲磁盘的(这里的磁盘管理是只EHCache data文件中的空闲磁盘)。

空闲磁盘管理数据结构和算法
在上一篇《 Java Cache-EHCache系列之AA-Tree实现溢出到磁盘的数据管理》有提到类似内存分配管理,空闲磁盘管理可以采用多种数据结构,也有多种算法实现,EHCache采用AA Tree作为空闲磁盘的数据结构,以及首次适应算法和最坏适应算法相结合的算法。
最坏适应算法(Worst Fit),在《计算机操作系统(汤子瀛版)》中对该算法描述如下:最坏适应分配算法要扫瞄整个空闲分区表或链表,总是挑选一个最大的空闲分区分分割给作业使用,其优点是可使剩下的空闲区不至于太小,产生碎片的几率最小,对中小作业有利,同时最坏适应算法查找效率很高。该算法要求将所有的空闲分区按其容量以从大到小的顺序形成一空闲分区链,查找时只要看第一个分区能否满足作业要求。但该算法的缺点也时明显的,它会使存储器中缺乏大的空闲分区。最坏适应算法与前面所述的首次适应算法、循环首次适应算法、最佳适应算法一起被成为顺序搜索算法。
首次适应算法在《计算机操作系统(汤子瀛版)》描述如下:我们以空闲分区链为例来说明采用FF算法时的分配情况。FF算法要求空闲分区链以地址递增次序链接。在分配内存时,从链首开始顺序查找,直到找到一个大小能满足要求的空闲分区为止;然后再按照作业的大小,从该分区中划出一块内存空间分配给请求者,余下的空闲分区仍留在空闲链中。若从链首直至链尾都不能找到一个满足要求的分区,则此次内存分配失败,返回。该算法倾向于有限利用内存中低地址部分的空闲分区,从而保留了高地址部分的大空闲区。这给尾以后到达的大作业分配大的内存空间创造了条件。其缺点时低地址部分不断被划分,会留下许多难以利用的、很小的空闲分区,而每次查找右都时从低地址部分开始,这无疑会增加查找可用空闲分区时的开销。

作为Cache的溢出数据文件作为Cache的交换区,显然我们希望数据文件越小越好,此时一般的选择是使用首次适应算法(First Fit)。然而虽然FF算法能尽可能多的利用数据文件低地址部分的磁盘空间以减少磁盘文件的大小,但是它的缺点也是明显的,而WF算法虽然效率很高,但是它很容易使数据文件膨胀导致磁盘利用率很低。因而EHCache的空闲磁盘去管理时采用了两种结合的方法:即空闲链(AA Tree链)的以磁盘地址顺序排列,树的每个节点包含一个字段用于纪录当前节点以及其所有子节点中的最大size,在查找时,以层级顺序遍历,并且优先选择左子树(磁盘地址低的Region)。

EHCache将一个空闲磁盘区抽象成一个Region类,它包含start、end字段,用于纪录在当前该区域在磁盘文件中的其实位置;并且每个Region实例是AA Tree中的一个节点,因而它继承自AATreeSet.AbstractTreeNode类,即继承了该类的left、right、level字段;根据Region的比较算法,它大致上以Region所在磁盘文件的位置排序(而不是以Region的大小来排序),因而为了提升查找性能,它还包含了一个long类型的contiguous字段,该单词字面意思是“临近的、连续的”,用于表示该当前Region临近节点的区域的最大Region大小,即该字段表示当前Region以及其所有子节点的最大Region的大小,从而当在查找时,只有如果要查找的size比当前Region的contiguous字段要大的话,就可以不用继续查找其子节点了,并且通过该字段也实现了最坏适应算法。在每次更新左子节点和右子节点时都会调整contiguous的大小,在新创建一个Region节点时也会更新contiguous字段大小,从而保证当前Region中的contiguous始终是其所有子节点的最大size,对于叶子节点,其contiguous的值是当前Region的size。在计算size时,start,end值是闭合的。
public abstract static class AbstractTreeNode<E> implements Node<E> {
    private Node<E> left;
    private Node<E> right;
    private int     level;
    .....
}
public class Region extends AATreeSet.AbstractTreeNode<Comparable> implements Comparable<Comparable> {
    private long start;
    private long end;
    private long contiguous;

    public Region(long start, long end) {
        super();
        this.start = start;
        this.end = end;
        updateContiguous();
    }

    public long contiguous() {
        if (getLeft().getPayload() == null && getRight().getPayload() == null) {
            return size();
        } else {
            return contiguous;
        }
    }

    private void updateContiguous() {
        Region left = (Region) getLeft().getPayload();
        Region right = (Region) getRight().getPayload();
        long leftContiguous = left == null ? 0 : left.contiguous();
        long rightContiguous = right == null ? 0 : right.contiguous();
        contiguous = Math.max(size(), Math.max(leftContiguous, rightContiguous));
    }

    public void setLeft(AATreeSet.Node<Comparable> l) {
        super.setLeft(l);
        updateContiguous();
    }

    public void setRight(AATreeSet.Node<Comparable> r) {
        super.setRight(r);
        updateContiguous();
    }

    public long size() {
        // since it is all inclusive
        return (isNull() ? 0 : this.end - this.start + 1);
    }

    //Region的比较算法,使Region在这棵AA Tree中大致保持其在数据文件中的排序顺序
    public int compareTo(Comparable other) {
        if (other instanceof Region) {
            return compareTo((Region) other);
        } else if (other instanceof Long) {
            return compareTo((Long) other);
        } else {
            throw new AssertionError("Unusual Type " + other.getClass());
        }
    }
    private int compareTo(Region r) {
        if (this.start > r.start || this.end > r.end) {
            return 1;
        } else if (this.start < r.start || this.end < r.end) {
            return -1;
        } else {
            return 0;
        }
    }
    private int compareTo(Long l) {
        if (l.longValue() > end) {
            return -1;
        } else if (l.longValue() < start) {
            return 1;
        } else {
            return 0;
        }
    }
}

EHCache中对空闲磁盘的分配与回收

在C语言中使用malloc和free来对内存的分配与回收,在C++中使用new和delete,在Java中只有new,在EHCache中则将磁盘空间的分配与回收抽象成FileAllocationTree类,它提供alloc、free、mark等接口用于管理磁盘区的分配与回收。另外EHCache还增加了RegionSet类,它继承子AATreeSet类,用于表达它专门用于存储Region节点。这里吐槽一下,FileAllocationTree竟然设计成继承自RegionSet而不是组合。。。。。所有这些类的结构图如下:


磁盘的分配
磁盘的分配分成一下几个步骤(逻辑比较简单,就不贴代码了):
  1. 根据传入的size,在AA Tree中查找到一个可以容纳传入size大小的Region节点,并将找到的Region的前size部分分配出一个新的Region并返回。
    查找逻辑在RegionSet类中实现(find方法),它从root节点向下查找,因为root节点的contiguous字段保存了整棵树的最大size,因而先检查root节点的contiguous,如果size比root的Contiguous要大,则抛异常,因为整棵树中已经没有比传入的size要大的Region。然后层级遍历AA树,如果当前节点的size要比传入的size大或相等,则找到足以容纳传入size大小的Region节点,以当前节点的size大小的前部分新创建一个Region返回;否则如果它的左子树的contiguous字段要比传入size大,则向左子树查找;否则如果它的右子树的contiguous字段要比传入的size大,则向右子树查找;否则,抛出异常,因为左右子树都找不到可以容纳size大小的Region。
  2. 将新创建的Region实例mark成已经使用(这个新创建的Region的start和AA树中某个Region节点的start值一样,而end大小则不一定一样)。
    因为这个新创建的Region实例是要从AA树中的某个节点分出部分空间,因而首先要将AA树中的那个节点从树中移除,然后如果树中移除的节点的end值和新创建的Region的end值一样,则直接移除就可以了,否则,要将树种移除的节点剩余部分的重新创建一个Region插回树中。从代码的角度,首先以新创建的Region的start值找到树中对应的Region(Region中接收Long作为参数的compare方法的存在以实现这种方式的查找),将其移除并返回移除后的Region(removeAndReturn方法,在RegionSet中重新拷贝一个Region实例是为了防止通过R返回的Region改变树内部的状态,因为Region即作为一个Node也作为payload存在,同时也可以给接下来的插入提供新的Region节点),然后把将新创建的Region从原树中的Region中移除,这里的移除逻辑假设新创建的Region可以是原树中的Region的前部分、中间部分以及末位部分,作为前部分和末位部分,因为Region是新创建的节点,因而直接更新当前节点即可,而如果是中间部分,则前部分作为当前节点,而后部分作为新节点返回。最后,如果原树中节点还剩余部分数据作为新的空闲磁盘区添回到空闲磁盘树中。最后,检查是否需要增加文件大小,这里只需要更新文件大小的字段即可而不需要实际增加文件的大小,因为文件在写入时会自动增加大小。Region中移除其中的部分Region代码如下:
        protected Region remove(Region r) throws IllegalArgumentException {
            if (r.start < this.start || r.end > this.end) {
                throw new IllegalArgumentException("Ranges : Illegal value passed to remove : " + this + " remove called for : " + r);
            }
            if (this.start == r.start) {
                this.start = r.end + 1;
                updateContiguous();
                return null;
            } else if (this.end == r.end) {
                this.end = r.start - 1;
                updateContiguous();
                return null;
            } else {
                Region newRegion = new Region(r.end + 1, this.end);
                this.end = r.start - 1;
                updateContiguous();
                return newRegion;
            }
        }
  3. 最后将分配出来的Region实例返回,并纪录在DiskMarker中,在以后需要将磁盘中的数据重新读取到内存中时用于定位该数据在磁盘中的位置,并可以将该Regin回收。
磁盘的回收
磁盘的回收也分几个步骤来完成:
  1. 对要回收的Region,查找在当前树中是否有一个Region节点其start和要回收的Region的(end - 1)值相等,如果有,则删除树中的原节点并返回,合并这两个节点,将合并后的节点重新插入到树中。Region中合并的代码逻辑如下:
        protected void merge(Region r) throws IllegalArgumentException {
            if (this.start == r.end + 1) {
                this.start = r.start;
            } else if (this.end == r.start - 1) {
                this.end = r.end;
            } else {
                throw new IllegalArgumentException("Ranges : Merge called on non contiguous values : [this]:" + this + " and " + r);
            }
            updateContiguous();
        }
  2. 对要回收的Region(或合并后的Region),继续查找当前树是否有一个Region节点,其end和要回收的(或已合并的)Region的(start - 1)的值相等,如果有,则删除树中的原节点并返回,合并这两个节点,将合并后的节点继续插入树中。
  3. 如果在树中找不到可以和回收的Region合并的Region节点,则只是将要合并的Region添加到树中。
  4. 最后如果回收后数据文件可以减小,更新数据文件大小的字段,并将数据文件的缩小,从而保持数据文件处于尽量小的状态。
最后我写了一个简单的测试程序,对磁盘分配与释放做了一些随机的模拟,以增加一些直观的感受(类似DiskStorageFactory,在创建FileAllocationTree时,给了Long.MAX_VALUE的初始值,我这也给这个值作为初始值,从而保证基本上在所有情况下,都能找到一个合适的Region节点,也就是说FileAllocationTree不用来控制数据文件的大小,数据文件的大小由其他逻辑来控制,这在后面会详细讲解): 
public class FileAllocationTreeTest {
    public static void main(String[] args) {
        final int count = 5;
        Random random = new Random();
        
        FileAllocationTree alloc = new FileAllocationTree(Long.MAX_VALUE, null);
        List<Region> allocated = Lists.newArrayList();
        for(int i = 0; i < count; i++) {
            int size = random.nextInt(1000);
            Region region = alloc.alloc(size);
            System.out.println("new size: " + size + ", " + toString(region) + ", filesize: " + alloc.getFileSize() + ", allocator: " + toString(alloc));
            allocated.add(region);
        }
        
        for(int i = 0; i < count; i++) {
            int size = random.nextInt(1000);
            Region region = alloc.alloc(size);
            System.out.println("new size: " + size + ", " + toString(region) + ", filesize: " + alloc.getFileSize() + ", allocator: " + toString(alloc));
            allocated.add(region);
            region = allocated.get(random.nextInt(allocated.size()));
            alloc.free(region);
            allocated.remove(region);
            System.out.println("Freed region: " + toString(region) + ", after file size: " + alloc.getFileSize() + ", allocator: " + toString(alloc));
        }
    }
    
    private static String toString(FileAllocationTree alloc) {
        StringBuilder builder = new StringBuilder("[");
        for(Region region : alloc) {
            builder.append(toString(region)).append(", ");
        }
        builder.replace(builder.length() - 2, builder.length() - 1, "]");
        return builder.toString();
    }
    
    private static String toString(Region region) {
        return "Regin(" + region.start() + ", " + region.end() + ")";
    }
}
输出的随机结果如下: 
new size: 397, Regin(0, 396), filesize: 397, allocator: [Regin(397, 9223372036854775806)] 
new size: 175, Regin(397, 571), filesize: 572, allocator: [Regin(572, 9223372036854775806)] 
new size: 210, Regin(572, 781), filesize: 782, allocator: [Regin(782, 9223372036854775806)] 
new size: 11, Regin(782, 792), filesize: 793, allocator: [Regin(793, 9223372036854775806)] 
new size: 432, Regin(793, 1224), filesize: 1225, allocator: [Regin(1225, 9223372036854775806)] 
new size: 226, Regin(1225, 1450), filesize: 1451, allocator: [Regin(1451, 9223372036854775806)] 
Freed region: Regin(572, 781), after file size: 1451, allocator: [Regin(572, 781), Regin(1451, 9223372036854775806)] 
new size: 500, Regin(1451, 1950), filesize: 1951, allocator: [Regin(572, 781), Regin(1951, 9223372036854775806)] 
Freed region: Regin(793, 1224), after file size: 1951, allocator: [Regin(572, 781), Regin(793, 1224), Regin(1951, 9223372036854775806)] 
new size: 681, Regin(1951, 2631), filesize: 2632, allocator: [Regin(572, 781), Regin(793, 1224), Regin(2632, 9223372036854775806)] 
Freed region: Regin(1951, 2631), after file size: 1951, allocator: [Regin(572, 781), Regin(793, 1224), Regin(1951, 9223372036854775806)] 
new size: 23, Regin(793, 815), filesize: 1951, allocator: [Regin(572, 781), Regin(816, 1224), Regin(1951, 9223372036854775806)] 
Freed region: Regin(0, 396), after file size: 1951, allocator: [Regin(0, 396), Regin(572, 781), Regin(816, 1224), Regin(1951, 9223372036854775806)] 
new size: 109, Regin(816, 924), filesize: 1951, allocator: [Regin(0, 396), Regin(572, 781), Regin(925, 1224), Regin(1951, 9223372036854775806)] 
Freed region: Regin(1225, 1450), after file size: 1951, allocator: [Regin(0, 396), Regin(572, 781), Regin(925, 1450), Regin(1951, 9223372036854775806)] 


DLevin 2013-10-29 01:59 发表评论

相关 [java cache ehcache] 推荐:

Java Cache-EHCache系列之Store实现

- - BlogJava-首页技术区
写了那么多,终于到Store了. Store是EHCache中Element管理的核心,所有的Element都存放在Store中,也就是说Store用于所有和Element相关的处理. EHCache中的Element. 在EHCache中,它将所有的键值对抽象成一个Element,作为面向对象的设计原则,把数据和操作放在一起,Element除了包含key、value属性以外,它还加入了其他和一个Element相关的统计、配置信息以及操作:.

Java Cache-EHCache系列之AA-Tree实现溢出到磁盘的数据管理

- - BlogJava-首页技术区
在EHCache中,如果设置了overflowToDisk属性,当Cache中的数据超过限制时,EHCache会根据配置的溢出算法(先进先出(FIFO)、最近最少使用算法(LRU)等),选择部分实例,将这些实例的数据写入到磁盘文件中临时存储,已减少内存的负担,当内存中的实例数减少(因为超时或手工移除)或某些实例被使用到时,又可以将这些写入磁盘的数据重新加载到内存中.

Java Cache-EHCache系列之计算实例占用的内存大小(SizeOf引擎)

- - BlogJava-首页技术区
计算一个实例内存占用大小思路. 在Java中,除了基本类型,其他所有通过字段包含其他实例的关系都是引用关系,因而我们不能直接计算该实例占用的内存大小,而是要递归的计算其所有字段占用的内存大小的和. 在Java中,我们可以将所有这些通过字段引用简单的看成一种树状结构,这样就可以遍历这棵树,计算每个节点占用的内存大小,所有这些节点占用的内存大小的总和就当前实例占用的内存大小,遍历的算法有:先序遍历、中序遍历、后序遍历、层级遍历等.

Java Cache-EHCache系列之AA-Tree实现溢出到磁盘的数据管理(2)

- - BlogJava-首页技术区
在上一篇《 Java Cache-EHCache系列之AA-Tree实现溢出到磁盘的数据管理(1)》已经详细讲解了EHCache中在AATreeSet中对AA Tree算法的实现,并且指出EHCache是采用它作为空闲磁盘管理数据结构,本文主要关注于EHCache是如何采用AATreeSet类来管理空闲磁盘的(这里的磁盘管理是只EHCache data文件中的空闲磁盘).

Java Cache系列之Guava Cache

- - BlogJava-首页技术区
然而作为工具库中的一部分,我们自然不能期待Guava对Cache有比较完善的实现. 因而Guava中的Cache只能用于一些把Cache作为一种辅助设计的项目或者在项目的前期为了实现简单而引入. 在Guava CacheBuilder的注释中给定Guava Cache以下的需求:. 对于这样的需求,如果要我们自己来实现,我们应该怎么设计.

从Java视角理解CPU缓存(CPU Cache)

- - 淘宝网通用产品团队博客
从Java视角理解系统结构连载, 关注我的微博( 链接)了解最新动态众所周知, CPU是计算机的大脑, 它负责执行程序的指令; 内存负责存数据, 包括程序自身数据. 同样大家都知道, 内存比CPU慢很多. 其实在30年前, CPU的频率和内存总线的频率在同一个级别, 访问内存只比访问CPU寄存器慢一点儿.

Guava cache

- - 孟飞阳的博客
Guava Cache是一个全内存的本地缓存实现,它提供了线程安全的实现机制. 整体上来说Guava cache 是本地缓存的不二之选,简单易用,性能好.    Guava Cache有两种创建方式:.   通过这两种方法创建的cache,和通常用map来缓存的做法比,不同在于,这两种方法都实现了一种逻辑——从缓存中取key X的值,如果该值已经缓存过了,则返回缓存中的值,如果没有缓存过,可以通过某个方法来获取这个值.

巧用query cache

- - OurMySQL
   收到一用户反馈其应用日志中狂报错误,获取连接超时:. 同时应用报错超出了数据库的最大连接数:max connections:. 这种情况很有可能是有慢sql占用了连接池中的连接没有释放,导致后续进来的请求迟迟获取不到连接池中的连接,导致请求报错,登录数据库排查发现如下sql出现执行非常的慢:.

在Spring、Hibernate中使用Ehcache缓存

- - BlogJava-首页技术区
前一篇 http://www.blogjava.net/hoojo/archive/2012/07/12/382852.html介绍了Ehcache整合Spring缓存,使用页面、对象缓存;这里将介绍在Hibernate中使用查询缓存、一级缓存、二级缓存,整合Spring在HibernateTemplate中使用查询缓存.

[转][转]Redis、Memcached、Guava、Ehcache中的算法

- - heiyeluren的blog(黑夜路人的开源世界)
缓存那些事,一是内存爆了要用LRU(最近最少使用)、LFU(最少访问次数)、FIFO的算法清理一些;二是设置了超时时间的键过期便要删除,用主动或惰性的方法. 今天看 Redis3.0的发行通告里说,LRU算法大幅提升了,就翻开源码来八卦一下,结果哭笑不得,这旧版的"近似LRU"算法,实在太简单,太偷懒,太Redis了.