Vue3 表单校验:把规则、时机和提交状态分开

背景 输入一个字符就报错会打断用户,提交时才发现全部错误又太晚。校验规则、触发时机和请求状态应分别管理。 核心判断 值、touched、错误和 submitting 各自表达明确职责。 同步格式检查先完成,远程校验需要防抖和响应序号。 提交期间禁用重复提交,失败后保留输入与可恢复提示。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 async function submit() { if (submitting.value) return; touched.email = true; errors.email = validateEmail(form.email); if (errors.email) return; submitting.value = true; try { await save({ ...form }); } finally { submitting.value = false; } } 边界与误区 示例依赖已有 form、errors、touched 和 save。客户端验证不能替代服务端校验;重复点击保护也不等于后端幂等。 验证方式 模拟慢请求时连续点击提交,并构造邮箱异步校验乱序、服务端字段错误与网络中断,确认提示位置与恢复行为一致。 小结 字段值、错误、触碰状态和提交状态分开管理,避免提示闪烁和重复请求。

2026年9月24日 · 1 分钟 · BvBeJ

C++ RAII 线程生命周期:让退出路径也遵守所有权

背景 构造线程之后抛出异常,如果对象仍持有 joinable 的 std::thread,退出路径可能直接终止程序。线程拥有关系需要覆盖异常。 核心判断 C++20 可用 jthread 表达请求停止与等待的拥有关系。 工作循环检查 stop_token,阻塞操作也要有可唤醒路径。 析构顺序保证线程先退出,再销毁它引用的资源。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 std::jthread worker([](std::stop_token stop) { while (!stop.stop_requested()) do_one_iteration(); }); 边界与误区 jthread 的析构会请求停止并等待,但不会神奇打断任意阻塞 I/O。持锁等待线程结束也可能形成死锁。 验证方式 测试异常退出、空闲阻塞和执行中停止,记录析构等待时间;故意调整成员析构顺序,确认被访问资源不会先于线程销毁。 小结 线程拥有者必须负责 join 或请求停止,异常路径不能留下 joinable 线程。

2026年9月23日 · 1 分钟 · BvBeJ

Tokio 任务所有权:每个 spawn 都应该有回收策略

背景 spawn 后丢掉 handle,会让调用方失去等待、错误检查和取消入口。后台任务应该有生命周期所有者,而不是依靠进程退出兜底。 核心判断 单任务保存 JoinHandle,多任务使用 JoinSet 集中回收。 区分业务 Result 与 JoinError 中的 panic、取消。 优先协作停止,必要时 abort 后继续 await 回收结果。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 let handle = tokio::spawn(async move { worker.run(shutdown).await }); match handle.await { Ok(Ok(())) => tracing::info!("worker stopped"), Ok(Err(err)) => tracing::error!(error = %err, "worker failed"), Err(err) => tracing::error!(error = %err, "worker join failed"), } 边界与误区 丢弃 JoinHandle 会分离任务,并不等于停止任务。worker 和 shutdown 是业务对象,示例只展示持有 handle 和等待的边界。 验证方式 让任务正常返回、失败、panic 和被取消各一次,确认监督者都有处理路径;服务停止时检查没有孤立后台任务继续访问资源。 小结 保存 JoinHandle,集中处理任务完成、错误、panic 和取消,不让后台任务失去所有者。

2026年9月22日 · 1 分钟 · BvBeJ

Go Worker Pool 关闭:让任务停止有边界、结果有去处

背景 关闭任务队列之前如果仍有提交者写入,会发生 send on closed channel。关闭一次和关闭安全不是同一件事。 核心判断 先通过统一入口停止提交,再等待所有提交者退出。 唯一协调者关闭 jobs,worker 遍历队列排空后退出。 最后等待 worker;任务本身仍应遵守超时与取消。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 // 前提:统一入口已拒绝新提交,所有可能发送 jobs 的协程都已退出。 // 仅由协调者调用,不从 worker 内调用。 func (p *Pool) drain() { p.once.Do(func() { close(p.jobs) p.wg.Wait() }) } func (p *Pool) worker(ctx context.Context) { defer p.wg.Done() for job := range p.jobs { job.Run(ctx) } } 边界与误区 sync.Once 只能防止重复 close,不能防止并发 send。示例的 drain 要在提交者已经退出后由协调者调用,不得从 worker 自身调用以免等待自己。 验证方式 用竞态检测测试提交与停止交错,再验证空队列、阻塞任务和重复停止。明确关闭是排空任务还是丢弃任务,并对未完成结果提供可追踪状态。 小结 关闭时先禁止新提交,再关闭队列并等待 worker,任务自身要响应 context。

2026年9月21日 · 1 分钟 · BvBeJ

TypeScript 运行时边界:类型声明不能替代输入校验

背景 给 JSON.parse 的结果加 User 类型,不会自动检查字段。类型系统接收到不可信数据之前,需要一道真正执行的校验。 核心判断 HTTP、消息与本地存储统一从 unknown 开始。 在边界执行 schema 校验,成功后才交给领域层。 转换、默认值与未知字段策略显式声明,不默默修改业务含义。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 const User = z.object({ id: z.number().int(), name: z.string().min(1) }); const user = User.parse(JSON.parse(body)); 边界与误区 parse 会抛出异常;接口需要可控错误返回时可以用 safeParse。示例依赖 zod,并且 JSON 语法错误与 schema 错误应分别分类。 验证方式 覆盖缺字段、错误类型、空字符串、超范围数字和畸形 JSON,确认输入错误返回客户端错误,而不是落入通用 500。 小结 HTTP、消息和存储数据进入类型系统前都不可信,边界必须执行 schema 校验。

