片段整合提取器 ScatteredSegmentPicker -- ESBasic 可复用的.NET类库(21)

1.缘起:

    当数据源中的数据量多到一定程度时,我们在查询时就经常使用分页策略。如果数据源是一个完整的整体,这没有什么大不了的,我们经常就在做类似的事情。但是,如果数据源不是一个完整的整体,而是由很多有序的片段构成的,并且不同的片段可能位于不同的位置(比如,位于不同的服务器节点上的内存中),甚至,每个片段内的数据还会随着时间的变化而变化的。

    在这种假设的情况下,来从这个“虚拟的完整”数据源获取某个分页就不再是那么简单的事情了。一个分页可能位于一个片段的内部的某个区间,也有可能会跨越多个片段。

    我们举个例子,以更形象地说明上面描述的问题。

    假设,我们有如下几个包含整数的有序片段:

片段A:2、3、5、8

片段B:33、34、45、51、56、78、86

片段C:9、12、14、15、18、23

片段D:90、92、97、108、127

      A、B、C、D四个片段,每个片段内部包含的元素的个数都不一样,每个片段内部的整数都是有序的。我们还发现,任意两个片段的取值区间是没有交集的,如我们可以认为A的区间为[2,8],B的区间为[33,86] ,C的区间为[9,23],D的区间为[90,127]。由于任意两个片段的取值区间没有交集,所以,我们可以对这些片段的取值区间进行排序,排序的结果是:A、C、B、D。

    现在我们把排序后的A、C、B、D四个片段作为一个虚拟的整体数据源,然后对其进行分页。假设PageSize为5,如果从小到大取第二页的数据应该是:12、14、15、18、23;如果从大到小取第二页的数据应该是:86、78、56、51、45。

    你也许会想,这个很容易啊,我把所有片段中的数据取出来,按顺序排列好:

      2、3、5、8、9、12、14、15、18、23、33、34、45、51、56、78、86、90、92、97、108、127。

    现在想取第几页就可以直接取第几页了,问题解决。是吗?记得我们前面还有一个假设的需求在这里没有考虑进来,那就是每个片段内的数据还会随着时间的变化而变化的,当然,约定好的取值区间是不会变化的。比如,B片段的数据在下一时刻增加了几个数据变成这样:

片段B:33、34、40、42、45、50、51、56、62、78、83、86

    片段B增加的数据都是属于其取值区间[33,86]的,所以B的取值区间并不会发生变化。

    上面提到的把每个片段中的所有的数据都提取出来排列好的方法就不好用了吧,因为每查询某分页一次,就需要重新排序一次。如果我们的片段非常多,而且每个片段中的数据也非常多,那么,其效率可想而知。

    我设计了ESBasic.ObjectManagement.Integration.ScatteredSegmentPicker片段整合提取器来解决类似的问题。

 

2.适用场合:

    当满足以下条件时,你可以使用ScatteredSegmentPicker来对数据源进行分页操作。

(1)数据源由多个片段(Segment)组成。

(2)每个片段内部的数据都是有序的。

(3)任意两个片段的取值区间的交集为空。

(4)片段内的数据可能发生变化,但是其最初预定的取值区间一直是恒定的。

(5)每个片段都有唯一的ID来标志它。

(6)需要从小到大的顺序或从大到小的顺序对所有片段中的数据作为一个整体进行分页。

 

3.设计思想与实现

    根据上述的描述看到,片段是一个核心概念,我们使用ISegment接口来抽象它,ISegment定义如下:

    /// <summary>
    /// ISegment 片段,一个片段有有序的多个元素TVal构成。
    /// </summary>   
    /// <typeparam name="TSegmentID">片段标志的类型</typeparam>
    /// <typeparam name="TVal">构成片段的元素的类型</typeparam>
    public interface ISegment<TSegmentID, TVal>
    {
        /// <summary>
        /// ID 每个片段的唯一标志。
        /// </summary>
        TSegmentID ID { get; }

        /// <summary>
        /// GetContent 获取片段中的所有元素,从小到大排列。
        /// </summary>        
        IList<TVal> GetContent();
    }

 

该接口有两个泛型参数:TSegmentID、 TVal。TSegmentID是片段ID的类型,TVal是片段中包含的元素的类型。GetContent方法返回片段中所有元素的列表,并且这个列表中的元素是从小到大的顺序排列的。

    我们设计ISegmentContainer接口来提供所有的片段访问,ISegmentContainer接口定义如下:

    /// <summary>
    /// ISegmentContainer 用于存放片段ISegment的容器。
    /// </summary>
    /// <typeparam name="TSegmentID">片段标志的类型</typeparam>
    /// <typeparam name="TVal">构成片段的元素的类型</typeparam>
    public interface ISegmentContainer<TSegmentID, TVal>
    {
        ISegment<TSegmentID, TVal> GetSmallestSegment();
        ISegment<TSegmentID, TVal> GetBiggestSegment();

        /// <summary>
        /// GetNextSegment 按照fromSmallToBig指定的顺序返回下一个Segment。
        /// 如果返回null,则表示不再有后续的Segment了。
        /// </summary>      
        ISegment<TSegmentID, TVal> GetNextSegment(TSegmentID curSegmentID, bool fromSmallToBig);
    }

 

