高并发事务怎么提速?把“一致性”换成“性能”的 3 个手段、2 条红线和 1 套补偿思路

9 阅读12分钟

大促一来,最先尖叫的往往不是 CPU,而是事务。

你明明已经加了机器、加了连接池、加了缓存,结果一下单还是慢;再一查,热点行锁住了、跨分区事务拉长了、重试还把系统打得更抖。这个时候,很多系统会做一件听上去有点危险、但在工程上非常常见的事:拿一部分一致性,去换更高的吞吐和更低的延迟。

这篇文章就讲清楚 4 件事:

  • 什么叫“一致性换性能”,它到底在换什么
  • 3 个最常见的手段:降低隔离级别、减少跨分区事务、幂等补偿
  • 什么场景适合这么做:高并发事务,尤其是热点写入和分布式写入
  • 代价到底是什么:正确性边界变窄,补偿逻辑变重,而且不是所有业务都玩得起

先给一句能直接带走的话:不是把正确性扔掉,而是把“必须立刻绝对正确”的范围缩小。

先把 4 个词讲人话

**一致性:**系统在并发和故障下,仍然守住业务规则。

生活类比:像超市收银,账上写着还剩 1 瓶可乐,就不能卖出 2 瓶。

迷你案例:电商下单时,“库存不能扣成负数”“一笔付款不能记两次”,这就是一致性约束。

**隔离级别:**多个事务同时执行时,彼此“互相看见多少、互相影响多少”的规则。

生活类比:像几个人同时改一份 Excel。规则越严,越不容易改乱;但大家也越容易排队。

迷你案例:把隔离级别调高,像让大家一个个进会议室签字;把隔离级别调低,像允许多人并行填表,但你得自己多做核对。

**跨分区事务:**一次事务要同时改多个分区、分片或节点上的数据。

生活类比:本来只要找一个仓库管理员签字,现在变成要北京仓、上海仓、广州仓一起点头。

迷你案例:订单在 order 分区,库存在 inventory 分区,账户余额又在 account 分区,一次下单全都要同步提交,这就是典型的跨分区事务。

**幂等补偿:**出错后允许你“再补一次”,但无论补多少次,最终结果都一样。

生活类比:快递员已经把“取消投递”操作点了一次,你再点两次,包裹也不会凭空再取消三回。

迷你案例:支付失败后释放库存。如果这条补偿消息因为网络抖动被重复投递 3 次,库存也只应该回补 1 次。

这套优化,本质上在换什么?

高并发事务里,性能慢,很多时候不是算得慢,而是协调得太多

  • 事务彼此要等锁
  • 不同分区要等提交结果
  • 失败以后还要重试
  • 一重试,又把并发冲突放大

所以,所谓“一致性换性能”,核心不是一句抽象口号,而是 3 个具体动作:

  1. 少等别人:降低隔离级别,减少锁等待和串行化开销
  2. 少拉远程同学开会:减少跨分区事务,把更多事务压回本地闭环
  3. 先跑起来,再纠偏:允许部分步骤异步完成,但把补偿做成幂等

你可以把它记成一句顺口的工程话:能局部严格,就别全局严格;能本地提交,就别跨分区一起交卷;必须异步,就把补偿做成“多来几次也不出事”。

图 1:什么时候可以考虑“一致性换性能”

高并发写入变慢
  -> 先问:这条业务规则能不能接受短暂不一致?
      -> 不能:保持强一致,优先优化数据模型和热点
      -> 能:继续
          -> 能不能把事务收敛到同一分区?
              -> 能:优先本地事务
              -> 不能:继续
                  -> 失败后能不能补偿,而且补偿能不能幂等?
                      -> 能:异步 + 补偿
                      -> 不能:不要为了性能硬降一致性

动作建议:这张图说明,先判断“错一小会儿能不能接受”,再判断“能不能局部化”,最后才决定要不要上补偿。

手段一:降低隔离级别——把大队伍拆小,但别把底线拆没

在数据库里,隔离级别越高,事务越像“排着队一个个办”;隔离级别越低,事务越像“大家先并行干起来”。前者更稳,后者更快。

