宁语之溪 - 语之溪
type: channel
link: https://nyzx.de/author/1/
description: 语之溪
items: AI 辅助研发中，为什么最终责任仍然在开发者
items: HAOVP 所谓高可用，边界到底在哪里
items: 高可用设计，如何落到可验证的部署方案
items: 代码生成之后，我如何做编译、接口验证和验收
items: MinIO 多节点同步不等于纠删码集群
items: RocketMQ 异步消息链路中的降级思路
items: 评论计数为什么会遇到锁等待
items: 旧 JAR 与源码不一致时，如何定位部署版本问题
items: 一次在线视频平台接口验收记录
items: Redis 缓存一致性：从热点数据到失效策略

AI 辅助研发中，为什么最终责任仍然在开发者
type: item
date: 2026-06-20T16:43:00+08:00
description: 从 2022 年第一次接触 ChatGPT，到现在使用 Codex 辅助 HAOVP 开发，我对 AI 的使用已经从对话和学习，逐步走到代码理解、SQL 与接口分析、问题排查、需求拆解、代码实现、测试补充和文档整理。工具能做的事情越来越多，但最终责任没有从开发者身上移开。相反，当 AI 可以一次生成更多代码、修改更多文件时，开发者需要判断的范围也更大。我的 AI 使用方式是怎样变化的这几年的变化并不是突然出现一套完整流程。2022 -> 对话、学习和了解生成式 AI 2023 -> 学习、代码理解、问题分析和文档辅助 2024 -> SQL、接口分析、代码排查和资料整理 2025 -> 更稳定地用于需求拆解、联调、问题和文档 2025 年 12 月以后 -> Codex 参与 HAOVP 的实现、重构、测试补充和排障早期使用方式比较简单，不能把现在的工程协作流程提前放进 2022 或 2023 年。到 HAOVP 阶段，AI 才真正进入较完整的研发链路。辅助实现不等于承担责任在 HAOVP 中，我负责：理解需求和确定业务边界；做技术和架构决策；拆分任务并组织上下文；审查代码和修改范围；判断测试是否充分；完成前后端联调；决定是否通过最终验收。Codex 辅助：生成或修改局部代码；做局部重构；补充测试结构；分析错误和排查方向；整理文档和部署说明。这两组工作可以相互配合，但不能交换责任。AI 不会因为生成了代码，就自动理解所有业务后果；也不会因为给出测试，就自动证明测试覆盖了真正风险。需求责任仍在开发者AI 只能根据提供的上下文工作。如果需求中遗漏了权限、状态流转或数据边界，它可能生成一套内部自洽、业务上却错误的实现。我需要先回答：谁可以执行这个操作 操作前需要什么状态 成功后哪些数据变化 失败时怎样回滚 是否允许重复请求 与其他服务有什么关系 哪些信息可以公开例如，“开放用户资料查询”不能被理解为取消整个用户模块的认证；“评论置顶”还要验证操作者身份和评论所属资源。上下文没有写清楚，代码看起来完整也可能放宽权限。架构选择不能由局部代码决定AI 很擅长根据局部目标给出实现，但架构选择需要知道全局约束。把一次跨服务计数更新改成异步调用，可能缩短主请求，却引入失败补偿和重复计数；增加缓存可能降低查询压力，却多出一致性和失效问题；增加一个副本可能改善恢复能力，却不等于自动切换。开发者需要判断：这项数据是否要求强一致；失败是否可重试；是否能从明细重建；运维复杂度是否可接受；当前项目是否真的需要新组件；方案有没有可验证的方法。代码能运行，只是架构判断的一部分。代码审查不能只检查语法AI 生成的代码通常可以很快达到语法正确，但审查还要看：事务边界是否合理；远端调用是否放在本地事务中；包装类型和值比较是否正确；异常是否被吞掉；日志是否包含敏感信息；默认值是否会进入生产环境；并发与重试是否幂等；公共接口变更是否影响其他模块；回滚和兼容方式是否存在。4 月接口验收中出现的旧 JAR、状态码比较、锁等待、审核身份透传、外键和 MyBatis 映射问题，都说明编译通过并不足以证明实现正确。测试设计不能只让 AI 自证让生成代码的同一个上下文同时生成测试，很容易让测试沿着实现假设前进。如果实现和测试共享同一个错误前提，测试可能全部通过。我会从业务结果反向设计用例：正常请求是否成功 缺少必要条件是否拒绝 无权限身份是否拒绝 重复请求是否安全 事务失败是否回滚 异步失败是否可追踪 数据是否写入正确位置 后续查询是否与写入一致还要有独立的负向用例和副作用检查。HTTP 返回 200 不是唯一判断，预期被拒绝的请求得到 400 或 403 也可能是正确结果。通过率必须带上测试范围HAOVP 的一轮后端验收中，三批共 115 个预设用例均符合预期。这个结果可以说明当时环境中的指定链路通过了这些检查。它不能证明：所有接口和参数组合都被覆盖；前端全部交互已经验证；高并发和长期运行稳定；多节点故障切换成功；消息、缓存、数据库和对象存储没有已知边界；系统已经达到生产质量。开发者对外说明结果时，需要同时说明环境、版本、用例范围和未覆盖部分。数字如果脱离口径，会比没有数字更容易误导。运行环境也属于责任范围源码正确不代表运行的是这份源码。旧 JAR 问题让我把版本链加入验收：源码 -> 构建产物 -> 镜像 -> 运行容器 -> 实际流量入口AI 修改代码后，开发者仍要确认构建成功、镜像更新、容器重建和接口行为对齐。否则测试的可能是另一份旧版本。配置同样如此。Nacos、环境变量和本地配置有不同优先级，修改了一个未被加载的文件，运行行为不会变化。数据安全不能交给自动生成AI 可能从项目上下文中看到：数据库连接和内部地址；账号、密码、Token 与密钥；测试用户和资源标识；日志中的…

