第一章 介绍
FastDb是高效的内存数据库系统,具备实时能力及便利的C++接口。FastDB不支持client-server架构因而所有使用FastDB的应用程序必须运行在同一主机上。(现在已经有了cli接口,所以这句话已不适用)FastDB针对应用程序通过控制读访问模式作了优化。通过降低数据传输的开销和非常有效的锁机制提供了高速的查询。对每一个使用数据库的应用数据库文件被影射到虚拟内存空间中。因此查询在应用的上下文中执行而不需要切换上下文以及数据传输。fastdb中并发访问数据库的同步机制通过原子指令实现,几乎不增加查询的开销。fastdb假定整个数据库存在于RAM中,并且依据这个假定优化了查询算法和接口。此外,fastdb没有数据库缓冲管理开销,不需要在数据库文件和缓冲池之间传输数据。这就是fastdb运行速度明显快于把数据放在缓冲池中的传统数据库的原因。
fastdb支持事务、在线备份以及系统崩溃后的自动恢复。事务提交协议依据一个影子根页面算法来自动更新数据库。恢复可以执行得非常快,为临界应用提供了高可用性。此外,取消事务日志改进了整个系统的性能,并且使得可以更有效的利用系统资源。
fastdb是一个面向应用的数据库,数据库表通过应用程序的类信息来构造。fastdb支持自动的模式评估,使你可以只需要在一个地方更改-你的应用程序的类。fastdb提供一个灵活方便的接口来从数据库中获取数据。使用一个类SQL的查询语言进行指定的查询。通过一些后关系特性如非原子字段,嵌套数组,用户定义类型和方法,对象间直接引用简化了数据库应用程序的设计并使之更有效率。
尽管fastdb的优化是立足于假定整个数据库配置在计算机的物理内存中,但是也有可能出现使用的数据库的大小超过了系统物理内存的大小的情况,在这种情况下标准的操作系统交换机制就会工作。但是整个fastdb的搜索算法和结构是建立在假定所有的数据都存在于内存中的,因此数据换出的效率不会很高。
第二章 查询语言
fastdb支持一个类sql句法的查询语言。fastdb使用更流行于面向对象程序设计的表达式而不是关系数据库的表达式。表中的行被认为是对象实例,表是这些对象的一个类。与sql不同,fastdb面向的是对对象的操作而不是对sql元组。所以每一次查询的结果就是来自一个类的对象的集合。fastdb查询语言与标准sql的主要差别在于:
1. 不支持多个表之间的连接(join)操作,不支持嵌套子查询。查询总是返回来自一个表的对象的集合。
2. 原子表列使用标准的c数据类型。
3. 没有NULL值,只有null引用。我完全同意C.J.Date对3值逻辑的批评以及他使用缺省值来代替NULL的意图。
4. 结构和数组可以作为记录元素。一个特别的exists算子(quantor)用来定位数组中的元素。
5. 可以为表记录(对象)也可以为记录元素定义无参用户自定义方法。
6. 应用程序可以定义只有一个串或者数值类型参数的用户自定义函数。
7. 支持对象间的引用,包括自动支持逆引用。
8. 通过使用引用,start from follow by执行递归的记录遍历。
9. 因为查询语言深度继承在了c++类中,语言标识符和关键字是大小写敏感的
10. 不进行整形和浮点型到串的隐含转换,如果需要这样的转换,必须显式进行。
下面类BNF表达式的规则指定了Fastdb查询语言搜索断言的语法:
Grammar conventions
Example | Meaning |
expression | non-terminals |
not | terminals |
| | disjoint alternatives |
(not) | optional part |
{1..9} | repeat zero or more times |
select-condition ::= ( expression) ( traverse ) ( order )
expression ::= disjunction
disjunction ::= conjunction
| conjunction or disjunction
conjunction ::= comparison
| comparison and conjunction
comparison ::= operand= operand
| operand != operand
| operand<> operand
| operand < operand
| operand <= operand
| operand > operand
| operand >= operand
| operand (not) like operand
| operand (not) like operand escape string
| operand (not) in operand
| operand (not) in expressions-list
| operand (not) between operand and operand
| operand is (not) null
operand ::= addition
additions ::= multiplication
| addition + multiplication
| addition || multiplication
| addition - multiplication
multiplication ::= power
| multiplication * power
| multiplication / power
power ::= term
| term ^ power
term ::= identifier| number | string
| true | false | null
| current | first | last
| ( expression )
| not comparison
| - term
| term [ expression ]
| identifier . term
| function term
| exists identifier : term
function ::= abs| length | lower | upper
| integer | real | string | user-function
string ::= '{ { any-character-except-quote} ('') } '
expressions-list ::= ( expression { , expression } )
order ::= order bysort-list
sort-list ::= field-order{ , field-order }
field-order ::= [length]field (asc | desc)
field ::= identifier{ . identifier }
traverse ::= startfrom field ( follow by fields-list )
fields-list ::= field{ , field }
user-function ::= identifier
标识符大小写敏感,必须以一个a-z,A-Z ,_ 或者$字符开头,只包含a-z, A-Z,0-9,_或者$字符,不能使用SQL保留字。保留字列表
abs | and | asc | between | by |
current | desc | escape | exists | false |
first | follow | from | in | integer |
is | length | like | last | lower |
not | null | or | real | start |
string | true | upper |
|
|
可以使用ANSI标准注释,所有位于双连字符后直到行末的字符都将被忽略掉。
fastdb扩展了ansi标准sql操作符,支持位运算。and/of操作符不仅可以运用到布尔操作数也可以操作整形操作书。and/or运用到整形操作数返回的结果是一个整形值,这个值是对操作数进行按位and或者按位or得到的结果。对于小的集合位运算是高效的。fastdb也支持对整形和浮点型的升幂运算(x^y)
第一章 c++ 接口
fastdb接受结构体作为记录的元组。结构的字段可以通过标准的点表达式访问:company.address.city
结构体的字段可以索引,从而可以按照指定的序列使用。结构体可以包含其他的结构体作为其元组,嵌套深度没有限制。
程序员可以为结构体定义方法,这些方法可以用在查询中,与对普通结构元组的句法是一样的。这些方法除了一个指向其隶属的对象的指针外(C++中的this指针)不能有参数,并且返回原子类型(bool型,数值、字符串或者引用类型)。这些方法也不应该改变对象实例(immutable method).如果方法返回字符串,该字串必须用new字符操作符分配,因为该字串值拷贝之后就会被删掉。
因此用户自定义方法可以用来创建虚元组-不是保存在数据库中而是使用其他元组计算出来的元组。例如:fastdb的dbDateTime类型只包含整形时间戳元组和象dbDateTime::year(), dbDateTime::month()...这样的方法。因此可以在应用中指定象"delivery.year= 1999"这样的查询,其中delivery记录字段拥有dbDateTime类型。方法在应用的上下文中执行,在其中定义,对其他应用和交互SQL是无效的
fastdb接受动态数组作为记录元组,不支持多维数组,但可以定义数组的数组,可以按照结果集中数组字段的长度对记录排序。fastdb提供了一个特别的构造集来处理数组:
1. 可以用length()函数来取得数组中元素的数目。
2. 数组元素可以用[]操作符来获取。如果索引表达式超出了数组范围,将产生异常
3. in操作符可以用来检查一个数组是否包含有一个由左操作书指定的值。该操作只能用于原子类型的数组:boolean , 数值,引用和字符串。
4. 数组可以用update方法更新,该方法复制数组然后返回一个非常量的引用。
5. 使用exists运算符迭代数组元素。exists关键字后指定的变量可以作为在exists算子后面的表达式中的数组的索引。该索引变量将迭代所有可能的数组索引值,直到表达式的值为true或者索引越界,下面的情况:
existsi: (contract[i].company.location = 'US')
将选择由位于‘US'的公司载运的合同的所有细节,而下面的查询:
notexists i: (contract[i].company.location = 'US')
将选择由'us'之外的公司载运的合同的所有细节。
可以由嵌套的exists子句。使用嵌套exist算子等同于使用相应的索引变量的嵌套循环。例如查询
existscolumn: (exists row: (matrix[column][row] = 0))
将选择matrix字段的元素为0的所有记录,该字段拥有整形数组的数组数据类型。这个构造等同于下面的两层嵌套循环:
bool result =false;
for (int column = 0; column <matrix.length(); column++) {
for (int row = 0; row <matrix[column].length(); row++) {
if (matrix[column][row] == 0) {
result = true;
break;
}
}
}
fastdb中的所有字符串都是变长的因此程序员无需费心去指定字符字段的最大长度,所有对数组适用的操作也适用于字符串。此外字符串也有属于自己的操作集。首先,字符串可用标准关系运算符相互比较。目前,fastdb只支持ascii字符集(对应于c的char类型)以及对字符串逐字节的比较而忽略本地设置。
like运算符可以通过一个包含通配符'%'和'_'的模式来匹配字符串, 。'_'字符匹配任意的单个字符,'%'匹配0个或多个字符。like运算符的一个扩展形式是与escape关键字一起用来处理模式中的'%'和'_'字符,如果他们出现在一个特定的逃逸字符之后(指escape关键字)就被当作普通字符处理而不是通配符。
可以用in操作符在字符串中查找子串。表达式 ('blue' incolor)对于所有包含color字段包含'blue'的记录都为真。如果被查找的字符串的长度大于某个门槛值(当前为512),则使用boyer-moore子串查找算法而不是直接查找方式。
字符串可以用+或者||运算符进行连接,后者是为了与ansi sql标准的兼容性而加入的。由于fastdb不支持在表达式中隐含的字符串转换,因此+运算符的语义可以为字符串重新定义。
引用可以用与访问结构元组同样的点表达式来解析,例如下面的查询:
company.address.city = 'Chicago'
将访问的Contract记录的company元组引用的Supplier表中的记录并展开其中的address字段的city元组。
引用可以用is null或isnot null断言来检查。引用也可以互相比较是否相等以及与null关键字比较。解析null引用时fastdb将抛出异常。
一个特别的关键字current可以用来在表查找时指向当前记录。通常current关键字用来当前记录标志符与其它引用的比较或者在引用数组中定位当前记录。例如,下面的查询将在Contract表中查找所有活动的合同(假定cancelContracts的数据类型为dbArray<dbReference<Contract>>)
current not in supplier.canceledContracts
fastdb提供特别的运算符以通过引用来递归遍历记录:
startfrom root-references
( follow by list-of-reference-fields)
这个结构的第一部分用来指定根对象,无结束符的root-references应该是一个引用变量或者一个引用数组类型的变量。这里可以使用两个特别的关键字first和last,分别用来定位表中第一个或最后一个记录。如果要要检查一个引用数组或者某些情况下一个引用字段引用的所有记录,这个结构可以无需follow部分。
如果指定了follow by部分,fastdb将递归遍历表中的记录,从根引用开始,使用list-of-reference-fields在记录间转换。list-of-reference-fields应当由引用字段或者引用数组。遍历是按照顶-左-右顺序的层次遍历(首先访问父结点然后是从左到右顺序的兄弟结点)。当遇到null引用或者一个已经被访问过的纪录的引用时递归终止。例如下面的查询将按照TLR顺序在一棵记录树中查找weight大于1的记录:
“weight > 1 start from first follow by left, right”
对于下面的树:
A:1.1
B:2.0 C:1.5
D:1.3 E:1.8 F:1.2 G:0.8
the result ofthe query execution will be:
查询结果将是:
('A', 1.1), ('B', 2.0), ('D', 1.3), ('E', 1.8), ('C',1.5), ('F', 1.2)
正如已经提到过的fastdb总是处理对象并且不支持连接。连接可以用引用来实现。考虑经典的Supplier-Shipment-Detail例子:
struct Detail {
char const* name;
double weight;
TYPE_DESCRIPTOR((KEY(name,INDEXED), FIELD(weight)));
};
struct Supplier {
char const* company;
char const* address;
TYPE_DESCRIPTOR((KEY(company,INDEXED), FIELD(address)));
};
struct Shipment {
dbReference<Detail> detail;
dbReference<Supplier>supplier;
int4 price;
int4 quantity;
dbDateTime delivery;
TYPE_DESCRIPTOR((KEY(detail,HASHED), KEY(supplier, HASHED),
FIELD(price), FIELD(quantity), FIELD(delivery)));
};
我们打算获得某些特定供应商的供应的某些特定细节。在关系数据库中这种查询将会写成这样:
select fromSupplier,Shipment,Detail where
Supplier.SID= Shipment.SID and Shipment.DID = Detail.DID
and Supplier.company like ? and Supplier.address like ?
and Detail.name like ?
fastdb中将会写成这样:
dbQuery q = "detail.namelike",name,"and supplier.company like",company,
"and supplier.addresslike",address,"order by price";
fastdb将首先在表Detail中进行索引查找获得匹配查找条件的记录。然后在所选的记录中再进行一次索引查找以定位装载记录。然后对剩余的选择断言进行顺序查找。
Predefined functions
Name | Argument type | Return type | Description |
abs | integer | integer | absolute value of the argument |
abs | real | real | absolute value of the argument |
integer | real | integer | conversion of real to integer |
length | array | integer | number of elements in array |
lower | string | string | lowercase string |
real | integer | real | conversion of integer to real |
string | integer | string | conversion of integer to string |
string | real | string | conversion of real to string |
upper | string | string | uppercase string |
fastdb允许用户自定义函数和运算符。函数应当至少有一个但不超过3个参数,参数类型可以是字符串、整形、布尔型、引用或者用户定义(源二进制)类型。返回值应当是整形、实数、字符串或者布尔型。
用户定义函数应当用USER_FUNC(f)宏来注册,该宏创建一个dbUserFunction类的静态对象,把函数指针和函数名绑定。
在应用中有两种方式来实现这些函数。第一个只能用于只有一个参数的函数,这个参数必须是int8、real8或者char * 类型。函数返回值应当是int8、real8、char* 或者bool。如果函数有不止一个参数或者接受不同类型的参数(多形)则参数应当以引用的方式传送给dbUserFunctionArgument结构。这个结构包含一个type字段,其值可以用在函数实现中检测传入的参数类型并且与参数值结合。下表映像了参数类型以及参数值的取值。
Argument type | Argument value | Argument value type |
dbUserFunctionArgument::atInteger | u.intValue | int8 |
dbUserFunctionArgument::atBoolean | u.boolValue | bool |
dbUserFunctionArgument::atString | u.strValue | char const* |
dbUserFunctionArgument::atReal | u.realValue | real8 |
dbUserFunctionArgument::atReference | u.oidValue | oid_t |
dbUserFunctionArgument::atRawBinary | u.rawValue | void* |
例如下面语句使得可以在sql语句中使用sin函数。
#include <math.h>
...
USER_FUNC(sin);
函数只能在定义的应用中使用。函数对于其他应用和交互sql是不可访问的。在返回字符串的函数中,返回的字符串必须用new运算符复制,因为fastdb在拷贝完返回值后将调用析构函数。
在fastdb,函数的参数可以(当不是必须)用圆括号括起来,因此下面的表达式都是合法的:
'$' + string(abs(x))
length string y
带两个参数的函数也可以当作运算符。考虑下面的例子,其中定义了函数contains进行大小写敏感的字串查找。
boolcontains(dbUserFunctionArgument& arg1, dbUserFunctionArgument& arg2) {
assert(arg1.type== dbUserFunctionArgument::atString
&& arg2.type ==dbUserFunctionArgument::atString);
return stristr(arg1.u.strValue, arg2.u.strValue) != NULL;
}
USER_FUNC(contains);
dbQuery q1, q2;
q1 = "select * fromTestTable where name contains 'xyz'";
q2 = "select * fromTestTable where contains(name, 'xyz')";
在这个例子中,查询q1和q2是等价的。
第三章 c++ 接口
fastdb主要的目标之一就是提供一个灵活并且方便的应用语言接口。任何使用过odbc或者类似的sql接口的人会明白我说的是什么。在fastdb中,一个查询可以用c++写成下面的样子:
dbQuery q;
dbCursor<Contract> contracts;
dbCursor<Supplier> suppliers;
int price, quantity;
q = "(price >=",price,"orquantity >=",quantity,
") anddelivery.year=1999";
// input price and quantity values
if (contracts.select(q) != 0) {
do{
printf("%s\n",suppliers.at(contracts->supplier)->company);
} while (contracts.next());
}
fastdb中的数据保存在表中,这些表对应于c++类,其中表记录对应于类实例。下面的c++数据类型可以作为fastdb记录的原子组件:
Type | Description |
bool | boolean type (true,false) |
int1 | one byte signed integer (-128..127) |
int2 | two bytes signed integer (-32768..32767) |
int4 | four bytes signed integer (-2147483648..2147483647) |
int8 | eight bytes signed integer (-2**63..2**63-1) |
real4 | four bytes ANSI floating point type |
real8 | eight bytes ANSI double precision floating point type |
char const* | zero terminated string |
dbReference<T> | reference to class T |
dbArray<T> | dynamic array of elements of type T |
除了上表定义的数据类型外,fastdb记录还可以包括这些元组的嵌套结构。fastdb不支持无符号数据类型以简化查询语言,清除由于符号数/无符号数比较产生的错误,减少数据库引擎的大小。
不幸的是c++没有提供在运行时获得一个类的元信息(metainformation)的方法(RTTI并不被所有编译器支持,并且也不能提供足够的信息)。因此程序员必须明确枚举包含在数据库表中的类字段(这也使得在类和表之间的映像更为灵活)。fastdb提供了一个宏和相关的类的集合来使得这个映像尽可能的灵活。
每一个要在数据库中使用的c++类或者结构,,都包含一个特别的方法来描述其字段。宏TYPE_DESCRIPTOR(field_list)构成了这个方法。这个宏的用括号括起来的单一参数是一个类字段描述符的列表。如果要为这个类定义一些方法并使之可以用于对应的数据库,则用宏CLASS_DESCRIPTOR(name, field_list)来代替TYPE_DESCRIPTOR。需要类名来取得成员函数的引用。
下面的宏可以用来构造字段描述符。
FIELD(name)
指定名字的非索引字段
KEY(name,index_type)
索引字段。index_type必须是HASHED和INDEXED标志的组合。当指定HASHED标志的时候,fastdb将为是用这个字段作为关键字的表创建一个hash表。当指定INDEXED标志时,fastdb将创建为使用这个字段作为关键字的表创建一个T_tree(一种特殊的索引).
UDT(name,index_type, comparator)
用户自定义原始二进制类型。数据库把这种类型作为指定大小的字节序列处理。这个字段可以用来查询(比较下同一类型的查询参数),可以通过order子句来索引和使用。通过程序员提供的comparator函数来进行比较操作。比较函数接受3个参数:两个指向待比较的原始二进制对象的指针及其大小。index_type的语义与KEY宏中的一致。
RAWKEY(name,index)
带有预定义比较算子的原始二进制类型。这个宏只是一个把memcmp作为比较算子的UDT宏的特例。
RAWFIELD(name)
另一个UDT宏的特例,使用memcmp作为预定义比较算子,并且没有索引。
SUPERCLASS(name)
指定当前类的基类(父亲)的信息。
RELATION(reference,inverse_reference)
指定类(表)之间的一对一、一对多或者多对多的关系。reference和inverse_reference字段都必须是引用或者引用数组类型。inverse_reference字段是一个包含了指向当前表的逆引用的引用表。逆引用自动由fastdb更新并用于查询优化。
OWNER(reference,inverse_reference)
指定类之间的一对一、一对多或者多对多的owner-member关系。当owner记录被删除时所有引用的member记录也会被删除(层叠式删除)。如果member记录要引用owner就必须通过RELATION宏声明。
METHOD(name)
为类指定一个方法。该方法必须是无参的实例成员函数,返回bool值、数值、引用或者字符串类型。方法必须在类的所有属性之后指定。
尽管只有原子字段可以被索引,但可以为一个结构指定一个索引类型。只有当该索引类型在该结构的索引mask中指定时才会为该结构的成员创建。这就允许程序员可以根据该结构在记录中的角色来设置或者取消索引。
下面的例子说明了头文件中类型描述符的创建过程:
classdbDateTime {
int4stamp;
public:
intyear() {
return localtime((time_t*)&stamp)->tm_year + 1900;
}
...
CLASS_DESCRIPTOR(dbDateTime,
(KEY(stamp,INDEXED|HASHED),
METHOD(year), METHOD(month), METHOD(day),
METHOD(dayOfYear), METHOD(dayOfWeek),
METHOD(hour), METHOD(minute), METHOD(second)));
};
classDetail {
public:
char const* name;
char const* material;
char const* color;
real4 weight;
dbArray< dbReference<Contract> > contracts;
TYPE_DESCRIPTOR((KEY(name, INDEXED|HASHED),
KEY(material, HASHED),
KEY(color, HASHED),
KEY(weight, INDEXED),
RELATION(contracts, detail)));
};
classContract {
public:
dbDateTime delivery;
int4 quantity;
int8 price;
dbReference<Detail> detail;
dbReference<Supplier> supplier;
TYPE_DESCRIPTOR((KEY(delivery, HASHED|INDEXED),
KEY(quantity, INDEXED),
KEY(price, INDEXED),
RELATION(detail, contracts),
RELATION(supplier, contracts)));
};
所有数据库中使用的类都要定义类型描述符。除了定义类型描述符外,还必须在C++类和数据库表之间建立一个映像。宏REGISTER(name)就做这个工作。与TYPE_DESCRIPTOR宏不同的是,REGISTER宏应该在实现文件中使用而不是在头文件中。该宏构造一个与类相连的表的描述符。如果你要在一个应用中使用多个数据库,那么就可能使用REGISTER_IN(name,database)宏在一个具体数据库中注册一个表。该宏的database参数应该是一个指向dbDatabase对象的指针。你可以像下面这样注册数据库的表:
REGISTER(Detail);
REGISTER(Supplier);
REGISTER(Contract);
表(以及对应的类)在每一时刻只能对应于一个数据库。当你打开一个数据库,fastdb向数据库中导入所有在应用中定义的类。如果一个同名的类在数据库中已经存在,则会比较描述符在数据库中的类与描述符在应用中的类,如果两个类的定义不同,则fastdb试图将该表转换成新的格式。数值类型之间的任何转换(整形到实型,实型到整形,扩展或者截断)都是允许的。增加字段也很容易,但是只有对空表才可以删除字段(以避免偶然的数据崩溃).
装载所有的类描述符后,fastdb就检查在应用程序的类描述符中指定的索引是否存在于数据库中、创建新的索引并且删除不再使用的索引。只有在不超过一个应用程序访问数据库是才可以进行表的重构以及增加/删除索引。所以只有第一个与数据库关联的应用程序可以进行表的转换,所有其余的应用只能向数据库中增加新类。
有一个特殊的内部表Metatable,该表包含了数据库中所有其他表的信息。C++程序员不需要访问这个表,因为数据库表的结构是由C++类指定的。但在交互SQL程序中,还是有必要检查这个表来获取记录字段的信息。
从版本2.30开始,fastdb支持自增字段(有数据自动赋值的值唯一的字段).要使用自增字段必须:
1.带上-DAUTOINCREMENT_SUPPROT标志重新编译fastdb和你的应用程序。(在fastdb makefile中的DEFS变量中增加这个标志)
注意:不带该标记编译的fastdb创建的数据库文件与带标记编译的fastdb创建的数据库文件不兼容。
2.如果你要使用初始值非0的计数器,则必须给dbTableDescriptor::initialAutoincrementCount赋个值。该变量由所有的表共享,因此所有的表都有一个共同初始值的自增计数器。
3.自增字段必须是int4类型,并且必须用AUTOINCREMENT标志声明
class Record {
int4 rid;
char const* name;
...
TYPE_DESCRIPTOR((KEY(rid,AUTOINCREMENT|INDEXED), FIELD(name), ...));
}
4.当要在数据库中插入带有自增字段的记录是不需要为自增字段赋值(将会被忽略)。当插入成功后,该字段将被赋给一个唯一的值(这样确保在数据库中从未使用过).
Record rec;
// no rec.rid should be specified
rec.name = "John Smith";
insert(rec);
// rec.rid now assigned unique value
int newRecordId = rec.rid; // and can be used to reference this record
5.当记录被删除该值将不会再使用,当事务中止时,表的自增计数器也将回滚。
query类用于两个目的:
1.构造一个查询并绑定查询参数
2.作为已编译的查询的缓存
fastdb提供重载的c++运算符'='和','来构造带参数的查询语句。参数可以在被使用的地方直接指定,消除了在参数占位符和c变量之间的任何映像,在下面的查询示例中,参数price和quantity的指针保存在查询中,因此该查询可以用不同的参数执行多次。c++函数重载使之可以自动的确定参数的类型,不需要程序员提供额外信息(从而减少了bug的可能性).
dbQueryq;
int price, quantity;
q = "price >=",price,"or quantity >=",quantity;
由于char *可以用来指定一个查询的分片(fraction)(例如"price >=")和一个字符串类型的参数,fastdb使用了一个特别的规则来解决这个模糊性。该规则基于这样一个假定即没有理由把一个查询文本分解成两个字符串如("price",">=")或者指定多于一个的参数序列("color=",color,color).因此fastdb假定第一个字符串是该查询文本的一个分片并且随之转换到操作数模式。在操作数模式中,fastdb认为char * 参数是一个查询参数然后切换回到查询文本模式,依此类推。也可以不用这个“句法糖”(syntax sugar)而是显示的通过dbQuery::append(dbQueryElement::ElementType type, void const* ptr)方法来构造查询元素。在向查询添加元素之前,必须通过dbQuery::reset()方法来重置查询('operator='自动作了这个事情)。
不能使用c++数值常量来作为查询参数,因为参数是通过引用来访问的。但可以使用字符串常量,因为字符串时传值的。有两种方法在一个查询中指定字符串参数:使用一个字符串缓冲或一个指向字符串指针的指针
dbQuery q;
char* type;
char name[256];
q = "name=",name,"and type=",&type;
scanf("%s", name);
type = "A";
cursor.select(q);
...
scanf("%s", name);
type = "B";
cursor.select(q);
...
查询变量既不能作为一个参数传给一个函数也不能赋给另一个变量。当fastdb编译查询时,会把编译树存到该对象中。下一次使用该查询时,不需要再次编译并且可以使用已编译好的树。这样节省了一些编译查询的时间。
fastdb提供了两个方法来集成数据库中的用户自定义类型。第一种方法-定义类方法-已经讨论过,另一个方法只处理查询构造。程序员要定义方法,该方法并不作确实的运算,而是返回一个表达式(根据预先定义的数据库类型),该方法来执行必要的查询。最好通过例子来说明这点。fastdb没有内置的日期时间类型,而是使用一个普通的c++类dbDateTime。该类定义了方法用来在有序列表中指定日期时间字段和使用通常的运算符来比较两个日期。
classdbDateTime {
int4stamp;
public:
...
dbQueryExpression operator == (char const* field) {
dbQueryExpression expr;
expr = dbComponent(field,"stamp"),"=",stamp;
return expr;
}
dbQueryExpression operator != (char const* field) {
dbQueryExpression expr;
expr = dbComponent(field,"stamp"),"<>",stamp;
return expr;
}
dbQueryExpression operator < (char const* field) {
dbQueryExpression expr;
expr = dbComponent(field,"stamp"),">",stamp;
return expr;
}
dbQueryExpression operator <= (char const* field) {
dbQueryExpression expr;
expr = dbComponent(field,"stamp"),">=",stamp;
return expr;
}
dbQueryExpression operator > (char const* field) {
dbQueryExpression expr;
expr = dbComponent(field,"stamp"),"<",stamp;
return expr;
}
dbQueryExpression operator >= (char const* field) {
dbQueryExpression expr;
expr = dbComponent(field,"stamp"),"<=",stamp;
return expr;
}
friend dbQueryExpression between(char const* field, dbDateTime& from,
dbDateTime& till)
{
dbQueryExpression expr;
expr=dbComponent(field,"stamp"),"between",from.stamp,"and",till.stamp;
return expr;
}
friend dbQueryExpression ascent(char const* field) {
dbQueryExpression expr;
expr=dbComponent(field,"stamp");
return expr;
}
friend dbQueryExpression descent(char const* field) {
dbQueryExpression expr;
expr=dbComponent(field,"stamp"),"desc";
return expr;
}
};
所有这些方法接受参数作为一个记录的字段的名字,该名字用来构造一个记录组件的全名。使用类dbComponent来作这个事情,该类把结构字段的名字和结构组件的名字组合成一个用'.'符号分隔的复合名字。类dbQueryExpression用来收集表达式项,表达式自动的用圆括号括起来,消除了运算符优先级引起的冲突。
假定一个记录包含了一个字段dbDateTime类型的字段delivery,可以如下构造查询:
dbDateTime from, till;
q1 = between("delivery", from, till),"orderby",ascent("delivery");
q2= till >= "delivery";
除了这些方法外,一些类指定方法也可以用这种方法定义,擂如一个区域类型的overlaps方法。这种方法的好处是数据库引擎可以使用预定义的类型并且可以使用索引和其他的一些优化方法来执行查询。另一方面,这些类的实现的封装已保留,因此程序员在一个类的表示改变时不应该重写所有的查询。
下面这些c++类型可以用作查询参数:
int1 | bool |
int2 | char const* |
int4 | char ** |
int8 | char const** |
real4 | dbReference<T> |
real8 | dbArray< dbReference<T> > |
游标用来访问选择语句返回的记录。fastdb提供了有类型的游标,也就是说,与具体表相关的游标。fastdb有两种游标:只读游标和用于更新的游标。fastdb中的游标用c++模板类dbCursor<T>来表示,其中T为与数据库表相关的C++类的的名字。游标类型必须在构造游标的时候指定。缺省创建一个只读游标。要创建一个用于更新的游标,必须给构造函数传递一个dbCursorForUpdate参数。
执行一个查询要么通过游标select(dbQuery &q)方法,要么通过select()方法,后者可以迭代表中的所有记录。两个方法都返回中选的记录的数量,并且把当前位置置于第一个记录(如果有的话)。游标可以前滚或者后滚。next(),prev(),first(),last()方法可以用来改变游标的当前位置。如果由于没有(更多)的记录存在而使得操作无法进行,这些方法将返回NULL,并且游标的位置不改变。
一个类T的游标包含一个类T的实例,用来获得当前的记录。这就是为什么表类必须要有一个缺省构造函数(无参数的构造函数)而没有副作用。fastdb优化了从数据库中获取记录,只从对象的固定部分复制记录。字符串本身并不复制,而是使相应的字段直接指向数据库中。数组也是如此:他们的组件在数据库中的表示根在应用程序中的表示是一样的(标量类型的数组或者标量组件的嵌套结构的数组).
应用程序不能直接改变数据库中的字符串和数据元素。当一个数组方法需要更新一个数组体时,先在内存中创建一个副本然后更新这个副本。如果程序员要更新字符串字段,他应该给这个指针赋一个新值,而不是直接修改数据库里的字符串。对于字符串元素建议使用char const * 而不是char *,以使编译器可以检查对字符串的非法使用。
游标类提供了get()方法来获得指向当前记录(保存在游标中)的指针。重载的运算符' ->'也可以用来访问当前记录的元素。如果一个游标一更新方式打开,就可以改变当前的记录并且用update()方法保存到数据库中或者被删除掉。如果当前记录被删除,下一个记录变为当前记录。如果没有下一个记录,则前一个记录(如果有的话)变为当前。removeAll()方法删除表中的所有记录。而removeAllSelected方法只删除游标选择的所有记录。
当更新记录后,数据库的大小可能会增加。从而虚拟存储器的数据库区域需要进行扩展。该重映射的后果之一就是,该区域的基地址可能发生变化,然后应用程序中保存的所有数据库指针都变得无效。当数据库区域重映象时fastdb自动更新所有打开的游标中的当前记录。因此,当一个数据库更新时,程序员应该通过游标的->方法来访问记录字段,而不应该使用指针变量。
当前选择使用的内存可以通过reset()方法来释放。该方法自动的由select()、dbDatabase::commit()、dbDatabase::rollback()方法以及游标的销毁(destructor)函数调用,因此大多数情况下不需要显式调用reset()方法。
游标也可以通过引用来访问记录。at(dbReference<T>const& ref)方法把游标指向引用所指的记录。在这种情况下,选择将只包含一个记录,而next(),prev()方法将总是返回NULL。由于游标和引用在fastdb重视严格类型化的,所有必须的检查可以有编译器静态的进行而不需要动态类型检查。运行时唯一要进行的检查是对空引用的检查。游标中当前记录的对象标识符可以用currentId()方法获得。
可以限制select语句返回的记录的数目。游标类有两个方法setSelectionLimit(size_t lim)和unsetSelectionLimit()用来设置/取消查询返回的记录数的限制。在某些情况下,程序员可能只需要几个记录或者头几个记录,从而查询的执行时间和消耗的内存大小可以通过限制选择的大小来降低。但如果你指定了被选记录的排序方式,只选择k个记录的查询并不返回关键字最小的头k个记录,而是返回任意k个记录然后排序。
于是所有数据库数据的操作都可以通过游标来进行,唯一的例外是插入操作,fastDB提供了一个重载的插入函数:
template<class T>
dbReference<T> insert(T const& record);
该函数将在表的末尾插入一个记录然后返回该对象的引用。fastdb中插入的顺序是严格指定的因而应用程序可以使用表中记录排序方式的假定。因为应用程序大量使用引用在对象之间导航,有必要提供一个根对象,从这个对象开始进行引用遍历。这样一个根对象的一个可取候选者就是表中的第一个记录(也是表中最老的记录).该记录可以通过不带参数执行select()方法来访问。游标中的当前记录就是表中的第一条记录。
fastdb的c++API为引用类型定义了一个特殊的null变量,可以用null变量与引用比较或者赋给一个引用:
void update(dbReference<Contract> c) {
if (c != null) {
dbCursor<Contract>contract(dbCursorForUpdate);
contract.at(c);
contract->supplier = null;
}
}
查询参数通常跟c++变量绑定。大多数情况下这是方便而且灵活的机制。但在多线程应用中,无法保证同一查询会在同一时刻不被另一线程以不同的参数执行。一个解决的方法是使用同步原语(临界区或者mutex)来排除查询的并发执行。但这种方法会导致性能退化。fastdb可以并行操作读操作而提高了整体系统吞吐量。另一个解决方法是使用延迟参数绑定。如下所示:
dbQueryq;
struct QueryParams {
int salary;
int age;
int rank;
};
void open()
{
QueryParams* params = (QueryParams*)NULL;
q = "salary > ", params->salary, "and age < ",params->age, "and rank =", params->rank;
}
void find(int salary, int age, int rank)
{
QueryParamsparams;
params.salary = salary;
params.age = age;
params.rank = rank;
dbCursor<Person> cusor;
if (cursor.select(q, ¶ms) > 0) {
do{
cout << cursor->name << NL;
} while (cursor.next());
}
}
在这个例子中open函数只为结构中的字段偏移绑定查询变量。然后再find函数中,指向带有参数的结构的真实的指针传递给select结构。find函数可以被多个线程并发执行而只有一个编译好的查询被所有这些线程使用。这种机制从版本2.25开始使用。
dbDatabase类控制应用与数据库的交互,进行数据库并发访问的同步,事务处理,内存分配,出错处理...
dbDatabase对象的构造函数允许程序员制定一些数据库参数。
dbDatabase(dbAccessType type = dbAllAccess,
size_t dbInitSize =dbDefaultInitDatabaseSize,
size_t dbExtensionQuantum =dbDefaultExtensionQuantum,
size_t dbInitIndexSize =dbDefaultInitIndexSize,
int nThreads = 1);
支持下面的数据库访问类型:
Access type | Description |
dbDatabase::dbReadOnly | Read only mode |
dbDatabase::dbAllAccess | Normal mode |
dbDatabase::dbConcurrentRead | Read only mode in which application can access the database concurrently with application updating the same database in dbConcurrentUpdate mode |
dbDatabase::dbConcurrentUpdate | Mode to be used in conjunction with dbConcurrentRead to perform updates in the database without blocking read applications for a long time |
当数据库已只读方式打开时,不能向数据库添加新的类定义,不能改变已有的类的定义和索引。
在数据库主要用只读模式访问而更新不应该长时间堵塞读操作的情况下应该同时使用dbConcurrentUpdate和dbConcurrentRead模式。在这种模式下更新数据库可以与读访问并发执行(读将不会看到改变的数据直到事务提交)。只有在更新事务提交时,才会设置排他锁然后在当前对象索引自增改变(incremental change)之后马上释放掉。
于是你可以使用dbConcurrentRead模式启动一个或多个应用而其读事务将并发执行。也可以使用dbConcurrentUpdate模式启动一个或多个应用。所有这些应用的事务将通过一个全局的mutex来同步。因此这些事务(甚至是只读)将排他性的执行。但是dbConcurrentUpdate模式的应用的事务可以与dbConcurrentRead模式的应用的事务并发运行。请参阅testconc.cpp例子,里边说明了这些模式的使用方法。
注意!不要把dbConcurrentUpdate和dbConcurrentRead模式与其他模式混合使用,也不要在一个进程中同时使用他们(因此不能启动两个线程其中一个用dbConcurrentUpdate模式打开数据库另一个用dbConcurrentRead模式)。在dbConcurrentUpdate模式下不要使用dbDatabase::precommit方法。(precommit方法主要作用是释放资源和解锁,但是不会同步到数据库磁盘映射文件)。
dbInitSize参数指定了数据库文件的初始大小。数据库文件按照需要增长;设置初始大小只是为了减少重新分配空间(会占用很多时间)的次数。在当前fastdb数据库的实现中该大小在每次扩展时至少加倍。该参数的缺省值为4兆字节。
dbExtensionQuantum指定了内存分配位图的扩展量子。简单的说,这个参数的值指定了再不需要试图重用被释放的对象的空间时分配多少连续内存空间。缺省值为4MB.详细情况参见Memory allocation小节。
dbInitIndexSize参数指定了初始的索引大小。fastdb中的所有对象都通过一个对象索引访问。该对象索引有两个副本:当前的和已提交的。对象索引按照需要重新分配,设置一个初始值只是为了减少(或者增加)重新分配的次数。该参数的缺省值是64K个对象标识符。
最后一个参数nThreads控制并行查询的层次。如果大于1,则fastdb启动一些查询的并行执行(包括对结果排序).在这种情况下,fastdb引擎将派生指定数目的并行线程。通常为该参数指定超过系统中在线CPU数目的值是没有意义的。也可以为该参数传递0值。在这种情况下,fastdb将自动侦测系统中在线cpu的数目。在任何时候都可以用dbDatabase::setConcurrency来指定线程数。
dbDatabase类包含一个静态字段dbParallelScanThreshold,该字段指定了在使用并行查询后表中记录数的一个阈值,缺省为1000。
可以用open(char const* databaseName, char const* fileName =NULL, unsigned waitLockTimeout = INFINITE)方法来打开数据库。如果文件名参数省略,则通过数据库名家一个后缀“.fdb"来创建一个文件。数据库名应该是由除了‘\’之外的任意符号组成的标识符。最后一个参数waitLockTimeout可以设置用来防止当工作于该数据库的所有活动进程中的某些进程崩溃时把所有的进程锁住。如果崩溃的进程锁住了数据库,则其他的进程都将无法继续执行。为了防止这种情况,可以指定一个等待该锁的最大延迟,当该延迟过期后,系统将试图进行恢复并继续执行活动的进程。如果数据库成功打开open方法返回true,否则返回false。在后面这种情况,数据库的handleError方法将带上DatabaseOpenError错误码被调用。一个数据库会话可以用close方法中止,该方法隐含的提交当前事务。
在一个多线程的应用中,每一个要访问数据库的线程都应该首先与数据库粘附(attach). dbDatabase::attach()方法分配线程指定数据然后把线程与数据库粘附起来。该方法自动由open()方法调用。因此没有理由为打开数据的线程调用attach()方法。当该线程工作完毕,应当调用dbDatabase::detach()方法。close方法自动调用detach()方法。detach()方法隐含提交当前事务。一个已经分离的线程试图访问数据库时将产生断言错误(assertion failure)。
fastdb可以并行的编译和执行查询,在多处理器系统中显著的提高了性能。但不能并发更新数据库(这是为了尽可能少的日志事务(log-lesstransaction)机制和0等待恢复的代价).当一个应用程序试图改变数据库(打开一个更新游标或者在表中插入新记录)时,首先就以排他方式锁住数据库,禁止其他应用程序访问数据库,即使是只读的查询。这样来避免锁住数据库应用程序过长的时间,改变事务应当尽可能的短。在该事务中不能进行堵塞操作(如等待用户的输入).
在数据库层只使用共享锁和排它锁使得fastdb几乎清除锁开销从而优化无冲突操作的执行速度。但是如果多个应用同时更新数据库的不同部分,则fastdb使用的方法将非常低效。这就是为什么fastdb主要适用于单应用程序数据局访问模式或者多应用读优势(read-dominated)访问模式模型。
在多线程应用中游标对象应该只由一个线程使用。如果在你的应用中有超过一个的线程,则在每个线程中使用局部游标变量。在线程间有可能共享查询变量,但要注意查询参数。查询要么没有参数,要么使用相关的参数绑定形式。
数据库对象由所有的线程共享,使用线程专有数据来进行查询的同步代价最小的并行编译和执行。需要同步的全局的东西很少:符号表,树结点池…..。但是扫描、解析和执行查询可以不需要任何的同步来进行,如果有多处理器系统的高层并发机制。
一个数据库事务由第一个选择或者插入操作开始。如果使用用于更新的游标,则数据库以排他方式锁住,禁止其他应用和线程访问数据库。如果使用只读游标,这数据库以共享模式锁住,防止其他的应用或者线程改变数据库,但允许并发读请求的执行。一个事务必须显示终止,要么通过dbDatabase::commit()方法提交该事务对数据库的所有更改,或者通过dbDatabase::rollback()方法来取消事务所作的所有更改。dbDatabase::close()方法自动提交当前事务。
如果你使用只读游标来执行选择从而启动一个事务然后又适用更新游标来对数据库作某些改变,则数据库将首先以共享模式锁住,然后锁变成排他模式。如果该数据被多个应用访问这种情况可能会造成死锁。想象一下应用A启动了一个读事务而应用B也启动了一个读事务。二者都拥有数据库的共享锁。如果二者都试图把它们的锁改变为排他模式,他们将永远被互相堵塞(另外一个进程的共享锁存在时不能授予排它锁)。为了避免这种情况,在事务开始就试着使用更新游标,或者显示的使用dbDatabase::lock()方法。关于fastdb中事务实现的信息可以参见《事务》这一节。
可以使用lock()方法来显示的锁住数据库。锁通常是自动进行的。只有很少的情况下你才需要使用这个方法。它将以排他方式锁住数据库知道当前事务结束。
可以用dbDatabase::backup(char const* file)方法来备份数据库。备份操作将以共享模式锁住数据然后从内存向指定的文件刷新数据库的映像。因为使用了影子对象索引,数据库文件总是处于一致状态,因此从备份恢复至需要把备份文件改一下名字(如果备份被放到磁带,则首先要把文件恢复到磁盘).
dbDatabase类也负责处理一些应用的错误,如编译查询时的句法错误,执行查询时的索引越界或者空引用访问。由一个虚方法dbDatabase::handleError来处理这些错误。
virtual void handleError(dbErrorClass error,
charconst* msg = NULL,
int arg = 0);
程序员可以从dbDatabase类来派生出自定义的子类,并重定义缺省的错误处理。
Errorclasses and default handling
Class | Description | Argument | Default reaction |
QueryError | query compilation error | position in query string | abort compilation |
ArithmeticError | arithmetic error during division or power operations | - | terminate application |
IndexOutOfRangeError | index is out if array bounds | value of index | terminate application |
DatabaseOpenError | error while database opening | - | open method will return false |
FileError | failure of file IO operation | error code | terminate application |
OutOfMemoryError | not enough memory for object allocation | requested allocation size | terminate application |
Deadlock | upgrading lock causes deadlock | - | terminate application |
NullReferenceError | null reference is accessed during query execution | - | terminate application |
前面介绍的接口提供了C++方便而且可靠的数据访问机制。但其有两个不足:
1.这是非常C++化的从而不能够用于其它语言
2.只适用于对数据库的本地连接(同属于一个系统)
下面说明的接口克服了这两个限制。采用了纯ANSI C函数集,因而利用其将任意的程序设计语言映射到fastdb是很容易实现的。客户端与服务器的连接通过socket(不论是本地的还是标准的TCP/IP sockets)。当然这个接口比起C++接口来说不是那么友好并且易于出错,但这是灵活性的代价。所有的类型,常量和函数都在cli.h文件中声明。
fastdb提供了多线程服务器来处理客户端的CLI会话。可以在SubSQL工具中用start server 'HOST:PORT' <线程数> 命令来启动这个服务器。该服务器可以接受本地(在一个系统中)和全局的客户连接然后将线程池中的一个线程与一个连接黏附。线程池的大小由“线程数”参数控制。但如果有更多的主动连接服务器可以派生出超过制定数字的线程,。一个线程与一个客户黏附知道会话结束。如果一个线程异常中止,该客户所作的任何改变都将被回滚。服务器可以通过相应的stop server 'HOST:PORT'命令停止。
CLI functions return codes
Error code | Description |
cli_ok | Succeful completion |
cli_bad_address | Invalid format of server |
cli_connection_refused | Connection with server could not be established |
cli_bad_statement | Text of SQL statement is not correct |
cli_parameter_not_found | Parameter was not found in statement - |
cli_unbound_parameter | Parameter was not specified |
cli_column_not_found | No sucj colunm in the table |
cli_incompatible_type | Conversion between application and database type is not possible |
cli_network_error | Connection with server is broken |
cli_runtime_error | Error during query execution |
cli_bad_descriptor | Invalid statement/session description |
cli_unsupported_type | Unsupported type for parameter or colunm |
cli_not_found | Record was not found |
cli_not_update_mode | Attempt to update records selected by view only cursor |
cli_table_not_found | There is no table with specified name in the database |
cli_not_all_columns_specified | Insert statement doesn't specify values for all table columns |
cli_not_fetched | cli_fetch method was not called |
cli_already_updated | cli_update method was invoked more than once for the same record |
cli_table_already_exists | Attempt to create existed table |
cli_not_implemented | Function is not implemented |
Supported types
Type | Description | Size |
cli_oid | Object identifier | 4 |
cli_bool | Boolean type | 1 |
cli_int1 | Timy interger type | 1 |
cli_int2 | Small interger type | 2 |
cli_int4 | Interger type | 4 |
cli_int8 | Big interger type | 8 |
cli_real4 | Single precision floating point type | 4 |
cli_real8 | Double precision floating point type | 8 |
cli_asciiz | Zero terminated string of bytes | 1*N |
cli_pasciiz | Pointer to zero terminated string | 1*N |
cli_array_of_oid | Array of references | 4*N |
cli_array_of_bool | Array of booleans | 1*N |
cli_array_of_int1 | Array of tiny integers | 1*N |
cli_array_of_int2 | Array of small integers | 2*N |
cli_array_of_int4 | Array of integers | 4*N |
cli_array_of_int8 | Array of big integers | 8*N |
cli_array_of_real4 | Array of reals | 4*N |
cli_array_of_real8 | Array of long reals | 8*N |
int cli_open(char const* server_url,
int max_connect_attempts,
int reconnect_timeout_sec);
与服务器建立连接
参数:
server_url-0终止的字符串,服务器地址和端口,如"localhost:5101", "195.239.208.240:6100"...
max_connect_attempts-建立连接的尝试次数
reconnect_timeout_sec-连接尝试的时间间隔,以秒为单位
返回值:
>= 0 - 将由所有其它cli调用使用的连接描述符
<0 - cli_result_code中说明的错误吗
int cli_close(int session);
关闭会话
参数:
session-cli_open返回的会话描述符
返回值:
如cli_result_code所述
int cli_statement(int session, char const* stmt);
指定要在服务器端执行的SubSQL语句。可以建立参数和列的绑定。
参数:
Session - cli_open返回的会话描述符
Stmt -0终止的SubSQL语句字符串
返回值:
>= 0 - 语句描述符
<0 cli_result_code所述的错误码
int cli_parameter(int statement,
char const* param_name,
int var_type,
void* var_ptr);
将参数与语句绑定
参数:
statement- cli_statement返回的描述符
param_name-0终止的参数名字符串。参数名必须以'%'开始
var_type- cli_var_type中所述的参数类型。只支持标量和0终止字符串类型
var_ptr-指向变量的指针。
返回值:
cli_result_code所述的结果编码
从2.49版本开始fastdb提供了可选的容错支持。可以启动一个主要的(活动的)和几个备用的结点,所有在主要结点发生的变化同时被复制到备用结点上。如果主结点崩溃,其中一个备用结点将变为活动的并成为主结点。一旦一个崩溃的结点重新启动,它要进行恢复,与主结点的状态同步,然后作为备用结点投入使用。结点通过套接字连接并规定放置在不同的计算机上。通信被假定为时可靠的。
要使用容错支持,应该使用REPLICATION_SUPPORT可选项来重新编译fastdb.在makefile开始把FAULT_TOLERANT变量设置为1来把它打开。应该使用dbReplicatedDatabase来代替dbDatabase。在open方法的参数中,除了数据库名和文件名之外,应当指定这个结点的标志符(从0到N-1的整数),包含所有结点地址(主机:端口)的数组以及结点数(N).然后就可以在N个结点的每一个启动程序。一旦所有的实例都启动,ID=0的结点成为活动的(主结点)。在这个实例中open方法返回true.其他结点在open方法堵塞。如果主结点崩溃,其中一个备用结点被激活(open方法返回true),然后这个实例继续执行。如果崩溃的实例重新启动,它将尝试连接所有服务器,恢复其状态然后作为备用结点,等待其代替崩溃的主结点的机会。如果主结点正常终止,所有备用结点的close方法返回false.
在容错模式下fastdb保留两个文件:一个包含数据库本身,另一个则是页更新计数器。带有页更新计数器的文件用于增量恢复。当崩溃结点重启动时,它将向主结点发送页计数器,并只接受这段时间在主结点发生变化的页(其时间戳大于被恢复的结点所发送的页)。
在复制模式中(在主结点)应用程序在事务提交期间并不阻塞知道所有的变化被刷新到硬盘。以改变的页由独立的线程异步的刷新到磁盘上。这样带来了显著的性能提升。但如果所有的结点都崩溃了,数据库就可能处于不一致状态。也可以指定向硬盘刷新数据的时间延迟:延迟越大,磁盘IO开销越小。但在崩溃的情况下,需要从主结点发送更多的数据以进行恢复。
可以在无盘模式中使用容错模式(DISKLESS_CONFIGURATION构建选项)。在这种情况下,没有数据保存在磁盘上(没有数据库文件,也没有页更新计数器).假定至少有一个结点总是活动的。只要有一个在线结点数据就不会丢失。当崩溃结点恢复时,主结点向其发送完整的数据库快照(增量恢复是无法实现的因为崩溃结点的状态已经丢失)。由于这种模式没有磁盘操作,操作性能是非常高的并且只受限于网络吞吐量。
当复制结点启动后就开始在指定的时限内尝试连接所有其他的结点。如果在这个时间内无法建立连接,则该结点被假定为自主启动的并作为普通(非复制)数据库开始工作。如果结点与其它结点建立了连接,则具有最小ID的节点被选为复制主结点。所有的其他结点被切换到旁置模式并等待来自主结点的复制请求。如果主结点和从结点的数据库的内容不一致(使用页计数器数组来决定),则主结点进行旁置结点的恢复,向其发送最近的页面。如果主结点崩溃,则旁置结点选择一个新的主结点(最小ID的节点)。所有的旁置结点都在open方法堵塞直到下面的情况之一发生:
1.主结点崩溃并且该节点被选为新的主结点。在这种情况下open方法返回true。
2.主结点正常关闭数据库。在这种情况下所有复制结点的open方法返回false。
可以从其他应用程序中对复制数据库进行只读的访问。在这种情况,复制的结点必须通过dbReplicatedDatabase(dbDatabase::dbConcurrentUpdate)构造掉用来创建。其他应用程序可以用dbDatabase(dbDatabase::dbConcurrentReadMode)实例来访问同一数据库。
并非所有应用都需要容错。许多应用使用复制只是为了提高可测量性,在许多结点间分担负载。对于这些应用,fastdb提供了简化复制模型。在这种情况下,有两种结点:读者和写者。任何一个写者结点都可以作为复制主结点。而读者结点只能从主结点接收复制的数据而不能自己更新数据库。与上面所述的复制模型的最主要区别是读者永远不能变成主结点并且这个结点的open方法一旦与主结点建立了连接就马上归还控制权。来自主结点的更新通过单独的县城接收。读者结点要用dbReplicatedDatabase(dbDatabase::dbConcurrentRead)构造器来创建。必须使用预主结点同样的数据库模式(类)。当主结点关闭连接时来自读者结点的数据库连接并不自动关闭,其仍然保持打开并且应用仍然可以以只读模式访问数据库。一旦主结点重启,就会与所有的旁置结点建立连接并继续向它们发送更新。如果没有读者结点,则复制模型就等同于前面所述的容错模型,如果只有一个写者结点和一个或多个读者结点,这就是经典的主从复制。
可以使用Guess例子来测试容错模式。这个例子用-DREPLICATION_SUPPORT编译展示了3个结点的簇(所有地址指向localhost,但你当然也可以用你的网络中真实的主机来代替他们)。必须用参数0..2来启动guess应用的3个实例。当所有的实例启动后,用参数0启动的应用开始正常的用户对话(这是游戏:“guess an animal”).如果你用Crtl-c来模拟该应用程序的崩溃,则其中的一个备用结点继续执行。
testconc示例演示了更复杂的复制模型。有3个复制结点,通过使用testconc update N 命令来启动,其中N是这些结点的标志符:0,1,2。启动了这3个结点后,它们就会互相连接,结点0成为主结点并开始更新数据库,把改变复制到结点1和2。可以启动一个或多个检查者,即用只读模式(使用dbConcurrentRead访问类型)来连接到复制的数据库的应用程序。检查者可以用testconc inspect N T来启动,其中N是检查者要连接的复制结点的标志符,T是检查线程的编号。
同一个testconc示例可以用来演示简化的复制模型。启动一个主结点:testconc update 0,然后启动两个只读复制结点:testconc coinspect 1 和 testconc coinspect 2。请注意与前面所述的场景的区别:在容错模式下,普通的复制结点使用testconc update N命令启动,而连接到同一数据库的只读结点(不包括复制进程)通过testconc inspect N命令启动。在简化的主-从复制模型中,有只读的复制结点,其不能变成主结点(因此如果最初的主结点崩溃,没有人能够扮演这个角色),但运行在这个结点上的应用可以同通常的只读应用一样访问复制结点。
Fastdb支持ACID事务。也就是说当数据库得到事务已经提交的报告后,可以保证该数据库在系统出错时(除了硬盘上的数据库镜像损坏外)能够恢复。在标准配置(例如没有非易变RAM)和通用操作系统中(windows,unix….)提供这种特性的唯一方法是对硬盘进行同步写。在这里“同步”意味着操作系统直到数据被真正写到硬盘上之后才会把控制权交回应用程序。不幸的是同步写是非常耗时的操作—平均磁盘访问时间是10ms,因此每秒很难达到处理100个事务的性能。
但是在很多情况下,丢失最后几秒的变化是可以接受的(但是要与数据库保持一致性)。依照这个假定,数据库性能可以显著得到提高,fastdb为这样的应用程序提供了“延迟事务提交模式”。当提交事务延迟非零时,数据库并不马上执行提交操作,而是根据一个指定的超时时间延迟操作。当超时时间过期,事务正常提交,这保证了在系统崩溃时只有在指定的超时时间内的变化才被丢失。
如果以延迟事务初始化的线程在被延迟的事务提交之前启动了新的事务,则延迟提交操作被忽略。因此fastdb可以把一个客户端执行的许多继起(subsequent)的事务组成一个单一的大事务。这样就极大地提高了性能,因为其减少了同步写的次数和创建的映像页的个数。(参看事务一节)。
如果其他客户端试图在延迟提交超时时间过期前启动事务,则fastdb强制进行延迟提交然后释放资源。因此同步不受延迟提交的影响。
延迟提交缺省是关闭的(超时时间为0)。你可以指定提交延迟参数作为dbDatabase::open方法的第二个可选参数。在SubSQL工具中也可以通过设置FASTDB_COMMIT_DELAY环境变量(秒)来指定事务提交延迟的值。
fastdb使用的事务提交模式保证了在软硬件出现故障时只要磁盘上的数据库没有损坏(写到盘上的数据可以正确的读出来)的恢复。如果由于某些原因数据库文件损坏了,则恢复的唯一途径是使用备份(但愿在不久之前做过这样的操作)。
当数据库离线是可以通过拷贝数据库文件来备份。dbDatabase类提供了backup方法来进行在线备份而不需要停止数据库。程序员在任何时候都可以调用这个方法。不过更进一步,fastdb提供了备份调度可以自动进行备份。唯一需要的是—备份文件名和备份之间的时间间隔。
dbDatabase::scheduleBackup(char const* fileName,time_t period)方法派生出单独的线程在指定的时间内(秒)向指定的位置进行备份。如果filename以"?"字符结尾,则备份初始化的时间被附加到文件名的末尾来产生唯一的文件名。在这种情况下所有的备份文件保存在磁盘上(把太老的备份文件移除或者把它们移到别的介质上是管理员的责任)。否则备份被写入到以fileName+".new"命名的文件中,备份完成后旧备份文件被删除新文件被重命名为fileName.在后一种情况下,fastdb也将检查旧备份文件(如果有的话)的创建日期然后按照这样的方式来调整等待时间,就是备份之间的时间差要等于指定的间隔(因此如果数据库服务器每天只启动8个小时,而备份间隔为24小时,则备份将每天都进行,这与唯一文件名模式不同)。
可以通过设置FASTDB_BACKUP_NAME环境变量在SubSQL工具中进行备份调度。如果指定了FASTDB_BACKUP_NAME则间隔值依此取定,否则设置为每天。从备份中恢复只需要用一些备份文件替代损坏的数据库文件。
与传统RDBMS的查询相比,因为所有的数据在内存中所以查询的执行是很快的。但fastdb通过应用许多优化措施更加提高了查询执行的速度:使用索引,逆引用和查询并行化。下面几节提供这些优化的详细信息。
第1节. 查询中使用索引
索引是提升RDBMS性能的传统方法。Fastdb使用两种类型的索引:extensiblehash table 和T-tree。第一种对指定了关键字的值的记录的访问速度最快(一般来是常量时间)。而T-tree,是AVL-tree和数组的混合体,在MMRDBMS的角色与B-tree在传统的RDBMS角色是一样的。提供了对数算法复杂度的搜索、插入和删除操作(也就是说,对一个有N个记录的表的搜索/插入/删除的操作的时间是C*log2(N),其中C是某一常量)。T-tree比B-tree更适用于MMDBMS,因为B-tree试图最小化需要装载的页面数目(对于基于磁盘的数据库来说页面装载代价是昂贵的),而T-tree则试图优化比较/移动操作的次数。T-tree最适合于范围操作或者记录有显著的顺序。
fastdb使用简单的规则来应用索引,让程序员来预言什么时候以及哪一个索引将被使用。索引的适用性检查在每一次查询执行期间进行,因此该决策可以依赖于操作数的值来决定。下面的规则说明了fastdb应用索引的算法:
1. 编译好的条件表达式总是从左到右检查
2. 如果最终(topmost)表达式是AND,则尝试在表达式的左半部分使用索引,右半部分作为过滤(filter)
3. 如果最终表达式是OR,则如果左半可以使用索引则使用,然后测试右半使用索引的可能性
4. 此外,当下列条件满足时,则索引适用于表达式
A. 最终表达式是关系操作 (= < > <= >= between like)
B. 操作数的类型是布尔型,数值型,字符串和引用
C. 表达式的右操作数是文本常量或者C++变量,或者
D. 左操作数是记录的索引字段
E. 索引与关系操作兼容
现在我们应当确认“索引与操作兼容”的意思以及在没种情况中使用什么类型的索引,一个哈希表在下列情况下可以使用:
1. 相等=比较;
2. Between操作并且两个端点操作数的值相等;
3. Like操作并且模式串 不包含特别字符(’%’或者’_’)并且没有转义字符(在escape部分指定)。
当hash表不适合并且如下条件满足时,可以使用T-tree:
比较运算( = < > <= >= between)
Like运算并且模式串包含非空前缀(也就是说模式的第一个字符不是’%’或者’_’)
如果用索引来搜索like表达式的前缀,并且其后缀不只是’%’字符,则这个索引搜索操作能够返回的记录比真正匹配模式的记录要多。在这种情况下,我们应当过滤模式匹配的索引搜索的结果。。
如果搜索条件是一些子表达式的析取(用or操作符连接的许多可选项的表达式),则查询的执行可以使用多个索引。为了避免此时的记录重复,在游标中使用位图来标记记录已经选中了。
如果搜索条件需要扫描线型表,在order by子句中包含了定义T-tree索引的单一记录字段,则可以使用T-tree索引。只要排序是一个非常昂贵的操作,使用索引来代替排序显著的减少了查询执行的时间。
使用参数-DDEBUG=DEBUG_TRACE编译fastdb,可以检查查询执行中使用了哪些索引,以及在索引搜索期间所作的许多探测。在这种情况下,fastdb将dump数据库操作性能包括索引的追踪信息。
第2节. 逆引用
逆引用提供了在表之间建立关系的高效并且可靠的方法。Fastdb在插入/更新/删除记录时以及查询优化时使用逆引用的信息。记录之间的关系可以是这些类型:一对一,一对多以及多对多。
1. 一对一的关系用自身以及目标记录的一个引用字段表示。
2. 一对多用自身的一个引用字段及目标表中的一个引用数组字段表示。
3. 多对一用自身的一个引用数组字段以及所引用的表中的记录的一个引用字段表示。
4. 多对多用自身及目标记录中的引用数组字段表示。
当一个声明了关系的记录被插入表中,所有表中的与该记录关联的逆引用,都被更新至指向这个记录。当更新了一个记录并且一个指明了该记录的关系的字段发生变化,则该逆引用自动重构,删除那些不再与该被更新的记录关联的记录对该记录的引用,并且设置包含在该关系中的新记录的的逆引用至该更新的记录。当一个记录被删除,所有逆引用字段中指向其的引用都被删除。
出于效率的原因,fastdb并不保证所有引用的一致性。如果你从表中删除一个记录,在数据库中仍然可能会有指向该记录的引用。访问这些引用将会造成应用程序不可预料的结果甚至数据库崩溃。使用逆引用可以清除这个问题,因为所有的引用都会自动更新从而引用的一致性得以保留。
使用下面的表作为例子:
class Contract;
class Detail {
public:
char const* name;
char const* material;
char const* color;
real4 weight;
dbArray<dbReference<Contract> > contracts;
TYPE_DESCRIPTOR((KEY(name,INDEXED|HASHED),
KEY(material, HASHED),
KEY(color, HASHED),
KEY(weight, INDEXED),
RELATION(contracts, detail)));
};
class Supplier {
public:
char const* company;
char const* location;
bool foreign;
dbArray<dbReference<Contract> > contracts;
TYPE_DESCRIPTOR((KEY(company,INDEXED|HASHED),
KEY(location, HASHED),
FIELD(foreign),
RELATION(contracts, supplier)));
};
class Contract {
public:
dbDateTime delivery;
int4 quantity;
int8 price;
dbReference<Detail> detail;
dbReference<Supplier>supplier;
TYPE_DESCRIPTOR((KEY(delivery,HASHED|INDEXED),
KEY(quantity, INDEXED),
KEY(price, INDEXED),
RELATION(detail, contracts),
RELATION(supplier, contracts)));
};
这个例子中,在表Detail-Contract 和 Supplier-Contract之间存在一对多的关系。当一个Contract记录插入数据库中,仅仅只要把引用detail和supplier设置到Detail和Supplier表的相应记录上。这些记录的逆引用contracts将自动更新。当一个Contract记录被删除时同样:从被引用的Detail和Supplier的记录的contracts字段中自动排除被删除的记录的引用。
此外,使用逆引用可以在查询执行时选择更有效的规划。考虑下面的查询,选择某公司装船的细节:
q = "existsi:(contracts[i].supplier.company=",company,")";
这个查询执行的最直接地方法是扫描Detail表,并用这个条件测试每一条记录。但使用逆引用我们可以选择另一种方法:用指定的公司名在Supplier表中进行记录的索引搜索,然后从表Detail中使用逆引用定位记录,Detail表与所选的supplier记录有传递关系。当然我们要清除重复的记录,这是有可能的因为一个公司可能运送许多不同的货物。这个通过游标对象的位图来实现。由于索引查找明显的快于顺序查找并且通过引用访问是非常快的操作,这个查询的总执行时间比直接方法要短得多。
从1.20版本开始,fastdb支持串联(cascade)删除。如果使用OWNER宏声明一个字段,该记录就被当作是这个层次关系的所有者(owner)。当所有者记录被删除,该关系的所有的成员(从所有者引用的记录)将被自动删除。如果该关系的成员记录要保持对所有者记录的引用,该字段应当用RELATION宏声明。
附1:FastDB不同访问模式带来的影响
FastDB中不同的访问DataBase的模式在程序中能体现不一样的结果。从试验可以知道,不同访问模式主要体现在对表的锁上。(具体的试验,可以自己做一下,用subsql -access [read-only concurrent-read concurrent-update normal]可以指定访问数据的模式,然后自己构建相关测试用例进行测试)。
dbDatabase::dbAllAccess,一旦某个进程使用该模式访问表,如果该进程使用了insert、update、delete等修改数据的操作,其他访问该库的进程的所有操作(包括open、select)都会被阻塞,直到该操作提交或回滚。
dbDatabase::dbConcurrentUpdate,使用该模式访问表,如果某个进程对数据进行修改性的操作,同时另外的进程使用dbDatabase::dbReadOnly或者dbDatabase::dbConcurrentRead读取数据,不会出现阻塞的情况。但是dbDatabase::dbReadOnly会把未提交的脏数据读出来;而dbDatabase::dbConcurrentRead则不会。(dbReadOnly和dbConcurrentUpdate时,必须先打开dbConcurrentUpdate,再打开dbReadOnly,否则dbConcurrentUpdate会打开失败)
如果多个进程都使用dbDatabase::dbConcurrentUpdate,实际效果和dbDatabase::dbAllAccess一样,一旦一个进程修改了数据,其他进程所有的操作(包括open、select)都将阻塞,直到该操作提交或回滚。
结论:FastDB没有提供商用数据库的记录锁甚至是页级锁的机制,锁的范围是一个DataBase文件,所以在日常的使用过程中:
1. 仔细分析业务需求,如果你只需要访问数据,那么最好是使用dbDatabase::dbReadOnly或者是dbDatabase::dbConcurrentRead。这样的话,你的访问操作不会因为这个表被其他进程修改数据而阻塞。
2. 如果一个表有不止一个使用者,那么涉及修改数据的进程应该使用dbDatabase::dbConcurrentUpdate,而只读的进程使用则使用dbDatabase::dbConcurrentRead。
3. 如果表只有一个使用者,则可以根据需要,使用dbDatabase::dbAllAccess或者dbDatabase::dbReadOnly。
4. 所有的数据修改操作,如果有并发要求,一定要及时提交。
附2:FastDB程序崩溃后的锁清理
FastDB在为了保证数据同步,使用了sem作为锁机制,同时还使用了shm作为记录读写操作计数器,所以如果程序异常中止,没有正常的close、commit数据的话,同时访问该库的程序将会被挂死。
在自带的文档中,作者提出如果出现这种情况,把所有使用这个库的程序退出后,重新启动就好了。这个基本能解决问题,在unix/linux环境下,借助fuser把所有使用库的程序停掉即可。
但是,在实际业务中,遇到这种情况,如果把其他程序都停掉是相当不可取的。实际上,作者在database.cpp中有一个watchdog的功 能,只是在默认的情况下,是不编译的。所以,只要修改一下database的编译选项,加上-DAUTO_DETECT_PROCESS_CRASH的话,就能够解决这个问题。
重新编译后,fastdb会自动起来一个线程,来检测异常退出的情况,如果出现异常退出的话,程序将自动将锁清理。