数据存取

基础:

GE的核心是一个处理原始二进制文件的键值存储。您可以将一个二进制数保存到由指定的键标识的GE键-值存储中,然后将其加载回程序中。密钥是64位整数;而引用GE数据对象(或GE术语中的单元格)的唯一本地方法就是通过这样一个键。但是,其他类型的键(如字符串键)可以通过将键散列到64位整数来轻松实现。注意,每个键最多只能与一个二进制数值关联;用同一个键保存两个二进制数值会导致一个被另一个覆盖。我们还将把键的概念称为id。

GE支持具有原子性的单元上的高性能并发键值运算符。内置的原子单元操作符包括:AddCell、SaveCell、LoadCell和RemoveCell。使用特定键访问单元格是序列化的;每个人都按照一定的顺序观察一个作家所做的改变。

GE的耐久性是可选的。在编写单元格之前,我们可以选择是否向本地持久存储提交预写日志(WAL)WAL保证了耐久性,但也带来了一些性能损失。明智的使用它。

数据存取模式:

GE提供了多种数据访问模式。在决定使用这里介绍的一种数据访问方法之前,我们应该权衡便利和性能之间的权衡。

内置键值存储接口:

最方便的方法是使用内置的键-值存储接口,比如SaveCell和LoadCell。在GE中,这些接口是在Trinity Global。用于分别在本地或集群上访问数据的CloudStorage。

内置接口将单元格视为二进制数。在TSL中定义了自己的单元格类型之后,其他类型的键-值存储接口将绑定在Trinity.Global.LocalStorage Triity.Global.CloudStorage.TSL生成的接口采用LoadMyType和SaveMyType的形式。例如,如果我们在TSL中定义一种单元格类型如下:

cell GraphNode
{
[Index]
   string       Name;
   float        Value;
   List<CellId> Neighbors;
}

两个新的接口LoadGraphNode和SaveGraphNode将绑定到LocalStorage和CloudStorage。为这些接口提供了多个方法重载。我们可以这样写:

var root = Global.CloudStorage.LoadGraphNode(123); //123 as the cell id
var sum  = 0.0f;

foreach(var neighbor in root.
Neighbors)

{
    sum += Global.CloudStorage.LoadGraphNode(neighbor).Value;
}

远程选择性数据访问:

GE支持服务器端计算。这意味着我们不需要将所有相关的单元放到网络上,然后在本地对他们应用操作。我们 通过网络传递我们感兴趣的信息。为此,我们为客户机-服务器通信定义了一个自定义协议。现在,我们可以向服务器发送用户定义的请求,以获得所需的结果,而不是发送单元格加载/保存操作。例如,我了获得一组GraphNode值的和,我们可以定义这样的协议,而不是将他们加载到客户端:

struct RequestMessage
{
    List<CellId> NodeSet;
}

struct ResponseMessage
{
    float Result;
}

protocol CalculateSum
{
    Type     : Syn;
    Request  : RequestMessage;
    Response : ResponseMessage;
}

在客户端,我们可以通过:

var sum = 0.0f;

for(int serverId = 0; serverId < Global.ServerCount; serverId++)
{
   using(var request  = new RequestMessageWriter(new List<long>(){1,2,3}))
   {
      using(var response = Global.CloudStorage.CalculateSumToMyServer(serverId, request))
      {
         var sum += response.Result;
      }
   }
}

服务器端,逻辑实现如下:

public override void CalculateSumHandler(
    RequestMessageReader  request,
    ResponseMessageWriter response)
{
    response.Result = .0f;
    foreach(var nodeId in request.NodeSet)
    {
        response.Result += Global.LocalStorage.LoadGraphNode(nodeId).Value;
    }
}

单元访问器:

实际上,我们可以通过键-值存储接口执行任何数据访问任务。但很快我们会注意到,即使我们只想访问单个数据字段,整个单元格也需要加载。在上面显示的代码片段中,对某个字段访问单元格。对于每个单元,GE首先在内存存储中确定其内存位置。然后它调用运行时来分配单元格对象,并将单元格内容从存储复制到对象。然后,从对象中读出该字段病将其输入到外部计算循环中。

单元格修饰是一个棘手的问题。只需修改单元格的一小部分就需要三个单元格操作:加载单元格、修改单元格和保存 单元格。在这个过程中产生的内存 副本浪费了大量的内存和网络带宽。而且,即使每个单元操作都是原子操作,单元修改作为一个整体也不是原子操作,因为不能保证三个单独的单元操作作为一个整体执行。

