我们知道TCP连接的代价是比较大的,因此很多时候我们都会使用长连接,对于客户端也就会使用连接池。
而各种客户端实现的方式不尽相同,API的最佳使用方式也不尽相同,如果使用不当则会发生很大的问题。
比如,在每次连接服务端的时候都初始化连接池,每次使用之后释放连接池,那么会导致很可怕的性能问题。
又比如,在每次用完连接池中的连接后没有能把连接归还,那么连接池会认为这个连接在使用,从而创建越来越多的连接。
其实很多时候用户对于类库的使用不当也体现了类库的设计不当。
现在我们来看几个不太好的设计:
1) 某个应用提供了一个Client类下面提供了若干API,比如:
Client client = new Client();
Database db = client.GetDatabase();
db.Open();
db.Add();
但用户不知道Client构造的时候会初始化一个连接池,这样相当于没一次调用创建10个连接,然后废弃。
效率极差,其实Client类提供了静态入口Instance,但是Client没有关闭构造方法,导致了这个问题。
在private构造方法之后,就只能通过Client client = Client.Instance来获取了,避免了这个问题。
2) 还是这个应用,它为Client类提供了Disconnect()方法,开发人员这么使用:
Client client = Client.Instance;
Database db = client.GetDatabase();
db.Open();
db.Add();
client.Disconnect();
而在Disconnect内部其实是释放了连接池,在Add方法中判断如果没有可用连接则创建。
那么这样也就相当于每次都会创建10个连接,性能同样非常差。
其实Client类不应该有Disconnect实例方法。注释了最后一行后问题解决。
(该释放的不释放,不该释放的释放了)
3) 即使这样还有问题,在调用Add方法之后,系统抛出异常,不能对未打开的数据库执行操作。
开发人员很容易想到去调用Open或Connect之类的方法。
但是最后却忘记调用Close()了,由于连接池设置了10000的上线,并且启用了5分钟空闲就回收的机制。
所以系统一开始上线并没有出现问题,但是运行一段时间之后时不时会出现连接池满的问题。
在db.Add()之后调用db.Close()解决问题。
其实,类库的设计人员在在Add方法内部进行连接的获取和回收那多好啊?
即使不知道操作的边界,也应该提供一个IDisposable的Database类。
那么现在就可以回答这些问题了:
1) 连接池的初始化方法应该怎么实现?是静态方法还是实例方法?
宗旨:连接池相对唯一,绝不能每次使用创建,每次用完关闭。
个人认为,不提供连接池的初始方法 > 静态方法 > 实例方法。
因为连接池本来就是一个内部的实现,作为类库的调用放本不应该知道怎么去使用。
在万不得已的情况下,API入口方法也应该以静态方法提供。
因为如果提供了非静态的方法,API的使用者很可能会尝试调用Close之类的Dispose方法去关闭连接池,释放所有连接。
(当然,不应该提供或谨慎提供连接池的Dispose是后话了)
2) 连接池的重置方法应该怎么实现?
个人认为最好不要提供,如果一定要提供那么就在单独的辅助方法中提供,避免误调用。
3) 涉及到操作的方法应该怎么实现?IDisposable?
涉及到操作的方法往往关联了连接的获取和释放。对于使用连接池的实践来说,应该尽早返回,尽量晚获取。
可以实现IDisposable要求开发人员确保连接在用完之后返回。
个人认为最好的方式还是直接提供静态API,在内部包装连接的获取和返回。
避免万一调用者没有Dispose的情况,对于特殊情况比如要求持续使用一个连接可以考虑创建一个IDisposable的OperationScope的类。
作为类库的编写者不但应该在设计类库上尽量考虑调用者的习惯和方便而且也应该在类库文档中提供最佳实践,避免一些悲剧的发生。
随着越来越多的开源服务端出现,开源的客户端也越来越多了,但是其中的实现各不相同,在使用之前还是应该大致了解使用实践,毕竟不是所有的开源客户端的API都像微软那样人性化的。