SPRING SERIES - LỖI IM LẶNG

Chiến Lược Mượn Connection

PROXY - XANH = TẦNG PHÓNG TO Đọc từ trái sang phải. Màu đậm dần nghĩa là gần cơ chế hơn. Dấu * đánh dấu điểm bài viết khai thác.

REQUIRED - chết lây

Hai logical scope dùng chung một physical transaction; inner rollback-only làm outer commit thất bại.

Cùng một câu hỏiKhi đã có transaction, scope mới tham gia, treo nó để mở transaction khác, hay chọn hành vi khác?

REQUIRES_NEW - cạn pool

Outer giữ tài nguyên khi bị treo; inner cần transaction độc lập và có thể phải chờ Connection thứ hai.

Mức 1 - around advice đồng bộ

TransactionInterceptor.invoke()

Đường chuẩn với PlatformTransactionManager

  1. invokeWithinTransaction(method, targetClass, invocation)Đọc TransactionAttribute và chọn transaction manager.
  2. createTransactionIfNecessary()Đặt tên joinpoint rồi chuẩn bị transaction status cho scope hiện tại.
  3. tm.getTransaction(txAttr)Propagation được quyết định tại transaction manager, không phải trong annotation processor.
  4. invocation.proceedWithInvocation()Chạy interceptor tiếp theo rồi target method.
  5. THROW: completeTransactionAfterThrowing()Rollback hoặc commit theo rule; participant có thể đánh dấu transaction chung.
  6. RETURN: commitTransactionAfterReturning()Gọi transactionManager.commit(status) sau khi khôi phục TransactionInfo trước đó.
Mức 2 - zoom quyết định propagation

AbstractPlatformTransactionManager

getTransaction()handleExistingTransaction()

  1. doGetTransaction()Lấy transaction object đặc thù của manager cho thread hiện tại.
  2. isExistingTransaction(transaction)Nếu chưa có, REQUIREDREQUIRES_NEW đều đi startTransaction().
  3. Đã có: handleExistingTransaction()Đây là điểm hai propagation tách nhánh.
  4. REQUIRED: regular participationprepareTransactionStatus() nhận newTransaction=false; logical scope mới tham gia physical transaction hiện có.
  5. REQUIRES_NEW: suspend(transaction)Cất resource và synchronization của outer rồi gọi startTransaction() cho inner.
  6. Inner hoàn tất: cleanupAfterCompletion()Nếu có suspended resources, gọi resume() để khôi phục outer.
Mức 3 - zoom JDBC resource

DataSourceTransactionManager

ConnectionHolder được bind theo thread

Nhánh REQUIRED - dùng resource đang bind

  • isExistingTransaction() thấy active ConnectionHolderParticipant không gọi doBegin(), nên không xin Connection mới.
  • Inner rollback đánh dấu holder chungOuter commit thấy global rollback-only và có thể ném UnexpectedRollbackException, đúng đường đã chốt ở 2.1.

Nhánh REQUIRES_NEW - thay resource đang bind

  • doSuspend(): unbind outer ConnectionHolderConnection outer vẫn đang mở; thao tác suspend không release nó về pool.
  • doBegin(): obtainDataSource().getConnection()Acquire Connection cho inner và bind holder mới vào thread.
  • doCleanupAfterCompletion()Unbind, reset và release Connection của inner.
  • resume() -> doResume()Bind lại holder của outer để caller chạy tiếp.
  • Outer giữ 1, inner chờ thêm 1Nếu mọi worker cùng giữ outer Connection và pool không còn suất cho inner, việc acquire có thể timeout hoặc tạo thế chờ vòng.

Phép kiểm: breakpoint tại handleExistingTransaction(), xem status.isNewTransaction(); với REQUIRES_NEW, theo tiếp doSuspend(), doBegin() và metric hikaricp.connections.pending.

Giải pháp an toàn

  • Hỏi lại contract của innerNếu không cần transaction, cân nhắc NOT_SUPPORTED hoặc chạy sau commit; nếu vẫn truy cập DB, nó vẫn cần Connection cho thao tác đó.
  • Nếu thật sự cần REQUIRES_NEWGiữ inner ngắn; định cỡ theo số outer đang giữ cộng số inner có thể đồng thời xin thêm, rồi đo pending/timeout dưới tải.

Khi KHÔNG được kết luận

  • Mỗi @Transactional mở một transaction mớiREQUIRED tham gia transaction hiện có; self-invocation còn có thể không đi qua proxy.
  • Pool luôn cần đúng concurrency * 2Đó chỉ là cận của một kịch bản lồng một cấp; tải, fan-out, manager và thời gian chồng lấn quyết định nhu cầu thật.