HAOVP 所谓高可用，边界到底在哪里
type: item
date: 2026-06-13T16:46:00+08:00
description: HAOVP 的名字里有“高可用”，项目设计也把高可用作为重点。项目做到这个阶段，我需要把这个词重新拆开：哪些只是技术方案，哪些已经进入仓库和部署文件，哪些经过了实际验证，以及哪些故障发生时系统仍然会不可用。如果只看组件清单，Nacos、Gateway、Sentinel、Redis Sentinel、MySQL 主从、多节点 Compose、MinIO 副本、RocketMQ、Prometheus 和 Grafana 都与高可用有关。但把这些组件放进项目，不等于整套系统已经达到生产级高可用。我用三层证据描述能力为了避免把设计目标写成结果，我把每项能力分成三层：理论设计 -> 这个方案理论上解决什么问题 仓库实现 -> 依赖、代码、配置和部署文件实际有什么 已验证结果 -> 在什么环境、什么前提和什么故障下得到过什么结果三层之间不能跳级。配置文件存在，只能证明项目有相应配置；容器启动成功，只能证明进程进入运行状态；接口能够返回预期结果，也不能自动证明节点切换和数据恢复已经完成。当前部署形态分别解决什么项目提供三种部署方式：部署形态适合场景主要边界单机 Docker Compose开发、联调和单机演示宿主机故障时整套服务可能同时不可用1Panel 单机部署单机服务器部署与维护仍然存在单主机、单磁盘和单网络入口风险Debian 多节点编排验证多实例与基础组件拓扑有编排文件不等于所有自动切换都已验证单机环境对复现和接口验收很有价值。4 月 18 日的 115 条分批接口用例就是在单机 Docker Compose 环境完成的。但这批结果不能用于证明多节点容灾，因为测试环境没有发生跨主机调度和故障切换。统一入口的边界Gateway 提供统一路由、JWT 校验、公开路径控制、CORS 和流量治理入口。客户端只需要访问统一入口，不必知道每个业务服务的位置。外部入口验收中，29 条预设用例得到预期结果，其中包括正常业务请求和预期被拒绝的认证请求。这能够说明当时的入口、路由、认证及若干核心业务链路可以连通。它不能证明：Gateway 已经部署多个实例并由外部负载入口自动切换；网关宿主机不可用时流量会迁移；所有动态路由变更都能无损生效；高并发下网关不会成为瓶颈；入口证书、DNS 和反向代理没有单点。统一入口解决访问收敛问题，但统一入口本身也需要冗余和健康检查。Nacos 的边界多节点方案为 Nacos 安排了多个节点，业务服务通过 Nacos 获取服务实例和配置。它能减少服务地址写死，也为多实例调用提供基础。但需要区分三种状态：Nacos 进程存活 < Nacos 集群能够选举并提供服务 < 业务服务能发现正确实例并完成完整调用注册中心看到服务健康，不代表数据库、Redis、对象存储和业务依赖都正常。服务可能成功注册，却因为配置错误或下游故障无法处理请求。当前文档提供了多节点配置和健康检查方法，但没有足够的故障演练记录证明：任意 Nacos 节点停止、网络分区或共享数据异常时，所有服务都能按目标恢复。业务服务多实例的边界服务多实例可以在单个实例停止时保留其他副本，但前提很多：请求入口能发现并选择健康实例；服务没有依赖本机临时状态；会话状态可以跨实例使用；数据库和缓存仍然可用；上传中的文件和异步任务能够恢复；超时、重试和幂等策略不会放大故障。一个服务启动两个容器，只能说明存在两个进程。要确认多实例容错，至少要在持续请求下停止一个实例，观察失败比例、恢复时间、连接池和日志，再验证业务副作用是否正确。目前仓库有多节点业务服务编排，但现有验收主要是接口功能和外部入口链路，不足以给出多实例切换时间或无损恢复结论。Sentinel 的边界Sentinel 可以提供限流、熔断和降级能力：限流：控制进入资源的流量 熔断：暂时停止持续失败或过慢的调用 降级：决定受保护时返回什么项目已经接入 Sentinel 相关依赖和配置方向，但规则存在不等于保护生效。每条规则都需要受控流量、慢调用、异常比例和恢复探测验证。Sentinel 也不能替代：数据库索引和查询优化；事务、幂等和消息可靠性；容器资源隔离；多节点部署；容量评估和根因修复。熔断可以限制故障扩散，却不会把失效的下游自动修好。Redis Sentinel 的边界多节点方案使用 Redis 主从和 Sentinel。Sentinel 可以监视 Redis 节点并参与主节点切换，业务服务通过 Sentinel 配置寻找当前主节点。但高可用不仅是“Sentinel 选出新主库”：主从复制可能存在延迟；故障前最后一部分写入可能尚未复制；客户端需要断开旧连接并连接新主节点；切换期间命令可能失败或重试；重试写操作需要考虑幂等；缓存数据与数据库事实仍可能不一致。Redis Sentinel 解决 Redis 服务连续性的一部分，…

