增量自动获取器 IIncreaseAutoRetriever -- ESBasic 可复用的.NET类库(25)

1.缘起:

    假设我们的订单报表系统,需要能够实时地统计当天的已成交订单的报表。最直观的解决方案就是,当每次接收到查询报表的请求时,就从存储设备读取当天所有已成交的订单,然后再进行分析计算给出结果。这是可行的,而且得到的结果也是非常实时的。

但是,这种方式无疑也是非常低效的,因为我们报表数据的统计过程可能相当复杂,而且,可能是成千上万的用户在同时查询报表数据。在这种情况下,将给存储服务器和业务服务器都造成巨大的压力。

现在我们假设需求能够放宽一点――报表数据不用非常实时,可以允许最大为1分钟的延迟。对于绝大多数业务系统来说,报表数据延迟在1分钟以内,都是能够接受的,所以这个假设并不过分,下面我们基于这个假设来继续讨论。

在报表数据能够允许最大延迟为1分钟许可条件下,我们就可以做缓存的动作了。因为如果要求是完全实时的,那么缓存报表数据能做到的程度就非常的有限,但是有了这一分钟的延迟允许,缓存就可以完全地发挥出它的优势了。

现在,我们可以每隔1分钟从存储设备读取上一分钟已成交的所有注单,这就是所谓的“增量数据”,然后将其做报表分析计算,然后将计算的结果“累加”到之前的“累积的报表数据”上。这样,当用户每次来请求报表数据时,只要将当前累积的报表数据直接返回就可以了。因为我们是每隔1分钟对增量进行累加一次,所以,用户看到的报表数据的延迟最多不会超过1分钟。

 

上述这个例子很好地体现了“增量缓存”的核心思想,上面提到的“累积的报表数据”实际上就是一个增量缓存的典型实例。关于增量缓存,我们会在下一节进行详细介绍。本节我们将先介绍增量缓存会用到的一个基础组件――增量自动获取器 ESBasic.ObjectManagement.Increasing.IIncreaseAutoRetriever

现在,我们将注意力集中到上述例子中关于数据增量获取的这个焦点上:

(1)增量可能需要从多个数据源获取。比如,不同类型的订单可能存放在不同的数据库中。

(2)如何体现以一天为单位进行轮转?如果要查看的是周报表,那么就需要将一周作为一轮(Round)。

(3)如果是一天为一轮,那么当经过每天的00:00:00时,增量获取器该如何完结前一天的增量,并切换到新的一天?

 

 这些都是IIncreaseAutoRetriever要解决的问题,而IIncreaseAutoRetriever很好的解决了这些问题,并以一种非常简单易用的接口提供给使用者。

    增量自动获取器的形象示意图如下:

   

 

2.适用场合:

    在满足以下这些条件时,你可以使用IIncreaseAutoRetriever

(1)需要定时从数据源获取新的增量数据。

(2)数据源可能不只一个。

(3)需要支持“轮”(Round)的概念。在Round切换时,需要能够准确识别增量断点。

(4)增量数据有某个字段是递增的。

 

3.设计思想与实现

   在正式解析IIncreaseAutoRetriever的源码之前,我们先将其会涉及到的一些重要概念说明一下。

   首先,数据源可能是多个,所以我们需要为每个数据源设置一个唯一标志(我们称之为Source Token),这标志的类型可能是一个整数,也可能是一个字符串或枚举类型等等。那么,我们可以将数据源标志的类型抽象为一个泛型参数。

   其次,上面我们提到Round,即表示增量完成累积的一个完整周期,比如,可能是一天、一周或一月,这取决于你系统的需求。同样的,我们需要每个Round都有唯一的标志(我们称之为Round ID),以将当前Round同历史的Round区分开来。Round ID的类型也是根据系统的需求来定的,所以,我们也将Round ID的类型抽象为一个泛型参数。

   再次,我们将缘起部分例子中的一分钟的时间间隔称为一个增量阶段Phase。所以,一个Round是由NPhase构成的,而且,N通常是个比较大的值。

