65.9K
CodeProject 正在变化。 阅读更多。
Home

面向开发者的现代边缘数据管理方法

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2020年7月29日

CPOL

10分钟阅读

viewsIcon

15671

Zen 数据库之于 SQLite,正如 SQLite 之于平面文件管理系统。它提供了满足现代化边缘数据管理需求所需的全部功能。

在物联网和复杂仪器仪表领域,绝大多数嵌入式开发人员使用 C、C++ 或 C# 来处理数据处理和本地分析。这在一定程度上是因为通过一些 `inp()` 和 `outp()` 语句变体,可以轻松处理设备和内部系统组件的直接 I/O,以及更复杂的数字增强型机械设备。使用诸如 `fopen()`、`fclose()`、`fread()` 和 `fwrite()` 等熟悉的文件系统语句来操作收集的数据也很容易。这是最简单的路径。几乎任何上过编程课(或花时间学习过)的人都可以使用这些语句与文件系统级别的数据进行交互。

问题在于文件系统非常简单。它们本身做不了多少事情。当涉及到文档和记录管理、索引、排序、创建和管理表等等时,只有一个操作语句:`DoItYourself()`。而且我们在这里谈论的不是罕见或火箭科学级别活动。这些是在任何数据库系统中都会发现的日常活动。

等等!是那个 D 开头的词!就像将 ASCII 字符指针加二到… 你懂的那个词。

我们是朋友,所以直说吧:嵌入式应用程序的开发人员*讨厌*数据库,而且不难理解为什么。传统数据库一直以来都太庞大,无法部署到物联网和移动环境中。它们的部署成本高昂且困难;它们往往需要只有昂贵的 DBA 才能提供的持续关注。当然,一些较新的开源数据库比 Oracle 或 Microsoft 数据库成本低一些(尽管你仍需为服务和支持付费),但其中大多数都是为云构建的。为物联网或移动设备构建的开发人员需要为边缘、为网关设计的东西,能够嵌入到设备本身中。

从原始粘液中诞生

在上述所有网络巨兽中,有一个开源数据库脱颖而出,成为例外——一个非常紧凑的数据库,它是无服务器的、可移植的、可嵌入的,并且可以通过大多数流行编程语言访问:SQLite。

SQLite 已经存在了 20 多年,并且几乎无处不在,是 Web 和移动应用程序中的数据缓存。顾名思义,SQLite 使用标准查询语言 (SQL),并且不像其他流行的开源 SQL 数据库(MySQL、Postgres、MariaDB 等),它*可以*以无服务器、零数据库管理 (Zero-DBA) 的方式运行。在部署 SQLite 时,开发人员发现他们可以依赖 SQL 进行面向事务的数据管理,而放弃所有 DIY 文件系统交互。开发人员还发现部署 SQLite 时具有性能优势,因为数据库可以执行块读写,处理大型数据集的速度比原生文件管理系统快约 35%。

SQLite 比简单的文件管理系统向前迈进了一大步,应用程序开发人员可以使用它比使用平面文件更有效地处理各种早期边缘用例。但是,就像进化阶梯下层许多生物一样,SQLite 也有其局限性。绝大多数明天的边缘用例将涉及多通道和 I/O 数据速率;多个应用程序、进程和线程;需要快速操作大型数据集;以及在静态和传输过程中保护数据的需求。SQLite 根本就不是为满足这些需求而设计的。

对于发现 SQLite 非常适合单用户嵌入式移动应用程序,并试图用它构建更复杂的边缘应用程序的开发人员来说,在试图满足所有这些需求时遇到了挑战。今天对这些挑战的响应与应对简单文件管理系统不足之处的方式基本没有区别:`DoItYourself()`。要将 SQLite 扩展到能够处理多通道、多进程、高速边缘数据环境的东西,DIY 现在涉及自行开发的代码、GitHub 下载以及从提供服务器支持、安全和连接功能附加组件的小型开发公司购买的混合体。这是一个更高级别的 DIY,但仍然是 DIY。

迈上进化阶梯

香蕉蛞蝓比变形虫更复杂,但它远不如游隼复杂。当谈到满足明天的边缘和嵌入式应用程序需求时,开发人员需要能够飞翔而不是爬行的东西。

Actian Zen 系列数据库正是为此而来。Zen 数据库之于 SQLite,正如 SQLite 之于平面文件管理系统。它提供了满足现代化边缘数据管理需求所需的全部功能。

Actian Zen Core 版本为开发人员提供了使 SQLite 成为有吸引力的数据库的相同特性:Zero-DBA、无服务器选项、可移植性以及对包括 C、C++ 和 C# 在内的大多数流行编程语言的嵌入式支持。Actian Zen 通过包含 NoSQL 和 SQL API 来扩展访问选项,并通过 SWIG(简化包装器和接口生成器)支持进一步扩展访问,这使得 Windows 和 Linux 系统上使用 Python、Perl 和 PHP 的开发人员可以访问 SQL 和 NoSQL API。

