[转载]基于Aaf的数据拆分

(本文适于使用Aaf框架的开发者阅读)

1. 基本原理

  在Aaf框架中,“对象”和“存储”的关系映射有一个关键的纽带StorageAlias,即“存储别名”,同样一个类型,在不同的存储别名下,可以自由映射到任意存储“位置”。
  “位置”有两个元素决定,一个是存储上下文StoargeContext,另外一个是数据表名TableName。缺省的StorageContext在Persistence.Config中配置,缺省的数据表名就是类的名称。
   所有的映射关系,存储在两个地方,一个地方是TypeDescription的ExtendedAttributes属性中,这里存储的映射主要来源于 配置文件Persistence.Config中的配置。关系的建立是在Aaf启动阶段,由PersistenceMappingService初始化完 成,调用StorageContextMappingService中的GetStorageAliases方法取得 Persistence.Config中的存储别名的配置信息。此外,默认的存储上下文和默认的表名,也初始在此扩展特性中。

  通过Persistence.Config方式配置的映射,是“相对稳定”的映射,数量也在一个非常有限的级别上,例如我们可以将订单信息按照 交易类型的不同,存储到不同的数据库上,也可以将交易完毕的订单信息搬迁到历史库上,我们就可以定义出来形如 “EscortOrderByHistory”这样的存储别名,一看就知道是“担保交易的历史交易数据”。

  另外一个存储映射关系的地方是存储别名信息助手StorageAliasesInfoAssistant,助手的映射关系有三个来源:IStorageAliasTeller提供、手动动态添加、数据库参数配置

  通过IStorageAliasTeller方式,我们可以根据系统要求,动态 的解析一个存储别名,不仅如此,我们还可以根据AgileObject.Id自定义规则,推导出其对应的存储别名,例如我们可以将用户按照一定规则分表存 储,然后在用户的Id中包含存储别名的信息,这样,我们就可以非常方便的将海量用户拆分到若干分表当中,实现数据的分表存储。此外,与IDataRadiationClassifier搭 配起来,还可以实现“多维存储”, 就是将同样一份数据,按照不同的应用需要,散射到多个存储位置(有可能是散射到不同的数据库上,例如商品信息散射出一个专门供查询的库上;也有可能是同一 数据库的不同表上,例如订单信息根据买卖不同散射到特定的数据表上。),同时自动维护各个数据库上数据的一致性。这样就可以分身有术,让多个的数据库分担 查询压力。

  手动动态添加,是通过IStorageAliasesInfoAssistantRegisterDynamicStorageAlias方法,将已知存储位置添加到映射表中,同时返回一个GUID码作为存储别名。

  数据库参数配置,这种情形是存储映射信息是以参数的形式存储在数据库中的, 在Aaf.ParaService加载过程中,会自动生成一个Aaf.StorageAliases的配置节点,节点路径是Aaf.StorageAliases/[StorageAlias](存储别名,可配多个),节点配置格式是:
[

StorageContext1,TableName1 \n

AppId1: StorageContext2,TableName2 \n

AppId2: StorageContext3,TableName3 \n

]

  其特别的地方是在分布式环境中,根据AppID的不同将数据持久化到特定的存储上。

 

 

2. 应用实例

  在5173系统中,有许多应用,尤其以用户、发布单、订单及资金明细比较典型。下面我们一起去看看。

2.1. 用户数据分拆

  用户数据是基础数据,而且是一种会持续增长的基础数据,这种数据的膨胀会让其性能表现越来越差,因为是基础数据又不能删除或者搬移,我们处理这种数据的基本策略是将所有用户按照一定的策略散射到多个数据表中,以缓解单表的压力,下面我们分析一下实现过程:

1、 将所有用户数据按照注册日期段分组,每一个分组共用一个独立的数据表,分拆存储,分拆策略启用有一个时间基线,之前的数据依然存储在UserInfo中, 自此时间基线之后的数据便存储在形如UserInfo_X的数据表中。例如,设立时间基线为2009-5-22,每隔90天建立一个新组,某用户注册时间 为2009-12-2号,与时间基线相差194 天,除以90得2,存储在UserInfo_2中。

