我刚在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.