之前介绍的应用程序信息中心模块中所有日志、异常、性能和状态数据都依赖Mongodb数据服务,Mongodb数据服务的接口也简单的可以:
public interface IMongodbInsertService : IDisposable { void Insert(object item); }
总之一点,不管什么数据,提交进来即可!在提交之前所要做的只是设计数据实体 ,并且为实体标注一些元数据Attribute,来告诉Mongodb数据服务,希望怎么样保存数据,又希望怎么样呈现数据。由于Mongodb数据服务的使用者就是开发人员,因此在本文使用篇里会站在程序的角度详细介绍这些Attribute,后一篇文章会详细介绍一下Mongodb数据服务的通用后台。
我们首先来看一下定义在类上的MongodbPersistenceEntity特性,它定义了实体相关的配置:
这里首先要注意一点,所谓定义在类上是定义在直接作为提交对象的根类上的,如果一个类内嵌了子类,那么在子类上定义这个特性是没什么意义的。分别介绍一下每一个参数。
首先是CategoryName:必填,并且不能包含.和_。通过Mongodb数据后台我们来看一下分类名的作用:
比如我们有大量的业务和非业务数据,混合在一起查看显然不方便,那么我们可以定义Aic分类、State分类,把相关的数据进行一个归类。比如这里点击了Aic分类可以看到:
这里的中文名就是DisplayName而实际数据库的名字就是Name,如果不定义Name的话就是类名,如果不定义DisplayName的话就是Name,因此Name和DisplayName都不是必填的。再看一下LogInfo的定义对比一下:
[MongodbPersistenceEntity("Aic", DisplayName = "日志", Name = "Log")]
在数据库中的数据库名是以CategoryName+两个下划线+Name构成的,并且进行了按月分库:
按月份分库可以减少每一个数据库的数据量,也可以方便独立删除和备份每一个库来方便运维。
最后来看看ExpireDays的配置,它可以配置数据在多久后过期被自动删除。如果我们的日志数据只需要保留一个年,那么只需要设置为365天即可。1年之后过期(取决于数据中时间列的时间)的日志数据将会被删除,可以为不同的类型配置不同的过期时间,这样就方便了运维。
接下来看一下定义在属性上的MongodbPersistenceItem特性,它定义了设计到持久化的一些配置:
在这里为了方便,我们要求所有需要保存到数据库中的数据项必须是属性。来解释一下每一个参数,这里每一个参数都是可选的:
1、ColumnName:实际保存在数据库中的列名。如果回顾一下前一篇文章的话,可以发现,为了减少数据的大小,我们为很多列都定义了ColumnName使用一个简洁的表示,比如日志消息:
因为我们知道Mongodb是采用类Json来存储数据,每一个数据项都会保存列名,所以列名越短,占用存储空间越小。如果省略的话就用属性名作为列名了。
2、IsContextIdentityColumn:这是一个很有趣的设计。在有的时候我们在一个请求中会提交很多数据,比如2条程序日志,1条已处理异常,2条业务日志,1条Wcf客户端调用日志,1条Wcf客户端消息日志。有没有什么办法把它们联系起来?使得我们在看到一条日志的时候才能去查看其它日志?可以使用一个列并标注为上下文标识列,这样的话,这些数据可以在后台统一查看,比如应用程序信息中心的AbstractInfo中就有这么一列:
比如在后台,我们可以直接根据一个上下文ID来查询所有关联的数据:
在这里可以看到,这一条日志的相同请求中还产生了一个异常、一个慢请求的数据、一个Wcf客户端异常、一个Wcf客户端消息、一个Wcf服务端异常以及一个Wcf服务端消息(应该是FaultMessage):
整个请求中发生的数据记录都串接在一起了,是不是非常直观?
3、IsIgnore:是否忽略,也就是是否不保存到数据库中去。还记得之前介绍过的代码性能测量的实体吗:
在这里,我们的sw和threadTime只是用作计算的临时数据,不需要保持到数据库中去,因此设置为忽略。
4、IsPrimaryKey:是否是主键。定义主键很重要,否则系统就不知道在查看详细信息的时候使用哪个列。一个类型必须有且仅有一个主键。在了解了所有特性之后会统一介绍配置的约束。
5、IsTableName:是否是表名列。如果这个列是表名列,那么这个列数据会作为分表的依据。往往我们会把业务名或应用程序名作为表名实现进一步的拆分(首先是通过分类+数据类型+时间确定数据库,然后是通过TableName列确定表的拆分),如果不定义的话就会使用General作为表名也就是所有数据都存在一个表中。在后台,我们也是选择了数据库再选择表的:
对应到数据库中就是:
当然,数据库中的按照年月分库对于用户来说是透明的。
6、IsTimeColumn:是否是时间列。时间列决定了分库,也决定了统计视图下(之后我们会介绍呈现数据的几种视图)时间轴依赖的列。由于一个类中可以有多个时间列,所以必须显式标注一个时间列,这个时间列会作为分库的依据。
7、MongodbIndexOption:列索引的方式,有这么几种:
public enum MongodbIndexOption { /// <summary> /// 不作索引 /// </summary> None = 0, /// <summary> /// 递增索引 /// </summary> Ascending = 1, /// <summary> /// 递减索引 /// </summary> Descending = 2, /// <summary> /// 唯一的递增索引 /// </summary> AscendingAndUnique = 3, /// <summary> /// 唯一的递减索引 /// </summary> DescendingAndUnique = 4, }
主要有唯一和不唯一两种。比如主键就需要是唯一的,如果主键有重复,这条数据将被删除。而比如一些需要搜索和排序的字段,它建立的索引就不能是唯一的了。至于索引的方向取决于排序的方向,比如我们往往是按照日期倒序查找数据的,那么这个列的索引就应该设置为递减索引,否则效果会大打折扣。
最后再来看一下同样定义在属性上的MongodbPresentationItemAttribute特性,它决定了列在后台如何呈现:
1、Description:列的描述信息。在详细数据显示页面上会显示这个信息。
2、DisplayName:列的友好显示名。比如我们可以用一个中文来定义列的显示名:
这是最最常用的。
3、ShowInTableView:是否显示在列表视图中,稍候会介绍每一种视图。
4、MongodbCascadeFilterOption,这是一个枚举,定义如下:
public enum MongodbCascadeFilterOption { /// <summary> /// 不作为级联过滤 /// </summary> None = 0, /// <summary> /// 级联过滤的第一级 /// </summary> LevelOne = 1, /// <summary> /// 级联过滤的第二级 /// </summary> LevelTwo = 2, /// <summary> /// 级联过滤的第三级 /// </summary> LevelThree = 3, }
对于一个数据实体平面(不能是作为属性的子类,但可以是派生类)上的所有属性,可以把其中三个设置为具有三级联动关系的分级,比如AbstractInfo中的大类和小类:
这样,用户就可以使用级联方式进行数据的筛选,后台效果如下:
5、MongodbFilterOption,这也是一个枚举,定义如下:
public enum MongodbFilterOption { /// <summary> /// 不作为过滤条件 /// </summary> None = 0, /// <summary> /// 下拉框单选过滤 /// </summary> DropDownListFilter = 1, /// <summary> /// 复选框多选过滤 /// </summary> CheckBoxListFilter = 2, /// <summary> /// 文本框搜索过滤 /// </summary> TextBoxFilter = 3, }
这个设置决定了列可以以单选、多选或是文本方式来搜索。比如在应用程序信息中心中,我们的ExtraInfo中就有这样的一些过滤列:
后台效果如下:
6、MongodbSortOption:它决定了在列表视图中数据呈现时的排序规则,比如我们定义应用程序信息中心中的所有数据是按照日期倒序的:
在后台查看数据的时候看看是不是这样排序了:
之前介绍了ShowInTableView的设置,表明列是显示在列表视图中的。在这里介绍一下定义的几种后台呈现数据的视图:
1、以表格方式呈现多条数据,既然是表格,肯定不可能显示几百个列的数据,因此显示在这里的数据一般而言是精挑细选的数据,或是过滤数据,比如我们可以看一下对于一条LogInfo我们显示哪些数据:
在这里可以看到,除了日志消息之外,其它数据都是做了索引的过滤数据。在表格中显示这些信息足够了,在上一篇文章中列出了AbatractInfo和LogInfo的定义,可以对比一下。还可以看到由于配置了DisplayName,表格头的显示都是中文。
2、以曲线统计图方式呈现数据。如下图:
在这里,我们查询一周的数据量的统计,其中每一个点是一小时。这样的数据量统计对于以下场景很有用:
1)比如业务数据,我们需要浏览业务量的变化。
2)比如监控数据,我们需要浏览某种日志(比如异常日志)的数据量会不会很多。
并且需要说明的是,这里一个图上可以显示多个表的统计数据,我们如果把表名想像成业务类型或是任何其它类型的话可以进行类比统计了。
3、以分组方式进行统计并以饼图方式进行呈现,如下图:
比如我们这里希望知道刚才那个高峰中日志数据中到底是什么数据居多。点击进入分组统计可以看到,大部分数据都是192.168.134.187这个IP产生的错误日志。此时就一下子可以把问题定位到这个服务器上。为什么其它服务器不出问题而唯独这个服务器出现问题了?很有可能不是程序问题,而是这个服务器特有的问题,比如磁盘满了,内存满了。那么为什么会有这些分组呢?系统自动对哪些数据进行分组统计,后面会介绍。
4、状态视图。所谓状态视图就是针对状态数据的。这类数据有一个特点,就是每一条数据的内容其实是差不多的,都是反映一个状态,只不过最新的数据就是最新的状态。我们往往会查看最新的一条数据,也就是XXX应用当前的状态,并且如果这条数据很大的话,我们希望滚动条始终停留在我们关注的那些列的位置上(稍候可以看到,我们以TreeView呈现详细数据)。
5、除了上面几种直接可以选择的视图,还可以从列表视图点击进入详细视图,如下图:
在详细视图中可以看到这里显示的列名都是DisplayName而不是实际数据库中存储的列名,在实现篇中会介绍,我们把元数据和实际数据分开保存在数据库中,在查询数据的时候再进行组合。
现在再回到MongodbPersistenceItem和MongodbPresentationItem来,对于这些配置,是有一些规则的:
1、作为上下文标识的列不能超过一个
2、作为表名的列不能超过一个、不能有索引,也不能定义作为单选、多选等过滤列
3、必须有并且只有一个主键列
4、必须有并且只有一个时间列
5、排序列不能超过两个
6、主键列必须具有唯一索引
7、时间列必须具有不唯一索引,并且类型必须为DateTime
8、排序列和上下文标识列必须具有不唯一索引
9、作为搜索条件的列必须具有索引(否则性能会很差)
最后,在这里说明一下对于复杂的实体使用上的一些规则。首先,Mongodb数据服务是支持列表和字典属性的,并且还支持自定义的数据。比如简单类型的:
[MongodbPersistenceItem(ColumnName = "DictionaryColumn22")] [MongodbPresentationItem(DisplayName = "Test.DictionaryColumn", Description = "子类中的字典列")] public Dictionary<int, string> DictionaryColumn2 { get; set; } [MongodbPersistenceItem(ColumnName = "ListColumn22")] [MongodbPresentationItem(DisplayName = "Test.ListColumn", Description = "子类中的列表列")] public List<int> ListColumn2 { get; set; }
原始数据是这样的:
ListColumn2 = Enumerable.Range(1, 2).ToList(), DictionaryColumn2 = new Dictionary<int, string> { { 1, "x" }, { 2, "y" } },
前者在后台呈现时候的样子是这样的:
对于列表列,我们使用列名+类型名+0、1、2这样的索引号来表示列名,而对于字典,我们使用列名+类型名+Key来表示列名。
再来看一个自定义类型的例子:
[MongodbPersistenceItem(ColumnName = "ExtListColumn11")] [MongodbPresentationItem(DisplayName = "TestBase.ExtListColumn", Description = "基类中的扩展列表列")] public List<ExtItem> ExtListColumn1 { get; set; } [MongodbPersistenceItem(ColumnName = "ExtDictionaryColumn11")] [MongodbPresentationItem(DisplayName = "TestBase.ExtDictionaryColumn", Description = "基类中的扩展字典列")] public Dictionary<string, ExtItem> ExtDictionaryColumn1 { get; set; }
原始数据是这样的:
ExtListColumn3 = new List<ExtItem> { new ExtItem { NormalColumn4 = 100, DictionaryColumn4 = new Dictionary<int, string> { { 1, "x" }, { 2, "y" } }, ListColumn4 = Enumerable.Range(1, 2).ToList(), EnumColumn4 = (Enum4)rnd.Next(1, 4), IgnoreColumn4 = "asdasdas", }, new ExtItem { NormalColumn4 = 200, DictionaryColumn4 = new Dictionary<int, string> { { 1, "x" }, { 2, "y" } }, ListColumn4 = Enumerable.Range(1, 2).ToList(), EnumColumn4 = (Enum4)rnd.Next(1, 4), IgnoreColumn4 = "asdasdas", }, }, ExtDictionaryColumn3 = new Dictionary<string, ExtItem> { { "Key1", new ExtItem { NormalColumn4 = 100, DictionaryColumn4 = new Dictionary<int, string> { { 1, "x" }, { 2, "y" } }, ListColumn4 = Enumerable.Range(1, 2).ToList(), EnumColumn4 = (Enum4)rnd.Next(1, 4), IgnoreColumn4 = "asdasdas", } }, { "Key2", new ExtItem { NormalColumn4 = 100, DictionaryColumn4 = new Dictionary<int, string> { { 1, "x" }, { 2, "y" } }, ListColumn4 = Enumerable.Range(1, 2).ToList(), EnumColumn4 = (Enum4)rnd.Next(1, 4), IgnoreColumn4 = "asdasdas", } }, }
后台呈现的时候是这样的:
这里可以注意到Key1和Key2是字典的Key。
由于ExtItem这个类型又有自己的子属性,这个子属性还可以是字典列和列表列,所以这是可以是无限级的。我们来看一下ExtItem的定义:
public class ExtItem { [MongodbPersistenceItem(ColumnName = "NormalColumn44")] [MongodbPresentationItem(DisplayName = "ExtItem.NormalColumn", Description = "项扩展类中普通的列")] public int NormalColumn4 { get; set; } [MongodbPersistenceItem(ColumnName = "EnumColumn44")] [MongodbPresentationItem(DisplayName = "ExtItem.EnumColumn", Description = "项扩展类中枚举的列")] public Enum4 EnumColumn4 { get; set; } [MongodbPersistenceItem(ColumnName = "IgnoreColumn44", IsIgnore = true)] [MongodbPresentationItem(DisplayName = "ExtItem.IgnoreColumn", Description = "项扩展类中忽略的列")] public string IgnoreColumn4 { get; set; } [MongodbPersistenceItem(ColumnName = "DictionaryColumn44")] [MongodbPresentationItem(DisplayName = "ExtItem.DictionaryColumn", Description = "项扩展类中的字典列")] public Dictionary<int, string> DictionaryColumn4 { get; set; } [MongodbPersistenceItem(ColumnName = "ListColumn44")] [MongodbPresentationItem(DisplayName = "ExtItem.ListColumn", Description = "项扩展类中的列表列")] public List<int> ListColumn4 { get; set; } } }
细心的话可以发现DisplayName中的.都替换为了_,这是因为显示名我们限制了不能包含.(原因是作为数据源绑定到DataGrid的时候,如果列名包含.则不是一个有效的属性名)。
除了支持复杂的自定义类型之外,我们的数据还支持枚举,比如LogInfo有一个LogLevel的枚举,在呈现数据的时候,我们看到的是枚举的名字而不是值:
并且在搜索的时候我们在下拉框中看到的也是枚举的名字:
总结一下,其实从使用上来说,对于Mongodb数据服务,我们只需要:
1、定义需要存储到数据库的实体类型(可以内嵌复杂的自定义类型,也可以是枚举等)
2、按照自己的需要定义一些Attribute来决定持久化(比如列名)和呈现的规则(比如如何查询数据)
3、调用MongodbService.MongodbInsertService.Insert(info)语句插入数据
然后,在后台就可以这些数据了,并不需要关心其它的。在Adhesive内部所有涉及到大数据存储的地方我们也都使用的是Mongodb数据服务,对于框架外部,如果有需要保存业务数据或其它监控数据也可以直接使用。下一篇我们会完整介绍一下Mongodb数据服务的后台。