然而,Actian Zen 所体现的进化飞跃是,Actian Zen Core 版本(占地空间 <5MB)拥有一个与之类似的 Edge 版本,可在服务器或网关上运行(占地空间 <50MB),以及 Enterprise 和 Cloud 版本,可在更强大的设备和容器上运行(仍占用空间不到 200MB)。每个版本都基于相同的架构,因此它们可以相互补充和交互,这是 SQLite 世界中前所未有的。

使用 Actian Zen 构建

将 Actian Zen 整合到代码库中,只需将 32-/64 位 C/C++ 库(btrieveC.h、btrieveCpp.h、btrieveC.lib、btrieveCpp.lib)添加到项目中即可。如果您想使用 Perl、Python 或 PHP,还可能需要添加 SWIG 的接口库。与 Zen 的 SQL 调用在很大程度上与您与 SQLite 交互使用的 SQL 调用相同。但对于那些努力克服 SQLite 性能限制的物联网和移动开发人员来说,更令人兴奋的是使用 NoSQL API (Btrieve 2) 的选项。Btrieve 2 API 的使用就像文件管理系统一样简单,但提供了对底层数据库所有功能的访问,并且性能是 SQLite 的 100 倍。

通过 Btrieve2 API 与 Zen 数据库引擎交互有多容易?让我们来看看。

首先,我们必须在应用程序代码中导入 `btrieveCpp.h` 头文件。

#include "btrieveCpp.h"

然后,要连接到引擎,您必须创建一个 `btrieveClient` 对象实例。

BtrieveClient btrieveClient (0x4232, 0);

所有设置都已完成。第一个参数是 `serviceAgentIdentifier`,它将您的应用程序的每个实例标识给引擎。它可以是大于“AA”(0x4141)的任何 2 字节值;在本例中,0x4232 = “B2”。第二个参数是 `clientIdentifier`,它必须是一个 2 字节的整数。如果您正在编写多线程应用程序,则每个线程都需要向引擎提供唯一的客户端标识符。

现在,让我们收集一些数据。

Zen 数据存储在一个文件中。例如,一个文件可能包含来自血压 (BP) 监视器的传感器数据。每条记录都包含以下数据:

  • 8 字节时间戳,指示血压读数的采集时间。
  • 2 字节整数,用于收缩压值。
  • 2 字节整数,用于舒张压值。
  • 1 字节字符,用于评估代码。

考虑到数据的特性,我们需要创建一个文件来存储 13 字节的记录。下面的代码片段定义了一个 `BPrecord_t` 结构,该结构可以容纳血压记录。`#pragma` pack 语句确保记录实际上是 13 字节长,编译器不会添加额外的对齐字节。

#pragma pack(1)
typedef struct {
  uint64_t timeTaken;
  uint16_t systolic;
  uint16_t diastolic;
  char EvalCode;
} BPrecord_t;
#pragma pack()

要创建用于这些记录的文件,我们需要分配一个 `btrieveFileAttributes` 对象,并使用 `SetFixedRecordLength` 属性指定我们的记录大小。

Btrieve::StatusCode status;
BtrieveFileAttributes btrieveFileAttributes;
status = btrieveFileAttributes.SetFixedRecordLength(sizeof(BPrecord_t));

您可以添加其他文件属性(例如,在写入磁盘之前压缩文件),但记录大小是唯一必需的属性。

接下来,您必须指示 Zen 引擎使用我们 `btrieveClient` 会话对象上的 `FileCreate()` 方法来创建数据文件。我们还可以指定一个创建模式,以指示文件是否应在已存在时被覆盖。

static char* btrieveFileName = (char*)"Pressures.btr";
status = btrieveClient->FileCreate(&btrieveFileAttributes, btrieveFileName, Btrieve::CREATE_MODE_NO_OVERWRITE);
if ((status != Btrieve::STATUS_CODE_NO_ERROR) &
   (status != Btrieve::STATUS_CODE_FILE_ALREADY_EXISTS))
{
printf("Error: BtrieveClient::FileCreate():%d:%s.\n", status,
   Btrieve::StatusCodeToString(status));
}

上面的示例包括使用 Btrieve 类中内置的 `StatusCode` 枚举进行的错误检查。为了简化起见,后续代码示例中将不显示错误检查。

要将数据插入文件,您首先需要打开文件。

BtrieveFile btrieveFile;
status = btrieveClient->FileOpen(btrieveFile, btrieveFileName, NULL,
   Btrieve::OPEN_MODE_NORMAL);

在这里,我们分配一个 btrieveFile 对象,并使用我们的 `btrieveClient` 会话打开我们之前创建的文件。第三个状态参数(在本例中为“NULL”)可用于传递 Btrieve 所有者名称,该名称充当用于保护(并可选加密)数据文件的密码。我们的文件没有所有者名称,因此我们传递 NULL。

要插入记录,我们为前面创建的 `BPrecord_t` 结构分配一个记录缓冲区,然后用我们的数据值填充记录结构。然后,我们使用 `RecordCreate` 方法插入它。

Btrieve::StatusCode status = Btrieve::STATUS_CODE_NO_ERROR;
BPrecord_t record;
  // Get current system time and convert to microseconds 
time_t now = time(0) * 1000000;
  //Convert time to Btrieve2 Timestamp format 