最后,由于获取增量数据的时候,我们需要依据数据的某个key来进行判断,哪些数据是在刚过去的一个Phase中新加入进来的,这就要求key是递增的,这对从数据源中提取增量会带来极大的便利。比如,可以使用订单的产生时间作为key,如果订单的编号是递增的,也可以使用订单编号作为key。在数据源是数据库的情况下,最好是使用主键作为key,当然,这个时候主键必须是递增的。

     IIncreaseAutoRetriever 增量自动获取器会每隔一段时间就从各个数据源(TSourceToken)获取上一阶段的增量数据(TObject),然后触发事件将得到的增量数据发布出去。

 

下面,我们来看IIncreaseAutoRetriever接口定义:

    /// <summary>
    
/// IIncreaseAutoRetriever 增量数据自动获取器。每隔一段时间就从各个来源(TSourceToken)获取一次增量(TObject),并触发事件将增量发布出去。
    
/// (1)一个Round由多个连续的Phase构成。当获取某Round的最后一个Phase增量时,触发的事件中的isLastPhaseOfRound参数为true。
    
/// (2)假设增量标志是逐渐递增的。
    
/// zhuweisky 2009.02.24
    
/// </summary>
    
/// <typeparam name="TSourceToken">增量来源的标志</typeparam>
    
/// <typeparam name="TKey">每个增量Object的标志</typeparam>
    
/// <typeparam name="TObject">增量Object的类型</typeparam>
    public interface IIncreaseAutoRetriever<TSourceToken,TRoundID, TKey, TObject>
    {
        
/// <summary>
        
/// AutoRetrieveSpanInSecs 设置多长时间为一增量阶段。
        
/// </summary>
        int AutoRetrieveSpanInSecs { getset; }
        
        
IPhaseIncreaseAccesser<TSourceToken, TRoundID, TKey, TObject> PhaseIncreaseAccesser { set; }

        
event CbIncreasementRetrieved<TRoundID ,TObject> IncreasementRetrieved;

        
/// <summary>
        
/// ExceptionOccurred 当提取增量数据出现异常或IncreasementRetrieved事件处理器抛出异常时,将触发此事件,并且引擎将停止运行。
        
/// </summary>
        event CbException ExceptionOccurred;   
        
        
void Initialize();

        
/// <summary>
        
/// ManualRefresh 手动刷新获取增量。
        
/// </summary>
        void ManualRefresh();        
    }

    
public delegate void CbIncreasementRetrieved<TRoundID ,TObject>(List<TObject> list, TRoundID currentRoundID, bool isLastPhaseOfRound);

 

这个接口有四个泛型参数:TSourceTokenTRoundIDTKeyTObject。其中TSourceToken就是我们刚刚讲到的数据源标志的类型,TRoundID就是RoundID的类型,TKey就是递增的key的类型,TObject即增量数据对象的类型。

     AutoRetrieveSpanInSecs属性指明需要多长时间获取一次增量数据,这个值以秒为单位。

     CbIncreasementRetrieved事件用于在每阶段的增量数据获取完毕之后触发,如此,事件预订者便可以处理增量数据。

   当从数据源中提取增量数据出现异常,或IncreasementRetrieved事件的预订者在处理增量数据时抛出异常,则IIncreaseAutoRetriever将触发此事件通知我们,并且内部的循环引擎将停止运行,后续将不再去获取增量数据。

     ManualRefresh方法用于手动调用以立即获取增量数据。即,当我们需要立刻读取最新的增量数据时,就不必等到引擎的定时触发,而是可以立即获取。通常,该方法只有在紧急情况下才会使用。

     IIncreaseAutoRetriever还有一个IPhaseIncreaseAccesser属性我们放到最后来介绍。IPhaseIncreaseAccesser称为“阶段数据访问器”,从其名字就可以了解到,真正与数据源打交道的是它。也就是说,IIncreaseAutoRetriever是通过IPhaseIncreaseAccesser来从数据源中提取数据的。通过这样的分工之后,这两个类的职责都变得相对的简单和单纯――IIncreaseAutoRetriever只需要管理循环引擎,管理Round的切换点,和整合从不同数据源提取的数据,并触发事件;而IPhaseIncreaseAccesser只需要完成从数据源中提取数据。

   至于数据源是数据库、还是文件或网络,我们现在不得而知,只有在具体的应用中才会确定下来。所以,IPhaseIncreaseAccesser是被设计为一个接口,ESBasic没有提供它的默认实现,我们需要在我们的应用中根据我们的数据源的类型来实现这个接口。

      