截至 2026-03,PostgreSQL 官方文档仍把 Read Committed 作为默认隔离级别,而不是最严格的 Serializable。这背后的工程含义很直接:很多业务并不需要把所有事务都提升到最强隔离,默认先选一个吞吐更友好的平衡点。

但注意,降低隔离级别不是一句“我不在乎正确性了”。真正的做法是:把原来靠事务整体兜底的正确性,改成靠单条原子语句、唯一约束、条件更新、版本号校验来兜底。

生活类比:以前你要求“所有人必须按顺序进窗口办理”;现在你改成“可以多窗口同时办理,但每张票必须有唯一编号,且盖章前还要验一次”。

迷你案例:秒杀扣库存。如果你先 SELECT stockUPDATE stock,在较弱隔离下很容易被并发穿透;但如果改成带条件的原子更新,风险就会小很多。

图 2:弱隔离下最容易踩的坑

T1: SELECT stock = 1
T2: SELECT stock = 1
T1: UPDATE stock = 0 并提交
T2: UPDATE stock = -1 并提交
结果:超卖

动作建议:这条时间线说明,别把“先查再改”的业务规则散落在多条语句里,应该尽量收进一条原子更新或唯一约束里。

一个更稳的写法通常像这样:

UPDATE inventory
SET stock = stock - 1
WHERE sku = 'A1001' AND stock > 0;

然后看受影响行数:

  • 更新成功 1 行:扣减成功
  • 更新成功 0 行:说明没库存了,不要继续创建可成交订单

这就叫:隔离变弱了,但不变量要更贴近数据层。

手段二:减少跨分区事务——别让一次下单摇醒半个机房

跨分区事务的痛点,不在“写了几条数据”,而在“为了让几条数据一起成功,系统要协调多少节点”。节点越多、网络越远、两阶段提交越频繁,吞吐越容易掉,尾延迟也越难看。

截至 2026-03,Azure Cosmos DB 的官方文档仍强调:事务批处理限定在同一逻辑分区键内。这个事实很值得记住,因为它几乎是在公开告诉你:分区内事务最便宜,跨分区事务最贵。

生活类比:如果一个班主任自己就能决定值日表,速度很快;但每次排值日都要三个年级组长一起签字,事情就慢了。

迷你案例:订单系统把 order_id、订单状态、订单明细放在同一个分区,用户下单的大多数写操作都能本地完成;库存扣减和积分发放则异步处理。这样做不是更“优雅”,只是更扛流量。

这里最关键的,不是事务技巧,而是数据建模

  • 能一起变化的数据,尽量按同一个分区键落在一起
  • 高频写热点,尽量别设计成一次事务必须跨多个分区
  • 能拆成“本地事务 + 异步事件”的,不要硬上“全局大事务”

一句大白话:把强一致留给局部,把最终一致留给全局。

手段三:幂等补偿——允许迟到,但不允许越补越乱

一旦你减少跨分区事务,很多步骤就不再是“同时提交”,而会变成:本地先提交,随后发消息,其他服务再处理。这个时候,失败和重试就不再是偶发小插曲,而是系统设计的一部分。

于是,幂等补偿就上场了。

截至 2026-03,Azure 的补偿事务模式文档仍明确建议:补偿步骤最好定义成幂等命令。原因很现实——分布式系统里,消息重复、超时重投、消费者重启,都是家常便饭。你如果只会“补一次”,那系统迟早会补出事故。

生活类比:你去前台退房,前台小哥可能因为网络卡顿多点了一次“退房确认”,但系统不能因为他多点一次,就给你退两次押金。

迷你案例:支付失败后回补库存。正确做法不是“收到失败消息就 stock = stock + 1”,而是“按 order_idevent_id 查补偿是否已执行,未执行才回补,并记录补偿流水”。

常见护栏有 3 个:

  • 幂等键:例如 order_idpayment_idevent_id
  • 去重表/处理日志:记住哪些事件已经处理过
  • 状态机约束:只有从 RESERVED -> RELEASED 才允许释放库存,别让状态倒着飞

如果少了这 3 个,你以为自己在做补偿,实际上是在做“二次事故制造机”。这名字虽然有点损,但线上真的很常见。

图 3:3 种手段怎么选