record.timeTaken = Btrieve::UnixEpochMicrosecondsToTimestamp(now);
  //sysdata and diasdata are provided at runtime 
record.systolic = sysdata; 
record.diastolic = diasdata;
  //Determine the EvalCode from the systolic & diastolic 
record.EvalCode = 'N'; // Default is Normal
if ((sysdata >= 120 and sysdata < 130) and (diasdata < 80)) 
    record.EvalCode = 'E'; //Elevated blood pressure
if ((sysdata >= 130 and sysdata < 140) or 
  (diasdata >= 80 and diasdata < 90)) 
    record.EvalCode = 'H'; //High blood pressure
if ((sysdata >= 140 and sysdata <= 180) or
 (diasdata >= 90 and diasdata <= 120)) 
    record.EvalCode = 'V'; //Very high blood pressure
if ((sysdata > 180) or (diasdata > 120))
    record.EvalCode = 'C'; //Hypertensive Crisis 
  // Insert the record 
status = btrieveFile->RecordCreate((char*)& record, sizeof(record));

所有这些活动使用 Btrieve 2 API 执行的速度都远快于标准 SQL API,在物联网或移动场景中提供的性能水平是 SQLite 无法企及的。

对于现代边缘数据管理,数据库比文件管理系统具有巨大优势,因为它能够快速索引和检索数据。通过为现有的 Zen 数据文件添加索引,您可以根据特定值或特定顺序快速检索记录。您也可以在插入任何记录之前向新创建的文件添加索引。

索引创建涉及三个简单的步骤:

  1. 设置索引段。
  2. 定义索引属性。
  3. 将索引添加到数据文件。

继续我们的示例文件,我们将为记录的时间戳部分添加索引。请注意,您必须先打开文件才能为其添加索引。

BtrieveKeySegment btrieveKeySegment;
BtrieveIndexAttributes btrieveIndexAttributes;
  // Create a time stamp index segment on the first 8 bytes of the record 
status = btrieveKeySegment.SetField(0, 8, Btrieve::DATA_TYPE_TIMESTAMP);
  // Add the segment to the Index object 
status = btrieveIndexAttributes.AddKeySegment(&btrieveKeySegment);
  // Specify the nonmodifiable index attribute 
status = btrieveIndexAttributes.SetModifiable(false);
  // Create the index 
status = btrieveFile->IndexCreate(&btrieveIndexAttributes);

就这样。您刚刚定义了一个索引段,它是一个从记录偏移量 0 开始的 8 字节字段。这 8 个字节中的数据将被解释为 Btrieve TIMESTAMP。这是上面发生的情况:

  • `AddKeySegment()` 方法将 `btrieveKeySegment` 实例附加到 `btrieveIndexAttributes` 对象,以定义单个段的键。
  • `SetModifiable()` 方法将索引指定为可修改(true)或不可修改(false)。其他属性可用于使索引唯一或指定特定的索引编号。
  • `IndexCreate()` 方法将索引 0 添加到与 BtrieveFile 对象关联的先前打开的数据文件中。索引将填充文件中所有当前的值,并将在所有后续的插入/更新/删除操作上自动更新。

创建索引后,以极快的速度检索记录是一个简单的过程。有方法可以根据索引定义检索第一条或最后一条记录,或者通过与提供的值的比较检索特定记录。在我们的示例中,我们将检索时间戳值最大的记录,这应该是最近插入的记录。

Btrieve::StatusCode status = Btrieve::STATUS_CODE_NO_ERROR; 
BPrecord_t record;
  // Retrieve last inserted record
if (btrieveFile->RecordRetrieveLast(Btrieve::INDEX_1, 
  (char*)& record, sizeof(record),
  Btrieve::LOCK_MODE_NONE) != sizeof(record))
{
  status = btrieveFile->GetLastStatusCode();
  printf("Error: BtrieveFile::RecordRetrieve():%d:%s.\n", status,
      Btrieve::StatusCodeToString(status));
}

注意:记录检索方法不像我们之前看到的其他调用那样返回状态代码。相反,函数调用返回检索到的记录的*大小*。如果调用未按预期返回大小,则可以使用 `GetLastStatusCode()` 方法找出发生了什么。

记录检索方法的最后一个参数提供了在检索记录时锁定该记录的选项。

检索到记录后,您可能会决定更新或删除它。在检索之前,您无法更新/删除记录。`btrieveFile->RecordUpdate()` 和 `btrieveFile->RecordDelete()` 方法用于这些操作。

完成文件操作后,通过调用 `btrieveClient` 对象上的 `FileClose()` 方法来关闭文件。

status = btrieveClient->FileClose(btrieveFile));

虽然您可能不会经常需要它,但 Btrieve2 甚至提供了一个用于删除数据文件的方法。

status = btrieveClient->FileDelete(btrieveFileName);

在关闭应用程序之前,您应该通过调用 `Reset()` 方法来释放与引擎的会话。

status = btrieveClient->Reset();

以上就是基本的创建、读取、更新、删除功能,正如您所看到的,它非常简单明了。

© . All rights reserved.