现在,我们来看看IPhaseIncreaseAccesser接口的定义:

 

    /// <summary>
    
/// IPhaseIncreaseAccesser 用于从各个源访问每一阶段的增量数据。
    
/// </summary>   
    
/// <typeparam name="TSourceToken">增量来源的标志</typeparam>
    
/// <typeparam name="TKey">Round的ID的类型</typeparam>
    
/// <typeparam name="TKey">每个增量Object的标志</typeparam>
    
/// <typeparam name="TObject">增量Object的类型</typeparam>
    public interface IPhaseIncreaseAccesser<TSourceToken, TRoundID, TKey, TObject>
    {
        
/// <summary>
        
/// GetMaxKeyOfPreviousRound 获取上一轮各个源中的数据的最大标志。
        
/// </summary>       
        IDictionary<TSourceToken, TKey> GetMaxKeyOfPreviousRound();

        
/// <summary>
        
/// NextIsLastPhaseOfRound 下一增量(基于now时刻)是否为当前Round的最后一个Phase。如果是,则out出每个源的最后Phase的最大标志。
        
/// </summary>               
        bool NextIsLastPhaseOfRound(DateTime now ,out TRoundID currentRoundID, out IDictionary<TSourceToken, TKey> lastKeyOfRoundDic);

        
/// <summary>
        
/// GetMaxKey 获取指定源中的截止now时刻(可以等于)的最大标志。
        
/// </summary> 
        TKey GetMaxKey(DateTime now, TSourceToken token);      

        
/// <summary>
        
/// Retrieve 获取某一阶段的增量。maxKeyOfPrePhase 《 本阶段增量 《=  maxKeyOfThisPhase
        
/// </summary>   
        IList<TObject> Retrieve(TSourceToken token, TKey maxKeyOfPrePhase, TKey maxKeyOfThisPhase);
    }

 

该接口有4个方法,都是用于从数据源中提取相关信息的。

GetMaxKeyOfPreviousRound方法用于获取上一Round结束时各个数据源中的数据的最大key值。由于我们前面的约定――key值是递增的,所以本Round的所有增量数据的key一定是大于该key值。

NextIsLastPhaseOfRound方法用于判断下一Phase是否为当前Round的最后一个Phase。如果是,则out出每个源的最后Phase的最大标志值。注意,无论是不是最后一个Phase,第一个out参数currentRoundID都必须赋予正确的值,以表示当前所处的是哪一个Round

GetMaxKey方法用于获取指定数据源中的数据的最大标志值。如此,和上一Phase的最大标志值结合起来便可以确定本Phase的增量的标志值的范围。

Retrieve是真正获取某一阶段的增量的方法。其中两个参数限定的了增量的标志值的取值范围,它满足以下公式:

maxKeyOfPrePhase  <  本阶段增量的key  <= maxKeyOfThisPhase

 

有了IPhaseIncreaseAccesser的支持,IncreaseAutoRetriever的实现就水到渠成了,我将其实现的源码中的关键点罗列如下:

(1)IncreaseAutoRetrieverBaseCycleEngine继承,这表明IncreaseAutoRetriever是借助我们前面介绍的循环引擎来完成定时检测动作的。

