问题
前几天,我负责的一个应用,有同事反馈说数据插入失败了,于是去线上查看日志,发现有如下情况:(下面放的是我复现的图) 如下图所示(该应用中的数据异常是时间异常): 后面通过与同事多次实验后,总结规律为(注:在同一sql,未分库分表的情况下):
- 正常数据插入成功(硬性条件)
- 异常数据插入失败(Exception)
- 数据(正常/异常)插入,报错no opration allowed after statement closed,如果是一条正常的数据插入,而此时插入失败,就出现了数据丢失问题。(TIMESTAMP值不能早于1970或晚于2037)
我们先看一下该信息是从哪里报出来的:
结合上图和下图知,执行SQL的Statement连接断开了。
Statement.49=No operations allowed after statement closed.
深入Druid探索原由
GitHub 1.0.29中没有关于这个bug的修复描述,这样看来,其实对Druid开发者来说是个小bug。
在整个过程中,主要问题在于对Druid不熟悉,平时接触的不多,导致花了挺多时间去了解其中的一些概念与原理。
概念介绍
在深入之前,我们先来了解几个概念:
- prepareStatementPool(LRUCache):key为 sql及其一些属性(如参数类型)组成的对象,value为prepareStatementHolder。可以减少PS的编译次数。PreparedStatementCache即用于保存与数据库交互的prepareStatement对象。在cache里的ps对象,不需要重新走一次DBMS连接请求去创建。
我们看一下这三者的关系:
大体流程:PS初始化和close过程
在这之中,有几个关键的地方:
- 从ConnectionHolder中获取holder的过程
- 根据PSHolder进行PS初始化的时候,inUseCount属性加1
- 在执行sql过程中,如果出现了异常,则会将exceptionCount+1作为标识
- 如果未进行close的过程,PSHolder中的inUseCount仍为1,则下一次无法从PSCache中获取holder。
- 如果进行close过程,PSHolder中的inUseCount-1,下一次仍可以从PSCache中获取holder。如果未出现异常,会把cache中对应的holder替换掉,出现异常则把Statement的connection置为空。
接下来我们来看一下,其中关键的一些源码。
prepareStatement():初始化PS
这个过程主要就是从缓存中获取PSHolder,然后判断是否进行新建PSHolder,再根据PSHolder进行初始化,新建一个PS的过程。
注意:开头中的checkClose的过程在initStatement中,initStatement只是对PSHolder对象做操作,并未对PS操作,此时还未开始进行新建PS。
我们看一下get里面的过程:
其实就是通过sql组成的key从缓存中获取holder,如果holder的inUseCount>0说明还在使用,则会返回为空;否则返回该holder。
closeStatement():关闭Statement
从代码中,我们可以很容易知道,如果Statement为空的话,就不进行close的过程了。
接下来,我们看看close中到底干了什么:
即将PSHolder中的inUseCount-1,然后根据statement是否执行sql出现了异常进行判断,是否将原来缓存中对应的PSHolder替换。
我们再来看一下closeInternal中发生了什么:
即做一些正常的关闭流程,重点在stmt.close中。
在stmt.close过程中,最终会进入到StatementImpl.realClose()中:
此时debug的时候发现,经过this.connection=null语句后,将PS置为异常,可以猜测应该是有监听器监听到将connection置为空后将PS置为异常的。
此时,缓存中放的PSHolder中存在的PS,其connection属性为空,且为异常。
而结合开头报错处,可知是获取到了缓存中的connection=null的PS,导致出现了bug。
根据代码与实践总结
接下来,我们了解了整个过程后,进行总结看为什么会有开头的规律:
- 第一次正常数据插入:
- 新建PSHolder,初始化inUseCount+1,SQL正常执行,PS关闭时,PS存在,inUseCount-1,将该PSHolder放入缓存。
- 第二次异常数据插入:
- 由于SQL一样,且缓存中对应PSHolder的inUseCount=0,故可从缓存中获取PSHolder,初始化inUseCount+1,SQL执行异常,exceptionCount+1,PS关闭时,PS存在,inUseCount-1,由于exceptionCount>0,故无法放入缓存,进行异常PS关闭流程,过程中将connection置为空。且由于是从缓存中获取的PSHolder,故缓存中的PSHolder其PS的connection为空。
- 第三次正常/异常数据插入:
- 由于SQL一样,且缓存中对应PSHolder的inUseCount=0,故可从缓存中获取PSHolder,inUseCount+1,初始化过程中,判断到该PSHolder中其PS的connection为空,抛出异常,不执行下面流程。进行关闭时,因为还未初始化完成PS就抛错了,没有PS,故而不需要进行关闭流程,而此时缓存中PSHolder中的inUseCount仍为1。
- 第四次正常/异常数据插入:
- 正常:由于SQL一样,且缓存中对应PSHolder的inUseCount=1,故无法从缓存中获取PSHolder,故新建PSHolder,初始化inUseCount+1,SQL正常执行,PS关闭时,PS存在,inUseCount-1,将该PSHolder替换缓存中对应SQL的PSHolder。
- 异常:由于SQL一样,且缓存中对应PSHolder的inUseCount=1,故无法从缓存中获取PSHolder,故新建PSHolder,初始化inUseCount+1,SQL执行异常,PS关闭时,PS存在,inUseCount-1,进入异常关闭流程,缓存SQL对应的PSHolder仍未改变。
druid 1.0.29中是如何修复的?
其实很简单,就是将发生了异常的PSHolder给从缓存中给移除了。
总结
从上述以及实践可知,这其实是Druid1.0.28及以下版本的bug,推荐升级到1.0.28以上版本。
如果使用Druid时开启了PSCache,则不推荐使用1.0.28及以下版本(1.0.27版本也会出现该bug),会出现数据丢失的问题。
在本文中,主要通过先让大家对整个PS的开启、关闭处理流程有个了解,再结合实际情况进行总结推论为何会出现那么有意思问题,尽管这个bug在Druid1.0.29修复中并未提及,但是这个过程还是非常让我享受的。
|