高可用设计，如何落到可验证的部署方案
type: item
date: 2026-06-06T16:38:00+08:00
description: 在 HAOVP 的高可用设计中，我把服务发现、流量控制、熔断降级、数据复制、缓存、对象存储和监控放进同一套能力框架。要让它成为可验证的部署方案，还需要把每项主张转换成明确的部署对象、故障操作、观测指标和判定标准。目前 HAOVP 已经有单机 Compose、1Panel 和多节点 Compose 文档，也完成了一轮接口与外部入口验收。但这些材料主要证明系统能够部署、启动和处理指定用例，不能直接证明节点故障时会自动恢复。先把设计主张写成可测试句子“系统具备高可用”无法直接测试。我会把它拆成更小的句子：停止一个业务实例后，其他实例仍能处理指定请求 停止一个注册中心节点后，服务发现仍能工作 Redis 主节点不可用后，客户端能重新连接可写节点 MySQL 主库不可用后，能明确完成选主和流量迁移 对象存储主写入点不可用后，能够从副本恢复指定对象 消息组件不可用时，核心业务按约定降级每句话都要有前置条件、操作步骤和可观察结果。如果一句话无法设计验证方法，它通常还只是架构口号。建立设计、实现和验证映射表我会为每个高可用方向建立一张映射表：能力方向方案设计仓库实现当前验证仍缺什么服务发现多节点注册中心Nacos 配置与多节点编排正常环境下服务可发现节点停止与网络分区结果流量保护限流、熔断、降级Sentinel 依赖与配置方向接口功能验收受控流量与恢复探测数据库主从与恢复一个主库、多个从库配置复制方案文档自动选主、隔离与入口迁移Redis主从与 SentinelSentinel 配置与连接方式正常状态检查方法实际切换时间与状态完整性对象存储多副本主写入点与单向镜像脚本对象存储功能链路同步调度、接管和回切消息链异步解耦与降级RocketMQ 与主表降级更新指定接口核心结果符合预期路由恢复、幂等、重放与对账这张表的作用不是让所有格子都填成“完成”，而是防止设计目标和仓库现状被写在同一层。单机接口通过不能替代多节点验证4 月 18 日的三批后端接口验收共有 115 个预设用例，均符合预期；外部统一入口另有 29 个用例符合预期。这可以支持：正常环境下主要接口链路可运行；入口、认证和业务路由能处理指定请求；正向和负向用例的结果符合预期；一些运行版本、权限、事务和映射问题已经修复。它不能支持：任意服务实例停止后请求无损迁移；MySQL 或 Redis 自动切换；Nacos 网络分区恢复；MinIO 副本自动接管；消息积压恢复后不丢不重；高并发和长期稳定性。两组验收也可能覆盖相同链路，不能相加为 144 个独立接口。为故障测试建立正常基线在注入故障前，先建立可重复的正常状态：固定代码和镜像版本 固定配置版本 准备脱敏测试数据 启动持续请求 记录正常成功率与延迟 记录服务、数据库和中间件状态如果正常基线本身不稳定，故障后的变化无法归因。基线还要确认运行产物与源码一致。4 月验收曾发现旧 JAR 问题，因此每次测试前都需要记录源码、JAR、镜像和容器版本。业务服务实例怎样验证服务多实例的测试可以从一个明确接口开始：持续请求统一入口 -> 确认请求落到多个实例 -> 停止其中一个实例 -> 记录失败请求和发现时间 -> 观察流量是否只到健康实例 -> 恢复实例并检查重新注册需要记录：故障前后实例列表；请求失败类型和数量；服务发现剔除与恢复时间；是否有本地会话或临时文件丢失；重试是否造成重复写入。只停止进程后看到另一个容器仍是绿色，不能证明业务请求已经完成切换。Nacos 节点怎样验证注册中心测试要区分：单节点进程停止 节点间网络中断 共享数据库异常 客户端与集群之间网络中断观测内容包括：集群成员和健康状态；已注册实例是否仍可查询；新实例能否注册；配置能否读取和更新；已运行服务是否继续使用本地缓存信息；网络恢复后是否自动追平。不同故障的表现不一样，不能用一次停止容器覆盖所有 Nacos 高可用结论。Sentinel 规则怎样验证规则存在只是准备。验证至少需要：正常基线 -> 逐步增加受控流量或制造慢调用 -> 观察限流或熔断触发 -> 检查返回与日志 -> 确认下游资源得到保护 -> 恢复依赖 -> 观察半开和恢复阈值需要结合接口耗时、实例数量、连接池和数据库容量确定。没有这些信息时，只能使用初始实验值，不能写成系统承载能力。Redis Sentinel 怎样验证Redis 测试不能停在“新主节点出现”。完整流程需要：持续执行可判定的读写；停止当前主节点；记录 Sentinel 发现与选主过程；观察应用连接失败与恢复；核对切换前最后写入是否存在；恢复旧节点并确认它不再接受错误写入；检查验证码、令牌或权限状态的业务影响。要区分缓存可丢数据和认证状态。某些缓存可以从数据库重建，令牌和临时状态丢失则可能直接影响用户会话。MySQL 主从怎样验证当前多节点方案确认了主从…

