在 HAOVP 的接口验收中，一条互动链路曾因为跨服务同步回写计数出现锁等待。报告中明确记录的实际问题发生在视频收藏后同步更新用户收藏数；评论创建和删除也有相似的“本地记录 + 远端计数”结构，因此适合用来分析同一类事务时序问题。这里需要先把两条链路分开：收藏链已经出现过锁等待；评论链存在同类跨服务计数更新，但目前没有同样的故障记录。它们共享一组事务时序问题，却不是同一次故障。一次评论操作涉及两份状态创建评论时，至少有两类数据变化：评论服务 -> 新增评论记录 内容服务 -> 视频评论数加一删除评论时则反向执行：评论状态改变，视频评论数减一。这两份数据属于不同服务和数据库事务。单个 @Transactional 只能保证当前服务本地事务，不能把一次 HTTP 或 Feign 调用自动纳入同一数据库事务。为什么事务内同步调用容易拖长锁时间假设评论服务在本地事务尚未提交时，同步调用内容服务更新计数：评论事务开始 -> 写入评论，持有本地锁 -> 同步调用内容服务 -> 内容服务开启自己的事务 -> 更新视频计数 -> 等待数据库资源或其他调用 -> 远端返回 -> 评论事务提交远端调用时间会被包含在本地事务持续时间内。网络等待、连接池等待、下游数据库锁和重试都会让本地事务更久才释放资源。如果另一条链路以不同顺序操作相同资源，就可能形成等待环：请求 A：先锁评论相关数据，再等待视频相关更新 请求 B：先锁视频相关数据，再等待评论相关更新这只是解释锁等待机制的通用时序，不对应报告中已经确认的具体 SQL 和锁顺序。现有材料没有给出死锁日志、锁记录或并发量，因此不能编造是哪一条 SQL 锁住哪一行。锁等待不一定等于死锁两者需要区分：锁等待：一个事务等待另一个事务释放资源，可能随后成功；锁等待超时：等待超过数据库配置时间后失败；死锁：事务之间形成循环等待，数据库通常会主动回滚其中一个事务。接口表现为“很慢”或超时，只能说明需要继续查事务和依赖，不能直接下结论为死锁。我会收集：请求开始、下游调用和事务结束的时间；数据库当前事务与锁等待信息；慢查询和异常日志；涉及表的索引与更新条件；是否存在重试；多条链路更新资源的先后顺序。没有这些证据时，更准确的说法是“出现锁等待或锁等待相关慢失败”。为什么把回写移出主事务收藏链的处理方式是把非核心计数回写调整为 afterCommit + async best-effort。顺序变为：完成本地核心写入 -> 本地事务提交 -> 启动异步任务 -> 调用远端更新计数 -> 失败时记录日志afterCommit 的意义是：只有本地事实已经提交，才通知其他服务更新派生计数。这样既缩短了本地事务持锁时间，也避免本地事务回滚后远端计数先被更新。一个简化结构是：public void runAfterCommit(Runnable task) { TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { @Override public void afterCommit() { CompletableFuture.runAsync(task); } } ); }这是重写后的结构示例，不包含实际服务、方法和线程池配置。只有异步，没有 afterCommit 会怎样评论计数代码在本地写入后启动异步任务。异步任务不等待主请求返回，但它可能在本地事务提交前开始。一种可能时序是：本地事务写入评论 -> 异步任务启动 -> 远端评论数加一 -> 本地事务随后回滚最终可能出现评论不存在，但视频评论数已经增加。另一种时序是异步任务读取依赖数据时，本地事务尚未提交，导致它看不到新状态。异步化减少等待，却不会自动保证执行顺序和一致性。因此，评论计数如果继续采用异步回写，也应该明确事务提交点、失败补偿和幂等规则。best-effort 到底是什么意思best-effort 表示尽力执行：主业务成功后尝试更新派生数据，失败通常只记录日志，不让已经成功的主事务回滚。它适合某些可重建、允许短时误差的计数，但代价是：异步任务可能执行失败；进程退出时任务可能尚未完成；没有持久化队列时任务无法可靠重放；网络超时后无法确定远端是否已经执行；直接重试可能造成重复加减；计数可能与明细数据暂时或长期不一致。所以 best-effort 不是“最终一定成功”，也不是消息队列的替代品。计数更新必须考虑幂等使用 +1、-1 传递增量很简单，但重试很危险：远端已经 +1 -> 响应在网络中丢失 -> 调用方认为失败并重试 -> 远端再次 +1可以选择不同策略：为事件分配唯一编号，远端只处理一次；使用评论状态变更事件，由消费者去重；定期按有效评论明细重新计算计数；将计数视为可修复的派生数据，提供对账任务；对关键场景直接查询明细，而不是完全依赖计数字段。哪种方式合适取决于实时性、数据量和故障容忍度。为什么不能简单使用分布式事务包住全部操作把两个服务的数据库更新放进强一致分布式事务，看起来可以一次解决问题，但会增加协议、锁持有、故障恢复和运维复杂度。对于评论数、收藏数这类派生计数，更常见的取舍是：明细记录作为事实来源 计数字段作为查询优化 允许短时不一致 通过事件、重试和对账恢复这不代表所有业务都适合最终一致。余额、库存扣减或权限变更等场景需要单独评估，不能直接套用互动计数的策略。怎样验证调整后的链路我不会只看接口变快，而会同时检查主记录和派生计数。正常流程新增评论后，评论记录存在；评论数最终增加一次；删除评论后，状态正确；评论数最终减少一次。事务失败本地写入回滚时，不应更新远端计数；远端更新失败时，主记录保持正确；失败任务有可定位日志或持久化记录。重试与重复同一请求重复提交不会重复创建；同一计数事件重复执行不会重复累加；超时后重试能判断第一次是否生效。对账SELECT video_id, COUNT(*) FROM comment WHERE status = 1 GROUP BY video_id;这是通用示例表名。将有效评论明细汇总与视频计数字段比较，可以发现漂移，但对账脚本还要考虑逻辑删除、审核状态和统计口径。线程池也是边界直接使用 CompletableFuture.runAsync 时，默认线程池是否适合业务负载需要单独评估。需要关注：队列是否有界；线程数量和拒绝策略；上下文和日志标识是否传递；应用停机时如何等待任务；下游故障时任务是否快速堆积；不同类型任务是否相互争抢资源。异步任务没有受控线程池和监控，也可能把同步等待变成后台资源耗尽。这次问题能确认到哪里实际验收能够确认：收藏数同步回写曾遇到锁等待，调整为本地事务提交后异步尽力更新，相关接口链路恢复。评论服务能够确认：评论创建、删除与视频评论数属于跨服务的两份状态，当前代码使用异步回写；这具有相似的事务时序与一致性风险，但材料没有证明评论计数实际发生了同一场锁等待。因此，解决方向不是简单地“把所有调用改成异步”，而是先决定哪份数据是事实来源，再明确提交顺序、幂等、失败重试和对账。afterCommit 可以缩短主事务并避免回滚后的提前回写，异步可以减少用户等待，但可靠一致仍需要持久化事件或补偿机制完成。