这个接口也有两个与ISegment一样的泛型参数,GetSmallestSegment方法和GetBiggestSegment方法用于返回取值区间最小和最大的片段。

    从GetNextSegment方法的设计我们知道,ISegmentContainer不是一下子返回所有的片段列表,而是根据当前使用的片段的ID返回下一个片段。之所以这样做,是因为我们一次性获取所有片段列表的代价可能是巨大的,所以一次只返回一个必需的片段。fromSmallToBig参数的值取决于我们是按降序分页还是按升序分页。

    如果不再有后续分页,GetNextSegment方法将返回null。

    在做了上述这些铺垫后,接下来我们来看本节真正的主角:ScatteredSegmentPicker,正是它将所有的片段整合为一个虚拟的完整数据源,然后对其进行分页提取。

      ScatteredSegmentPicker类图如下所示:     

         

  PickFromSmallToBig属性表明是要按升序还是降序来提取分页数据。Pick方法执行真正的分页提取动作,startIndex参数是指要以哪条记录作为分页的起始,pickCount参数表示要提取多少条记录,即分页的大小。

     Pick方法之所以传递startIndex参数和pickCount参数,而不是传递pageSize和pageIndex,是为了更灵活地支持从任意位置开始的连续记录的提取,而且,这也兼容了分页的提取。比如,你将pageSize*pageIndex作为startIndex参数传入,将pageSize作为pickCount参数传入即是一个标准的分页获取动作。

 

    关于ScatteredSegmentPicker的实现要注意以下几点:

(1)虽然ScatteredSegmentPicker内部实现没有使用到任何加锁机制,但是它可以被使用在多线程的环境中。

(2)ScatteredSegmentPicker的DoPick方法采用遍历策略来提取目标页的数据,PickFromSmallToBig属性决定了遍历片段的方向,是从小到大还是从大到小。

(3)如果要查询的分页不存在,则将返回一个不包含任何元素的List,而不是返回null。

 

4. 使用时的注意事项

(1)在使用ScatteredSegmentPicker之前,你必须根据你的应用的需求实现ISegment接口和ISegmentContainer接口,然后将ISegmentContainer实例的引用注入到ScatteredSegmentPicker。

(2)虽然我们使用ISegmentContainer来提供所需的每个片段,但是在实现该接口时,不一定非要将所有的片段都存放在ISegmentContainer这个容器中。ISegmentContainer可以作为一个片段获取器的角色从其它地方获取某个片段。比如,某个片段可能位于另外一台服务器的内存中,ISegmentContainer可以通过Remoting的方式从那台服务器获取这个片段的数据。

(3)也许你的片段中的数据不是从小到大或从大到小的顺序,而是依据另外一个性质进行排序,比如由不重要到重要,由不急迫到急迫等,这个只需要与PickFromSmallToBig做正确映射就可以正常使用ScatteredSegmentPicker了。

 

5.扩展

      片段整合提取器 ScatteredSegmentPicker暂时没有任何扩展。

 

注: ESBasic已经开源,点击这里下载源码。
    ESBasic开源前言

 

时间: 2024-10-15 16:08:27

片段整合提取器 ScatteredSegmentPicker -- ESBasic 可复用的.NET类库(21)的相关文章

对象获取器IObjectRetriever -- ESBasic 可复用的.NET类库(17)

1.缘起: ESBasic中许多管理对象的容器都用到了这个ESBasic.ObjectManagement.IObjectRetriever接口,所以单独将其提出来介绍一下. 当我们向对象容器(Container)请求某个对象时,也许目标对象还未加载到容器中,这可能是因为容器在初始化的时候就没有加载这个对象,也有可能是因为这个对象是容器初始化以后新增到数据库(当然也有可能是其它的持久化存储)的.在这种情况下,对象容器就可以借助IObjectRetriever来将目标对象从数据库等持久化存储中加载

优先级管理器 IPriorityManager -- ESBasic 可复用的.NET类库(14)