代码生成之后，我如何做编译、接口验证和验收
type: item
date: 2026-05-30T16:41:00+08:00
description: 从 2025 年 12 月开始开发 HAOVP 后，Codex 逐渐参与代码实现、局部重构、测试补充、问题排查和文档整理。但代码生成完成，只代表得到了一份待验证的变更，不代表功能已经完成。在这套协作里，我负责需求理解、技术与架构决策、任务拆解、上下文组织、代码审查、测试判断、前后端联调和最终验收。Codex 可以加快实现，但是否接受一项变更，仍要由我根据代码、构建和运行结果判断。先确认生成代码解决的是正确问题开始看代码前，我先回到需求本身：输入是什么 正常结果是什么 哪些请求必须拒绝 会修改哪些数据 失败时怎样回滚或补偿 与哪些服务和前端页面有关如果任务定义不清楚，即使代码能够编译，也可能实现了错误的规则。比如“公开资料接口”不等于整个用户模块都可以匿名访问；“统计消息降级”也不等于消息失败时所有数据都已完整更新。因此，我会先检查变更是否符合业务边界，再判断实现细节。阅读差异，而不是只看最终文件AI 可能一次修改多个文件。直接打开最终代码容易漏掉无关改动，我更习惯先看差异：git status git diff --stat git diff检查重点包括：是否只改了任务需要的模块；公共 DTO 或接口签名是否影响其他服务；是否意外删除原有校验；异常是否被静默吞掉；事务边界是否扩大；是否引入硬编码地址、密码或测试数据；日志是否泄露 Token 和个人信息；新增依赖是否真的需要。命名和代码风格也要与项目现有习惯一致，但格式正确不能代替逻辑正确。编译是第一道机械检查代码审查后，我先让项目完成编译和打包：mvn clean package具体是否运行全部测试要按项目当前状态决定。构建至少能发现：类型、导包和方法签名错误；模块依赖不完整；接口修改后调用方没有同步；资源文件或配置无法加载；测试代码不能编译。但“BUILD SUCCESS”只说明构建过程完成，不说明接口权限、数据库副作用和跨服务调用正确。前端也需要独立检查：npm run build类型检查和生产构建能够发现一部分接口字段、组件属性和路由问题，却仍不能代替浏览器中的真实交互验证。确认运行的就是刚构建的代码4 月验收中，我发现部分容器运行的是旧 JAR，与本地源码不一致。这让我把版本核对放到接口测试之前：源码状态 -> 新 JAR -> 新镜像 -> 重新创建的容器 -> 启动日志和版本信息只重启容器不会重新编译代码；只生成 JAR 也不会自动替换镜像。若运行版本不明确，后面的成功与失败都无法准确归因。接口验证同时覆盖允许和拒绝功能接口不能只测成功路径。以认证和权限为例，我会同时验证：合法身份能够完成操作；缺少必要校验信息时被拒绝；普通用户不能调用内部管理接口；资源所有者与非所有者得到不同结果；被禁用账号不能继续登录；无效或过期状态得到明确响应。用例是否通过，要比较实际结果和预期，而不是机械要求 HTTP 状态全部为 200。boolean passed = actualHttp == expectedHttp && businessStateMatches() && sideEffectMatches();这是通用判断结构，不对应真实验收脚本。响应正确还要检查副作用接口返回成功只是表面结果。写操作还要检查：数据是否写入正确表；外键和资源归属是否正确；事务失败时是否回滚；重复请求是否产生重复数据；派生计数是否只更新一次；异步任务失败后是否留下可追踪记录；后续查询能否读回刚才的变更。4 月验收中出现过跨服务返回值判断、收藏数锁等待、审核身份透传、外键指向和 MyBatis 映射等问题。这些问题有些接口可以返回，有些代码也能编译，只有结合数据和业务前提才会暴露。分批验收比一条长脚本更容易定位HAOVP 后端验收最终分为三批：认证与用户、内容评论推荐、后台管理。三份结构化结果分别记录 29、63 和 23 个用例，合计 115 个，均符合预期。分批的价值在于：每批可以独立准备数据；某个请求失败不会让全部结果失去意义；修复后可以只重跑相关链路；模块之间的前置依赖更清楚；结果可以用 JSON 再次统计。这个 115/115 只代表当时单机 Docker Compose 环境中的 115 个预设用例符合预期。它不等于全部接口、所有参数、高并发、多节点故障和前端交互都已经验收。失败时先判断是哪一层接口失败后，我不会立即让 AI 再改一次代码，而是先定位层级：需求或测试前提错误 源码逻辑错误 构建产物过期 容器或配置未更新 网关与权限规则错误 跨服务调用错误 数据库约束或事务问题 消息、缓存或对象存储异常例如，评论置顶失败曾与测试身份和资源关系有关；旧 JAR 则是运行版本问题。若不先分层，AI 很容易根据错误现象继续修改无关代码。我会把日志、最小复现步骤和已经确认的事实组织成上下文，再让 …