手段换来什么最适合的场景主要风险必备护栏
降低隔离级别更高吞吐、更少锁等待热点写入、高并发短事务脏业务逻辑、并发异常原子更新、唯一约束、版本号
减少跨分区事务更短提交路径、更低协调成本分布式订单、库存、计数数据模型改造成本高合适分区键、本地事务、事件外发
幂等补偿更强弹性、更容易异步扩展服务拆分后的一致性修复重复补偿、状态错乱幂等键、去重表、状态机

动作建议:这张表说明,别只盯着“快没快”,要同步检查你有没有配上对应的护栏。

一个可以复用的实战走法

假设你在做一个高并发下单系统,目标是“扛住流量”,不是“把所有写操作塞进一个全局事务里”。一个更现实的落地方式,往往像这样:

第一步:先划红线,哪些绝不能错

把业务数据先分 3 类:

  • 绝不能短暂出错:余额、总账、清结算、券核销唯一性
  • 可以短暂不一致,但最终必须补平:库存预留、积分发放、优惠资格回收
  • 错一会儿问题不大:展示用计数、排行榜、已售数量、推荐状态

这一步不是文档游戏,而是系统生死线。红线没划,后面所有“优化”都很像碰运气。

第二步:把事务尽量收敛到本地

例如:

  1. 在订单分区内创建订单,状态记为 PENDING
  2. 在库存分区用条件更新做预留,成功则写 RESERVED
  3. 把事件写入 Outbox,再异步投递给积分、通知、履约系统

如果你非要把这几步绑成一个全局分布式事务,吞吐往往先跪下。

第三步:所有异步步骤都带幂等键

伪代码可以长这样:

on OrderCreated(event_id, order_id):
  if event_id 已处理:
    return
  执行库存预留或积分发放
  记录 event_id 已处理

on PaymentFailed(order_id):
  if ReleaseInventory(order_id) 已执行:
    return
  回补库存
  标记补偿完成

第四步:把补偿当成正式主流程来设计

别把补偿写成“失败了再补个 if”。你应该像设计主交易链路一样设计它:

  • 有状态流转
  • 有审计日志
  • 有重试策略
  • 有死信处理
  • 有人工介入开关

补偿不是备胎,它是这套方案的一半。

两条不能踩的红线

红线 1:不能补偿的业务,不要用这套思路硬换。

比如银行转账最终入账、券码唯一核销、核心总账记账。这类业务一旦短暂错误,就不是“晚点修回来”这么简单,而是可能直接变成资损、合规事故或审计问题。

红线 2:团队没有补偿治理能力,也别贸然降一致性。

很多团队不是不会写异步,而是不会处理“重复消息、顺序错乱、补偿重试、人工对账”。结果就是前面省下来的那点事务开销,后面全让值班同学夜里 2 点补回去了。

你可以怎么判断该不该用

如果你看到下面这些信号,说明这套思路值得评估:

  • 高并发下大量时间花在锁等待、串行化失败、事务冲突上
  • 请求一旦跨库、跨分片、跨服务,延迟明显拉长
  • 业务允许“先完成核心动作,外围动作稍后补齐”
  • 团队已经具备幂等、重试、死信、对账这些工程能力

反过来,如果你的核心诉求是“任何时刻都绝不允许短暂错误”,那优先级应该是:保一致性,再谈性能。

最后记 5 个实用结论

  • 先划红线:先写清楚哪些数据必须强一致,哪些数据允许最终一致。
  • 再收范围:优先把事务收敛到单分区、单服务、本地事务里。
  • 把不变量下沉:用原子更新、唯一约束、版本号校验守住底线,不要只靠应用层“先查再改”。
  • 给补偿上护栏:检查幂等键、去重表、状态机、死信队列是不是齐了。
  • 用压测验证:同时测吞吐、P95/P99、补偿成功率和人工介入率,别只盯 QPS 漂亮不漂亮。

如果要把全文浓缩成一句话,那就是:高并发事务里,“一致性换性能”不是偷懒,而是一种有边界、有代价、有补偿能力要求的工程取舍。 用得好,它能救系统;用得随意,它会把问题从数据库里搬到业务层,而且通常更难查。