2、 创建一个类UserInfoSaTeller,实现IStorageAliasTeller接口

3、 将UserInfoSaTeller注册到别名信息助手,注册方法如下: PersistenceMappingService.StorageAliasesInfoAssistant.RegisterTeller(typeof(User), new UserInfoSaTeller()),这个注册在UserService的Run方法中。

4、 保存新用户时,内核将调用UserInfoSaTeller的GetPrimarySaveStorageAlias方法,取得存储别名,传入参数是灵便对象的Id号,因此我们需要构建一个含有分表特征信息的Id,我们现用的Id为“US09120282660351-00F7”,其中红色加粗部分为注册日期,通过注册日期,我们可以计算出一个特定的存储别名,UserInfo$2, 内核将继续调用UserInfoSaTeller的GetTableName,传入参数就是GetPrimarySaveStorageAlias计算出 来的“UserInfo$2”,很显然我们轻易可以得出实际的存储表名“UserInfo_2”。可见,我们只要按照规则构造好用户的Id,程序便可以自 动识别出来应该放到哪里存储。

5、 根据Id号取用户实例的过程与保存过程基本类似,内核调用UserInfoSaTeller的GetPrimaryLoadStorageAliases方法,取得加载时存储别名,之后的映射关系与对象创建时相同。对于用户数据,保存和获取时的存储别名是一致的。

6、 登陆时怎么办?登陆使用的用户名进行登陆,而不是用户Id。没什么太好的办法,基本思路还是遍历所有分表,好在,我们的分表时间区间都是以月为单位,十年 下来也不过一百多个表。此外,当一次遍历完成之后,我们可以将此用户对应的存储别名状态记录在某个地方,例如Cookie中,这样减少遍历的频度。

2.2. 发布单数据分拆

  发布单数据的拆分策略稍微复杂一点。根据实际应用需要,除了发布单默认的存储位置之外,发布单还有几种存储形态:根据用户分组、根据游戏、交易 完成的历史数据。根据用户分组分拆,是指把所有用户在逻辑上分成若干个用户组,每个用户组共用一个存储表;根据游戏更容易理解,即每一款游戏共用一个存储 表;交易完成的历史数据,就是那些单子不会再发生变化,这种数据,主要通过数据库Job定期搬移的。与用户数据分拆不同的是,发布单是一份数据在不同的地 方用不同的策略存储,即多维存储(游戏维度、用户分组维度)。下面我们分析一下数据创建和加载的过程:

1、 先看创建过程。因为是多维存储,BizOffer需要实现IDataRadiationClassifier接口,Aaf内核在工作时将调用其中的GetDataRadiationClasses方法,来获取每个维度的维度标识

2、 使用维度标识,调用IStorageAliasTeller的GetDimensionStorageAlias方法,翻译出维度对应的存储别名,形如:Search$0043和BizOfferBy023F-Escort,加黑的分别是“游戏标识”和“用户逻辑分组标识”。

3、 解释下用户逻辑分组的来历,我们将用户分成1000个逻辑小组,分组的方法依然与注册时间、时间基线相关,用两个时间的差(天数)对1000取模,这样就 会得到一组数字,将这组数字用四位16进制格式化,就是散射用户的逻辑分组标记,这个标记,我们也在用户Id中记录了,“US09120282660351-00F7”,即蓝色加粗部分。当一个用户创建发布单的时候,我们便知道其逻辑分组为00F7。游戏标识要简单一些,每一款游戏都会有一个自增长的表号,此表号也是用四位16进制表示。注意,用户分组目前是固定为1000个组的,而游戏分组是不断增长的。

4、 接下来就是分别对每个维度的存储别名进行存储动作,通过IStorageAliasTeller的GetContextName和GetTableName,取得实际的存储位置信息。

两个维度路径是:

存储别名–>存储上下文名称–>数据库名称–>数据表名

游戏散射:Search$0043–> RadiationOffer1–> SearchOffer–> BizOfferby0043

用户散射:BizOfferBy023F-Escort–>OfferDataRadiations–>OfferDR–>BizOfferBy023F

5、 还有一个维度是历史库的发布单数据,这个是Job搬迁创造的,数据表的命名规则是一个月三张表,按旬存储,形如:BizOfferby200607_1
。虽然创建过程不是Aaf做的,但查询数据时是Aaf做的,后面我们在做查询分析的时候要考虑这部分数据。

6、 在单条数据加载的时候,重点是确定查询策略了,就是什么数据该先从哪个维度去找,策略确定之后,就是拼存储别名来定位查询的数据库表了。我们现在的查询策 略是先查找当前库,拼EscortByCurrent的存储别名,此存储别名在Persistence.Config中配置了映射关系;接着检查“用户分 组维度”;再接着,查找历史库。

几个查询路径如下:

当前库:EscortByCurrent–>ConsignmentByCurrent–>Consignment–>BizOffer

用户散射:BizOfferBy023F-Escort–>OfferDataRadiations–>OfferDR–>BizOfferBy023F

按发布单结束时间历史库(后台使用):Escort|200912_1–>OfferHistory–>OfferHistory–> BizOfferby200912_1

按发布单创建时间历史库(前台使用):
Escort~200912_1–> OfferHistorybyCreatedDate–> OfferHistorybyCreatedDate–> BizOfferby200912_1

7、 接下来我们梳理一下各种发布单列表是怎么出来的,当一个查询过来的时候,我们首先判断查询条件中是否含有用户信息,如果有,直接走“用户分组维度”。如果 没有用户信息,至少会包含一个游戏信息,即GameId,走游戏维度的存储。注意,列表查询没有走“默认维度”,这样默认维度就可以专心用于交易了。

8、 8、 有关发布单查询,还没有结束。就是另有一个发布单搜索对象BizOfferSearch,这个是另外生成的一份数据,专门供搜索用,与当前交易平台隔离。同样也有两个维度:

存储别名–>存储上下文名称–>数据库名称–>数据表名

游戏散射:Search$0043–> RadiationOffer1–> SearchOffer–> BizOfferSearchby0043

用户散射:BizOfferSearchBy023F-Escort–>OfferDataRadiations–>OfferDR–>BizOfferSearchBy023F

2.3. 订单数据分拆

  订单的数据分拆方法类似,根据实际应用需要,除默认存储外,也有几个维度的存储:买家分组散射、卖家分组散射、历史库数据。从技术实现方式来讲完全一个套路。。

1、 订单创建过程与发布单雷同,不再赘述。需要注意的是,买卖维度的数据表名中也是附加了用户分组标识。一笔订单,会因为买家和卖家的分组不同,而存在于不同标识的分表中。

2、 订单取得过程,先从默认库查找,找不到再去买家维度找,找不到再去卖家维度找,还找不到就到历史库中找。

维度路径是:

存储别名–>存储上下文名称–>数据库名称–>数据表名

买家散射:OrderByBuyer023F-Escort–> OrderDataRadiations–> OrderDR –> OrderByBuyer023F

卖家散射:OrderBySeller009B-Escort–> OrderDataRadiations–>OrderDR–> OrderBySeller0339

历史库:Escort|200912_1–>OrderHistory–>OrderHistory–> Orderby200912_1

3、 在前台列表查询中,买卖双方呈现自己的订单时,是分别从买卖维度即OrderDR这个库中取数据的。后台列表查询,走的是默认维度的订单库。同时,对于交易完成的数据,到历史库OrderHistory中查询。

2.4. 资金分表

  AccountDetail,除默认在外,还有用户分组维度、历史库维度。

1、 多维存储路径:

存储别名–>存储上下文名称–>数据库名称–>数据表名

