Beagle SQL,适用于 Linux 的客户端/服务器数据库
我想向您介绍一个客户端-服务器数据库软件包,自从 1996 年 5 月以来,我一直在 Linux 上开发它。我选择 Linux 是因为它是在高级客户端-服务器应用程序开发方面可用的最强大的开发环境之一。Beagle SQL 最初是一个学习项目。我一直对弄清楚事物在底层是如何运作的着迷。在使用了许多主要的(和次要的)数据库软件包多年之后,我决定构建自己的数据库。从那时起,我收到了许多人的评论,他们正在寻找更多数据库软件包的选择,特别是那些支持 Linux 的软件包。我从许多人为 Linux 和其他免费可用工具的开发所做的辛勤工作中受益匪浅。Beagle SQL (BSQL) 是我回馈社会的一种方式。
数据库管理系统有许多不同的形式。在这里,我将讨论 BSQL 的客户端/服务器架构。此架构的三个基本组件是客户端进程、连接管理器和数据库管理器。
客户端进程是用户应用程序,它向数据库管理器发送请求。客户端只是一个使用 DBMS 提供的可用 API(应用程序编程接口)编写的程序,用于访问数据库中的数据。BSQL 附带 C 和 Perl API。
连接管理器处理所有到数据库管理器的传入连接。当客户端程序发出连接请求时,连接管理器会派生数据库管理器的副本,以处理来自客户端的所有后续请求。一旦客户端和数据库管理器开始通信,连接管理器就可以自由处理来自其他客户端的连接请求。
在这种情况下,每个客户端进程都由远程计算机上自己的服务器进程提供服务。这种架构的一个优点是服务器进程只需要担心自己的客户端,从而使客户端和服务器进程之间的通信易于处理。不幸的是,这种架构是内存密集型的,因为每个客户端都会派生一个服务器进程。
另一个缺点是,锁定算法变得更加复杂,因为每个服务器进程都需要了解更新数据库的其他服务器进程。数据库管理系统通常采用两种锁定方法之一:粗粒度锁定和细粒度锁定。
粗粒度锁定,也称为表级锁定,是两者中最容易实现的。它要求每个写入表的客户端进程请求对整个表及其关联索引进行锁定。一旦数据库管理器授予此锁,客户端进程就有权写入表。任何其他需要写入同一表的客户端都必须等到第一个客户端完成。通常,锁的持续时间是使用事务来处理的。在其最简单的形式中,事务将是单个 UPDATE 或 DELETE 语句。一些数据库管理器允许客户端使用关键字扩展事务的大小,以将多个语句块组合在一起。这在数据在数据库中的多个表中复制的系统中可能非常关键。这种类型的锁定的主要问题是,当多个客户端同时尝试更新同一表时,可能会创建瓶颈。
细粒度锁定,也称为行级锁定,实现起来要复杂得多。当客户端写入表时,它仅请求锁定当前正在更新的表中的行。这允许多个客户端同时更新同一表,只要它们不尝试锁定同一行。当客户端尝试更新具有关联索引的表时,复杂性就来了。BSQL 使用一种称为 B 树的索引方法。每当客户端更新、删除或插入表中的行时,表的 B 树索引可能需要重新平衡。B 树的并发平衡远远超出了本文的范围,但是已经有很多书籍和论文专门讨论这个主题。
目前,BSQL 使用不带锁定的客户端->连接->数据库架构。我计划首先实现粗粒度锁定,最终随着时间的推移发展为细粒度锁定。
客户端进程通常是用户编写的程序,它使用提供的 API(在本例中为 BSQL 的 C API)访问数据库。对于那些喜欢 Perl 的人,BSQL 发行版中提供了带有完整 Perl 源代码的演示客户端,可从 http://www.beaglesql.org/ 下载。客户端程序需要做的第一件事是使用 API 函数 BSQLConnect() 请求连接到服务器进程。connect 函数返回服务器所有后续通信所需的文件句柄。接下来,使用 BSQLSetCurrentDB() 函数调用设置要操作的数据库,传递 BSQLConnect() 返回的文件句柄以及您希望连接的数据库的名称。以下代码示例说明了客户端进程如何连接到在同一台机器上运行的服务器进程
s = BSQLConnect (host); if (!BSQLSetCurrentDB (s, "test")) { fprintf (stdout, "\nCan't send current database"); exit (s); }
连接到数据库后,您可以开始使用 BSQLQueryDB() 函数发送 SQL 查询,传递分配给连接的文件句柄和包含您的 SQL 查询的字符串。返回一个指向结果结构的指针,其中包含您的请求的状态。状态信息包括查询是否成功,以及在 SQL SELECT 的情况下,返回到客户端进程的记录或元组的数量。列表 1 中的代码片段显示了发送到数据库管理器的 SELECT 语句的结果。
在上面的示例中,BSQLnfields() 函数返回 SELECT 语句返回的每个记录的字段数。BSQLFieldName() 函数返回一个字符串,其中包含返回的第 n 个字段的字段名称。函数 BSQLntuples() 返回与 SELECT 的 WHERE 子句匹配的记录数。上面示例中省略 WHERE 子句告诉服务器进程返回电话簿表中的所有记录。对 BSQLFieldValue() 函数的调用返回一个字符串,其中包含来自第 i 个记录的第 n 个字段的数据。由于 BSQLQueryDB() 函数返回的结果结构是动态分配的,因此在使用完后必须释放它。BSQLFreeResult() 函数正是这样做的。任何客户端程序中调用的最后一个 BSQL API 函数应该是 BSQLDisconnect() 函数。调用时,它会向服务器进程传递退出消息,以便它可以终止连接并干净地退出。如果没有它,您将在您的系统中留下占用系统资源的游离服务器进程。
这可能是客户端-服务器难题中最直接的部分。连接管理器只是一个循环,等待来自客户端进程的传入消息。首先,为 “beagled” 服务(在您的 /etc/services 文件中定义)打开一个套接字,以便连接管理器可以监听传入的连接。然后进入一个无限循环。一旦连接管理器收到来自客户端进程的信号,对 accept() 的调用将返回客户端和服务器进程将通过其通信的套接字号。此时,连接管理器 fork() 数据库管理器,传递 accept() 返回的套接字号。在数据库管理器成功启动后,连接管理器开始监听下一个传入连接。
数据库管理器完成所有工作。数据库管理器的基本组件是表达式解析器、查询优化器(目前,BSQL 中未进行查询优化)、索引管理器、锁定管理器和低级 I/O 管理器。SELECT 语句是数据库管理器执行的最复杂的操作。由于 BSQL 支持显式连接,因此单个 SELECT 语句可以搜索多个表以返回请求的信息。表达式解析器必须足够智能,能够分辨出 SELECT 列表中的哪些字段属于哪些表。如果要连接具有重复字段名称的两个表,则 SELECT 语句必须显式声明哪个字段属于哪个表。允许使用通配符。当表达式解析器在字段列表中看到通配符时,它会将相应的字段名称插入到列表中。
侧边栏中有三个示例,让您了解表达式解析器是如何工作的。示例 3 中的语句失败,因为 field1 是不明确的。表达式解析器无法判断它属于表 A 还是表 B,因为两者都有一个名为 field1 的字段。
在连接表时,SELECT 语句的 WHERE 子句可以包含几个不同的部分,需要分别处理。当连接两个表时,这些部分可以包括第一个表的条件、第二个表的条件和连接条件。数据库管理器使用 WHERE 子句中的相应条件搜索两个表中的每一个。接下来,它使用 SELECT 字段列表中的字段以及连接条件中使用的字段将两个表连接到临时表中。最后,临时表中的记录与连接条件匹配,并将相应的记录提供给客户端进程检索。
当处理大型和多个表时,此操作可能会变得非常复杂且耗时。这就是查询优化器发挥作用的地方。它的目的是确定搜索和连接表的最有效顺序。BSQL 目前不进行查询优化,并按照表在 SELECT 语句中出现的顺序从左到右连接表。这使得编写 SQL 语句的人员需要考虑表在连接中出现的顺序。
在执行搜索时,数据库管理器使用一组低级 I/O 例程从数据库中检索记录。大多数商业数据库供应商都使用专有的文件系统来存放他们的数据库。在 BSQL 的情况下,Linux 文件系统就足够了。未来的增强功能将是灵活的文件格式,可以允许诸如 BLOB、图像、文本文档和任何其他内容。(BLOB 是一种大型二进制数据类型,用于在数据库表中存储图像、声音片段、程序等。)
用于存储这些可变长度记录的方法将显着影响数据库管理器的性能。当记录写入数据库时,它会被分解为固定大小的段。数据库管理员可以为每个数据库设置这些段的大小。如果将包含 850 字节的记录写入使用 256 字节段的表,则它将被分解为链接在一起的四个段。如果在稍后的时间记录大小更改为 1200 字节,则会将一个额外的段添加到链中。如果记录减少到 700 字节,则未使用的段将被标记为可重用。这里的一个缺点是随着时间的推移,数据库可能会变得碎片化。对于经常进行 UPDATE 和 DELETE 操作的数据库,应执行使用碎片整理实用程序的日常维护。此实用程序将在 BSQL v1.0 的第一个正式版本中提供。