1.缘起:     假设我们的订单处理系统所要处理的订单是有优先级的,也就是说,不同的订单类型所要求被处理的紧迫程度不同,对那些优先级高的注单要先处理,对于优先级低的注单可稍后处理.对于处于同一优先级的订单了,就按照其到达的先后顺序进行处理.     这是一个典型的管理具有优先级的对象的需求,注单就是具有优先级(With Priority)的对象.我设计了ESBasic.ObjectManagement.Managers.IPriorityManager优先级管理器(确切地说,应该称之为"具有优

Round缓存管理器RoundCacheManager--ESBasic 可复用的.NET类库(26)

1.缘起:     在增量自动获取器章节的缘起部分,我们曾提到增量缓存,本节我们将深入探讨它以及用于管理增量缓存的管理器.我们还是以增量自动获取器章节提到的例子作为基础,并做更进一步的讨论.       OK,现在让我们开始这有趣的旅程. 首先,基于前面例子给出的上下文,我们知道IIncreaseAutoRetriever获取的增量是用于累积当天的已成交订单报表的."当天已成交报表"就是一个典型的增量缓存,每当有新的增量到来,都会累加到上面. 我们假设今天是2009.07.08,那么我

循环任务切换器 CircleTaskSwitcher -- ESBasic 可复用的.NET类库(06)

 1.缘起:     假设我的订单处理系统有这样的需求:将一天24小时分为4个时段,凌晨2:15到8:30采用A类型的处理器处理接收到的订单,8:30到14:00采用B类型的处理器,14:00到20:00采用C类型的处理器,20:00到第二天凌晨2:15采用D类型的处理器.     即我们的订单处理器需要在任一天的2:15.8:30.14:00.20:00这四个时刻发生切换,这就是一个循环切换器所要做的工作.     我设计了ESBasic.Threading.Application. ICir

定时刷新缓存管理器 IRefreshableCacheManager --ESBasic 可复用的.NET类库(16)

1.缘起:     为了提升系统的性能或减轻数据库的压力等原因,我们经常在系统中使用缓存来把那些经常使用的数据保留在内存中.如果因为某些原因,缓存中这些经常使用的数据不能及时与数据源进行同步更新,那么采用定时刷新缓存中的数据有可能就是一种合适的选择.     如果你的缓存是定时刷新,那么你就需要自己为其维护一个定时器或循环引擎.如果你的系统中像这样定时刷新的缓存有多个,而且每个缓存定时刷新的时间间隔又要求不一样,那么,使这些缓存按照你预想的情况进行运转,你就需要花费一些气力.     我设计了定

热缓存 IHotCache --ESBasic 可复用的.NET类库(19)

1.缘起:     假设我们有一个订单系统,现在这个系统要增加一个功能――允许客人查核他认为有问题的订单的详细信息.当客人觉得自己的某个订单不对劲时,他首先会从订单系统查询这个订单的详细信息,然后打电话告诉我们的客服有问题的订单的编号,客服再去查核,如果属实,客服还要进一步上报,如果该订单非常重要,则可能需要更进一步上报复查等.     从这个需求我们看到,同一个订单可能会在比较短的时间内查询数次甚至数十次,所以我们可以称这个订单为"热点"订单.而其它的成千上万的订单可能在一个月内都不

灵巧多叉树 IAgileMultiTree -- ESBasic 可复用的.NET类库(23)

1.缘起:     我们还是以多叉树IMultiTree章节介绍的那个例子来继续讲解.假设,在系统运行的过程中,集团又成立了分公司D及其下属的一些单位,这些资料已经被存入了数据库中,但是这些信息在我们当前正在运行的MultiTree实例中并不存在,如果此时向MultiTree实例请求与D分公司相关的信息,那么将一无所获.除非,你手动地将D分公司及其下属单位的节点值添加到MultiTree实例中.这是一个比较麻烦的动作.     试着设想一下这样一种情况,当我们要请求的节点在当前MultiTree

ESBasic 可复用的.NET类库(00) -- 开源前言(附下载)

自从03年正式使用.NET开发以来,已经走过了6个年头,这期间我积累了几套类库和框架,ESBasic便是其中最基础的一个类库.ESBasic是Enterprise Service Basic的缩写,虽然也简写为ESB,但是它和Enterprise Service Bus(企业服务总线)没有任何关系.ESBasic是我能够快速和高效开发应用程序的利器之一,开这个专门的blog是想将它介绍给大家,希望能对大家有所启发. ESBasic覆盖的内容包括:对象管理.插件.网络(Socket).多线程.Em

对象管理器 IObjectManager -- ESBasic 可复用的.NET类库(12)

1.缘起: 我们经常需要对一些动态对象进行管理,最常见的例子就是在线用户管理.当一个用户成功登陆到服务器后,我们就需要将其管理起来:当他退出后,就不再需要再管理他了.这就是所谓动态对象的含义,这些对象并不是一直需要被管理,只有当其被激活后,才需要被管理.它们总是在"激活"状态和"非激活"状态之间不断地切换. 我设计了对象管理器ESBasic.ObjectManagement.Managers.IObjectManager来管理类似的动态对象.这个类是ESBasic提