Spring中@Transactional可防止連線池洩漏

banq發表於2024-05-08

在任何微服務中,精確管理資料庫互動對於維護應用程式效能和可靠性至關重要 。通常,我們會在 效能測試時解決資料庫連線方面的奇怪問題。

最近, 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(<font>"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(<font>"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 微服務的可靠性和效率,為消費應用程式或終端使用者提供穩定、響應迅速的服務。
 

相關文章