Adhesive框架系列文章--内存队列服务模块使用和实现
之前我们提到过,Mongodb数据服务的客户端和服务端都使用了内存队列服务模块来提交数据,使用内存队列服务有下列好处:
1、操作异步化:比如客户端对数据进行转换再调用Wcf把数据提交到服务端的时间需要10毫秒,那么,使用了队列服务之后,客户端向队列插入数据的方法只需要1毫秒调用即可完成,之后的9毫秒只会发生在后台。
2、降低瞬时的流量:比如在某个时刻有特别多的数据需要提交,那么这种不均匀的提交对服务端或者对数据库来说是一个瞬时的压力,使用了队列服务之后,队列服务会按照指定的间隔提交,并不会产生瞬时的压力,未提交的数据会保存在内存中。当然,在这里我们实现的是内存队列服务,因为不适合保存不允许丢失的数据。其实,在一般情况下,内存队列服务也可以满足需求了。
3、可以应对错误:队列服务有多种错误处理策略,可以满足各种的错误处理需求。
4、可以提高性能:队列服务允许进行批量化的数据提交,特别对于跨机器的数据提交来说,可以提高性能。
5、可以提升重用:对于任何有数据产生和消费的地方都可以使用队列服务,并不仅仅局限于数据提交到数据库,比如在报警服务模块中我们也使用队列服务来发送短信和发送邮件,避免对短信通道和邮件服务器产生瞬时的压力。抽取了内存队列服务模块之后,可以大大提升此类需求的重用。
在本文中,我们先来看一下如何使用内存队列服务。拿Mongodb数据服务的客户端作为例子,首先需要初始化内存队列服务:
var memoryQueueService = LocalServiceLocator.GetService<IMemoryQueueService>(); memoryQueueService.Init(new MemoryQueueServiceConfiguration(string.Format("{0}_{1}", ServiceName, typeFullName), InternalSubmitData) { ConsumeErrorAction = config.ConsumeErrorAction, ConsumeIntervalMilliseconds = config.ConsumeIntervalMilliseconds, ConsumeIntervalWhenErrorMilliseconds = config.ConsumeIntervalWhenErrorMilliseconds, ConsumeItemCountInOneBatch = config.ConsumeItemCountInOneBatch, ConsumeThreadCount = config.ConsumeThreadCount, MaxItemCount = config.MaxItemCount, NotReachBatchCountConsumeAction = config.NotReachBatchCountConsumeAction, ReachMaxItemCountAction = config.ReachMaxItemCountAction, });
这里可以看到,我们通过LocalServiceLocator来获取IMemoryQueueService的实现,IMemoryQueueService的定义如下:
public interface IMemoryQueueService : IDisposable { /// <summary> /// 初始化队列服务 /// </summary> /// <param name="configuration"></param> void Init(MemoryQueueServiceConfiguration configuration); /// <summary> /// 入列一条记录 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="item"></param> void Enqueue<T>(T item); /// <summary> /// 入列多条记录 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="item"></param> void EnqueueBatch<T>(IList<T> item); /// <summary> /// 获取队列状态 /// </summary> /// <returns></returns> MemoryQueueServiceState GetState(); }
在获得了实现之后,就需要进行初始化,也就是提供一个MemoryQueueServiceConfiguration,定义如下:
public class MemoryQueueServiceConfiguration { /// <summary> /// 内存队列名 /// </summary> [MongodbPresentationItem(DisplayName = "内存队列名")] public string MemoryQueueName { get; set; } /// <summary> /// 消费数据的委托 /// </summary> [MongodbPersistenceItem(IsIgnore = true)] public Action<IList<object>> ConsumeAction { get; set; } /// <summary> /// 队列最大项数 /// </summary> [MongodbPresentationItem(DisplayName = "队列最大项数")] public int MaxItemCount { get; set; } /// <summary> /// 消费数据的时间间隔毫秒 /// </summary> [MongodbPresentationItem(DisplayName = "消费数据的时间间隔毫秒")] public int ConsumeIntervalMilliseconds { get; set; } /// <summary> /// 遇到错误时消费数据的时间间隔毫秒 /// </summary> [MongodbPresentationItem(DisplayName = "遇到错误时消费数据的时间间隔毫秒")] public int ConsumeIntervalWhenErrorMilliseconds { get; set; } /// <summary> /// 达到最大项数后的策略 /// </summary> [MongodbPresentationItem(DisplayName = "达到最大项数后的策略")] public MemoryQueueServiceReachMaxItemCountAction ReachMaxItemCountAction { get; set; } /// <summary> /// 消费数据时不足批次数的策略 /// </summary> [MongodbPresentationItem(DisplayName = "消费数据时不足批次数的策略")] public MemoryQueueServiceNotReachBatchCountConsumeAction NotReachBatchCountConsumeAction { get; set; } /// <summary> /// 消费数据遇到错误的策略 /// </summary> [MongodbPresentationItem(DisplayName = "消费数据遇到错误的策略")] public MemoryQueueServiceConsumeErrorAction ConsumeErrorAction { get; set; } private int consumeThreadCount; /// <summary> /// 消费的线程总数 /// </summary> [MongodbPresentationItem(DisplayName = "消费的线程总数")] public int ConsumeThreadCount { get { return consumeThreadCount; } set { if (value <= 0) throw new ArgumentException("Invalid argument!", "ConsumeThreadCount"); consumeThreadCount = value; } } private int consumeItemCountInOneBatch; /// <summary> /// 消费数据的批量项数 /// </summary> [MongodbPresentationItem(DisplayName = "消费数据的批量项数")] public int ConsumeItemCountInOneBatch { get { return consumeItemCountInOneBatch; } set { if (value <= 0) throw new ArgumentException("Invalid argument!", "ConsumeItemCountInOneBatch"); consumeItemCountInOneBatch = value; } } public MemoryQueueServiceConfiguration(string queueName, Action<IList<object>> consumeAction) { MemoryQueueName = queueName; ConsumeAction = consumeAction; MaxItemCount = 10000; ReachMaxItemCountAction = MemoryQueueServiceReachMaxItemCountAction.AbandonOldItems .Add(MemoryQueueServiceReachMaxItemCountAction.LogExceptionEveryOneSecond) .Add(MemoryQueueServiceReachMaxItemCountAction.ChangeConsumeErrorActionToAbandonAndLogException) .Add(MemoryQueueServiceReachMaxItemCountAction.DecreaseConsumeIntervalOnce) .Add(MemoryQueueServiceReachMaxItemCountAction.DecreaseConsumeIntervalWhenErrorOnce); ConsumeErrorAction = MemoryQueueServiceConsumeErrorAction.AbandonAndLogException; ConsumeThreadCount = 1; ConsumeIntervalMilliseconds = 10; ConsumeIntervalWhenErrorMilliseconds = 100; ConsumeItemCountInOneBatch = 10; NotReachBatchCountConsumeAction = MemoryQueueServiceNotReachBatchCountConsumeAction.ConsumeAllItems; } }
在这里我们可以看到,构造方法中必须给出队列名以及消费数据的回调方法。其它可选参数如下:
1、队列最大项数:队列中未消费的数据不会超过这个限制,内存中数据越多也就占用越多的内存,一般而言需要定义一个合适的值,以免消费方法出错的时候不会占用过多的内存。但是也不建议把这个值定义太小,太小的话在数据提交高峰的时候可能就会丢失数据。默认值是10000。
2、消费数据的时间间隔毫秒:就是在成功消费一次数据,或者说成功调用消费数据的回调方法之后的休眠时间。如果这个值设置过大,那么很可能数据来不及消费。默认值10毫秒。
3、遇到错误时消费数据的时间间隔毫秒:在调用消费方法失败的时候的休眠时间,一般而言,如果是远端服务不可用或是太忙造成的失败,是需要一定时间恢复的,如果重试间隔过短也反而会对远端服务造成压力。默认值100毫秒。
4、达到最大项数后的策略:这是一个位枚举,定义如下:
[Flags] public enum MemoryQueueServiceReachMaxItemCountAction { AbandonOldItems = 0x1, //抛弃老数据 DoubleMaxItemCountOnce = 0x2, //扩大一次最大队列项数 ChangeConsumeErrorActionToAbandonAndLogException = 0x4, //把遇到错误的处理策略修改为抛弃数据并且记录异常 DecreaseConsumeIntervalOnce = 0x8, //减少一次提交数据的间隔时间 DecreaseConsumeIntervalWhenErrorOnce = 0x10, //减少一次遇到错误时提交数据的间隔时间 LogExceptionEveryOneSecond = 0x20, //每秒记录一次异常 }
默认值是每秒记录一次异常,并且把遇到错误的处理策略改为抛弃数据和记录异常(还有其它策略,见6),并且减少一次遇到错误提交数据时间的间隔(比如原来是100现在改为50),并且减少一次提交数据的时间间隔(比如原来是10现在改为5)。当然,你也可以选择仅仅是抛弃老的数据,或是增大一次最大队列项数。
5、消费数据时不足批次数的策略:这是一个枚举,定义如下:
public enum MemoryQueueServiceNotReachBatchCountConsumeAction { WaitForMoreItem = 1, //等待更多数据 ConsumeAllItems = 2, //直接消费当前所有数据 }
默认值是直接消费当前所有的数据。需要注意的是,如果选择等待更多数据,并且数据量不大的话,那么数据消费的延迟很可能就会非常大,因为只有队列中剩余未消费的数据超过了定义的批次数之后,才会进行一次消费。
6、消费数据遇到错误的策略:这是一个枚举,定义如下:
public enum MemoryQueueServiceConsumeErrorAction { /// <summary> /// 抛弃数据 /// </summary> Abandon = 1, /// <summary> /// 抛弃数据并且记录异常 /// </summary> AbandonAndLogException = 2, /// <summary> /// 永远重新入列 /// </summary> EnqueueForever = 3, /// <summary> /// 永远重新入列并且记录异常 /// </summary> EnqueueForeverAndLogException = 4, /// <summary> /// 重新入列两次 /// </summary> EnqueueTwice = 5, /// <summary> /// 重新入列两次并且记录异常 /// </summary> EnqueueTwiceAndLogException = 6, }
它定义了消费数据遇到错误的时候的处理策略,可以选择直接抛弃,也可以选择抛弃并且记录异常,也可以选择永远重新入列,以及重新入列并且记录异常。这里需要提醒的话,如果选择永远重新入列,并且消费数据遇到错误是由于BUG引起的,比如数据要提交到远端,并且这个数据不支持序列化的,那么这个数据会永远提交出错,也就是这个数据永远会在队列中,随着此类数据增多,队列完全会被这些BUG数据占满,造成队列不能正常工作。更多的一种策略是重新入列两次以及重新入列两次和记录异常。如果因为瞬时的网络问题,数据不能消费成功的话,重新入列两次或者说重试两次应该可以排除瞬时网络问题的情况了,如果两次后数据还未能提交的话,数据则会抛弃。默认值是抛弃并且记录异常。
7、消费数据的批量项数:每一次消费数据的时候,传递给消费回调方法的数据数量。如果消费数据涉及到跨机器,那么这个值不能太大,太大的话很可能会因为一次性数据量太大而提交失败(比如Wcf的限流),并且如果消费数据时不足批次数的策略是等待更多数据的话,那么很可能内存队列中会堆积等待提交的数据也会很多;但是这个值也不应该设置过小,如果太小的话,频繁进行网络调用性能也是比较低下的。默认值10。
8、消费的线程总数:也就是后台有多少个线程来消费数据。一般情况下,如果消费数据的行为不设计跨机器,后台使用1个线程即可,如果跨机器并且如果觉得数据量很大来不及消费数据的话可以把这个值设置为CPU的数量,再多的话也只会引起更多的线程调度并不见得会有更大的好处。默认值1。
在这里,我们可以看到Mongodb数据服务客户端使用内存队列的配置是通过配置服务定义在后台的,我们选择的默认方案是:
public MongodbInsertServiceConfigurationItem() { TypeFullName = ""; SubmitToServer = true; ReachMaxItemCountAction = MemoryQueueServiceReachMaxItemCountAction.AbandonOldItems .Add(MemoryQueueServiceReachMaxItemCountAction.LogExceptionEveryOneSecond); ConsumeErrorAction = MemoryQueueServiceConsumeErrorAction.AbandonAndLogException; ConsumeThreadCount = 1; ConsumeIntervalMilliseconds = 10; ConsumeIntervalWhenErrorMilliseconds = 1000; ConsumeItemCountInOneBatch = 100; NotReachBatchCountConsumeAction = MemoryQueueServiceNotReachBatchCountConsumeAction.ConsumeAllItems; MaxItemCount = 10000; }
100条数据一次提交,10毫秒提交一次(也就是最多1秒提交1万条),遇到错误等待1秒,达到最大项之后抛弃老数据并且每秒记录一次异常,后台1个线程提交,队列最大项10000,提交的时候数据不足100条则有多少条提交多少条。
在初始化之后,提交数据就非常简单了,只需要调用入列方法即可:
submitDataMemoryQueueServices[typeFullName].Enqueue(item);
或者也可以批量提交:
submitDataMemoryQueueServices[typeFullName].EnqueueBatch(dataList);
使用队列服务也就是初始化和入列这么简单,在合适的时候,队列服务会调用我们之前定义的消费数据的回调方法。此外,队列服务还公开了获取队列服务状态的接口:
MemoryQueueServiceState GetState();
MemoryQueueServiceState定义如下:
[MongodbPersistenceEntity("State", DisplayName = "内存队列服务状态", Name = "MemQueue")] public class MemoryQueueServiceState { /// <summary> /// 内存队列名 /// </summary> [MongodbPresentationItem(DisplayName = "内存队列名")] public string MemoryQueueName { get; set; } /// <summary> /// 内存队列的配置 /// </summary> [MongodbPresentationItem(DisplayName = "内存队列的配置")] public MemoryQueueServiceConfiguration Configuration { get; set; } /// <summary> /// 总消费的项数量 /// </summary> [MongodbPresentationItem(DisplayName = "总消费的项数量")] public long TotalConsumeItemCount { get; set; } /// <summary> /// 总消费出错的项数量 /// </summary> [MongodbPresentationItem(DisplayName = "总消费出错的项数量")] public long TotalConsumeErrorItemCount { get; set; } /// <summary> /// 当前队列剩余项数 /// </summary> [MongodbPresentationItem(DisplayName = "当前队列剩余项数")] public int CurrentItemCount { get; set; } /// <summary> /// 当前错误重试的项数 /// </summary> [MongodbPresentationItem(DisplayName = "当前错误重试的项数")] public int CurrentErrorRetryItemCount { get; set; } /// <summary> /// 上次消费出错的时间 /// </summary> [MongodbPresentationItem(DisplayName = "上次消费出错的时间")] public DateTime LastConsumeErrorOccurTime { get; set; } /// <summary> /// 上次达到最大项数的时间 /// </summary> [MongodbPresentationItem(DisplayName = "上次达到最大项数的时间")] public DateTime LastReachMaxItemCountOccurTime { get; set; } /// <summary> /// 上次消费出错的异常信息 /// </summary> [MongodbPresentationItem(DisplayName = "上次消费出错的异常信息")] public string LastConsumeErrorMessage { get; set; } }
为了方便把这个状态数据保存到Mongodb中,我们也定义了Mongodb数据服务的一些特性。至此,介绍了内存队列服务模块的使用。对于内存队列服务模块的实现在这里就不介绍了,这个模块只有一个MemoryQueueService类,代码也非常简单,感兴趣的可以查看源代码。需要提醒一点是,由于每一个队列是独立的,因此队列服务并没有使用静态成员来保存队列数据,对于队列服务的使用者来说,有义务把队列服务的示例以静态成员保存起来,防止GC回收。