Firebird嵌入式数据库服务使我们可以发布脱离数据库服务器的轻型程序,因此收到广大小型项目开发者的欢迎,但在嵌入式数据库服务器给大家带来便捷的同时,一些问题也会困扰我们,比如,只能单线程访问数据库文件,数据库效率低下等。
而最近的一个嵌入式数据库项目的问题,却让我寝食难安,经过反复验证,最终找到了原因。
起因:
最近的一个由Delphi语言和.NET语言共同开发的项目使用到了Firebird数据库,而为了方便起见,开发过程中使用的是Firebird的Server模式,没有出现任何问题。而前几天对各子系统进行集成的时候,我将数据库切换到了嵌入式版本,恶梦开始。
首先是ASP.NET下,当可执行程序放到含有中文的目录中时,会出现找不到数据库的问题,这个问题随后得到解决(见前文:修正Firebird Net Provider 1.7中文路径BUG(提供下载) ),而紧接着,更致命的问题出现了:程序关闭时,进程却无法退出,虽然界面已经退出,但在任务管理器中却还可以看到其占用内存,必须手动结束。而且无论是Delphi还是.NET工程,均有此问题。
一叶障目:
问题出现之后,首先想到的是嵌入式服务器的原因,因此,我使用Delphi建立了简单的测试项目,使用TIBDataBase来连接嵌入式数据库,但,经测试,一切正常,程序可以正常退出,因此排除了嵌入式服务器的原因。(而就是这一结果,造成了之后几天的努力都是与答案背道而驰)。
由于我在项目中使用到了自己写的一个简单数据层,因此,我又把目光集中在了这个层上,当然,首先想到的就是内存泄漏和连接泄漏。而由于嵌入式数据库服务器使用到了线程自管理,那么出现连接泄漏的可能性最大。
与之前测试项目不同的地方在于,数据层使用了动态创建的数据库连接组件,很容易造成这两种泄漏,而且在数据连接上使用了自定义连接池来处理不同字符串创建的连接。
经过仔细查验,我似乎找到了问题所在。为了提高效率和数据的一致性,我在连接池中保存的连接,默认情况下会保持连接状态(因为嵌入式数据库连接唯一的特殊性,也让我对此方法没有疑虑),直到程序退出的时候,连接会被关闭。
对于自定义的数据库组件连接池,我使用了Delphi 2006最新引进的class var关键字来定义静态成员,而这个成员连接了一个基于TstringList的对象来保存和管理数据库连接,该对象会在析构函数中关闭所有的连接并释放组件对象。
而问题似乎恰恰出现在这个析构函数的执行上。使用debug信息来跟踪代码执行,发现,该析构函数并未发出实现设置好的debug信息,由此可以初步断定,就是连接泄漏造成了Firebird嵌入式数据库服务器无法正常结束线程,造成始终占用内存的问题。
对此,我在每个数据层的对象中都显式的设置为,对象销毁即关闭连接,并在程序退出的时候,手动清理连接池,折衷的解决了该问题。问题解决的结论是:Delphi的静态数据管理在某种情况下存在连接泄漏问题(似乎原因很牵强),但故事仍然继续…
迷雾尚存:
Delphi下的问题解决了,但ASP.NET下同样问题的迷雾却始终没有消散。ASP.NET的模块,我使用了Cassini嵌入式Web服务器,使用一个winform程序来提供Web服务,由于项目使用了NHibernate数据层,且.NET环境无法直接手动释放一个对象,因此该问题一直没有有效解决。
我已经在代码中显式close了session,而且将session设置为null,甚至手动调用了GC来强行收回内存,但关闭的服务器仍然阴魂不散。
峰回路转:
为了进一步验证和探查Delphi对于静态数据类型的管理机制,我专门写了一个程序来验证。考虑到程序在结束并清理内存的时候,不一定能够传出debug信息(在编译器级别,如果内存的释放是由执行环境控制,则上层debug信息无法被执行),因此我使用了文件独占检测模式,即,在程序运行时独占一个文件,并在静态对象释放的时候(注意不是手动显式释放)释放该文件。当关闭程序之后,再验证该文件释放被正确释放。其关键代码如下:
interface
uses Classes, SysUtils, ucTDataLayer, DB, ADODB, ib, IBDatabase;
type
TWillFreeClass = class(TObject)
private
Fconn1: TADOConnection;
Fconn2: TIBDatabase;
FTmpFile: Integer;
public
constructor Create;
destructor Destroy; override;
end;
TStaticClass
= class(TObject)private
class var
FWillFreeClass: TWillFreeClass;
public
class procedure executeClassMethod;
end;
var
gacWillFreeClass: TWillFreeClass;
implementation
uses ucTConsoleDebuger;
constructor TWillFreeClass.Create;
begin
FTmpFile :
Fconn1 := TADOConnection.Create(nil);
Fconn1.ConnectionString := ‘Provider=Microsoft.Jet.OLEDB.4.0;Data Source=E: est.mdb;Persist Security Info=False’;
Fconn1.LoginPrompt := false;
Fconn1.Open;
Fconn2 := TIBDatabase.Create(nil);
Fconn2.LoginPrompt := false;
Fconn2.Params.Text := ‘user_name=SYSDBA’+#10#13+’password=masterkey’+#10#13+’lc_ctype=GB_2312′;
Fconn2.DatabaseName := ‘E:developcodeClassVarTestBIDDB.FDB’;
Fconn2.Open;
end;
destructor TWillFreeClass.Destroy;
begin
Fconn1.Close;
Fconn1.Free;
Fconn2.Close;
Fconn2.Free;
inherited;
end; class procedure TStaticClass.executeClassMethod;
begin
if not Assigned(FWillFreeClass) then
begin
FWillFreeClass := TWillFreeClass.Create;
end;
end; end.
经过验证,使用fileopen函数独占的文件,无论是否在静态对象析构函数中释放独占,程序关闭之后都会正确释放该文件。同样Access数据库,无论是否在静态对象析构函数中释放独占,程序关闭之后都会正确释放数据库文件,只是会留下*. ldb文件。而对于TIBDatabase控件连接firebird嵌入式数据库,则再次出现“阴魂不散”的问题。难道,是TIBDatabase存在连接泄漏吗?为了验证这一点,我打开了Firebird Server模式,并跟踪数据库连接数,测试发现,虽然没有手动释放TIBDatabase,它却在程序退出之后正确的还回了数据库连接。
那么很明显,是嵌入式数据库版本的问题,由于在解决“可执行程序放到含有中文的目录中时,会出现找不到数据库”的问题的时候,我看到新的Firbird数据库版本2.1.0发布了,因此就顺便更新了嵌入式数据库的版本,而随后的Delphi工程问题的顺利解决,使我进一步忽略了这个动作,当然恰恰就是那个看似正确的结论让我一周寝食难安。
我将2.1.0版本换回2.0.1版本,问题顺利解决。
结论:
1、 对于问题的排查和验证,需要在同一环境下一一测试,每次仅更换一种配置,以达到最高的可能性覆盖率。
2、 解决问题过程需记录下每一次操作,以备后续排查工作。
3、 不迷信任何程序。