Spring中@Transactional可防止连接池泄漏

在任何微服务中,精确管理数据库交互对于维护应用程序性能和可靠性至关重要 。通常,我们会在 性能测试时解决数据库连接方面的奇怪问题。

最近, Spring 微服务应用程序的存储库层出现了一个关键问题,异常处理不当导致性能测试期间出现意外故障和服务中断

本文深入探讨了该问题的具体情况,并强调了注释的关键作用@Transactional,它解决了该问题。

Spring 微服务应用程序严重依赖稳定高效的数据库交互,通常通过Java Persistence API (JPA)进行管理。正确管理数据库连接,特别是防止连接泄漏,对于确保这些交互不会对应用程序性能产生负面影响至关重要。

问题背景
在最近一轮的性能测试中,我们的一个基本微服务中出现了一个关键问题,该微服务 被指定 用于发送客户端通信。该服务开始出现重复的网关超时错误。根本问题根源在于我们在存储库层的数据库操作。

对这些超时错误的调查表明存储过程始终失败。失败 是 由传递给过程的无效参数触发的,这引发了存储过程的业务异常。存储库层没有有效地处理这个异常;它冒泡了。下面是存储过程调用的源代码:  

public long createInboxMessage(String notifCode, String acctId, String userId, String s3KeyName,
                               List<Notif> notifList, String attributes, String notifTitle,
                               String notifSubject, String notifPreviewText, String contentType, boolean doNotDelete, boolean isLetter,
                               String groupId) throws EDeliveryException {
    try {
        StoredProcedureQuery query = entityManager.createStoredProcedureQuery("p_create_notification");
        DbUtility.setParameter(query, "v_notif_code", notifCode);
        DbUtility.setParameter(query, "v_user_uuid", userId);
        DbUtility.setNullParameter(query, "v_user_id", Integer.class);
        DbUtility.setParameter(query, "v_acct_id", acctId);
        DbUtility.setParameter(query, "v_message_url", s3KeyName);
        DbUtility.setParameter(query, "v_ecomm_attributes", attributes);
        DbUtility.setParameter(query, "v_notif_title", notifTitle);
        DbUtility.setParameter(query, "v_notif_subject", notifSubject);
        DbUtility.setParameter(query, "v_notif_preview_text", notifPreviewText);
        DbUtility.setParameter(query, "v_content_type", contentType);
        DbUtility.setParameter(query, "v_do_not_delete", doNotDelete);
        DbUtility.setParameter(query, "v_hard_copy_comm", isLetter);
        DbUtility.setParameter(query, "v_group_id", groupId);
        DbUtility.setOutParameter(query, "v_notif_id", BigInteger.class);

        query.execute();
        BigInteger notifId = (BigInteger) query.getOutputParameterValue("v_notif_id");
        return notifId.longValue();
    } catch (PersistenceException ex) {
        logger.error("DbRepository::createInboxMessage - Error creating notification", ex);
        throw new EDeliveryException(ex.getMessage(), ex);
    }
}

问题分析
正如我们的场景所示,当存储过程遇到错误时,产生的异常将从存储库层向上传播到服务层,最后传播到控制器。这种传播是有问题的,导致我们的 API 以非 200 HTTP 状态代码进行响应 - 通常为 500 或 400。在发生几次此类事件后,服务容器达到了无法再处理传入请求的程度,最终导致 502 网关超时错误。这种严重状态反映在我们的监控系统中,Kibana 日志表明了该问题: 

 “HikariPool-1 - 连接不可用,请求在 30000 毫秒后超时。”

问题在于异常处理不当,因为异常在系统层中不断涌现,而没有得到适当的管理。这会阻止将数据库连接释放回连接池,从而导致可用连接耗尽。因此,在耗尽所有连接后,容器无法处理新请求,导致Kibana 日志中报告错误和非 200 HTTP 错误。

解决
为了解决这个问题,我们可以优雅地处理异常:

  • 让 JPA 和 Spring 上下文释放池的连接。
  • 另一种选择是@Transactional对方法使用注释。

下面是带有注释的相同方法:

@Transactional
public long createInboxMessage(String notifCode, String acctId, String userId, String s3KeyName,
                               List<Notif> notifList, String attributes, String notifTitle,
                               String notifSubject, String notifPreviewText, String contentType, boolean doNotDelete, boolean isLetter,
                               String groupId) throws EDeliveryException {
    ………
}


下面方法的实现演示了一种异常处理方法,通过在方法本身中捕获和记录异常,防止异常进一步向堆栈上层传播:

public long createInboxMessage(String notifCode, String acctId, String userId, String s3KeyName,
                               List<Notif> notifList, String attributes, String notifTitle,
                               String notifSubject, String notifPreviewText, String contentType, boolean doNotDelete, boolean isLetter,
                               String loanGroupId) {
    try {
        .......
        query.execute();
        BigInteger notifId = (BigInteger) query.getOutputParameterValue("v_notif_id");
        return notifId.longValue();      
    } catch (PersistenceException ex) {
        logger.error("DbRepository::createInboxMessage - Error creating notification", ex);
    }
    return -1;
}

使用@Transactional
@TransactionalSpring 框架中的注释管理事务边界。它在带注释的方法启动时开始事务,并在方法完成时提交或回滚事务。当发生异常时,@Transactional确保事务回滚,这有助于适当地将数据库连接释放回连接池。

如果没有@Transactional
如果调用存储过程的存储库方法 没有使用@Transactional 注释 ,Spring 不会管理该方法的事务边界。  如果存储过程抛出异常,则必须手动实现事务处理。如果管理不当,可能会导致数据库连接未关闭且未返回到池中,从而导致连接泄漏。

最佳实践

  • 当方法的操作应在事务范围内执行时,应始终使用 @Transactional。这对于涉及存储过程(可修改数据库状态)的操作尤为重要。
  • 确保方法中的异常处理包括适当的事务回滚和关闭任何数据库连接,主要是在不使用 @Transactional 时。

结论
有效的事务管理对于维护使用 JPA 的 Spring 微服务应用程序的健康和性能至关重要。通过使用 @Transactional 注解,我们可以防止连接泄漏,并确保数据库交互不会降低应用程序的性能或稳定性。遵守这些准则可以提高 Spring 微服务的可靠性和效率,为消费应用程序或终端用户提供稳定、响应迅速的服务。