做微服务这几年,我收到最多的技术问题其实翻来覆去就两类:线上服务无缘无故被打垮,然后数据账目对不上。前者是服务保护没做好,后者是分布式事务没捋清。尤其当你把单体应用拆成十几个微服务之后,这两个问题会像影子一样跟着你,躲都躲不掉。
这篇文章我想把这两块硬骨头放在一起讲,原因也很简单:它们都是微服务架构从"能跑"走向"稳定"必须跨过的坎。服务保护解决的是"别让一个故障拖垮全局",分布式事务解决的是"跨服务的数据别因为故障变成烂账"。我会把原理、选型、实操细节和踩坑经验都摊开来说,适合正在维护微服务系统、或者刚拆完服务准备上生产的同学参考。如果你是架构师,这篇文章也能给你一套现成的方案对比口径。
1. 微服务拆完,真正的麻烦才刚开始
1.1 雪崩是怎么发生的
很多人对微服务的理解停留在"把大系统拆小块",但真正上过生产的人都知道,拆完以后最怕的不是某个服务挂了,而是挂一个倒一片。
举个最常见的订单链路:前端请求进来先打到订单服务,订单服务要调库存服务扣库存,还要调账户服务扣余额。假设账户服务某一天因为慢 SQL 响应从 50ms 涨到了 5 秒,订单服务里那些等待响应的线程就会全部堆积。线程池一旦被占满,新的请求进不来,订单服务也跟着雪崩。紧接着网关发现订单服务不可用,开始疯狂重试,流量又打回库存服务,最后整条链路上的服务全部瘫痪。
这就是经典的雪崩效应。它跟多米诺骨牌不一样,骨牌倒完就完了,服务雪崩会通过调用链不断向上传播,即使故障源恢复了,错误也未必能立刻收敛回来。我见过最夸张的一次,一个底层服务的延迟抖动,半小时内把网关后面的所有业务服务全部打挂,整个系统直接进入"假死"状态。
生活里怎么理解这件事?就像早高峰地铁,一个人在地铁口摔倒,后面的人不知情还在往前挤,最后整个出入口堵死,谁也进不去出不来。微服务系统里的服务保护,就是给每道地铁口装上闸机和限流护栏,让一个人摔倒不至于导致全线瘫痪。
1.2 本文要解决的两件事
明确了雪崩的成因,我们就能划清边界。微服务治理里最核心的两个"防御工事"需要重点建设:一是服务保护,包括限流、熔断、降级、隔离,核心目标是"单个故障局部化,别让异常无限传播";二是分布式事务,核心目标是"跨服务的数据操作要么一起成功要么一起失败,或者退而求其次,保证最终一致性"。
这两件事还经常是一起发生的。比如分布式事务里某个分支事务执行超时,触发熔断降级,人一"降级"数据就写入失败,如果事务框架没有做好状态记录,就会出现"钱扣了库存没减"的烂账。所以我的结论是:做微服务的人,不能只懂其中一样。服务保护是外科手术里的止血钳,分布式事务是缝合线,缺了哪样手术都做不透。
2. 服务保护:先保住自己不倒
2.1 超时控制:最便宜的第一道防线
服务保护的第一道防线往往被忽视,就是超时控制。很多系统挂了,不是服务真的不可用,只是响应变慢,但调用方又永远在等,于是好消息变成坏消息。
超时要分成两层看:连接超时和读取超时。连接超时解决"目标服务到底通不通",读取超时解决"目标服务要多久才有响应"。我见过不少团队图省事,把两个超时时间设成一样,比如都是 3 秒,结果前提条件就错了——连接超时通常应该短,1 秒左右就够;读取超时看业务类型,普通接口 2~3 秒,批量接口可以放宽到 5 秒以上。
这里有一个容易被忽略的细节:超时时间要按链路的层级别设置,不能全局一刀切。网关层超时要短一些,因为用户等不起;服务间调用的超时次之;底层基础服务(比如数据库调用)的超时要在连接池里单独设。比如网关 2 秒,Feign 调用 3 秒,MyBatis 连接池超时 4 秒,这样层层设防,才能保证最外层先掐断,不会让流量穿透到最底层。
2.2 限流:把流量挡在门外
超时解决的是"已进入系统的请求别拖死我们",限流解决的是"不合理的流量别进来"。常见的限流算法主要有三种:固定窗口、滑动窗口、令牌桶和漏桶。
我用实际场景来说:某商品秒杀活动,瞬间涌入 10 万请求,订单服务的处理能力只有 1 万 TPS。这时候如果不加限流,服务必然被击穿。固定窗口计数器是最简单的实现,但存在临界问题——窗口切换的一瞬间可能放进来两倍流量;滑动窗口把时间段再切细分,能缓解这个问题;令牌桶算法允许一定程度的突发,因为桶里积攒的令牌可以一次性消耗;漏桶则强制匀速处理,哪怕来了 10 万个请求,我也按每秒 1 万个的速率往外排队。
选择建议上,常规接口限流用滑动窗口,因为它实现简单、精度也够;秒杀、促销这类有突发特征的入口用令牌桶,允许瞬间冲一波但不会持续打满;下游是很弱的依赖(比如第三方 API)时用漏桶,强制限速确保对方不会被冲垮。实际落地用 Sentinel 最省心,它把滑动窗口、令牌桶、漏桶都封装好了,你只要在控制台里配置规则,或者写一段声明式代码就行。
2.3 熔断:给系统装个跳闸开关
限流是站在"入口"防御,熔断是站在"调用关系"上防御。它的思想跟家用电路的保护开关一模一样:电流异常就跳闸断电,保护电器不会被烧毁;调用下游连续失败就熔断开路,后续请求快速失败,不再继续消耗资源。
熔断器的核心状态机有三个:关闭(Closed)、打开(Open)、半开(Half-Open)。关闭状态下请求正常放行,但会记录失败率和慢调用比例;当失败率超过阈值(比如 50%)并且调用量达到最小触发数(比如 10 次),熔断器切换到打开状态,后续请求直接拒绝,快速返回兜底结果;经过一段休眠时间后进入半开状态,放一小部分试探流量,看看下游恢复了没有,恢复正常就切回关闭,否则继续打开。
配置这一块要把两个参数给够:滑动窗口大小和失败率阈值。滑动窗口不能太小,太小容易误判,比如 5 秒内 5 次失败就熔断,某个接口偶尔抖动一下就把主干链路切了,属于误伤;但也不能太大,太大会让熔断反应迟钝,系统已经被打到半死都不触发。按我的经验,线上接口滑动窗口设 10 秒、失败率阈值 50%、最小调用数 20,半开探测放行 1 个请求,这套参数在一半以上业务场景里都合理。
2.4 降级和隔离:保住核心功能
降级和隔离是一对组合拳。降级意味着当系统资源不足时,主动牺牲一些非核心功能,保证核心链路可用。最典型的就是:电影票 App 在购票高峰期,系统压力过大,把"评价列表"这种非关键接口降级成"暂时无法查看";但"下单支付"这种核心功能必须正常服务。
降级的关键是在降级逻辑里绝对不要再做耗时操作。我见过有人把兜底逻辑写成"调另一个下游服务取缓存数据",结果两个服务同时挂了,降级逻辑自己也被卡死,白白占用线程。真正的降级应该有本地预案,比如直接返回默认值、从本地缓存取旧数据,或者依赖已加载的静态配置。
隔离则是个更底层的防御。把不同依赖关系放在不同的线程池或者信号量隔离舱里,避免一个慢调用把整个容器的线程耗尽。线程池隔离的代价更大但隔离最彻底,适合关键链路;信号量隔离更轻量,适合并发不高但需要限制并发数的场景。你可以把服务想象成船上不同的水密舱,即使一个舱进水,也不会导致整艘船沉没。
2.5 工具选型:Hystrix、Resilience4j、Sentinel 怎么选
现在落到工具层面。Spring Cloud 生态里最出名的三件套:Hystrix、Resilience4j、Sentinel。很多同学一上来就问哪个好,我的看法是:
| 维度 | Hystrix | Resilience4j | Sentinel |
|---|---|---|---|
| 维护状态 | 已停止开发 | 活跃维护 | 活跃维护 |
| 熔断 | 支持,状态机简单 | 支持,基于滑动窗口 | 支持,策略更丰富 |
| 限流 | 几乎不擅长 | 不擅长 | 最擅长,内置多种算法 |
| 隔离 | 线程池/信号量 | 线程池/信号量 | 信号量为主,线程池需自己做 |
| 控制台 | 无 | 无 | 有,实时监控+规则推送 |
| 上手难度 | 简单 | 简单 | 稍高 |
Hystrix 当年是标杆,但已经停更了,新项目我不建议再引入。Resilience4j 是官方推荐的轻量替代品,适合"够用就好"的团队,库很小,集成容易,熔断和隔离的 API 写起来也顺手。Sentinel 的优势在限流和可视化监控,尤其适合流量特征明显的业务,比如电商大促、秒杀、突发流量治理。如果团队对运维监控要求高,冲 Sentinel。
我现在的默认选型是:流量入口和业务性限流场景用 Sentinel,服务间调用链上的熔断和线程池隔离用 Resilience4j。二者都能接 Spring Cloud,也不冲突。关键在于别把熔断、限流的规则写死在代码里,一定要用外部配置中心或者 Sentinel 控制台去管理,这样调整策略不需要重新发版。
3. 分布式事务:数据一致性不能靠运气
3.1 为什么要上分布式事务
服务保护解决"活不活"的问题,分布式事务解决"对不对"的问题。单体时代,一笔订单从创建到扣库存再到扣款,都在一个数据库的本地事务里,一个 BEGIN/COMMIT 就能搞定 ACID。
拆成微服务后,订单库、库存库、账户库各自独立,谁也没法再"一次性"控制三个库的事务。经典的订单-库存-账户模型——准备工作如下:订单服务写入"订单表",调用库存服务写入"库存扣减流水",再调用账户服务写入"扣款流水"。如果账户扣款失败,前面已扣的库存必须回滚,否则账就平不了。
关于一致性,这里要想清楚一个概念:最终一致性不等于脏数据。最终一致性允许中间状态存在片刻(比如"已扣款但库存还没扣"),但要求在没有故障干扰的情况下,系统会通过重试、补偿等手段最终达到数据一致。强一致性则要求每个时刻都一致,代价极高。微服务架构里的分布式事务,本质上就是在这条光谱上找位置。
3.2 强一致路线:2PC 与 XA
要聊分布式事务,躲不开 2PC(两阶段提交协议)。它把事务拆成准备阶段和提交阶段:协调者先问所有参与者"准备好了吗",所有人都说 OK 才开始提交;只要有一个人说不行,全员回滚。
XA 是 X/Open 组织定义的基于 2PC 的规范,很多数据库原生支持 XA 协议。优点是一致性最强,实现也简单;缺点是明显的:准备阶段资源被锁住,阻塞时间很长,并发能力下降严重;协调者一旦挂了,参与者既等不到提交也等不到回滚,事务卡死在中间状态。实际生产系统中,纯 2PC 用得很少,除非你是针对少数几个关键服务、低并发场景。
我不建议把 XA 当成微服务分布式事务的默认选项。微服务架构强调独立性和弹性,用 XA 会把各个库的资源强耦合在一起,本质上是"把单体事务拉长跨了服务",系统的可用性和扩展能力都会被拖垮。
3.3 最终一致路线:TCC
TCC 是目前业务上用得非常多的一种方案,核心思想是把一个完整的事务拆成三个阶段:Try(预留资源)、Confirm(确认执行)、Cancel(取消补偿)。
以订单扣款为例:Try 阶段不是真的扣款,而是冻结一笔金额;Confirm 阶段把冻结的金额扣除,真正完成扣款;Cancel 阶段把冻结的金额解冻。这样做的好处是业务控制力很强,不一致窗口小;坏处是对代码侵入极深——你需要为每个事务操作实现三段逻辑,开发量翻倍。
TCC 有两个著名的坑我非说不可:空回滚和悬挂。
先说空回滚。Try 请求因为网络超时丢了,Cancel 请求到了,但业务上根本没有可取消的资源,这时 Cancel 不能报错,必须返回成功,这就是空回滚。处理办法是在事务记录表里做控制,Cancel 发现没有 Try 记录就标记为已取消,直接返回成功。
再说悬挂。Try 请求因为网络阻塞延迟到达,但此时 Cancel 已经执行完,等 Try 终于到了,资源被预留就会永远卡住,形成事务悬挂。解决办法是在 Try 执行前先查事务记录,发现已经有 Cancel 记录就直接拒绝执行。
这就是 TCC 的复杂度所在:框架只能帮你调度三个阶段,但对于"什么叫空回滚""什么叫悬挂"这些业务状态,必须你自己设计和守护。
3.4 Saga 和本地消息表
Saga 模式的核心思路是:把一个长事务拆成一系列子事务,每个子事务有对应的补偿事务。正着执行,失败就反着做补偿,把已经提交的子事务一个个撤销。它跟 TCC 的区别在于不需要预留资源,直接执行业务操作,用补偿去"找回损失",所以实现更简单,但隔离性更差——补偿执行前,别人可能已经读到了中间状态的数据。
Saga 有两种组织方式。编排式(Choreography):每个服务执行完本地事务后,自己发事件触发下一个服务,没有中心调度者;协调式(Orchestration):有一个专门的 Saga 协调服务负责告诉每个参与者"该干什么、出错了怎么补偿"。我建议项目初期用协调式,因为流程可视化强、好排查;编排式事件链一旦长了,出了问题非常难追踪。
本地消息表是另一种务实的方案。核心就是"先写业务,再发消息":在事务参与方本地建一张消息表,业务操作和消息写入放在同一个本地事务里,然后定时任务扫描消息表,把未发送的消息投递到 MQ。下游消费成功后,标记消息为已发送。
这套方案的亮点是理解门槛低,不依赖外部框架,只要每张表多建一张消息表就能跑。缺点是业务代码和消息表耦合,无法做到开箱即用,而且如果定时任务没有及时扫描,消息的时效性会受影响。我用它处理过一些物流状态同步、用户积分变更这类不追求实时性的场景,效果意外地扎实。
3.5 事务消息:RocketMQ 的方案
RocketMQ 提供事务消息,算是把本地消息表的思路内化进了中间件。它的执行过程是这样的:Producer 先发送一条半消息(Half Message),这条消息对 Consumer 不可见;然后 Producer 执行本地事务;本地事务执行成功就提交消息,Consumer 才能看到;执行失败就回滚消息,消息被丢弃。
这里最关键的机制是事务状态回查。如果 Producer 在本地事务执行过程中崩了,RocketMQ 会反向回查生产者,问"你那个本地事务到底成没成功",生产者通过本地事务表记录的状态回答,broker 再决定提交或回滚。
事务消息和本地消息表本质是"异曲同工",但事务消息把状态表和发送逻辑封装进消息中间件里,代码侵入少很多。它是目前很多公司在支持 MQ 的前提下首选的最终一致性方案。唯一要注意的是:它只保证"Producer 本地事务和消息发送一致",如果 Consumer 消费失败或者消息被重复投递,你依然要在 Consumer 端做幂等处理。
3.6 方案对比与选型
把几种主流方案摆在一起看:
| 方案 | 一致性类型 | 代码侵入 | 性能影响 | 适用场景 |
|---|---|---|---|---|
| XA | 强一致 | 中 | 大,资源锁定 | 低并发、且业务和数据强耦合 |
| TCC | 最终一致 | 大,三段式开发 | 中 | 高并发、关键链路、业务可控 |
| Saga | 最终一致 | 中 | 中 | 长流程、跨多个微服务业务 |
| 本地消息表 | 最终一致 | 中 | 较低 | 对实时性要求不高的异步场景 |
| 事务消息 | 最终一致 | 低 | 较低 | 已有 MQ,且依赖清晰可幂等 |
我给非强一致场景的选型口诀是:高并发、钱相关、业务规则严格,用 TCC;流程长、无强隔离要求,用 Saga;有 MQ、可以异步、时效要求不高,用事务消息。
但要记住,任何最终一致方案都离不开"消息不丢、消费幂等"这两条底线。消息不丢靠 MQ 的确认机制和本地状态表;消费幂等靠业务主键去重,比如每条消息带一个全局唯一 ID,消费端先查后写。
4. Seata 实战:订单和库存的落地
4.1 Seata 的整体组成
聊完理论,必须讲落地框架。目前国内用得最广泛的分布式事务开源框架是Seata。它由三个核心角色组成:TC(事务协调者)负责全局事务的注册、提交和回滚决策;TM(事务管理器)负责开启全局事务、发起全局提交或回滚;RM(资源管理器)负责把本地事务纳入全局事务,并执行分支提交/回滚。
Seata 支持四种事务模式:AT、TCC、Saga、XA。其中AT 模式是它的招牌,核心卖点是"对业务代码侵入极小,几乎可以用注解一键开启分布式事务"。
4.2 AT 模式原理
AT 模式是怎么做到侵入小的?秘密在于一张undo_log 表和全局锁机制。
执行流程是这样的:全局事务开启后,每个分支事务解析 SQL,要先查一遍旧数据(前镜像);执行 SQL 操作,再查一遍新数据(后镜像);把数据行锁住,同时向前镜像和后镜像写入 undo_log 表;然后提交本地事务。
如果全局事务要提交,各分支直接提交本地事务就行;如果全局事务要回滚,TC 通知每个分支,RM 根据 undo_log 里的前镜像反向生成"补偿 SQL",把数据改回去,然后删除 undo_log 记录。这套思路很像数据库的 UNDO 日志,只是搬到了业务层。
AT 模式的最大优点是业务方几乎无感,写一个普通的业务方法,加上@GlobalTransactional注解就行了。但这也有代价:全局锁会导致分布式并发场景的数据行长时间锁定,性能有损耗;多表操作时需要额外注意锁的粒度和顺序。
4.3 核心配置与代码
实操之前先把表结构准备好。AT 模式要求每个参与分布式事务的业务库都创建 undo_log 表,建表脚本如下(MySQL 8 为例):
CREATE TABLE `undo_log` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `branch_id` BIGINT NOT NULL, `xid` VARCHAR(128) NOT NULL, `context` VARCHAR(128) NOT NULL, `rollback_info` LONGBLOB NOT NULL, `log_status` INT NOT NULL, `log_created` DATETIME NOT NULL, `log_modified` DATETIME NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;然后在业务服务里引入 Seata 依赖,配置 application.yml:
spring: cloud: alibaba: seata: tx-service-group: my_test_tx_group seata: registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: "" config: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: "" enabled: true application-id: order-service tx-service-group: my_test_tx_group enable-auto-data-source-proxy: true业务方法上只需要加上一个注解:
@Service public class OrderService { @GlobalTransactional public void createOrder(OrderDTO orderDTO) { // 1. 本地事务:写入订单表 orderMapper.insert(orderDTO); // 2. 远程调用:扣减库存 stockFeignClient.deduct(orderDTO.getSkuId(), orderDTO.getQuantity()); // 3. 远程调用:扣减账户余额 accountFeignClient.debit(orderDTO.getUserId(), orderDTO.getAmount()); // 4. 本地事务:更新订单状态为已创建 orderMapper.updateStatus(orderDTO.getOrderId(), OrderStatus.CREATED); } }当createOrder方法执行时,TM 会向 TC 开启全局事务并拿到全局 XID;每次调用远程接口时,XID 会通过 RPC 调用透传给库存服务和账户服务,库存/账户服务里的数据源代理自动把本地数据操作注册为分支事务。任何一个分支抛异常,TC 就能协调所有已注册分支回滚。
这段流程看着简单,背后是数据源代理、全局锁、ucall 链路透传这些机制在默默工作。也正因为它"看起来太简单",很多人出了问题时根本无从下手排查。
4.4 不回滚的排查思路
用 AT 模式最常见的问题就是"明明挂了异常,数据却不回滚"。我的排查清单如下:
先看全局事务有没有真正开启。加@GlobalTransactional只会对当前方法所在 Bean 生效,如果方法内部用了本地this调用(就是同类里方法直接调方法),注解就没生效,因为代理没有拦截内部调用。这个坑我至少看到过十次。
再看数据源代理有没有生效。Seata 需要代理业务数据源,如果配置里关闭了enable-auto-data-source-proxy或者项目里自定义了数据源且没有交给 Seata 代理,分支事务根本不会被拦截,自然也没有回滚可言。
再看 undo_log 表对不对。表名、字段如果和 Seata 要求的不一致,回滚时解析前镜像会失败。我建议表结构严格用官方脚本建,不要自己在基础表上改字段。
然后检查线程和网络。全局事务超时时间如果设置太短,比如默认 60 秒不够,长事务直接被 TC 强制回滚,也会出现"莫名回滚"的现象。反过来,超时太长又影响性能,需要针对业务压测后合理配置。
5. 常见问题与避坑锦囊
5.1 服务保护容易踩的坑
第一个坑是熔断参数过于敏感。某次线上监控显示熔断频繁触发,排查发现阈值设置得太低,慢调用比例超过 20% 就熔断,结果一个接口因为促销流量正常变慢,直接被熔断,核心链路瘫痪。参数不是越小越安全,而是要结合实际压测数据调整。
第二个坑是降级逻辑里又做了远程调用。前面提过,降级是为了"快速失败",如果降级代码里又去查数据库、调远程服务,一旦这个被调的服务也处于故障状态,降级线程照样卡死。降级必须走本地预案,比如本地缓存、默认值。
第三个坑是限流没有对用户维度和接口维度分开。只说接口全局限流,某个用户疯狂刷接口,其他用户请求全被挤掉。合理做法是针对接口设置总限流,再针对用户设置子限流,比如"每个用户每秒最多 5 次,所有用户总和每秒最多 5000 次"。
5.2 分布式事务容易踩的坑
分布式事务的坑比服务保护更深。
最常见的还是回滚不生效。我见过一个经典场景:库存服务调用成功,账户服务调用失败,理论上库存应该回滚,但实际库存没回滚。排查发现库存服务的 Feign 接口上没有透传 XID,导致库存服务的分支事务根本没有注册到全局事务里。一般 Seata 的 RPC 拦截器会自动透传,但如果你的参数里手动改了 Feign 请求头、或者用了自定义的RequestInterceptor,很可能把 Seata 的透传逻辑覆盖了。
第二个高频问题就是重复消费导致的数据不一致。事务消息投递到 MQ 后,消费端的"消费成功"和"业务数据落库"如果不在一个本地事务里,消息重试时就会重复执行业务逻辑。没有幂等设计的场景,扣款两次、加积分两次就是这么来的。解决方案是在消费端先查业务幂等表,或者利用数据库主键冲突去重。
第三个是TCC 的空回滚和悬挂。这块前面详细讲过,这里再强调一次:TCC 框架本身不会帮你处理空回滚和悬挂,业务方必须在事务记录表上自己照顾两种边界状态。
第四是长事务和性能的矛盾。AT、XA 都会锁资源,全局事务牵扯的服务越多、耗时越长,锁竞争就越激烈。我在一个订单场景里见过全局事务跨了 6 个服务,高峰期光等全局锁就等出 3 秒延迟,直接把接口拖垮。后面把不重要的服务从全局事务里拆出来,用本地消息表异步化,性能立刻提升一大截。
5.3 排查工具与方法
排查这些线上问题时,我的通用方法论是"先找链路,再定状态"。
链路分析靠TraceId。把网关和服务间调用的日志全部统一格式,每次请求生成全局 TraceId,日志里打出来,跨服务追问题就方便了。Seata 的全局事务也带 XID,把 XID 和 TraceId 关联起来,基本能把"业务走到哪一步、事务状态是什么"还原出来。
状态确认靠"日志+事务记录表"。TCC 方案要实时关注事务表里 Try、Confirm、Cancel 的状态;AT 方案要关注 undo_log 表是否存在异常残留——如果某次回滚失败,undo_log 里的数据会一直留着,这本身就是排查线索。进一步还可以通过监控 MQ 的未消费堆积量、接口的熔断事件数来辅助定位。
另外一点:每次线上事故处理完,我都强烈建议把"事故复盘记录"沉淀成文档。因为服务保护和分布式事务的问题,很多是环境、时间、流量综合作用的结果,不会每次都复现,但记录下来以后,同类问题再出现时排查时间能缩短一半以上。
最后再分享一点实际体会
我做微服务的真实感受是:服务保护和分布式事务,哪个单独拿出来都不算难,难的是它们交织在一起时的取舍。保护措施太激进,可能导致该成功的事务被降级掉、造成数据不一致;事务方案太重,又会拖垮系统性能和可用性。选型前一定要把业务的容忍度想清楚,把钱和核心状态放在 TCC 或强一致方案里,把通知、日志、积分这类不敏感的数据大胆丢到异步消息里,安全感和性能往往能同时兼顾。你踩过的那些坑,最后都会变成你方案落地的底气。