宁语之溪 - 评论
type: channel
link: https://nyzx.de/tag/comments/
items: 评论计数为什么会遇到锁等待

评论计数为什么会遇到锁等待
type: item
date: 2026-05-09T16:43:00+08:00
description: 在 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可以选择不同策略：为事件分配唯一编号，远端只处理一次；使用评论状态变更事件，由消费者去重；定期按有…