1.缘起:
在增量自动获取器章节的缘起部分,我们曾提到增量缓存,本节我们将深入探讨它以及用于管理增量缓存的管理器。我们还是以增量自动获取器章节提到的例子作为基础,并做更进一步的讨论。
OK,现在让我们开始这有趣的旅程。
首先,基于前面例子给出的上下文,我们知道IIncreaseAutoRetriever获取的增量是用于累积当天的已成交订单报表的。“当天已成交报表”就是一个典型的增量缓存,每当有新的增量到来,都会累加到上面。
我们假设今天是2009.07.08,那么我们可以将当前的增量缓存的ID标记为20090708,每隔一分钟都会有新的增量累加到该增量缓存实例中去。直到时钟指向2009.07.09的00:00:00时,表示当前这一天已经结束,此时,将创建一个新的ID为20090709的增量缓存实例来容纳新的增量。而ID为20090708的增量缓存实例将不会再有新的增量进来,所以,其内容也将不再发生变化。此时,我们可以将ID为20090708的增量缓存实例序列化后进行存储。这样做的好处是,当以后任何时候我们需要查询2009.07.08这一天的报表时,可以直接从存储设备加载20090708的增量缓存到内存中并反序列化,然后返回结果,这样就不用再去重新统计这一天的所有已成交的订单了。
所以,当时间进入2009.07.09,那么ID为20090708的增量缓存实例就变成了一个历史缓存,而标记为20090709的新缓存会处理新来的增量。如此,我们可以使用一个管理器将所有的历史缓存和当前正在运行的缓存管理起来。我设计的ESBasic.ObjectManagement.Increasing.Management.RoundCacheManager就是为的这个目的。
Round缓存管理器的形象示意图如下:
2.适用场合:
RoundCacheManager是依赖于IIncreaseAutoRetriever的,所以如果要使用RoundCacheManager,首先必须满足IIncreaseAutoRetriever所需的条件,另外还有RoundCacheManager需要的额外增强的条件。
(1)需要定时从数据源获取新的增量数据。
(2)数据源可能不只一个。
(3)需要支持“轮”(Round)的概念。在Round切换时,需要能够准确识别增量断点。
(4)增量数据有某个字段是递增的。
(5)需要将获取的增量数据放入到正确的增量缓存。
(6)在Round发生切换时,需要产生新的增量缓存来接收新的增量。而历史的增量缓存也需要被管理起来。
(7)被管理的历史缓存在其过期的时候,会自动从管理器中移除。
3.设计思想与实现
在分析RoundCacheManager的源码之前,我们先将其会涉及到的一些重要概念说明一下。
由于增量缓存是基于Round的,比如上面提到的一天为一个Round,所以,在ESBasic中称这样的缓存为Round Cache,如果是当前正在使用的增量缓存,则称为Round Increasing Cache。
当Round的切换点到来时,当前的Round Increasing Cache就会演变成一个属于历史的Round Cache。
首先,ESBasic要求Round Cache必须实现IRoundCache泛型接口,IRoundCache接口定义如下:
/// <summary>
/// IRoundCache 某一Round的完整的数据缓存。用于被序列化存储。
/// </summary>
public interface IRoundCache<TRoundID>
{
TRoundID RoundID { get; }
}
该接口的泛型参数TRoundID用于抽象Round ID的类型,比如我们前面例子中提到的20090708,其TRoundID就是一个整数类型。
IRoundCache接口只有一个RoundID属性,用于返回该Round Cache的ID。
一般来说,我在实现IRoundCache接口时,会将实现类标记为可序列化的,这是为我们前面提到的“可以将历史的Round Cache序列化后进行存储”做准备的。
当前正在使用的Round Cache必须实现IRoundIncreasingCache泛型接口,其定义的源码如下:
/// <summary>
/// IRoundIncreasingCache 用于存储当前Round数据的增量缓存
/// </summary>
public interface IRoundIncreasingCache<TRoundID, TRoundCache, TObject> where TRoundCache : IRoundCache<TRoundID>
{
TRoundID RoundID { get; }
void AddIncreasement(IList<TObject> list);
/// <summary>
/// CreateRoundCache 当需要序列化存储当前Cache时,先转化为轻量的TRoundCache对象,然后再进行存储。
/// </summary>
TRoundCache CreateRoundCache();
}
该接口有三个泛型参数:TRoundID、TRoundCache、 TObject。其中TRoundID和 TObject我们已经很熟悉了,而TRoundCache就是实现了IRoundCache接口的类型。
IRoundIncreasingCache接口的RoundID属性与IRoundCache接口的同名属性是一样的含义,这表明当前正在使用的增量缓存也有自己的唯一ID。
AddIncreasement方法用于将获取到的增量数据累加到IRoundIncreasingCache中。
CreateRoundCache方法通常在Round切换点出现并完成当前Round的最后一次增量累积时,用于将IRoundIncreasingCache转化为一个历史的Round Cache,这样便可将其序列化存储。
从存储设备加载历史的Round Cache以及将Round Cache序列化存储的工作是由IRoundCachePersister接口来完成的,其定义如下:
/// <summary>
/// IRoundCachePersister 用于持久化或加载RoundCache。
/// </summary>
public interface IRoundCachePersister<TRoundID, TRoundCache> where TRoundCache : IRoundCache<TRoundID>
{
/// <summary>
/// Persist 注意,该方法不得抛出异常。
/// </summary>
void Persist(TRoundCache roundCache);
void Delete(TRoundID roundID);
IDictionary<TRoundID, TRoundCache> LoadCaches(int maxHistoryCountInMemory);
}
该接口的Persist方法用于将一个Round Cache持久化到存储设备中;Delete方法用于从存储设备删除指定ID 的Round Cache记录;LoadCaches用于从存储设备加载离当前最近的N个Round Cache。
接下来,我们将注意力转移到本节的主角RoundCacheManager上来,RoundCacheManager的工作职责包含以下几个方面:
(1)初始化时,RoundCacheManager会做以下几个动作:首先,从存储设备加载一定数量的历史Round Cache到内存中并管理起来。其次,创建一个属于当前的Round Increasing Cache来处理即将到来的增量数据。
(2)当Round切换点到来时,将当前的Round Increasing Cache转化为一个历史的Round Cache,并将其序列化存储。然后创建一个新的Round Increasing Cache来处理后续的增量数据。
(3)如果管理器中的某些历史Round Cache不再被需要,则会从管理器中移除。
RoundCacheManager的类图如下所示:
我将RoundCacheManager的实现要点罗列如下:
(1)CurrentRoundCache只读属性用于暴露当前正在工作的增量缓存。
(2)LastRefreshTime属性记录了最后一次获取增量并完成增量累积的时间。
(3)MaxHistoryCountInMemory用于指示在内存中最多保存多少个历史的Round Cache。在Round切换点时,将会产生新的历史Round Cache,这时如果历史Round Cache的数量超过MaxHistoryCountInMemory,则管理器会从内存中移除最老的那个Round Cache。
(4)当Round切换点发生时,管理器将触发NewRoundStarted事件以通知相关预订者。
(5)AddRoundCache方法和RemoveRoundCache方法用于操作管理器中的历史Round Cache。这两个方法都有一个bool参数,用于指示是否要针对存储设备做相应的动作。
(6)GetHistoryRoundCache用于返回管理器中存在的某个历史Round Cache。
如果要获取当前正在工作的Round Cache,应该直接使用CurrentRoundCache属性。如果目标Round Cache在管理器中不存在,GetHistoryRoundCache将返回null。为避免返回null,可以先调用ContainsHistory以查明目标是否存在于管理器中。
RoundCacheManager是一个abstract类,其有三个abstract方法留给派生类去实现。这三个方法的实现取决于具体应用的需求。
CreateNewRoundIncreasingCache方法用于创建一个当前正使用的增量缓存。该方法之所以要设计为abstract,是因为在RoundCacheManager中,我们并不知道该如何创建一个TRoundIncreasingCache对象,它有哪些构造函数、需要些什么参数等等,我们对此一无所知。
GetNextRoundID方法用于根据当前的Round ID创建下一个Round ID。这也是由具体应用来决定的。
GetExpiredHistoryList方法会在每次管理器中的历史Round Cache的数量发生增加时被调用,RoundCacheManager会依据该方法返回的列表来移除那些过期的历史Round Cache。
4. 使用时的注意事项
(1) RoundCacheManager以基类被继承的方式提供复用。RoundCacheManager搭建起了一个关于增量数据的获取、增量缓存的累积、Round的自动切换、增量缓存的自动持久化这样一个流程的完整骨架。使用者只需要从其继承并override几个抽象方法便可复用这样的处理流程。
(2) 通常来说,RoundCacheManager的继承者除了override几个抽象方法外,还会提供一系列的与应用相关的查询方法给外部使用。比如,在我们缘起部分的例子中,我们可以在派生类中设计一个类似下面的方法来提供对某个用户某段时间内的报表数据进行查询:
ReportData GetUserReportData(string userID, int startDate, int endDate);
(3) 本模型的驱动源在于IIncreaseAutoRetriever的循环引擎,如果其抛出异常而停止运行,那么在此同时,本RoundCacheManager也就丧失了驱动力,后续将再无任何增量进入,Round切换事件NewRoundStarted将永远也不会再触发。
(4) 外部在调用RoundCacheManager的AddRoundCache、RemoveRoundCache方法时要捕获异常,因为持久化过程中可能会出错。
(5) 同介绍IIncreaseAutoRetriever时同样的道理,如果IIncreaseAutoRetriever抛出异常导致引擎停止运行,那么最好的方法就是重启应用程序。当然,这种情况发生的可能性是非常非常微小的。
5.扩展
Round缓存管理器RoundCacheManager暂时没有任何扩展。
注: ESBasic已经开源,点击这里下载源码。
ESBasic开源前言