紧接上一篇,支付回调是一个可能被重复触发的业务入口,因此不能假设每次收到回调时,订单都处于相同状态。
例如,某笔交易第一次回调已经完成发货,交易状态变为
Fulfilled。如果后续又收到同一笔交易的成功通知,后端就不应该再次发放奖励。
这也是支付状态机的主要作用:根据当前状态判断请求是否仍然允许执行,而不是无条件重复执行支付履约流程。
摘要:支付回调可能被重复触发,需依赖状态机判断请求是否允许执行。本文说明状态变化须符合业务规则,终态检查可拦截串行重复回调,并区分「先查后改」与「数据库条件更新」两种实现方式,指出仅有状态判断尚不能证明并发场景下绝对安全。
二、状态
2.1 状态变化必须符合业务规则
结合当前项目已经确认的状态处理逻辑,可以关注以下几种情况:
| 当前状态 | 收到支付成功通知时的处理原则 |
|---|---|
Pending |
在满足支付验证及订单条件后,允许进入 Paid |
Paid |
支付已经确认,但履约可能尚未完成,需要判断后续处理逻辑 |
Fulfilled |
已完成发货,不应再次触发奖励发放 |
Refunded |
不能简单地重新变更为 Paid 并发放奖励 |
Expired |
不能绕过过期规则直接恢复支付履约 |
这里需要注意,表格描述的是业务状态的处理原则,并不意味着每种状态在代码中都采用相同的判断方式。具体行为仍应以实际状态判断和更新代码为准。
尤其是
Paid状态:它与Fulfilled不同。Paid代表支付状态已经确认,但不一定代表奖励发放已经完成。因此,不能把所有非Pending状态都直接视为业务成功,也不能仅凭状态不是Fulfilled就无条件重复执行发奖。
下面是支付状态机的整体流转图:
#mermaid-svg-9is6XjwsHGKMe7ZJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9is6XjwsHGKMe7ZJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9is6XjwsHGKMe7ZJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9is6XjwsHGKMe7ZJ .error-icon{fill:#552222;}#mermaid-svg-9is6XjwsHGKMe7ZJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9is6XjwsHGKMe7ZJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9is6XjwsHGKMe7ZJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9is6XjwsHGKMe7ZJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9is6XjwsHGKMe7ZJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9is6XjwsHGKMe7ZJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9is6XjwsHGKMe7ZJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9is6XjwsHGKMe7ZJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9is6XjwsHGKMe7ZJ .marker.cross{stroke:#333333;}#mermaid-svg-9is6XjwsHGKMe7ZJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9is6XjwsHGKMe7ZJ p{margin:0;}#mermaid-svg-9is6XjwsHGKMe7ZJ defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-9is6XjwsHGKMe7ZJ g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-9is6XjwsHGKMe7ZJ g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-9is6XjwsHGKMe7ZJ g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-9is6XjwsHGKMe7ZJ g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-9is6XjwsHGKMe7ZJ g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-9is6XjwsHGKMe7ZJ .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-9is6XjwsHGKMe7ZJ .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-9is6XjwsHGKMe7ZJ .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-9is6XjwsHGKMe7ZJ .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-9is6XjwsHGKMe7ZJ .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-9is6XjwsHGKMe7ZJ .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-9is6XjwsHGKMe7ZJ .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-9is6XjwsHGKMe7ZJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-9is6XjwsHGKMe7ZJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-9is6XjwsHGKMe7ZJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-9is6XjwsHGKMe7ZJ .edgeLabel .label text{fill:#333;}#mermaid-svg-9is6XjwsHGKMe7ZJ .label div .edgeLabel{color:#333;}#mermaid-svg-9is6XjwsHGKMe7ZJ .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-9is6XjwsHGKMe7ZJ .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-9is6XjwsHGKMe7ZJ .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-9is6XjwsHGKMe7ZJ .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-9is6XjwsHGKMe7ZJ .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-9is6XjwsHGKMe7ZJ .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9is6XjwsHGKMe7ZJ .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9is6XjwsHGKMe7ZJ #statediagram-barbEnd{fill:#333333;}#mermaid-svg-9is6XjwsHGKMe7ZJ .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9is6XjwsHGKMe7ZJ .cluster-label,#mermaid-svg-9is6XjwsHGKMe7ZJ .nodeLabel{color:#131300;}#mermaid-svg-9is6XjwsHGKMe7ZJ .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-9is6XjwsHGKMe7ZJ .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-9is6XjwsHGKMe7ZJ .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-9is6XjwsHGKMe7ZJ .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-9is6XjwsHGKMe7ZJ .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-9is6XjwsHGKMe7ZJ .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-9is6XjwsHGKMe7ZJ .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-9is6XjwsHGKMe7ZJ .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-9is6XjwsHGKMe7ZJ .note-edge{stroke-dasharray:5;}#mermaid-svg-9is6XjwsHGKMe7ZJ .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-9is6XjwsHGKMe7ZJ .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-9is6XjwsHGKMe7ZJ .statediagram-note text{fill:black;}#mermaid-svg-9is6XjwsHGKMe7ZJ .statediagram-note .nodeLabel{color:black;}#mermaid-svg-9is6XjwsHGKMe7ZJ .statediagram .edgeLabel{color:red;}#mermaid-svg-9is6XjwsHGKMe7ZJ #dependencyStart,#mermaid-svg-9is6XjwsHGKMe7ZJ #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-9is6XjwsHGKMe7ZJ .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-9is6XjwsHGKMe7ZJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 支付验证通过且订单条件满足
履约完成(发放奖励)
退款
超时未支付
Pending
Paid
Fulfilled
Refunded
Expired
2.2 为什么终态检查能够处理串行重复回调?
假设支付网关先后发送两次相同的成功通知。
第一次回调:
Pending
↓
Paid
↓
发放奖励
↓
Fulfilled
第二次回调到达时,如果业务代码能够正确识别 Fulfilled 状态,就可以直接结束处理,而不再执行奖励发放。
因此,对于串行重复回调,终态检查提供了一道有效的保护。
但这个结论有一个前提:第二次请求必须能够观察到第一次请求已经提交的最终状态,而且业务代码不能在检查之后绕过状态限制继续发奖。
下面是串行重复回调的处理流程:
数据库 后端服务 支付网关 数据库 后端服务 支付网关 #mermaid-svg-HVhlAMsgLcD66IZc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HVhlAMsgLcD66IZc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HVhlAMsgLcD66IZc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HVhlAMsgLcD66IZc .error-icon{fill:#552222;}#mermaid-svg-HVhlAMsgLcD66IZc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HVhlAMsgLcD66IZc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HVhlAMsgLcD66IZc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HVhlAMsgLcD66IZc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HVhlAMsgLcD66IZc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HVhlAMsgLcD66IZc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HVhlAMsgLcD66IZc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HVhlAMsgLcD66IZc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HVhlAMsgLcD66IZc .marker.cross{stroke:#333333;}#mermaid-svg-HVhlAMsgLcD66IZc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HVhlAMsgLcD66IZc p{margin:0;}#mermaid-svg-HVhlAMsgLcD66IZc .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HVhlAMsgLcD66IZc text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-HVhlAMsgLcD66IZc .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-HVhlAMsgLcD66IZc .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-HVhlAMsgLcD66IZc .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-HVhlAMsgLcD66IZc .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-HVhlAMsgLcD66IZc #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-HVhlAMsgLcD66IZc .sequenceNumber{fill:white;}#mermaid-svg-HVhlAMsgLcD66IZc #sequencenumber{fill:#333;}#mermaid-svg-HVhlAMsgLcD66IZc #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-HVhlAMsgLcD66IZc .messageText{fill:#333;stroke:none;}#mermaid-svg-HVhlAMsgLcD66IZc .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HVhlAMsgLcD66IZc .labelText,#mermaid-svg-HVhlAMsgLcD66IZc .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-HVhlAMsgLcD66IZc .loopText,#mermaid-svg-HVhlAMsgLcD66IZc .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-HVhlAMsgLcD66IZc .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-HVhlAMsgLcD66IZc .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-HVhlAMsgLcD66IZc .noteText,#mermaid-svg-HVhlAMsgLcD66IZc .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-HVhlAMsgLcD66IZc .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HVhlAMsgLcD66IZc .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HVhlAMsgLcD66IZc .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HVhlAMsgLcD66IZc .actorPopupMenu{position:absolute;}#mermaid-svg-HVhlAMsgLcD66IZc .actorPopupMenuPanel{position:absolute;fill:#ECECFF;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-HVhlAMsgLcD66IZc .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HVhlAMsgLcD66IZc .actor-man circle,#mermaid-svg-HVhlAMsgLcD66IZc line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-HVhlAMsgLcD66IZc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第一次回调 第二次回调(重复通知) 支付成功通知读取当前状态(Pending)校验通过,执行履约更新状态为 Fulfilled返回成功支付成功通知(重复)读取当前状态(Fulfilled)终态检查,直接结束返回成功(不重复发奖)
2.3 状态检查与数据库条件更新不是一回事
这里还需要区分两种实现方式。
第一种是先读取状态,再在业务代码中判断:
读取当前状态
↓
判断是否允许处理
↓
执行状态更新
第二种是把状态条件直接放进数据库更新操作:
UPDATE / FindOneAndUpdate
条件:订单 ID 匹配且状态为 Pending
↓
只有满足条件的记录才能被更新
第二种方式通常更适合实现数据库级的条件更新,因为判断与更新由同一条数据库操作完成,能够避免单纯的"先查后改"所产生的部分竞态窗口。
不过,当前已确认的代码事实是:FulfillPaymentTransaction 更新交易时只按 transactionId 匹配,没有附带 status = Paid 条件。因此,不能直接将这段实现描述为基于 Paid 状态的条件更新。
这并不自动证明并发发奖一定会出错,但意味着我们还需要继续检查事务冲突处理、提交结果及其他并发保护机制。
小结:状态机负责约束业务状态变化,终态检查可以拦截正常的串行重复回调;但仅有状态判断,不能直接证明并发情况下也绝对安全。
下面是两种实现方式的对比流程图:
#mermaid-svg-SgxkJdGm3NIbGemp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SgxkJdGm3NIbGemp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SgxkJdGm3NIbGemp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SgxkJdGm3NIbGemp .error-icon{fill:#552222;}#mermaid-svg-SgxkJdGm3NIbGemp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SgxkJdGm3NIbGemp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SgxkJdGm3NIbGemp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SgxkJdGm3NIbGemp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SgxkJdGm3NIbGemp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SgxkJdGm3NIbGemp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SgxkJdGm3NIbGemp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SgxkJdGm3NIbGemp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SgxkJdGm3NIbGemp .marker.cross{stroke:#333333;}#mermaid-svg-SgxkJdGm3NIbGemp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SgxkJdGm3NIbGemp p{margin:0;}#mermaid-svg-SgxkJdGm3NIbGemp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SgxkJdGm3NIbGemp .cluster-label text{fill:#333;}#mermaid-svg-SgxkJdGm3NIbGemp .cluster-label span{color:#333;}#mermaid-svg-SgxkJdGm3NIbGemp .cluster-label span p{background-color:transparent;}#mermaid-svg-SgxkJdGm3NIbGemp .label text,#mermaid-svg-SgxkJdGm3NIbGemp span{fill:#333;color:#333;}#mermaid-svg-SgxkJdGm3NIbGemp .node rect,#mermaid-svg-SgxkJdGm3NIbGemp .node circle,#mermaid-svg-SgxkJdGm3NIbGemp .node ellipse,#mermaid-svg-SgxkJdGm3NIbGemp .node polygon,#mermaid-svg-SgxkJdGm3NIbGemp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SgxkJdGm3NIbGemp .rough-node .label text,#mermaid-svg-SgxkJdGm3NIbGemp .node .label text,#mermaid-svg-SgxkJdGm3NIbGemp .image-shape .label,#mermaid-svg-SgxkJdGm3NIbGemp .icon-shape .label{text-anchor:middle;}#mermaid-svg-SgxkJdGm3NIbGemp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SgxkJdGm3NIbGemp .rough-node .label,#mermaid-svg-SgxkJdGm3NIbGemp .node .label,#mermaid-svg-SgxkJdGm3NIbGemp .image-shape .label,#mermaid-svg-SgxkJdGm3NIbGemp .icon-shape .label{text-align:center;}#mermaid-svg-SgxkJdGm3NIbGemp .node.clickable{cursor:pointer;}#mermaid-svg-SgxkJdGm3NIbGemp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SgxkJdGm3NIbGemp .arrowheadPath{fill:#333333;}#mermaid-svg-SgxkJdGm3NIbGemp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SgxkJdGm3NIbGemp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SgxkJdGm3NIbGemp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SgxkJdGm3NIbGemp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SgxkJdGm3NIbGemp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SgxkJdGm3NIbGemp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SgxkJdGm3NIbGemp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SgxkJdGm3NIbGemp .cluster text{fill:#333;}#mermaid-svg-SgxkJdGm3NIbGemp .cluster span{color:#333;}#mermaid-svg-SgxkJdGm3NIbGemp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SgxkJdGm3NIbGemp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SgxkJdGm3NIbGemp rect.text{fill:none;stroke-width:0;}#mermaid-svg-SgxkJdGm3NIbGemp .icon-shape,#mermaid-svg-SgxkJdGm3NIbGemp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SgxkJdGm3NIbGemp .icon-shape p,#mermaid-svg-SgxkJdGm3NIbGemp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SgxkJdGm3NIbGemp .icon-shape .label rect,#mermaid-svg-SgxkJdGm3NIbGemp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SgxkJdGm3NIbGemp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SgxkJdGm3NIbGemp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SgxkJdGm3NIbGemp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 方式二:数据库条件更新
方式一:先查后改
读取当前状态
业务代码判断是否允许
执行状态更新
UPDATE / FindOneAndUpdate
条件:订单 ID 匹配且状态为 Pending
只有满足条件的记录才能被更新
存在竞态窗口
判断与更新原子完成,避免竞态