在了解了使用键-值存储接口造成的问题之后,我们现在给出了解决方法。在GE中,我们通过一种称为数据访问器的机制来解决上述问题。对于TSL脚本中定义的任何单元结构,TSL编译器将自动生成单元访问器。访问器不拥有任何数据。相反,所有字段都作为c#属性提供;对这些属性的操作将转换为对底层单元格二进制的就低内存操作。使服务器逻辑重写为:

public override void ComputeAverageHandler(
    RequestMessageReader  request,
    ResponseMessageWriter response)
{
    response.Result = .0f;
    foreach(var nodeId in request.NodeSet)
    {
        using( var neighbor = Global.LocalStorage.UseGraphNode(nodeId) )
        {
            response.Result += neighbor.Value;
        }
    }
}

在这个新版本中,访问相邻节点的值,将访问4个字节的内存。有关单元格访问器的更多信息,请参阅:我的博客-TSL 访问器

只要可能,使用单元格访问器而不是键-值存储接口来访问数据。

单元访问设置:

我们可以为大多数单元格访问接口提供单元格访问选项。根据单元格接口,可以应用下面列出的一个或多个选项。

public enum CellAccessOptions
{
   // Throws an exception when a cell is not found.
   ThrowExceptionOnCellNotFound,

   // Returns null when a cell is not found.
   ReturnNullOnCellNotFound,

   // Creates a new cell when a cell is not found.
   CreateNewOnCellNotFound,

// Specifies that write-ahead-log should be performed with strong durability.
   StrongLogAhead,

   // Specifies that write-ahead-log should be performed with weak durability.
   // This option brings better performance, but under certain circumstances
   // the log may fail to be persistent, for example, during a power outage
   // or equipment failure.
   WeakLogAhead
}

单元访问选项用于控制单元操作的行为。例如,我们可以为可能改变单元格状态的单元格访问接口制定write-ahead-log级别。

 long cellId = 1;

// MyCell is a cell type defined in a TSL project
Global.LocalStorage.SaveMyCell(CellAccessOptions.StrongLogAhead, cellId, ...);

Global.LocalStorage.RemoveCell(CellAccessOptions.WeakLogAhead, cellId);

using (var cell = Global.LocalStorage.UseMyCell(cellId, CellAccessOptions.ReturnNullOnCellNotFound))

 {
    // Do something here
}

using (var cell = Global.LocalStorage.UseMyCell(3, CellAccessOptions.CreateNewOnCellNotFound))
{
    // Do something here
}

单元选择器:

GE提供了用于迭代存储在本地内存存储中的单元格的枚举数。注意,目前不支持云内存存储上的枚举。

对于TSL中定义的每个单元格类型,TSL编译器在本地内存存储上生成一个选择器接口:Global.LocalStorage.MyCellType_Selector。顾名思义,选择器接口选择给定类型的所有单元,并返回IEnumerable<MyCellType>集合。然后,我们可以使用foreach便利集合:

 foreach( var node in Global.LocalStorage.GraphNode_Selector() )
{
    //Do something...
}

支持单元对象和单元访问器。这里是访问器对等物:

foreach( var accessor in Global.LocalStorage.GraphNode_Accessor_Selector() )
{
    //Do something...
}

通过选择器枚举单元格是线程安全的;多个枚举可以同时执行。但是,不允许缓存访问器,因为访问器对象将在枚举期间被重用泳衣指向其他单元。因此,以下代码将导致数据损坏或系统崩溃:

// Attempting to cache cell accessors
List<GraphNode_Accessor> accessor_cache = new List<GraphNode_Accessor>();
foreach( var accessor in Global.LocalStorage.GraphNode_Accessor_Selector() )
{
    accessor_cache.Add(accessor);
}
// It will crash on visiting the cached accessors!
var f = accessor_cache.Sum(accessor.AdditionalData);

LINQ:

选择器实现了IEnumerable接口,因此他们支持LINQ/PLINQ查询。如果为某些单元格字段指定了子字符串索引属性,那么一些查询可以直接利用反向索引来加速查询处理。使用LINQ实现通用数据查询逻辑很方便,例如:

 var results = from node in Global.LocalStorage.GraphNode_Accessor_Selector()
               where node.name.Contains("Alice")
               select node.Value;
var min     = results.Min();

在本例中,node.name.Contains子句被转换为子字符串查询。然后结果投影到一个浮点数列表中,然后使用内置的LINQ接口Min()进行聚合。

详细介绍参考:Language-Integrated Query

子串查询:

如果在TSL中的单元格字段中指定了子字符串索引属性,将为指定的单元格字段生成一组子字符串查询接口。子字符串查询接口接受一个或多个查询字符串,并返回匹配的单元格id列表。

详细介绍参考:substring query

 

 

 

 

 

 

 

posted @ 2018-09-18 10:29  v-haoz  阅读(294)  评论(0编辑  收藏  举报