MinIO 多节点同步不等于纠删码集群
type: item
date: 2026-05-23T16:44:00+08:00
description: HAOVP 的多节点部署中，每个节点都可以运行 MinIO 实例，但业务写入仍然统一指向主写入点。仓库提供了一条从主写入点到一个副本节点的对象镜像脚本，另一个实例作为备用方向，并没有进入这条已定义的同步链。这种方案能增加一份对象副本，却不能称为 MinIO 原生分布式纠删码集群。两者在写入路径、故障处理、一致性和恢复方式上都有根本区别。当前方案的真实结构可以把当前拓扑简化为：用户服务 / 内容服务 -> 主 MinIO 写入点 -> 单向镜像 -> 副本 MinIO 备用 MinIO 实例 -> 当前没有进入已定义的自动同步链多节点业务服务仍把对象写入目标指向同一个主节点。也就是说，即使多个 MinIO 容器都在运行，业务写路径仍有单点。同步脚本使用对象存储客户端执行单向镜像并覆盖目标同名对象：mc mirror --overwrite source/bucket replica/bucket这是脱敏结构示例，不包含真实别名、地址、凭据和桶名。脚本存在可以证明项目具备一条可执行的镜像方案，但不能证明它已经被定时调度、每次运行成功，也不能证明所有对象始终一致。镜像副本解决什么单向镜像主要解决的是“额外保留一份对象数据”。当主节点磁盘损坏或数据误丢失时，副本可能提供恢复来源。它可以带来：对象的第二份存储；简单、容易理解的同步流程；副本节点上的人工核对与恢复可能；在当前规模下较低的部署复杂度。但它更接近备份或异步副本，而不是一个统一对外服务的分布式存储集群。纠删码集群是什么思路原生分布式纠删码会把对象拆分为数据片和校验片，分布在多个磁盘或节点上。读取和写入由集群共同完成，系统根据可用分片重建数据。抽象来看：对象 -> 数据分片 + 校验分片 -> 分布到多个节点或磁盘 -> 部分分片不可用时仍可读写或恢复集群成员、仲裁、分片布局和故障容忍由存储系统自身管理。而镜像方案是：完整对象先写主节点 -> 稍后把完整对象复制到副本节点它没有分片、校验片和集群级仲裁，也没有自动把业务流量切换到副本。为什么多个 MinIO 实例不等于集群看到三台主机各有一个 MinIO 容器，很容易在架构图上画成“MinIO 集群”。但判断是否为集群，需要看它们是否以一个统一拓扑协同工作。我会检查：启动命令是否包含多个节点和磁盘；各实例是否共享集群成员信息；对象写入是否由集群分布到不同节点；客户端是否访问统一、可切换的入口；节点故障时是否由存储系统继续完成读写；恢复节点能否自动重新同步分片。当前各节点是独立实例，业务指向主写入点，副本依赖外部镜像脚本，因此不能按原生集群描述。同步延迟会形成数据窗口单向镜像不是与主写入事务同时完成。新对象写入主节点后，到下一次镜像完成前，副本中可能还没有该对象。T1：对象写入主节点成功 T2：主节点发生故障 T3：同步任务尚未运行如果故障发生在这个窗口，副本无法提供刚写入的对象。要描述副本能恢复到哪个时间点，需要知道：同步多久执行一次；单次同步需要多久；失败后何时重试；如何发现漏同步；最近一次成功同步的位置或时间。当前没有经过验证的调度与恢复结果，因此我不能给出固定恢复点或“零数据丢失”承诺。覆盖同步还要考虑删除脚本中的 --overwrite 会让目标同名对象使用源端版本覆盖，但覆盖不等于完整处理删除语义。需要明确：源端删除对象后，目标是否保留旧副本；如果同步删除，误删会不会扩散到副本；对象覆盖时是否保留版本；两端同时存在不同版本时以谁为准；未完成上传和临时对象如何处理。备份通常希望抵抗误删，而镜像通常希望跟随源端状态。两种目标并不完全相同，脚本参数也应该按恢复目标设计。副本节点如何接管业务主写入点不可用时，副本存在并不意味着业务自动恢复。还需要完成：确认主节点不可写 -> 确认副本数据完整度 -> 决定是否允许副本写入 -> 修改对象存储访问入口 -> 让服务重建连接 -> 验证上传和读取 -> 处理主节点恢复后的数据方向如果主节点只是网络隔离，贸然让副本接受写入，可能形成两个独立写入点。主节点恢复后，两边对象集合和版本可能不同。因此，接管前需要可靠隔离旧写入点，接管后需要明确新的事实来源和反向同步方式。当前方案没有已验证的自动接管记录，所以更准确的表述是“副本可用于人工恢复方向”，不是“故障时自动切换”。客户端入口也是单点的一部分即使副本数据完整，业务服务如果把对象存储地址固定为主节点，主节点故障后仍然无法访问副本。可以考虑：受控更新配置中心中的存储入口；在存储前增加健康检查和代理入口；让应用通过稳定域名访问；为读和写设计不同入口；切换后让连接和客户端重新初始化。这些方式都要验证缓存、DNS、连接超时和回滚。仅修改配置文件，不代表运行中的服务已经切换。对象元数据也要一致视频文件保存在对象存储，视频状态、路径和业务信息保存在数…

