Skip to content

04 · 一致性与事务边界:本地 / 分布式 / 幂等 ​

"数据对不对"是后端最难的部分。本篇把事务从单库讲到跨服务:ACID、隔离级别、分布式事务方案、幂等与最终一致。

速览卡 ​

项内容
一句话定位多步写操作怎么保证"要么全成功要么全失败",跨服务时又怎么妥协
核心内容ACID、事务隔离级别、本地事务边界、分布式事务方案(SAGA/本地消息表)、幂等设计、最终一致
读完能回答什么时候该用事务?跨服务怎么保证一致性?重复请求怎么不重复扣款?
相关板块C10 · 数据建模 | C06 · 异步与消息 | C13 · 缓存与 Redis

一、先看清一致性光谱 ​

级别含义代价
强一致写后立刻读到延迟高、跨服务难
最终一致短暂不一致,最终一致可接受窗口,要处理补偿
无所谓无所谓缓存随便用

后端 90% 的"一致性难题"其实是"该用哪档"的问题:账务要强,榜单可最终一致。


二、本地事务:ACID 与隔离级别 ​

2.1 ACID ​

特性含义通俗
Atomicity 原子性全成或全败要么都做
Consistency 一致性约束不被破坏状态合法
Isolation 隔离性并发互不干扰各做各的
Durability 持久性提交后不丢断电还在

2.2 隔离级别(使用侧) ​

级别防脏读防不可重复读防幻读用在哪
读未提交❌❌❌别用
读已提交✅❌❌多数默认
可重复读✅✅~MySQL 默认
串行化✅✅✅极少数

使用原则:默认用数据库的默认隔离级别;只有具体场景(如金额校验)才考虑升级,升级前先理解性能代价。


三、事务边界:包什么、不包什么 ​

python
def create_order(order, user):
    with db.begin():
        lock_user(user.id)              # 锁账户
        if user.balance < order.total:
            raise BusinessError("INSUFFICIENT_BALANCE")
        db.insert(order)
        db.update(user.balance -= order.total)
该包不该包
多步写操作(扣款+下单)纯读
状态必须原子变化外部调用(短信/HTTP)
有前置校验的写长时间运行的逻辑

⚠️ 外部调用不要放在事务里:短信、第三方 HTTP 不可控,事务回滚不了外部副作用。正确顺序是"事务内做本地写 → 提交后发外部消息/事件"。


四、跨服务:分布式事务方案 ​

单个服务管不了多个服务的数据库,方案都是"妥协的艺术":

方案思路适用
SAGA拆成多步 + 每步有补偿主流
本地消息表业务表 + 消息表同一事务写,异步投递可靠投递
事务消息(MQ)半消息 + 确认依赖 MQ 能力
TCCTry-Confirm-Cancel 三段强管控但复杂
2PC两阶段提交少用,阻塞问题

SAGA 是默认答案:比如"下单 → 扣款 → 减库存",每一步失败就执行反向补偿(退款、加回库存)。代价是中间状态可见(最终一致)。


五、幂等设计:重复请求不重复扣款 ​

前端可能重试、用户可能连点、MQ 可能重投——后端必须幂等:

手段做法场景
唯一键请求带 idempotency_key,建唯一约束下单/支付
状态机UPDATE ... WHERE status='pending',影响行数=0 则跳过任务处理
查重后写先查后写,但要配合唯一约束防并发弱校验
python
def create_payment(req):
    # 唯一约束兜底:同一个幂等键只能成功一次
    row = db.insert_payment(idempotency_key=req.key)
    if row is None:      # 撞唯一约束,说明已处理
        return db.get_payment_by_key(req.key)

幂等三件套:客户端传幂等键 + 数据库唯一约束 + 返回已存在的结果。


六、最终一致的落地姿势 ​

text
下单服务 ──写 orders + outbox 表(同一事务)──▶ MQ ──▶ 积分服务
                          ↑
             投递失败则定时任务重扫 outbox 重投
环节保证
本地消息表业务写和消息写同事务,不丢
投递MQ 至少一次
消费幂等消费方按业务键去重
补偿失败重试,终态重扫

最终一致的关键不是"不失败",而是"失败了还能补到一致"——重试 + 幂等 + 补偿三件套。


自检清单 ​

  • [ ] 能按场景选择强一致 / 最终一致
  • [ ] 会说清隔离级别该选哪档、为什么
  • [ ] 知道外部调用不能放事务里
  • [ ] 能讲 SAGA 的补偿思路,并说出它与强一致的区别
  • [ ] 会给写接口做幂等(幂等键 + 唯一约束)

常见误区 ​

  • 误区:跨服务也能靠数据库事务。 不能,跨服务只能靠分布式方案 + 最终一致。
  • 误区:最终一致 = 不保证。 不是,要有重试、幂等、补偿把它补到一致。
  • 误区:事务包住外部调用。 外部副作用回滚不了,提交后再发消息。
  • 误区:幂等 = 查一下再写。 并发下查重会漏,必须唯一约束兜底。

动手练习 ​

练习 A:给转账加事务 ​

实现转账接口,用事务保证两边余额变化原子,并验证失败回滚。

练习 B:幂等下单 ​

给下单接口加幂等键 + 唯一约束,用两次相同请求验证只产生一单。

练习 C:SAGA 演练 ​

设计"下单 → 扣款 → 减库存"的补偿表:列出每步的补偿动作和失败分支。


相关文档 ​

本区 ​

其他域 ​