最近因为工作原因,从 OCICPP 改为用 OTL 做 Oracle 开发。初时挺诧异的,怎么只有 .h 没有 .cpp?看过才知道原来一个头文件就全做完了。它是对 OCI 的一个封装,可以用 stream 的方式去操作数据库。用起来还是比较简便,但是感觉封装得多了点。虽然 OCI 那些函数也挺复杂的,但至少觉得一切都在自己掌握之中。OTL 这么一封装了之后,有一些实现细节就不得而知了,这就导致了今天发现的一个 bug。
根据 OTL 关于 otl_connect 的说明,其构造函数之一是可以直接用来连接数据库的。虽然正儿八经做这个事情的是 rlogon(db_string),但是对构造函数 otl_connect(connect_str, ...) 的说明是其等于 otl_connect(void) 再加上一个 rlogon(connect_str)。所以,一般就这样写了:
otl_connect* pdb = new otl_connect("...");
然后把 pdb 存在自己的数据库连接池中。如果连接失败,那么会抛出一个 otl_exception 异常。
可是,今天却遇到意外。连接字符串中,tnsname 写对了,但用户名 / 密码没对。于是测试的时候发现,多次连接之后,数据库那边内存爆掉了,ORACLE.EXE 的线程数也多到疯掉。别的客户端全连接不上了,报 ORA-00020 错误。
process 数量多,这倒也在情理之中。可是看了看 session,发现会话其实很少。猜测是什么东西没有释放掉,不是 connect 就是 cursor。查了一下代码,发现没有什么大问题,connect 都是连接池管理,不会无限多下去的。otl_stream 也都是在栈中声明,花括号之后就自动析构了,cursor 也不应该是问题。郁闷中,发现正常的连接反而不会有泄漏发生,会泄漏的都是连接错误的时候。但是连 tnsname 都不对反而就不会了,估计是因为 OCI 那边根本就无法建立一个连接,也就耗不了资源。这下定位到了,就是这个创建连接的代码上有问题。
这段创建连接的代码是这样写的。注意其中捕捉异常的部分:
otl_connect* pdb = NULL; try { pdb = new otl_connect("..."); } catch(otl_exception& e) { …… if (NULL != pdb) delete pdb; pdb = NULL; } |
可以看到,如果连接不成功时没有生成对象,那么应该返回空指针,这样 delete 指令也不会发生。如果有对象生成,那么在异常处理代码中应该已经把这个对象给销毁了,而对象占用的资源应该也已经释放了。但事实就是不同,由于 OTL 在这个构造函数中封装了实现细节,而显然这个实现并不完美,于是有了泄漏。
采用如下的代码,便不存在这个问题了:
otl_connect* pdb = NULL; try { pdb = new otl_connect(); pdb->rlogon("..."); } catch(otl_exception& e) { …… if (NULL != pdb) delete pdb; pdb = NULL; } |
根据 OTL 的说明,这两种建立连接的方法应该没有区别。但事实上 OTL 提供的范例代码中都是采用后一种方法。
追进 OTL 的头文件中去看,应该可以弄明白原委。不过为了完成任务,没有那么多时间了,这个任务只好留到下次再说。大致估计了一下,应该是因为在 new otl_connect(connect_str) 的时候就抛了异常,于是代码跳转到了 catch 处,pdb 根本没得到对象指针的赋值,这样新生成的对象就丢了。建议用 OTL 的各位,在栈中是无所谓啦,但如果要在堆中初始化一个连接,千万不要图省事想用构造函数直接一步到位哦!
后记:
其实,如果构造函数中抛了异常,而对象中存在着自己管理的资源,那么很可能会发生资源泄漏,这对于 C++ 程序员而言几乎可以作为一条准则了。在我后来看到《Effective C++》之后,就知道本文属于其中案例之一。我认为,OTL 就不应该支持在构造函数中进行连接这种方式。你给程序员两条路可走,那每条路都肯定会有人走。如果其中一条有坑,那这条路就没必要对外开放了。