用户散射:AccountDetailBy00F7–>DataRadiations–>BkDR–> AccountDetailBy00F7

历史库:200912_1–>BkHistory–> BkHistory–> AccountDetailBy200912_1

2、 特别要说明的是,每种资金类型都有自己的独立的类,他们如何共用AccountDetail这样一个存储的呢?原来所有的独立的资金类型都派生自 AccountDetail,而AccountDetail中设置的“[AgileObjectStorage(TableName = "AccountDetail", ContextName="BkUser")]”,也被继承下来了。其实更重要的是它们也继承了的IDataRadiationClassifier实 现,此外,在PaymentService的Run方法中,几乎所有类型都注册了AccountDetailSaTeller,于是实现了所有类型的存储 规则统一。

2.5. 映射列表

以下是开发环境中的映射关系列表:

对于这些类型,如果要修改字段,每一个关联维度上对应的分表都要变更,我们经常碰到的缺字段,就是某个维度上有缺失。

类型

数据库名

数据表名

存储上下文名

存储别名

User BkUser
(默认主库)
UserInfo_1
UserInfo_2…

UserInfo_n

BkUser UserInfo$1
UserInfo$2…

UserInfo$n

BizOffer Consignment
(默认主库)
BizOffer ConsignmentByCurrent EscortByCurrent
SearchOffer
(游戏散射)
BizOfferby0000

BizOfferBy07D0…
RadiationOffer1 Search$0000

Search$07D0…
OfferDR
(用户散射)
BizOfferBy0000

BizOfferBy03E8
OfferDataRadiations BizOfferBy0000-Escort

BizOffeBy03E8-Escort
OfferHistory
(历史库By交易完成时间)

BizOfferby200912_1
BizOfferby200912_2
BizOfferby200912_3
OfferHistory
Escort|200912_1
Escort|200912_2
Escort|200912_3…
OfferHistorybyCreatedDate
(历史库By创建时间)

BizOfferby200911_1
BizOfferby200911_2
BizOfferby200911_3
OfferHistorybyCreatedDate
Escort~200911_1
Escort~200911_2
Escort~200911_3…
BizOfferSearch Consignment
(默认主库)
BizOfferSearch ConsignmentByCurrent EscortByCurrent
SearchOffer
(游戏散射)
BizOfferSearchby0000

BizOfferSearchBy07D0
RadiationOffer1 Search$0000

Search$07D0
OfferDR
(用户散射)
BizOfferSearchBy0000

BizOfferSearchBy03E8
OfferDataRadiations BizOfferSearchBy0000-Escort

BizOfferSearchBy03E8-Escort
Order Consignment
(默认主库)
Order ConsignmentByCurrent EscortByCurrent
OrderDR
(买家散射)
OrderByBuyer0000

OrderByBuyer03E8
OrderDataRadiations OrderByBuyer0000-Escort

OrderByBuyer03E8-Escort
OrderDR
(卖家散射)
OrderBySeller0000

OrderBySeller03E8
OrderDataRadiations OrderBySeller0000-Escort

OrderBySeller03E8-Escort
OrderHistory
(历史库)

Orderby200912_1
Orderby200912_2
Orderby200912_3
OrderHistory
Escort|200912_1
Escort|200912_2
Escort|200912_3
AccountDetail BkUser
(默认主库)
AccountDetail BkUser Current
BkDR
(用户散射)
AccountDetailBy0000

AccountDetailBy03E8
DataRadiations AccountDetailBy0000

AccountDetailBy03E8
BkHistory
(历史库)
..
.
AccountDetailBy200912_1
AccountDetailBy200912_2
AccountDetailBy200912_3
BkHistory
200912_1
200912_2
200912_3…
说明: Escort,是交易类型,此处只是举例,应用中可能是其它标识。

  除了上面的映射,自定义映射还有一些,顺着IstorageAliasTeller的实现类去寻找吧。

posted @ 2011-04-26 14:51  陈 锋  阅读(640)  评论(0编辑  收藏  举报