(2)maxKeyOfLastPhaseDictionary成员用于存储最后一阶段的每个数据源中增量的最大key值。在Initialize方法初始化时,其初始值被设置为上一Round数据源中增量的最大key值。

(3)IncreaseAutoRetriever使用了lock加锁控制,为什么需要锁了?因为循环引擎本身就是在单线程中运行的啊,岂不是多此一举?实际上,该锁的主要作用在于防止手动调用ManualRefresh刷新增量的同时,循环引擎也正好开始进行定时检测动作。如果不加锁,那么当这两个动作同时发生时,可能会导致发布的增量数据出现重复的错误。

(4)IncreaseAutoRetriever中最核心的是DoDetect方法,它的逻辑稍微有些复杂,我这里简单说明一下。

首先,它会判断是否到了Round的切换点,如果不是,则正常获取增量,并将增量整合进一个列表,触发事件。如果是,则根据IPhaseIncreaseAccesserNextIsLastPhaseOfRound方法out出的当前Round的最大的key值,来确定当前Round最后一个Phase的增量范围。

其次,当在每一Phase的增量获取完毕时,都会将该Phase增量的最大key值存储到maxKeyOfLastPhaseDictionary字典中,该缓存值将会用于下一Phase的增量key范围的起始值。

当增量收集完毕,将触发的IncreasementRetrieved事件,该事件包含了三个参数,其含义分别是:增量数据列表,当前的RoundID,当前Phase是否为该Round的最后一个Phase

最后,如果上述过程抛出任何异常,DoDetect方法将返回false,以指示循环引擎不再继续运行。

 

4. 使用时的注意事项

(1)虽然在缘起部分我们提到,IIncreaseAutoRetriever是给下节要介绍的“增量缓存”使用的一个基础组件,但是,这并不表示IIncreaseAutoRetriever只能给ESBasic中的“增量缓存”使用。实际上,IIncreaseAutoRetriever是一个独立的可复用组件,凡是需要使用定时增量获取的地方都可以使用它。

(2)如果不需要Round支持,或者说所有增量都是在同一个Round里面,那么也可以照样使用IIncreaseAutoRetriever,只是在实现IPhaseIncreaseAccesser接口的NextIsLastPhaseOfRound方法时,全部返回false即可。在这种情况下,IIncreaseAutoRetriever就永远不会发生Round的切换。

(3)如果你的系统只需要一个数据源,那么TSourceToken对你来说就是没有意义的,ESBasic准备好了ESBasic.ObjectManagement.Increasing.SingleSource来应对这种情况,SingleSource被设计为Singleton(单件)。所以,此时你可以使用SingleSource来取代TSourceToken泛型参数,并且使用SingleSource. Singleton来作为唯一数据源的标志对象。

(4)DoDetect方法中如果抛出异常,循环引擎将停止运行,这个设计是经过深思熟虑的。在这个方法中,最可能出现异常的是IPhaseIncreaseAccesser从数据源中提取数据的方法。当这些方法抛出异常时,会导致maxKeyOfLastPhaseDictionary字典中存储的值的状态不一致――即有些项存储的已经是当前Phase的最大key值,而另外一些项存储的还是上一Phase的最大key值。在这种情况下,想要恢复其到正确状态并继续运行下去就非常困难了,所以还不如直接停止引擎。如果这种情况真的发生,最简单的办法就是重新启动应用程序即可。

在我的经验中,IPhaseIncreaseAccesser都是从数据库提取数据,并且它们位于同一局域网内,所以至今还没出现过因IPhaseIncreaseAccesser抛出异常而导致IncreaseAutoRetriever停止运行的情况。

 

5.扩展

       增量自动获取器IIncreaseAutoRetriever暂时没有任何扩展。

 

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

 

 

posted @ 2010-10-06 14:46  zhuweisky  阅读(2743)  评论(2编辑  收藏  举报