问题

前几天,我负责的一个应用,有同事反馈说数据插入失败了,于是去线上查看日志,发现有如下情况:(下面放的是我复现的图)
如下图所示(该应用中的数据异常是时间异常):
在这里插入图片描述
在这里插入图片描述
后面通过与同事多次实验后,总结规律为(注:在同一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不熟悉,平时接触的不多,导致花了挺多时间去了解其中的一些概念与原理。

概念介绍

在深入之前,我们先来了解几个概念:

  • PrepareStatement:是java.sql包中的接口,表示预编译的 SQL 语句的对象,继承了Statement,SQL 语句被预编译并存储在PreparedStatement对象中。相比Statement可以防注入、多次使用时执行速度更快(预编译)等优点。

  • DruidConnectionHolder: 包含了connection中的一些属性

在这里插入图片描述

  • prepareStatementPool(LRUCache):key为 sql及其一些属性(如参数类型)组成的对象,value为prepareStatementHolder。可以减少PS的编译次数。PreparedStatementCache即用于保存与数据库交互的prepareStatement对象。在cache里的ps对象,不需要重新走一次DBMS连接请求去创建。

在这里插入图片描述

  • prepareStatementHolder:

在这里插入图片描述

我们看一下这三者的关系:

在这里插入图片描述

大体流程: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修复中并未提及,但是这个过程还是非常让我享受的。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