一、背景:
最近在开发过程中遇到一个问题,项目上线后几分钟后就会报错”数据库连接池已满、连接超时”,项目就宕掉了。后来细心的同事发现了可疑的点,操作数据库后没有将连接Close,也没有using连接对象,经过简单的修改后果然问题解决了。
异常如下:
Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.
二、原因:
在不关闭数据库连接的情况下,即便数据库已经返回了操作结果,但连接一直处于占用中没有断开,当操作数次后(Sql Server默认最大100个连接)连接就被全部占满,后续的连接请求会先等待前面占用的连接释放,直到成功建立连接或建立连接超时。
三、探索:
由于我们这个项目使用的ORM框架是Dapper,而我的印象中使用Dapper几乎是不需要我们手动调用的Open()、Close()方法的,自己之前开发的项目运行多年也并没有出现过这个问题,那究竟问题出现在了哪一个环节?
版本bug?不是,无论是更新或是降级问题依然存在!
难道这么多年真是我用错了,没有报错只是巧合?
翻翻官方给出的案例并也并没有发现关于Open()、Close()方法的调用。
1.先来撸代码实际测试一下:
测试发现执行Query<T>方法即使不执行Open()依然可以查询到结果,并且并不影响后续的查询操作,而操作完成后数据库状态处于Closed状态,说明Dapper其内部在进行操作前会自行Open(),操作完成后会自行Close()。经测试正常情况下Execute()方法也并不需要Open()、Close(),且操作也能执行成功。
注:后面将介绍什么情况下需要调用Open()、Close()。
2.那我们在来试一下Open()后的效果:
当Open()后在执行Query<T>方法,执行结束后连接并不会自动关闭!!!
难道这就是导致此次事故的罪魁祸首?翻翻代码,在项目底层封装中,有这样一行代码,果然就是它!!!
经过实际测试发现,处于Closed状态的连接Dapper执行后会自动关闭,而处于Open状态下的连接Dapper执行后需要我们自己来Close。当然,这只是从结果推导出来的结论,那Dapper实际执行中真的是这样吗?
3.接下来就让我们翻翻源码一探究竟吧!
Dapper最核心的代码都在SqlMapper类中,而Query<T>相关方法在其内部都调用的是一个QueryImpl<T>方法。
在我们调用Query<T>方法时Dapper将sql及参数事务等保存到CommandDefinition对象中传递给QueryImpl<T>方法,之后由command.SetupCommand()方法生成IDbCommand对象,再由ExecuteReaderWithFlagsFallback方法执行数据库操作,生成IDataReader的对象。这里有很重要的几行代码如下图(图片太长,省略了部分与本文无关的代码)。
1082行,当连接处于关闭状态时(新连接初始状态,调用Close()方法后也是这个状态)将其打开。
1083行,执行数据库操作,用IDataReader来存储结果。
1084行,连接状态标志位wasClosed置为false
1124行,当wasClosed值为true时关闭连接。
这就很奇怪了,当1084行执行后1124行一定不会被执行,而1124行什么情况下才会被执行呢?既然不是1124行执行的Close(),那到底是谁把连接关闭了呢?
带着这两个疑问,我们继续往下看:
先来说1124行的执行条件,1083行执行成功,1083行执行报错,1084行未被执行,抛出异常被try-finally捕获,此时wasClosed值为true,1124行执行成功,数据库连接关闭。
再来说另一点,答案就藏在1083行的ExecuteReaderWithFlagsFallback方法中,我们看到wasClosed被传递到了这个方法中,那在这个函数内部用它来干什么了呢?
ExecuteReaderWithFlagsFallback中将wasClosed参数传递给了GetBehavior方法,继续往下看。
发现个可疑点CommandBehavior.CloseConnection,看看他是干嘛的,“关闭DataReader对象时,关联的Connection对象也会关闭”,就是他了!!!QueryImpl<T>方法使用的就是DataReader对象来取数据的,执行完毕后第1108行reader.Dispose()将其释放,Connection对象也就随之关闭了!!!
经过阅读、编译、调试源码后发现结论与推断相同。其他查询操作的内部实现原理与此类似,而Execute<T>等的逻辑就比较简单了,这里就不再过多解释了。
4.什么情况下需要我们自己管理连接状态?
事务!通过报错可以看到,创建事务时连接必须处于Open状态。而事务执行完毕后一定要记得Close() !!!
连续操作数据库不执行业务逻辑时也算是一种情况,但也有可能在高并发、数据量大的情况下造成连接池占满的情况,这种情况就留给大家自行测试吧。
四、结论
在使用Dapper时不需要管理连接状态,只有在执行事务时才需要去管理,开启事务前要Open(),事务后要Close(),切记!切记!切记!!!
注:为什么不直接将连接using掉?这个要综合的去分析,跟数据库连接池也有关,看编程的写法,看具体的业务,不在本文的重点范围内。