2026年9月20日 · 1 分钟 · BvBeJ

PostgreSQL 事务隔离:先说明异常,再选择级别

背景 先说需要避免哪一种业务异常,再讨论隔离级别,远比直接选择最高级别有效。库存扣减和账户转账关注的约束并不相同。 核心判断 用并发事务复现丢失更新、写偏差等风险。 能通过原子 SQL、唯一约束或行锁表达的规则,尽量交给数据库。 Serializable 冲突可能导致事务失败,调用方准备有限重试。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 BEGIN ISOLATION LEVEL SERIALIZABLE; UPDATE accounts SET balance = balance - 10 WHERE id = 1; COMMIT; 边界与误区 示例只有一次扣款,不是完整转账。重试必须从事务开始重新执行,事务外已经发生的副作用也要考虑幂等。 验证方式 使用两个连接安排读写交错,验证业务不变量;对 serialization failure 注入重试,并检查重试预算与外部副作用是否重复。 小结 从业务不变量出发选择隔离级别,并为 serialization failure 准备有限重试。

2026年9月19日 · 1 分钟 · BvBeJ

指标基数预算:拒绝把用户 ID 贴进 label

背景 把 user_id 加进指标 label,看起来方便筛选,实际上可能为每个用户制造一组新的时间序列。成本会乘上其他标签组合。 核心判断 标签值使用路由模板、状态分类和有限业务枚举。 用户、订单与 trace ID 留在日志或追踪系统,必要时使用 exemplar 关联。 给每个指标定义标签组合上限,并监控活跃序列增长。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 sum by (route, status) (rate(http_requests_total[5m])) # 单独执行,统计该指标的活跃序列数 count(http_requests_total) 边界与误区 /users/123 不是稳定 route 标签,应归一成 /users/:id。两个 PromQL 示例分别执行,分号不是查询之间的通用连接符。 验证方式 估算 route×status×method×instance 的组合数,压测随机用户输入,确认时间序列数量不随用户总数持续增长。 小结 指标标签只表达有限集合,用户、订单和 trace id 应放在日志或 trace 中。

2026年9月18日 · 1 分钟 · BvBeJ

Linux 文件描述符泄漏:从增长曲线定位资源所有权

背景 FD 用量不断上升,最终表现可能是无法建立连接,也可能是日志写入失败。需要先分清增长的是文件、socket 还是管道。 核心判断 按时间采集数量和符号链接目标,区分正常连接池与泄漏。 沿着打开、移交、关闭三个动作检查资源所有者。 重点覆盖错误分支、重试循环和提前返回。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 pid=$(pidof app); printf 'total='; find "/proc/$pid/fd" -mindepth 1 -maxdepth 1 | wc -l; ls -l "/proc/$pid/fd" | head 边界与误区 提高 ulimit 只延迟触顶,不能修复泄漏。短暂连接峰值也不是泄漏证据,必须与请求量和回收过程对照。 验证方式 固定负载运行一段时间再停止,确认 FD 回落。采集 /proc 需要相应权限,示例的 pidof 单实例假设在多进程环境应改成逐 PID 处理。 小结 按进程统计 FD 总数和目标类型,沿着增长曲线寻找没有闭合的资源生命周期。

2026年9月17日 · 1 分钟 · BvBeJ

Kubernetes 滚动更新预算:用容量换取可控发布

背景 滚动更新不是旧副本换成新副本这么简单:流量、节点余量和连接排空都会影响中间阶段的可用容量。 核心判断 用可承受的容量下降确定 maxUnavailable,用节点余量确定 maxSurge。 就绪时间、minReadySeconds 与退出宽限期一起评估。 保留版本级别的成功率和延迟指标,异常时暂停发布。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 strategy: type: RollingUpdate rollingUpdate: { maxUnavailable: 1, maxSurge: 1 } replicas: 4 边界与误区 副本数并不等于有效处理能力,新副本未预热时不能立即承担峰值流量。终止中的 Pod 仍可能占用资源,预算不能只按理想副本数计算。 验证方式 在接近峰值的负载下发布,检查待调度 Pod、就绪时间和错误率。节点余量不足时验证发布如何停住,以及回滚是否仍有资源可用。 小结 根据最低服务容量计算 maxUnavailable 和 maxSurge,而不是照抄默认值。

2026年9月16日 · 1 分钟 · BvBeJ

Docker 镜像层顺序:把变化频率高的内容放后面

背景 只改一行业务代码却重新下载所有依赖,会让构建越来越慢。镜像层顺序应该围绕文件的变化频率安排。 核心判断 先复制依赖声明并下载依赖,再复制频繁变化的源码。 设置 WORKDIR,使用多阶段构建将工具链留在构建阶段。 观察缓存命中率,并定期验证没有缓存时也能构建。 最小示例 下面保留核心逻辑;业务类型、调用入口和环境配置按项目补齐。 COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go build -o /out/app ./cmd/app 边界与误区 片段是 Go 构建阶段的一部分;go.mod/go.sum 变化仍会失效依赖层。缓存只是加速手段,不能承担产物正确性的证明。 验证方式 分别只改源码、依赖和构建参数,比较构建日志与耗时;最后禁用缓存再构建一次,确认产物和测试结果一致。 小结 先复制依赖描述并安装,再复制源码,让缓存真正服务于日常开发和 CI。

2026年9月15日 · 1 分钟 · BvBeJ