RocketMQ 异步消息链路中的降级思路
type: item
date: 2026-05-16T16:39:00+08:00
description: HAOVP 使用 RocketMQ 处理部分视频互动统计。点赞、收藏、播放、分享和弹幕等操作产生统计消息，消费者再更新视频主表和每日统计数据。4 月接口验收时，RocketMQ 路由仍存在异常。内容服务增加了直接更新视频主表的降级路径，使弹幕等当前接口用例能够得到预期的核心计数结果。这次处理让我更清楚：消息发送失败时做一次数据库回写，可以保住某个核心结果，但不等于整条异步消息链已经可靠。正常消息链做了什么一条简化的统计链路是：业务操作完成 -> 生产统计消息 -> RocketMQ 接收和存储 -> 消费者取得消息 -> 更新视频主表计数 -> 更新每日统计数据使用消息队列的目的，是让主请求不必同步等待所有派生统计完成，同时把统计处理从业务操作中拆出来。消费者处理的不只是一列总数。正常路径还会更新按日期保存的增量统计。因此，消息链承担了两部分工作：核心展示计数 每日统计明细降级时只更新主表，能够让页面上的核心计数发生变化，却不能自动补齐每日统计。当前降级路径怎样工作统计消息使用异步发送。发送成功时只记录结果；发送异常或异步回调失败时，生产者直接按动作类型更新视频主表。可以抽象为：messageQueue.asyncSend(message, new Callback() { @Override public void onSuccess() { log.info("message sent"); } @Override public void onFailure(Throwable error) { updateMainRecord(message); } });这是脱敏结构示例，不包含实际主题、服务、字段和认证信息。这条降级路径的优点很直接：消息组件不可用时，播放、点赞或弹幕等核心计数不必完全依赖消息消费才能变化。它的边界也同样明确：只更新主表，不更新消费者负责的每日统计；失败只记录日志，没有持久化失败事件；应用退出后，内存中的异步回调无法继续；主请求成功与计数更新成功仍是两件事；消息状态不确定时直接降级可能带来重复更新。发送失败有不同阶段“发送失败”不能只看成一种结果。调用发送方法前失败 连接 Broker 失败 Broker 未接受消息 Broker 已接受，但响应丢失 回调执行异常 消息发送成功，但消费者失败前几种情况可能适合在生产者侧降级；最后一种已经不属于发送失败，生产者通常不知道消费者是否完成。最难处理的是状态不确定：Broker 可能已经接收消息，但发送方没有得到明确成功响应。如果此时直接更新数据库，而消息随后又被消费者处理，同一个计数可能执行两次。因此，可靠降级不能只靠捕获异常，还要回答：能否确认消息没有进入 Broker；降级更新和消息消费是否具备幂等性；是否保存了事件唯一编号；后续如何识别和修正重复结果。为什么直接加一容易重复统计消息通常携带动作和增量：video = 某个资源 operation = like increment = +1如果同一事件被消费两次，主表会连续加两次。单纯的 count = count + 1 无法判断这次增量是否已经应用。可以给事件增加唯一标识：{ "eventId": "example-event-id", "resourceId": "example-resource-id", "action": "like", "delta": 1 }示例中的标识均为占位值。消费者先检查事件是否处理过，再更新计数并记录处理结果。这里仍要考虑“更新业务数据”和“记录已消费”是否处于可靠事务中，否则两步之间失败也可能产生重复或遗漏。主表降级不等于完整消费正常消费者同时维护主表和每日统计，降级路径只改主表后，会出现：视频主表计数：已经增加 每日统计记录：没有对应增量页面可能看起来正常，但运营统计或趋势查询出现缺口。可以选择的补偿方向包括：将失败事件写入可靠的本地表，稍后重放；定期用互动明细重算主表和每日统计；对主表与统计表做差异检查；恢复消息组件后补发未完成事件；让统计查询明确区分实时总数和每日聚合结果。当前代码只支持直接主表更新，不能据此声称统计数据已经完整一致。降级是否应该影响主请求不同业务需要不同选择。对于可修复的展示计数，可以接受：核心明细成功 统计消息失败 -> 主请求仍成功 -> 记录失败事件并补偿对于不能丢失的关键状态，如果异步链失败后又没有可靠落盘，就不应简单返回成功。判断依据包括：主业务事实是否已经保存；派生数据是否能从明细重建；用户是否依赖计数立即准确；失败事件是否可以可靠重试；重复执行是否安全；补偿需要多长时间。“接口可用”不能以静默丢失不可重建数据为代价。生产者、Broker 和消费者分别监控什么消息链要可排查，至少需要三段指标。生产者发送请求数、成功数和失败数；同…

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

旧 JAR 与源码不一致时，如何定位部署版本问题
type: item
date: 2026-05-02T16:37:00+08:00
description: 在 4 月的 HAOVP 接口验收中，我遇到过一种很容易误导排查的问题：本地源码已经包含修复，但运行中的接口行为仍像旧代码。继续检查业务逻辑时，怎么都解释不通。最后确认，部分服务容器运行的还是旧 JAR，与本地源码不一致。重新构建并更新运行产物后，相关服务才与源码对齐。这类问题提醒我，部署中的“版本”不是一个点，而是一条链：源码版本 -> 编译生成的 JAR -> 镜像中的 JAR -> 运行容器实际加载的 JAR -> 对外表现出来的接口行为任何一层没有更新，都会出现“代码明明改了，服务为什么没变化”。先判断是不是版本问题遇到以下现象时，我会先怀疑运行版本，而不是立即继续改业务代码：本地源码中明确存在修复，运行结果却完全没有变化；日志格式或错误信息仍是旧版本内容；某些服务行为已更新，另一些服务仍保持旧行为；重启容器后问题不变；接口文档、源码和运行响应互相矛盾；同一个问题在 IDE 运行时消失，在容器环境中仍然存在。这些现象不能单独证明 JAR 过期，但足以让版本核对提前，而不是等业务逻辑排查一圈后再检查部署。HAOVP 的构建链路HAOVP 的业务服务先由 Maven 构建，在各模块的 target 目录生成 JAR。Dockerfile 再把匹配的模块 JAR 复制到镜像内，作为固定名称的应用文件运行。一个简化结构是：FROM eclipse-temurin:17-jre WORKDIR /app COPY target/service-*.jar app.jar ENTRYPOINT ["java", "-jar", "app.jar"]示例隐藏了实际模块名称，不包含镜像仓库和服务器路径。这条链路至少包含四个可能过期的位置：Git 工作区 Maven target 目录 Docker 镜像层 当前运行容器只执行容器重启，不会自动重新编译源码；只重新编译 JAR，也不会自动替换已经构建好的镜像；只构建镜像，如果容器没有重新创建，运行实例仍可能使用旧镜像。第一步：确认源码本身先确认准备验证的修复确实存在于当前工作区：git status git log -1 --oneline git diff这些命令只用于说明检查思路。实际仓库如果有未提交改动，需要同时记录提交版本和工作区差异，不能只写一个提交号就认为源码状态完整。我会进一步确认：修改发生在哪个模块；构建命令是否会构建该模块；修改是否已经保存；当前分支是否正确；是否存在同名类或旧目录；配置来自本地文件还是配置中心。有时接口行为不变不是 JAR 旧，而是修改了没有被实际加载的配置或另一个同名实现。第二步：检查 JAR 是否重新生成项目文档中的构建顺序是先在后端目录执行 Maven 构建，再重建业务服务：mvn clean package -DskipTests docker compose up -d --build service-a service-b服务名是通用占位示例。clean 会清理旧构建目录，package 重新生成 JAR。跳过测试只能减少构建环节，不能证明代码正确；正式验收前仍要运行适合的编译、测试和接口检查。构建完成后，我会确认：目标模块的 JAR 是否存在；文件时间是否对应本次构建；构建日志是否真的包含目标模块；Maven 是否因为依赖失败而跳过后续模块；target 中是否残留多个能匹配通配符的 JAR；Dockerfile 的 COPY 模式最终会选择哪个文件。如果目录里同时存在旧版本、原始 JAR 和重新打包 JAR，宽泛通配符可能产生歧义。更稳妥的做法是让构建产物名称可预测，并在镜像构建前验证匹配结果。第三步：确认镜像真的重建docker compose restart 只会重启现有容器，不会重新执行 Dockerfile。需要构建新镜像时，应明确使用构建操作：docker compose build service-a docker compose up -d service-a或者使用项目文档中的 up -d --build。即使带了 --build，仍要观察构建日志。Docker 会使用构建缓存；如果上下文、忽略规则或文件时间判断出现偏差，某些层可能被复用。我会检查：Docker 的构建上下文是否包含新 JAR；.dockerignore 是否排除了目标文件；COPY 层是否重新执行；新镜像的创建时间和标识是否变化；Compose 使用的镜像名是否就是刚构建的镜像；是否有同名旧镜像或远程镜像覆盖本地构建。不能只看到“build completed”就结束，还要确认目标服务使用了这次产物。第四步：确认容器被重新创建镜像更新后，运行容器还需要指向新镜像。docker compose ps docker inspect <container>这…

