> 技术文档 > 你的代码“炸”了吗?深度剖析为何 TransactionProcessingService 必须加悲观锁!✨

你的代码“炸”了吗?深度剖析为何 TransactionProcessingService 必须加悲观锁!✨


你的代码“炸”了吗?💣 深度剖析为何 TransactionProcessingService 必须加悲观锁!

大家好!今天我们不聊宽泛的理论,而是直接深入一段真实的代码——TransactionProcessingService。这段代码处理的是一个非常典型的业务场景:在一个原子操作中,既要核销用户的优惠券,又要抵扣积分 这是一个涉及用户“真金白银”的核心流程,如果出现任何差错,后果不堪设想。😱

你可能会问:“我已经用了 @Transactional,为什么还不够?为什么非要加上那把看起来很‘重’的悲观锁?”

别急,这篇博客将带你一步步揭开谜底,让你彻底明白,在这段代码中,悲观锁不是“可选项”,而是“必需品”!🛡️

快速概览:为什么你的代码需要悲观锁?📝

核心操作 (Core Operation) 并发风险 (Concurrency Risk) 无锁的后果 (Consequence without Lock) 解决方案 (Solution) 使用优惠券 (UserCoupon) “读-改-写”冲突:多个请求同时读取到同一张“未使用”的优惠券,并各自将其状态更新为“已使用”。 优惠券超用:一张本应只能使用一次的优惠券,被核销了多次,导致公司资产损失。 悲观锁 (PESSIMISTIC_WRITE): 在读取 UserCoupon 时加锁,确保只有一个事务能操作它。 抵扣积分 (MemberUser) “读-改-写”冲突:多个请求同时读取用户当前的积分余额,然后各自计算并扣除,最后更新回数据库。 积分超扣/数据错乱:用户的积分可能被扣成负数,或者最终的积分余额与实际流水不符。 悲观锁 (PESSIMISTIC_WRITE): 在读取和更新 MemberUser 的积分时加锁,保证积分计算和更新的原子性。 整个交易流程 原子性问题:如果券用了,但扣积分时系统崩溃,会导致数据不一致。 数据不一致:用户资产状态混乱,难以追溯和修复。 数据库事务 (@Transactional): 将整个流程包裹在事务中,保证所有操作要么全部成功,要么全部失败回滚。

结论: @Transactional 保证了流程的原子性,但无法阻止并发事务同时读取旧数据。悲观锁正是为了解决这个“同时读取”的问题,它确保在“读-改-写”的整个周期内,数据行被独占。

代码剖析:并发的“魔鬼”藏在哪里?😈

让我们回顾一下 TransactionProcessingService 的简化逻辑:

@Transactionalpublic TransactionProcessResultDto processTransactionByAdmin(...) { // 1. 加载用户 MemberUser memberUser = memberUserRepository.findById(...); // 2. 处理优惠券 (如果提供了) if (payload.getUserCouponId() != null) { // --- 风险点 A: 读-改-写 UserCoupon --- // 读: 查询优惠券状态是否为“未使用” UserCoupon userCoupon = userCouponRepository.findById(...); validateUserCouponForUsage(userCoupon, ...); // 改: 计算优惠,更新应付金额 // 写: 将优惠券状态更新为“已使用” userCoupon.setStatus(USER_COUPON_STATUS_USED); userCouponRepository.save(userCoupon); } // 3. 处理积分抵扣 (如果提供了) if (payload.getPointsToUse() > 0) { // --- 风险点 B: 读-改-写 MemberUser --- // 读: 查询用户可用积分 MemberUser freshMemberUser = memberUserRepository.findById(...); long availablePoints = freshMemberUser.getTotalPoints() - freshMemberUser.getUsedPoints(); // ... 校验积分 ... // 改: 计算抵扣金额 // 写: 更新用户的 usedPoints freshMemberUser.setUsedPoints(...); memberUserRepository.save(freshMemberUser); // 写: 创建积分交易流水 pointTransactionRepository.save(...); } return result;}

风险点 A (优惠券超用)风险点 B (积分超扣) 就是并发问题的重灾区。

可视化分析:无锁世界的混乱 🌪️

流程图:并发下的“读-改-写”冲突

这个流程图展示了两个事务(A和B)在没有锁的情况下,如何同时进入“读-改-写”的关键区域。

#mermaid-svg-RRzQolTa9R7ZBIVN {font-family:Arial,sans-serif;font-size:14px;fill:#34495E;}#mermaid-svg-RRzQolTa9R7ZBIVN .error-icon{fill:hsl(185.6375838926, 63.4042553191%, 51.0784313725%);}#mermaid-svg-RRzQolTa9R7ZBIVN .error-text{fill:rgb(203.8468085108, 60.5170212767, 45.6531914895);stroke:rgb(203.8468085108, 60.5170212767, 45.6531914895);}#mermaid-svg-RRzQolTa9R7ZBIVN .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-RRzQolTa9R7ZBIVN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RRzQolTa9R7ZBIVN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RRzQolTa9R7ZBIVN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RRzQolTa9R7ZBIVN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RRzQolTa9R7ZBIVN .marker{fill:#5D6D7E;stroke:#5D6D7E;}#mermaid-svg-RRzQolTa9R7ZBIVN .marker.cross{stroke:#5D6D7E;}#mermaid-svg-RRzQolTa9R7ZBIVN svg{font-family:Arial,sans-serif;font-size:14px;}#mermaid-svg-RRzQolTa9R7ZBIVN .label{font-family:Arial,sans-serif;color:#FFFFFF;}#mermaid-svg-RRzQolTa9R7ZBIVN .cluster-label text{fill:rgb(203.8468085108, 60.5170212767, 45.6531914895);}#mermaid-svg-RRzQolTa9R7ZBIVN .cluster-label span{color:rgb(203.8468085108, 60.5170212767, 45.6531914895);}#mermaid-svg-RRzQolTa9R7ZBIVN .label text,#mermaid-svg-RRzQolTa9R7ZBIVN span{fill:#FFFFFF;color:#FFFFFF;}#mermaid-svg-RRzQolTa9R7ZBIVN .node rect,#mermaid-svg-RRzQolTa9R7ZBIVN .node circle,#mermaid-svg-RRzQolTa9R7ZBIVN .node ellipse,#mermaid-svg-RRzQolTa9R7ZBIVN .node polygon,#mermaid-svg-RRzQolTa9R7ZBIVN .node path{fill:#C0392B;stroke:#922B21;stroke-width:1px;}#mermaid-svg-RRzQolTa9R7ZBIVN .node .label{text-align:center;}#mermaid-svg-RRzQolTa9R7ZBIVN .node.clickable{cursor:pointer;}#mermaid-svg-RRzQolTa9R7ZBIVN .arrowheadPath{fill:undefined;}#mermaid-svg-RRzQolTa9R7ZBIVN .edgePath .path{stroke:#5D6D7E;stroke-width:2.0px;}#mermaid-svg-RRzQolTa9R7ZBIVN .flowchart-link{stroke:#5D6D7E;fill:none;}#mermaid-svg-RRzQolTa9R7ZBIVN .edgeLabel{background-color:hsl(-114.3624161074, 63.4042553191%, 46.0784313725%);text-align:center;}#mermaid-svg-RRzQolTa9R7ZBIVN .edgeLabel rect{opacity:0.5;background-color:hsl(-114.3624161074, 63.4042553191%, 46.0784313725%);fill:hsl(-114.3624161074, 63.4042553191%, 46.0784313725%);}#mermaid-svg-RRzQolTa9R7ZBIVN .cluster rect{fill:hsl(185.6375838926, 63.4042553191%, 51.0784313725%);stroke:hsl(185.6375838926, 23.4042553191%, 41.0784313725%);stroke-width:1px;}#mermaid-svg-RRzQolTa9R7ZBIVN .cluster text{fill:rgb(203.8468085108, 60.5170212767, 45.6531914895);}#mermaid-svg-RRzQolTa9R7ZBIVN .cluster span{color:rgb(203.8468085108, 60.5170212767, 45.6531914895);}#mermaid-svg-RRzQolTa9R7ZBIVN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:Arial,sans-serif;font-size:12px;background:hsl(185.6375838926, 63.4042553191%, 51.0784313725%);border:1px solid undefined;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RRzQolTa9R7ZBIVN :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}#mermaid-svg-RRzQolTa9R7ZBIVN .start>*{fill:#2ECC71!important;stroke:#28B463!important;color:white!important;}#mermaid-svg-RRzQolTa9R7ZBIVN .start span{fill:#2ECC71!important;stroke:#28B463!important;color:white!important;}#mermaid-svg-RRzQolTa9R7ZBIVN .process>*{fill:#3498DB!important;stroke:#2980B9!important;color:white!important;}#mermaid-svg-RRzQolTa9R7ZBIVN .process span{fill:#3498DB!important;stroke:#2980B9!important;color:white!important;}#mermaid-svg-RRzQolTa9R7ZBIVN .critical>*{fill:#E74C3C!important;stroke:#C0392B!important;color:white!important;}#mermaid-svg-RRzQolTa9R7ZBIVN .critical span{fill:#E74C3C!important;stroke:#C0392B!important;color:white!important;}#mermaid-svg-RRzQolTa9R7ZBIVN .data>*{fill:#F1C40F!important;stroke:#F39C12!important;color:#333!important;}#mermaid-svg-RRzQolTa9R7ZBIVN .data span{fill:#F1C40F!important;stroke:#F39C12!important;color:#333!important;}#mermaid-svg-RRzQolTa9R7ZBIVN .ending>*{fill:#95A5A6!important;stroke:#7F8C8D!important;color:white!important;}#mermaid-svg-RRzQolTa9R7ZBIVN .ending span{fill:#95A5A6!important;stroke:#7F8C8D!important;color:white!important;}#mermaid-svg-RRzQolTa9R7ZBIVN .noteStyle>*{fill:#FEF9E7!important;stroke:#F7DC6F!important;stroke-width:1px!important;color:#7D6608!important;}#mermaid-svg-RRzQolTa9R7ZBIVN .noteStyle span{fill:#FEF9E7!important;stroke:#F7DC6F!important;stroke-width:1px!important;color:#7D6608!important;}事务 B事务 A几乎同时读取数据
(例如:库存=10)开始在内存中计算
(10 - 2 = 8)写入新值 \'8\' 到数据库结束读取数据
(例如:库存=10)开始在内存中计算
(10 - 1 = 9)写入新值 \'9\' 到数据库结束问题:事务A的更新被覆盖了!
最终库存是8,而不是7。
这就是\'丢失更新\'问题。

时序图:积分超扣的“完美犯罪”

这个时序图生动地展示了两个请求如何让用户的积分被错误地扣减。

#mermaid-svg-DjzasCTaaixqm8Kd {font-family:Arial,sans-serif;font-size:14px;fill:#34495E;}#mermaid-svg-DjzasCTaaixqm8Kd .error-icon{fill:hsl(185.6375838926, 63.4042553191%, 51.0784313725%);}#mermaid-svg-DjzasCTaaixqm8Kd .error-text{fill:rgb(203.8468085108, 60.5170212767, 45.6531914895);stroke:rgb(203.8468085108, 60.5170212767, 45.6531914895);}#mermaid-svg-DjzasCTaaixqm8Kd .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-DjzasCTaaixqm8Kd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DjzasCTaaixqm8Kd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DjzasCTaaixqm8Kd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DjzasCTaaixqm8Kd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DjzasCTaaixqm8Kd .marker{fill:#5D6D7E;stroke:#5D6D7E;}#mermaid-svg-DjzasCTaaixqm8Kd .marker.cross{stroke:#5D6D7E;}#mermaid-svg-DjzasCTaaixqm8Kd svg{font-family:Arial,sans-serif;font-size:14px;}#mermaid-svg-DjzasCTaaixqm8Kd .actor{stroke:#922B21;fill:#C0392B;}#mermaid-svg-DjzasCTaaixqm8Kd text.actor>tspan{fill:#FFFFFF;stroke:none;}#mermaid-svg-DjzasCTaaixqm8Kd .actor-line{stroke:grey;}#mermaid-svg-DjzasCTaaixqm8Kd .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#34495E;}#mermaid-svg-DjzasCTaaixqm8Kd .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#34495E;}#mermaid-svg-DjzasCTaaixqm8Kd #arrowhead path{fill:#34495E;stroke:#34495E;}#mermaid-svg-DjzasCTaaixqm8Kd .sequenceNumber{fill:#a29281;}#mermaid-svg-DjzasCTaaixqm8Kd #sequencenumber{fill:#34495E;}#mermaid-svg-DjzasCTaaixqm8Kd #crosshead path{fill:#34495E;stroke:#34495E;}#mermaid-svg-DjzasCTaaixqm8Kd .messageText{fill:#34495E;stroke:#34495E;}#mermaid-svg-DjzasCTaaixqm8Kd .labelBox{stroke:#922B21;fill:#C0392B;}#mermaid-svg-DjzasCTaaixqm8Kd .labelText,#mermaid-svg-DjzasCTaaixqm8Kd .labelText>tspan{fill:#FFFFFF;stroke:none;}#mermaid-svg-DjzasCTaaixqm8Kd .loopText,#mermaid-svg-DjzasCTaaixqm8Kd .loopText>tspan{fill:#FFFFFF;stroke:none;}#mermaid-svg-DjzasCTaaixqm8Kd .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:#922B21;fill:#922B21;}#mermaid-svg-DjzasCTaaixqm8Kd .note{stroke:hsl(52.6829268293, 60%, 73.9215686275%);fill:#fff5ad;}#mermaid-svg-DjzasCTaaixqm8Kd .noteText,#mermaid-svg-DjzasCTaaixqm8Kd .noteText>tspan{fill:#333;stroke:none;}#mermaid-svg-DjzasCTaaixqm8Kd .activation0{fill:hsl(-114.3624161074, 63.4042553191%, 46.0784313725%);stroke:hsl(-114.3624161074, 63.4042553191%, 36.0784313725%);}#mermaid-svg-DjzasCTaaixqm8Kd .activation1{fill:hsl(-114.3624161074, 63.4042553191%, 46.0784313725%);stroke:hsl(-114.3624161074, 63.4042553191%, 36.0784313725%);}#mermaid-svg-DjzasCTaaixqm8Kd .activation2{fill:hsl(-114.3624161074, 63.4042553191%, 46.0784313725%);stroke:hsl(-114.3624161074, 63.4042553191%, 36.0784313725%);}#mermaid-svg-DjzasCTaaixqm8Kd .actorPopupMenu{position:absolute;}#mermaid-svg-DjzasCTaaixqm8Kd .actorPopupMenuPanel{position:absolute;fill:#C0392B;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-DjzasCTaaixqm8Kd .actor-man line{stroke:#922B21;fill:#C0392B;}#mermaid-svg-DjzasCTaaixqm8Kd .actor-man circle,#mermaid-svg-DjzasCTaaixqm8Kd line{stroke:#922B21;fill:#C0392B;stroke-width:2px;}#mermaid-svg-DjzasCTaaixqm8Kd :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}请求A (扣500分)请求B (扣800分)TransactionService数据库 (用户积分: 1000)开始处理开始处理请求A: 读取用户积分返回 1000请求B: 读取用户积分返回 1000请求A: 校验通过 (1000 >= 500)请求B: 校验通过 (1000 >= 800)请求A: 更新积分为 1000 - 500 = 500请求B: 更新积分为 1000 - 800 = 200成功成功灾难!用户被扣了 500+800=1300 分,但实际只拥有 1000 分。最终数据库的值是 200 (请求B覆盖了请求A)。请求A (扣500分)请求B (扣800分)TransactionService数据库 (用户积分: 1000)

解决方案:悲观锁如何力挽狂澜 🦸

悲观锁通过在事务开始读取数据时就锁定它,强制其他事务等待,从而将并发操作串行化。

状态图:被锁定的数据行

#mermaid-svg-S912BLes6WQubaSj {font-family:Arial,sans-serif;font-size:14px;fill:#34495E;}#mermaid-svg-S912BLes6WQubaSj .error-icon{fill:hsl(185.6375838926, 63.4042553191%, 51.0784313725%);}#mermaid-svg-S912BLes6WQubaSj .error-text{fill:rgb(203.8468085108, 60.5170212767, 45.6531914895);stroke:rgb(203.8468085108, 60.5170212767, 45.6531914895);}#mermaid-svg-S912BLes6WQubaSj .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-S912BLes6WQubaSj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-S912BLes6WQubaSj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-S912BLes6WQubaSj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-S912BLes6WQubaSj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-S912BLes6WQubaSj .marker{fill:#5D6D7E;stroke:#5D6D7E;}#mermaid-svg-S912BLes6WQubaSj .marker.cross{stroke:#5D6D7E;}#mermaid-svg-S912BLes6WQubaSj svg{font-family:Arial,sans-serif;font-size:14px;}#mermaid-svg-S912BLes6WQubaSj defs #statediagram-barbEnd{fill:#5D6D7E;stroke:#5D6D7E;}#mermaid-svg-S912BLes6WQubaSj g.stateGroup text{fill:#922B21;stroke:none;font-size:10px;}#mermaid-svg-S912BLes6WQubaSj g.stateGroup text{fill:#34495E;stroke:none;font-size:10px;}#mermaid-svg-S912BLes6WQubaSj g.stateGroup .state-title{font-weight:bolder;fill:#FFFFFF;}#mermaid-svg-S912BLes6WQubaSj g.stateGroup rect{fill:#C0392B;stroke:#922B21;}#mermaid-svg-S912BLes6WQubaSj g.stateGroup line{stroke:#5D6D7E;stroke-width:1;}#mermaid-svg-S912BLes6WQubaSj .transition{stroke:#5D6D7E;stroke-width:1;fill:none;}#mermaid-svg-S912BLes6WQubaSj .stateGroup .composit{fill:#f4f4f4;border-bottom:1px;}#mermaid-svg-S912BLes6WQubaSj .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-S912BLes6WQubaSj .state-note{stroke:hsl(52.6829268293, 60%, 73.9215686275%);fill:#fff5ad;}#mermaid-svg-S912BLes6WQubaSj .state-note text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-S912BLes6WQubaSj .stateLabel .box{stroke:none;stroke-width:0;fill:#C0392B;opacity:0.5;}#mermaid-svg-S912BLes6WQubaSj .edgeLabel .label rect{fill:#C0392B;opacity:0.5;}#mermaid-svg-S912BLes6WQubaSj .edgeLabel .label text{fill:#34495E;}#mermaid-svg-S912BLes6WQubaSj .label div .edgeLabel{color:#34495E;}#mermaid-svg-S912BLes6WQubaSj .stateLabel text{fill:#FFFFFF;font-size:10px;font-weight:bold;}#mermaid-svg-S912BLes6WQubaSj .node circle.state-start{fill:#5D6D7E;stroke:#5D6D7E;}#mermaid-svg-S912BLes6WQubaSj .node .fork-join{fill:#5D6D7E;stroke:#5D6D7E;}#mermaid-svg-S912BLes6WQubaSj .node circle.state-end{fill:#922B21;stroke:#f4f4f4;stroke-width:1.5;}#mermaid-svg-S912BLes6WQubaSj .end-state-inner{fill:#f4f4f4;stroke-width:1.5;}#mermaid-svg-S912BLes6WQubaSj .node rect{fill:#C0392B;stroke:#922B21;stroke-width:1px;}#mermaid-svg-S912BLes6WQubaSj .node polygon{fill:#C0392B;stroke:#922B21;stroke-width:1px;}#mermaid-svg-S912BLes6WQubaSj #statediagram-barbEnd{fill:#5D6D7E;}#mermaid-svg-S912BLes6WQubaSj .statediagram-cluster rect{fill:#C0392B;stroke:#922B21;stroke-width:1px;}#mermaid-svg-S912BLes6WQubaSj .cluster-label,#mermaid-svg-S912BLes6WQubaSj .nodeLabel{color:#FFFFFF;}#mermaid-svg-S912BLes6WQubaSj .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-S912BLes6WQubaSj .statediagram-state .divider{stroke:#922B21;}#mermaid-svg-S912BLes6WQubaSj .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-S912BLes6WQubaSj .statediagram-cluster.statediagram-cluster .inner{fill:#f4f4f4;}#mermaid-svg-S912BLes6WQubaSj .statediagram-cluster.statediagram-cluster-alt .inner{fill:hsl(185.6375838926, 63.4042553191%, 51.0784313725%);}#mermaid-svg-S912BLes6WQubaSj .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-S912BLes6WQubaSj .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-S912BLes6WQubaSj .statediagram-state rect.divider{stroke-dasharray:10,10;fill:hsl(185.6375838926, 63.4042553191%, 51.0784313725%);}#mermaid-svg-S912BLes6WQubaSj .note-edge{stroke-dasharray:5;}#mermaid-svg-S912BLes6WQubaSj .statediagram-note rect{fill:#fff5ad;stroke:hsl(52.6829268293, 60%, 73.9215686275%);stroke-width:1px;rx:0;ry:0;}#mermaid-svg-S912BLes6WQubaSj .statediagram-note rect{fill:#fff5ad;stroke:hsl(52.6829268293, 60%, 73.9215686275%);stroke-width:1px;rx:0;ry:0;}#mermaid-svg-S912BLes6WQubaSj .statediagram-note text{fill:#333;}#mermaid-svg-S912BLes6WQubaSj .statediagram-note .nodeLabel{color:#333;}#mermaid-svg-S912BLes6WQubaSj .statediagram .edgeLabel{color:red;}#mermaid-svg-S912BLes6WQubaSj #dependencyStart,#mermaid-svg-S912BLes6WQubaSj #dependencyEnd{fill:#5D6D7E;stroke:#5D6D7E;stroke-width:1;}#mermaid-svg-S912BLes6WQubaSj :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}\"事务A:findByIdWithLock()\"\"事务A:commit() / rollback()\"\"事务B:findByIdWithLock()\"\"事务A释放锁后,事务B获取锁\"未锁定 (Unlocked)已锁定 (Locked)等待中 (Blocked)Fork_State

类图:实现悲观锁的 Repository 设计

#mermaid-svg-u33I5CvxyM15nOiE {font-family:Arial,sans-serif;font-size:14px;fill:#34495E;}#mermaid-svg-u33I5CvxyM15nOiE .error-icon{fill:hsl(185.6375838926, 63.4042553191%, 51.0784313725%);}#mermaid-svg-u33I5CvxyM15nOiE .error-text{fill:rgb(203.8468085108, 60.5170212767, 45.6531914895);stroke:rgb(203.8468085108, 60.5170212767, 45.6531914895);}#mermaid-svg-u33I5CvxyM15nOiE .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-u33I5CvxyM15nOiE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-u33I5CvxyM15nOiE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-u33I5CvxyM15nOiE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-u33I5CvxyM15nOiE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-u33I5CvxyM15nOiE .marker{fill:#5D6D7E;stroke:#5D6D7E;}#mermaid-svg-u33I5CvxyM15nOiE .marker.cross{stroke:#5D6D7E;}#mermaid-svg-u33I5CvxyM15nOiE svg{font-family:Arial,sans-serif;font-size:14px;}#mermaid-svg-u33I5CvxyM15nOiE g.classGroup text{fill:#922B21;fill:#34495E;stroke:none;font-family:Arial,sans-serif;font-size:10px;}#mermaid-svg-u33I5CvxyM15nOiE g.classGroup text .title{font-weight:bolder;}#mermaid-svg-u33I5CvxyM15nOiE .nodeLabel,#mermaid-svg-u33I5CvxyM15nOiE .edgeLabel{color:#34495E;}#mermaid-svg-u33I5CvxyM15nOiE .edgeLabel .label rect{fill:#C0392B;}#mermaid-svg-u33I5CvxyM15nOiE .label text{fill:#34495E;}#mermaid-svg-u33I5CvxyM15nOiE .edgeLabel .label span{background:#C0392B;}#mermaid-svg-u33I5CvxyM15nOiE .classTitle{font-weight:bolder;}#mermaid-svg-u33I5CvxyM15nOiE .node rect,#mermaid-svg-u33I5CvxyM15nOiE .node circle,#mermaid-svg-u33I5CvxyM15nOiE .node ellipse,#mermaid-svg-u33I5CvxyM15nOiE .node polygon,#mermaid-svg-u33I5CvxyM15nOiE .node path{fill:#C0392B;stroke:#922B21;stroke-width:1px;}#mermaid-svg-u33I5CvxyM15nOiE .divider{stroke:#922B21;stroke:1;}#mermaid-svg-u33I5CvxyM15nOiE g.clickable{cursor:pointer;}#mermaid-svg-u33I5CvxyM15nOiE g.classGroup rect{fill:#C0392B;stroke:#922B21;}#mermaid-svg-u33I5CvxyM15nOiE g.classGroup line{stroke:#922B21;stroke-width:1;}#mermaid-svg-u33I5CvxyM15nOiE .classLabel .box{stroke:none;stroke-width:0;fill:#C0392B;opacity:0.5;}#mermaid-svg-u33I5CvxyM15nOiE .classLabel .label{fill:#922B21;font-size:10px;}#mermaid-svg-u33I5CvxyM15nOiE .relation{stroke:#5D6D7E;stroke-width:1;fill:none;}#mermaid-svg-u33I5CvxyM15nOiE .dashed-line{stroke-dasharray:3;}#mermaid-svg-u33I5CvxyM15nOiE #compositionStart,#mermaid-svg-u33I5CvxyM15nOiE .composition{fill:#5D6D7E!important;stroke:#5D6D7E!important;stroke-width:1;}#mermaid-svg-u33I5CvxyM15nOiE #compositionEnd,#mermaid-svg-u33I5CvxyM15nOiE .composition{fill:#5D6D7E!important;stroke:#5D6D7E!important;stroke-width:1;}#mermaid-svg-u33I5CvxyM15nOiE #dependencyStart,#mermaid-svg-u33I5CvxyM15nOiE .dependency{fill:#5D6D7E!important;stroke:#5D6D7E!important;stroke-width:1;}#mermaid-svg-u33I5CvxyM15nOiE #dependencyStart,#mermaid-svg-u33I5CvxyM15nOiE .dependency{fill:#5D6D7E!important;stroke:#5D6D7E!important;stroke-width:1;}#mermaid-svg-u33I5CvxyM15nOiE #extensionStart,#mermaid-svg-u33I5CvxyM15nOiE .extension{fill:#5D6D7E!important;stroke:#5D6D7E!important;stroke-width:1;}#mermaid-svg-u33I5CvxyM15nOiE #extensionEnd,#mermaid-svg-u33I5CvxyM15nOiE .extension{fill:#5D6D7E!important;stroke:#5D6D7E!important;stroke-width:1;}#mermaid-svg-u33I5CvxyM15nOiE #aggregationStart,#mermaid-svg-u33I5CvxyM15nOiE .aggregation{fill:#C0392B!important;stroke:#5D6D7E!important;stroke-width:1;}#mermaid-svg-u33I5CvxyM15nOiE #aggregationEnd,#mermaid-svg-u33I5CvxyM15nOiE .aggregation{fill:#C0392B!important;stroke:#5D6D7E!important;stroke-width:1;}#mermaid-svg-u33I5CvxyM15nOiE .edgeTerminals{font-size:11px;}#mermaid-svg-u33I5CvxyM15nOiE :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}\"uses\"\"uses\"«interface»JpaRepository«interface»MemberUserRepository+<> Optional findByIdWithLock(Integer id)«interface»UserCouponRepository+<> Optional findByIdWithLock(Integer id)TransactionProcessingService-MemberUserRepository memberUserRepository-UserCouponRepository userCouponRepository+<> processTransactionByAdmin()

实体关系图 (ERD - Entity Relationship Diagram)

这个 ERD (Entity Relationship Diagram, 实体关系图) 展示了 TransactionProcessingService 操作的核心实体及其关系。

#mermaid-svg-QduY06lN3UzMkrCE {font-family:Arial,sans-serif;font-size:14px;fill:#34495E;}#mermaid-svg-QduY06lN3UzMkrCE .error-icon{fill:hsl(185.6375838926, 63.4042553191%, 51.0784313725%);}#mermaid-svg-QduY06lN3UzMkrCE .error-text{fill:rgb(203.8468085108, 60.5170212767, 45.6531914895);stroke:rgb(203.8468085108, 60.5170212767, 45.6531914895);}#mermaid-svg-QduY06lN3UzMkrCE .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-QduY06lN3UzMkrCE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QduY06lN3UzMkrCE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QduY06lN3UzMkrCE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QduY06lN3UzMkrCE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QduY06lN3UzMkrCE .marker{fill:#5D6D7E;stroke:#5D6D7E;}#mermaid-svg-QduY06lN3UzMkrCE .marker.cross{stroke:#5D6D7E;}#mermaid-svg-QduY06lN3UzMkrCE svg{font-family:Arial,sans-serif;font-size:14px;}#mermaid-svg-QduY06lN3UzMkrCE .entityBox{fill:#C0392B;stroke:#922B21;}#mermaid-svg-QduY06lN3UzMkrCE .attributeBoxOdd{fill:#ffffff;stroke:#922B21;}#mermaid-svg-QduY06lN3UzMkrCE .attributeBoxEven{fill:#f2f2f2;stroke:#922B21;}#mermaid-svg-QduY06lN3UzMkrCE .relationshipLabelBox{fill:hsl(185.6375838926, 63.4042553191%, 51.0784313725%);opacity:0.7;background-color:hsl(185.6375838926, 63.4042553191%, 51.0784313725%);}#mermaid-svg-QduY06lN3UzMkrCE .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-QduY06lN3UzMkrCE .relationshipLine{stroke:#5D6D7E;}#mermaid-svg-QduY06lN3UzMkrCE :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;}MEMBER_USERintidPK主键stringnickname昵称longtotal_points总积分longused_points已用积分bytestatus状态USER_COUPONintidPK主键intmember_user_idFK用户IDintcoupon_template_idFK模板IDbytestatus状态datetimeused_at使用时间POINT_TRANSACTIONintidPK主键intmember_user_idFK用户IDlongpoints_change积分变动stringsource_business_id业务单号拥有产生

思维导图总结 🧠

在这里插入图片描述

结语

在处理核心交易逻辑时,我们必须像悲观锁一样“悲观”思考,预见所有可能出错的并发场景。TransactionProcessingService 的例子完美地诠释了这一点:@Transactional 提供了宏观的保护,而悲观锁则在微观层面,为每一个关键的数据行站岗放哨。

记住,对于用户资产,再谨慎也不为过。正确地使用锁和事务,是每一位负责任的后端工程师的必备修养!✨