Spring Boot 2.1.4 + JDBI + HikariCP + PostgreSQL 发生错误后连接未释放到池

Posted

技术标签:

【中文标题】Spring Boot 2.1.4 + JDBI + HikariCP + PostgreSQL 发生错误后连接未释放到池【英文标题】:Connection not released to the pool after an error occured Spring Boot 2.1.4 + JDBI + HikariCP + PostgreSQL 【发布时间】:2020-03-15 00:55:19 【问题描述】:

我有一个包含在 API 中的 Spring Boot 应用程序。它公开了一个端点/api/persons/:id。 在引擎盖下 Spring Boot 带有 Hibernate 和 HikariCP。我的数据库是 PostgreSQL 10。 该应用程序是在 Kotlin 中构建的。

当 API 接收到同时请求时,我发现了一个问题,该应用似乎需要 2 个活动连接才能执行端点调用的操作findpersonById()

池是这样配置的: spring.datasource.hikari.maximumPoolSize=5

当我向/api/persons/:id 发送 5 个并发请求时,从池中获取 5 个连接以执行后面的请求,并且 5 个处于挂起状态。 池最终抛出异常,因为 5 个挂起的连接一直在等待 connectionTimeout 期间,请求也失败了。

我面临的问题是,在那之后 HikariCP 仍然说池中有 5 个活动连接。如果我查看 PostgreSQL 统计信息,所有连接都是空闲的。

仅供参考,如果我同时只发送 4 个请求,一切都按例外进行,它从 5 个活动连接和 3 个待处理开始,所有请求都返回例外结果。

我尝试将 Pool 更改为 Tomcat JDBC,但结果完全相同。

我不明白为什么首先需要 2 个连接。

如果有人知道我做错了什么,我会在这里传递一些代码..

@RestController
@RequestMapping("/api/person")
class PersonResource(private val personService: PersonService,
                     private val personUpdateService: PersonUpdateService,
                     private val identityManagementService: IdentityWithManagementService) : PersonApi 

    @GetMapping("/id")
    override fun findPersonById(@PathVariable id: String): PersonDto? 
        return personService.findFull(id)
    


personService

@Service
class PersonService(private val documentsService: DocumentService,
                    private val postalAddressService: PostalAddressService) : EntityService<Person>(repository) 

    fun findFull(personId: String): PersonDto? 
        return find(personId)?.let  person ->
            PersonDto(
                    person,
                    postalAddressService.findByPersonId(personId).map  it.toDto() ,
                    documentsService.findByPersonId(personId).map  it.toDto() 
            )
        
    

PersonPostgresRepository:

@Repository
class PersonPostgresRepository : AbstractPostgresRepository(), PersonEntityRepository 

    override fun find(id: String): Person? 
        return withHandle<Person?, Exception> 
            it.createQuery(
                    "select * " +
                            "from identiti_person " +
                            "where id = :id")
                    .bind("id", id)
                    .map(PersonRowMapper())
                    .firstOrNull()
        
    

AbstractPostgresRepository

abstract class AbstractPostgresRepository 

    @Autowired
    private lateinit var handleManager: JdbiHandleManager

    @Autowired
    private lateinit var jdbi: Jdbi

    protected fun <R, X : Exception> withHandle(callback: (handle: Handle) -> R): R 
        val handle = handleManager.handle
        return if (handle.isPresent) 
            callback.invoke(handle.get())
         else 
            jdbi.withHandle(HandleCallback<R, X> 
                callback.invoke(it)
            )
        
    

如果你问,JdbiHandleManager

@Component
@Scope("singleton")
class JdbiHandleManager(private val jdbi: Jdbi) 

    private val currentHandle = ThreadLocal<Handle>()

    val handle: Optional<Handle>
        get() = Optional.ofNullable(currentHandle.get())

    internal fun openHandle(): Handle 
        val handle = jdbi.open()
        currentHandle.set(handle)
        return handle
    

    internal fun closeHandle() 
        val handle = currentHandle.get()
        currentHandle.remove()
        handle?.close()
    

JdbiConfig初始化Jdbi:

@Configuration
open class JdbiConfig 

    @Bean
    open fun jdbi(dataSource: DataSource): Jdbi 
        // JDBI wants to control the Connection wrap the datasource in a proxy
        // That is aware of the Spring managed transaction
        val dataSourceProxy = TransactionAwareDataSourceProxy(dataSource)
        val jdbi = Jdbi.create(dataSourceProxy)
        jdbi.installPlugins()

        return jdbi
    

我所有的@Service 都是事务性的,感谢:

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    override fun transactionAttributeSource(): TransactionAttributeSource 
        /*
            Defines an annotation transaction source (the default) which consider that @Service components
            are transactional by default (making the use of @Transactional optional on those classes).
            Note that the class has to be processed by a TransactionInterceptor for that source to be applied,
            this is the responsibility of the auto proxy creator below.
         */
        return object : AnnotationTransactionAttributeSource() 
            override fun findTransactionAttribute(clazz: Class<*>): TransactionAttribute? 
                return if (clazz.getAnnotation(Service::class.java) != null && clazz.getAnnotation(Transactional::class.java) == null) 
                    DefaultTransactionAttribute(TransactionAttribute.PROPAGATION_REQUIRED)
                 else super.findTransactionAttribute(clazz)
            
        

【问题讨论】:

您没有交易。您的服务必须是 @Transactional 才能正常工作。 是的,对不起,我忘了解释他们是一个捕手,它会自动将@Transactionnal 注释添加到我所有的@Service。只是为了确保我在我的服务上添加了注释,但这并没有改变任何东西.. 【参考方案1】:

我没有运行您的代码,但我确信根本问题如下:

您的JdbiHandleManager 是多余的并导致问题(打开了2 个连接)。为什么?因为TransactionAwareDataSourceProxy 已经处理了连接的打开和关闭。当 Spring 遇到“事务性”(通过注解或方面)的方法调用时,会打开连接并绑定到当前线程。

这意味着只使用jdbi.withHandle(...) 就足够了,因为下一个打开连接的调用将返回活动的事务连接,并且对close 的调用被代理,因为 spring 会自行关闭连接。

由于您实现了自己的“管理器”,这种情况发生了两次,一次是在春季,一次是您自己。两个线程本地连接(一个已经包裹在 Handle 中)

这里我的建议是去掉你的自定义代码,完全依赖TransactionAwareDataSourceProxy的正确操作

【讨论】:

谢谢@winson。我忘了更新票,但您准确地描述了解决方案。只需要删除这个 JdbiHandleManagerJdbiSetHandleInterceptor 并让 Spring 处理连接。

以上是关于Spring Boot 2.1.4 + JDBI + HikariCP + PostgreSQL 发生错误后连接未释放到池的主要内容,如果未能解决你的问题,请参考以下文章

我们如何使用 jdbi 在 Dropwizard 中强制资源(控制器)级别的事务?

为啥从 Spring Boot 版本 2.1.4 更改为 2.1.5 会出现未知配置 Maven 错误?

使用 jdbi 迭代 ResultIterable 对象

Playframework 1.2.5 和 JDBI

如何在 JDBI 中动态绑定表名

JDBI DAO 实例可以复用吗?