一次在线视频平台接口验收记录
type: item
date: 2026-04-25T16:42:00+08:00
description: 4 月 18 日，我完成了一轮 HAOVP 后端接口验收。这次不是挑几个接口确认能返回数据，而是按认证与用户、内容与评论及推荐、后台管理三批执行，并把每一项的请求方法、脱敏路径、预期结果和实际状态写进 JSON 明细。最终汇总为：验收批次用例数通过失败认证与用户29290内容、评论与推荐63630后台管理23230合计1151150这里的“通过”表示实际结果符合用例预期，不等于每个请求都返回 200。例如，无验证码登录应该被拦截，普通用户访问内部审核接口也应该被拒绝；这些负向用例在得到预期拒绝结果时同样计为通过。先统一验收口径接口文档中大约有 186 条 endpoint，但其中包含重复文档项、内部接口和管理接口映射，不能直接把这个数字当作实际测试数量。我采用 JSON 明细中的独立用例作为最终口径。每条记录至少包含：{ "method": "POST", "path": "/example/resource", "status": "PASSED", "http": 200, "note": "脱敏后的测试说明" }这是结构示例，不是实际接口、账号或请求数据。三份结果文件分别有 29、63 和 23 条记录，与汇总报告完全一致。这样可以避免出现“报告写了全部通过，但明细数量对不上”的问题。为什么按模块分批执行最初如果把所有接口放进一个长脚本，某个请求超时或状态依赖出错后，后面的结果也会受到影响。为了让问题更容易定位，我按业务链路拆成三批：认证与用户 -> 注册、登录、刷新、资料、关注、通知、私信 内容、评论与推荐 -> 分类、标签、视频、互动、弹幕、统计、评论、推荐 后台管理 -> 管理员认证、用户治理、内容审核、评论管理、仪表盘分批后，每一批都可以独立准备数据、执行、清理和重测。某一模块失败时，不需要从头运行全部用例。不是只测正常请求如果只验证合法请求返回成功，很难判断认证和权限控制是否真正生效。这次验收同时包含正向和负向场景。例如：合法验证码和账号能够登录；缺少验证码的登录请求会被拦截；刷新令牌能够换取新的认证信息；用户被禁用后不能继续登录；后台重置密码后可以使用新密码登录；普通用户访问内部审核接口会被拒绝；内容置顶操作必须满足作者身份和正确的资源关系。因此，不能用“HTTP 状态不是 200”直接判断用例失败。验收器需要同时比较预期 HTTP 状态、业务状态和业务前提。一个简化判断可以写成：boolean passed = actualHttp == expectedHttp && matchesExpectedBusinessState(response) && verifySideEffect();真正的验收还要检查数据库或后续查询中的副作用，不能只看响应正文。第一类问题：运行代码和本地源码不一致排查用户和评论链时，我发现部分容器运行的还是旧构建产物，与本地源码不一致。这种问题很容易造成错觉：代码中明明已经修复，接口行为却没有变化；继续修改源码，只会让排查方向越来越偏。我先重新构建产物并更新运行环境，再核对服务实际加载的版本。以后遇到类似情况，我会先检查：当前提交版本 -> 构建产物生成时间 -> 镜像或容器中的文件 -> 服务启动日志 -> 实际接口行为只有这几层一致，才适合继续判断代码逻辑。第二类问题：公开接口被认证规则挡住用户公开资料、关注列表、分类树和标签列表等接口，本来需要允许未登录访问，但实际运行时受到认证规则限制。修复这类问题不能简单扩大白名单。每个接口都要先确认：返回内容是否确实允许公开；是否需要隐藏敏感字段；路径变量能否访问其他用户的数据；查询量是否需要限制；写操作是否仍然要求认证和权限。公开读取接口和内部管理接口必须保持边界。验收通过的是指定公开路径，不代表整个模块都应跳过认证。第三类问题：跨服务返回值判断错误内容链中曾出现视频详情和列表里的用户展示信息为空。调用用户服务本身有返回，但成功分支没有进入。根因是对包装类型状态码使用了不合适的比较方式。跨服务反序列化后得到的是对象，不能依赖对象引用相同来判断数值相等。// 容易产生问题 if (result.getCode() == expectedCode) { // ... } // 更明确的数值判断 if (Objects.equals(result.getCode(), expectedCode)) { // ... }这是简化示例，不对应实际类名和常量。这次问题提醒我：接口已经返回 200，不代表业务分支一定执行正确。还要检查关键字段是否完整、远程结果是否被正确解释。第四类问题：事务内同步回写造成等待收藏链曾在主事务中同步调用其他服务更新统计，出现锁等待和慢失败。调整后，把非核心的同步回写移到事务提交之后，并以异步尽力执行的方式处理。完成核…

