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
invokeWithinTransaction(method, targetClass, invocation)ĐọcTransactionAttributevà chọn transaction manager.createTransactionIfNecessary()Đặt tên joinpoint rồi chuẩn bị transaction status cho scope hiện tại.tm.getTransaction(txAttr)Propagation được quyết định tại transaction manager, không phải trong annotation processor.invocation.proceedWithInvocation()Chạy interceptor tiếp theo rồi target method.- THROW:
completeTransactionAfterThrowing()Rollback hoặc commit theo rule; participant có thể đánh dấu transaction chung. - RETURN:
commitTransactionAfterReturning()GọitransactionManager.commit(status)sau khi khôi phụcTransactionInfotrước đó.
Mức 2 - zoom quyết định propagation
AbstractPlatformTransactionManager
getTransaction() và handleExistingTransaction()
doGetTransaction()Lấy transaction object đặc thù của manager cho thread hiện tại.isExistingTransaction(transaction)Nếu chưa có,REQUIREDvàREQUIRES_NEWđều đistartTransaction().- Đã có:
handleExistingTransaction()Đây là điểm hai propagation tách nhánh. REQUIRED: regular participationprepareTransactionStatus()nhậnnewTransaction=false; logical scope mới tham gia physical transaction hiện có.REQUIRES_NEW:suspend(transaction)Cất resource và synchronization của outer rồi gọistartTransaction()cho inner.- Inner hoàn tất:
cleanupAfterCompletion()Nếu có suspended resources, gọiresume()để 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 activeConnectionHolderParticipant không gọidoBegin(), 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 outerConnectionHolderConnection 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_SUPPORTEDhoặ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
@Transactionalmở một transaction mớiREQUIREDtham 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.