我剛在PS5上買了GTA 6,錢扣了,游戲沒進庫。支付服務忠實地把款劃走了,可游戲的入庫記錄卻沒寫進去——這會兒我的狀態就是:付了錢,但庫是空的。這不是什么玄學,就是典型的“多個操作沒綁在一起”造成的原子性塌方。
事務最核心的價值就是原子性:要么全成,要么全滾蛋。但Spring怎么替你自動搞定這一切?憑啥加個@Transactional就能讓好幾步操作共進退?我把踩過的坑拆成幾條,對著GTA6買游戲的例子掰開聊。
![]()
1. 你寫的代碼根本沒接觸連接池——是代理在替你“裝框”
Spring不會傻到讓你手動 begin/commit/rollback。它給你的 Service bean 外面包了一層代理,你調 purchaseGame() 時,真正先跑的是那個代理。代理在方法執行前開啟事務,等你的邏輯跑完,再根據有沒有拋異常決定提交還是回滾。你寫的 recordPayment 和 addToLibrary 完全不用操心事務管線,代理把臟活全攬了。這就是為什么事務在同一個線程里才生效——代理靠線程綁定連接,跨線程它就撒手不管。
2. 隔離級別:別讓并發請求把你的庫存搞成“薛定諤的限量版”
兩個交易同時搶同一行數據,到底誰讀到的才是準的?沒隔離好的話,臟讀、不可重復讀、幻讀就全來了。Spring 用 @Transactional(isolation=...) 讓你選嚴格程度,從最寬松的 READ_COMMITTED 到更嚴格的 REPEATABLE_READ。原文里購買限量版游戲時,直接在注解里把隔離級別提到了 REPEATABLE_READ,還加了個 5 秒超時,防止一個卡死的事務鎖住熱門行不撒手。代價是啥?并發性能肉眼可見地往下掉。別一上來就無腦 SERIALIZABLE,先搞清楚你的業務能不能容忍偶爾的幻讀再說。
3. 傳播行為:一個 @Transactional 方法叫另一個,究竟該合租還是分家?
這是最讓我當初腦殼疼的問題。purchaseGame() 里調了 recordPayment() 和 addToLibrary(),那這三個操作是窩在一個事務里滾,還是各起爐灶?要是內部方法自己也有 @Transactional,Spring 就要根據 propagation 屬性來判:到底是蹭現有事務,還是另開新事務,甚至干脆不跑事務。買游戲付款和入庫顯然得捆在一起,所以 parent 方法應該用 REQUIRED 把兩個子方法拉進同一個事務。如果 propagation 配成 REQUIRES_NEW,付款單成一個事務提交了,入庫沒成也不會回滾付款——你的錢包就要出殯了。
4. 幾個省心的小提示:readOnly 和 timeout 別當擺設
純查詢方法加上 @Transactional(readOnly=true),給數據源一個優化提示,雖然 Hibernate 這類框架不強制遵守,但起碼可以杜絕無意中的臟寫。像 getGameDetails 這種只讀操作,加這個就跟給車貼了“實習”標一樣——不丟人,反而讓別人更照顧你。而 timeout 則是在競爭激烈的行上給自己買的一道保險:與其讓鎖一直僵著,不如到點拋異常,別讓一個舉棋不定的連接拖死整條鏈路。
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.