Redis 缓存一致性：从热点数据到失效策略
type: item
date: 2026-03-28T16:38:00+08:00
description: 在 HAOVP 中接入 Redis 之后，我越来越觉得“用了缓存”和“正确使用缓存”是两件事。把数据写进 Redis 并不难，真正需要判断的是哪些数据值得缓存、缓存多久、数据变化时怎样失效，以及 Redis 不可用时业务应该怎样处理。目前我先把 Redis 用在认证状态上：验证码、刷新令牌、角色和权限等数据会进入 Redis，并设置过期时间或在特定操作后删除。视频详情、热门列表等热点数据缓存还处在策略梳理阶段，接下来需要按真实查询和更新链路逐步接入。先区分三类数据不同数据不能共用同一套缓存策略。临时状态验证码、一次性校验信息和短期会话状态有明确生命周期：写入 Redis -> 设置过期时间 -> 使用成功后主动删除 -> 超时后自动失效这类数据通常不需要长期保留。验证码即使仍在有效期内，验证成功后也应删除，避免重复使用。可从数据库重建的数据角色、权限、用户基础信息或视频详情可以从数据库重新读取。Redis 在这里用于减少重复查询，数据库仍是事实来源。读取缓存 -> 命中：返回 -> 未命中：查询数据库 -> 写回缓存 -> 返回这就是常见的旁路缓存。缓存丢失不会让原始数据消失，但会增加数据库访问。累计状态播放量、点赞数和其他高频计数如果先在 Redis 中累积，再异步写回数据库，就不再只是可随时丢弃的副本。此时要处理重复累计、落库失败、任务重试和 Redis 故障等问题。HAOVP 的视频统计任务还没有接通这条累计与落库链路，这部分需要先定义计数口径、幂等方式和失败补偿，再继续实现。什么才算热点数据热点数据不是“看起来重要的数据”，而是在一段时间内被集中访问、直接回源会造成明显压力的数据。在线视频平台中可能出现的热点包括：首页推荐或热门列表；某个突然被大量访问的视频详情；高频读取的标签和分类；登录用户的角色与权限；短时间内频繁校验的会话状态。“可能出现”不等于已经通过监控确认。是否缓存、缓存多长时间，需要结合访问频率、数据库耗时、数据变化速度和一致性要求判断。我会先问四个问题：这个数据读得是否足够频繁？ 缓存未命中时，回源成本有多高？ 数据更新后允许旧值存在多久？ 缓存丢失后能否从事实来源重建？如果这四个问题没有答案，直接增加缓存可能只是在系统中多放一份需要维护的数据。旁路缓存怎样处理读取一个通用的查询结构可以写成：public VideoView queryVideo(Long id) { String key = "video:detail:" + id; VideoView cached = cache.get(key); if (cached != null) { return cached; } VideoView value = repository.findView(id); if (value != null) { cache.set(key, value, ttlWithJitter()); } return value; }这是我用来梳理旁路缓存流程的简化示例，实际接入视频详情时还要按当前对象结构和键空间重新设计。读取流程看似简单，但还需要决定：查询不到数据时是否短暂缓存空值；多个请求同时未命中时怎样限制回源；缓存序列化格式变更后怎样兼容；Redis 超时后是降级到数据库，还是直接失败；回源失败时能否返回旧值。缓存逻辑如果把数据库异常也当成“数据不存在”，就可能写入错误空值。为什么更新后通常删除缓存对于数据库是事实来源的旁路缓存，常用流程是：更新数据库 -> 数据库事务成功 -> 删除相关缓存 -> 下一次查询重新回填相比同时更新数据库和缓存，删除缓存可以减少在两个存储中拼装同一份复杂对象的机会。下一次读取会从数据库获得新值。一个简化示例：@Transactional public void updateProfile(ProfileUpdate command) { repository.update(command); afterCommit(() -> cache.delete(profileKey(command.userId()))); }示例强调“数据库提交成功后再失效缓存”。如果事务最终回滚，却提前删除了缓存，数据不会错误，但会产生一次不必要的回源；如果数据库更新失败却错误地写入新缓存，则可能直接产生脏数据。先更新数据库再删除缓存也不是绝对一致即使采用“更新数据库，再删除缓存”，并发下仍可能出现竞态：请求 A：缓存未命中，读取旧数据库值 请求 B：更新数据库为新值，删除缓存 请求 A：把刚才读到的旧值写回缓存最终缓存里仍然可能是旧值。因此，这个策略的目标通常是降低不一致概率和持续时间，而不是提供严格的线性一致性。可以根据业务要求继续增加保护：给缓存设置合理 TTL，让旧值最终失效；更新后延迟再…