7AA7 Blog

Zones · Curated

Latest posts, this week's hits and top-rated picks — everything worth reading in the 7AA7 community, in one place.

1updated in 24h 21published this week 5Active authors
雾梨 Mist·2026-10-20
用内容哈希做重复文件检测:从思路到实现

磁盘清理里最实在的一块是找重复文件:照片导了两遍、下载目录和文档目录有同一份 PDF、备份目录里躺着历史版本的全家福。按文件名找是不可靠的,按内容找才准——而”按内容”这件事,工程上就是哈希。这篇文章把重复文件检测的完整思路走一遍,从筛选策略到索引设计,再到清理的安全流程。 一、文件名不可靠,内容才算数 重复文件的定义是”内容完全相同的文件”,而不是”名字相同的文件”。现实中的重复文件形态五花八门: 同名同内容:报告.docx 在两个目录各一份 同名不同内容:报告.docx 是不同版本,名字一样内容不同——这不是重复,不能删 不同名同内容:最终版.pdf 和 copy2.pdf 是同一份文件的两次落盘 所以判断重复的唯一可靠依据是内容。比对内容的朴素做法是逐字节比较,但两两比较的开销是 O(n²),几百个文件就跑不动了。正确做法是给每个文件算一个内容哈希(MD5、SHA-1、SHA-256 都可),哈希相同即视为内容相同——这把比对降到了 O(n) 的哈希计算加 O(n) 的哈希查找。 二、先按大小初筛:砍掉 99% 的工作量 全量哈希的问题是慢。对大目录里的视频、镜像文件,读一遍磁盘算哈希就是几分钟到几小时的 IO。工程上的第一刀是按文件大小分组: 内容相同的文件,大小一定相同。 所以先按 size 建桶,只有同桶的文件才有必要进下一步比较。一个典型的场景是十万个文件里有几百组同大小候选——99% 以上的文件在这一层就被排除了,一次 stat 就够,不用读内容。 大小这一层还能再细化:取文件头尾的少量字节做”局部哈希”(比如首 4KB + 尾 4KB),作为第二层筛选。头部尾部不同,内容必然不同;头尾相同再进全量哈希。对大文件特别有效——几兆的局部读取远比几百兆的全量读取便宜。 候选 = 所有文件 候选 = 按 size 分桶,桶内 >= 2 的进下一轮 候选 = 按 头尾采样哈希 分桶,桶内…

189 15 0
用内容哈希做重复文件检测:从思路到实现
苏念禾·2026-10-20
NoSQL 选型思考:文档、键值与关系型的边界

“NoSQL” 这个词覆盖面很宽,键值存储、文档库、列族数据库、图数据库都算,彼此之间的差异比它们和关系型数据库之间的差异还要大。讨论选型时如果说”我们要用 NoSQL”,这句话本身几乎不含信息量。真正有用的问题是:这个业务问题的数据访问模式是什么,哪一类存储引擎的原生语义和它最接近。 一、CAP 与 BASE 的工程视角 CAP 定理说分布式系统在分区发生时要在一致性(C)和可用性(A)之间取舍。这个描述容易让人以为是三选二的设计题,实际工程里的含义要具体得多: 分区是一定会发生的,网络抖动、机器故障、机房割接,都会产生分区。问题不是”要不要处理分区”,而是”分区时你希望系统表现如何”。 C 和 A 的取舍是分区期间的行为,不是平时的行为。平时你可以既一致又可用,分区时才需要选边。 一致性本身有程度之分。线性一致、因果一致、最终一致,代价依次递减。多数业务需要的是”读到的数据不能是错的”,不一定是”任何时刻读到的都是最新写入”。 BASE(基本可用、软状态、最终一致)是很多 NoSQL 系统的实际姿态。它的工程价值在于:把”写入要等多数派确认”这类开销从关键路径上挪走,换成更宽松的一致性模型,换取延迟和吞吐。代价是应用层要处理读到旧数据、处理并发冲突,这个代价不是免费的,只是从数据库转移到了开发者身上。 选型时先问自己:这个数据读错一笔和写慢一点,哪个代价更高?账务、库存这类问题答案是前者,就倾向强一致;日志、计数、feed 流这类答案是后者,就可以接受最终一致。这个判断比记忆各产品的宣传语有用得多。 二、按访问模式划边界 文档库的卖点是灵活的 schema 和对嵌套结构的天然支持。一个用户对象带一堆可选字段、带一个地址子对象、带一个标签数组,用文档存是一条记录读出来就完事,不需要 JOIN。内容管理、用户画像、商品目录(属性差异大)是它的舒适区。 代价在跨文档的关联查询上。文档库也能做”关联”(应用层拼、$lookup 之类的语法),但多对多关系、跨集合的聚合统计,效率和表达力都不如关系型的 JOIN。当你的核心查询是”从 A 表找到 B 再找 C”时,文档库不是好的选择。此外,schema 灵活是双刃剑:写入没有约束,脏数据会悄悄积累,查询字段名拼错了也不会报错,需要应用层或迁移脚本严格把关。 键值存储是最简单的一类:给一个 key,存取一个 value。它把复杂性全部推给了应用——value 里存什么、怎么序列化、怎么建索引,都是应用的事。它的强项是极简模型带来的极致性能和水平扩展能力:缓存、会话、分布式锁、计数器、去重集合,都是”一次读写搞定”的场景。数据结构简单的场景,键值就是最优解;一旦需要”按字段查”、”按范围扫”,就应该考虑别的选项,而不是自己在应用层建索引。 列族数据库按列族组织数据,一行可以有上百万列,写入吞吐很高,适合”写多读少、按行键访问、列不固定”的场景:时序数据(传感器、指标)、宽表(用户行为明细)、日志类数据。它的模型对”按行键点查”和”按行键范围扫”很友好,对”按列值反查”很不友好——除非额外建反查表。常见的做法是用它做明细存储,把查询结果同步到别的系统里做分析。 图数据库解决的是关系遍历问题。”朋友的朋友推荐”、”最短路径”、”社群发现”,在关系型里要写一堆 JOIN,在图库里是几个 hop 的事。社交网络、风控反欺诈、知识图谱是它的主场。代价是数据量级通常比其他类型小得多,运维生态也更专门,不适合当通用主存储。 这几类没有高低之分,只有匹配度。判断匹配度的方法很朴素:把你最核心的三条查询写出来,看哪类存储引擎的原生模型能直接支撑,哪类需要你绕路。 三、多模型混合是常态 现实中几乎没有一个系统只用一种存储。典型的线上系统可能长这样: 关系型数据库做主存储,存订单、账户、配置,负责事务和强一致; 键值存储做缓存和会话,挡住热点读; 搜索引擎做全文检索和复杂过滤; 列族或时序库存监控指标和行为日志; 文档库存内容、草稿、用户生成内容。 这不是架构不够纯粹,而是每个存储引擎都在自己擅长的位置上。混合的代价是运维面扩大:备份、监控、容量规划、故障排查,每多一种存储就多一套工具链和一套心智模型。引入一个新存储之前,值得问一句:这个需求用现有存储加点妥协能不能扛?能扛就先扛,扛不住再引入。…

196 12 0
NoSQL 选型思考:文档、键值与关系型的边界
VectorQ·2026-10-19
事务与隔离级别:脏读、不可重复读与幻读

ACID 四个字母里,A、C、D 在工程上相对好验证:原子性靠日志回滚,持久性靠刷盘策略,一致性是业务与约束共同保证的结果。真正让人头疼的是 I——隔离性。它的定义本身就带着妥协:完全隔离等价于串行执行,性能不可接受;降低隔离又会引入各种读写异常。工程上的隔离级别,本质是在正确性和并发度之间选一个自己能接受的点。 一、三种读异常的定义 讨论隔离级别之前,先把三个现象区分清楚,它们经常被混用: 脏读(Dirty Read):读到了别的事务尚未提交的数据。对方回滚后,你读到的东西就凭空消失了。 不可重复读(Non-repeatable Read):同一个事务内,两次读同一行得到不同结果,中间被别的事务的提交改掉了。 幻读(Phantom Read):同一个事务内,两次执行同一范围查询,返回的行数不同,中间被别的事务插入了新行并提交。注意区分:不可重复读针对”某一行变了”,幻读针对”多出或少掉了行”。 四个隔离级别与这些现象的对应关系如下: 隔离级别 脏读 不可重复读 幻读 READ UNCOMMITTED 可能 可能 可能 READ COMMITTED 不会 可能 可能 REPEATABLE READ 不会 不会 可能(标准 SQL 定义) SERIALIZABLE 不会 不会 不会 这张表是 ANSI SQL 的标准定义。需要注意的是,很多实际数据库(包括 InnoDB 默认的 REPEATABLE READ)通过多版本与间隙锁的组合,把幻读也挡住了大部分,比标准定义更严格。理论表格和实际实现之间总有缝隙,排查问题时要以实现为准。 二、MVCC:快照读与当前读 InnoDB 的高并发读能力主要来自 MVCC(多版本并发控制)。每行记录除了业务字段,还隐含了两个系统列:最近修改事务 ID 和回滚指针。回滚指针把该行的历史版本串成一个链,旧版本存放在…

831 33 0
事务与隔离级别:脏读、不可重复读与幻读
Cache 孙·2026-10-18
数据库事务日志:WAL、redo 与 undo 的作用机制

把某一行从 1 改成 2,如果直接改磁盘上的数据页,这是一次随机写。数据库每秒要改成千上万行,数据页在磁盘上的位置又是分散的,全靠随机写会立刻碰到 IOPS 上限。日志的作用就是把这个随机写换成顺序写。 为什么需要 WAL 顺序写和随机写不在同一个量级:机械盘随机写的 IOPS 通常在几百量级,顺序写吞吐可达上百 MB 每秒;SSD 上差距缩小但仍存在,随机写还带来更多写放大。具体数字取决于硬件与阵列配置,量级关系是稳定的。 WAL(Write-Ahead Logging)的规则只有一句:修改数据页之前,必须先保证描述这个修改的日志已落盘。它带来两个结果: 持久性有保障了。数据页没来得及写也不要紧,只要日志在,就能重新做出来。 性能也变好了。事务提交只要求日志落盘,不要求脏页落盘,多次随机写被推迟,可以合并、可以在后台慢慢做。 顺序写还带来并发上的好处:多个事务同时提交时,日志追加到同一文件尾部,是一次 IO 而不是多次。 redo 与 undo 的分工 一个事务产生的日志里同时包含两种记录,它们解决的问题不同。 redo undo 记录内容 物理日志:某个页面的某个偏移改成了什么 逻辑日志:如何撤销这次修改 保证的特性 持久性(已提交的不丢) 原子性(未提交的能回滚) 重放方向 向前,把修改再做一遍 向后,把修改撤销 额外用途 崩溃恢复 MVCC 的一致性读 redo 是物理的,它不关心这行数据的业务含义,只记录”页面 X 偏移 Y 处的值从 A 变成 B”。这让重放变得简单且幂等——配合 LSN 判断,已做过的修改可以直接跳过。…

2334 55 0
数据库事务日志:WAL、redo 与 undo 的作用机制
Cache 孙·2026-10-18
代码重构的安全手法:小步、可回滚、有测试

重构失败很少是因为设计能力不够,多数是因为一次改动太大:改到一半发现行为理解错了,此时进退两难——往前是未知,往后是几百个文件的 diff 已经无法撤销。让重构可控的核心不是技术,是把不可控的部分切小。 重构与改行为是两件事 重构的严格定义是:在不改变外部可观测行为的前提下,改变内部结构。一旦同一次改动里既改结构又改行为,就失去了判断依据——出了问题是结构改错了,还是新行为本来就有缺陷?无法分辨,也就无法回滚到某个已知正确的中间状态。 实操上的约束很朴素: 一个提交要么只改结构,要么只改行为,不同时做。 改结构时,所有现有测试必须保持绿,且不允许顺手”优化”任何判断条件。 需要改行为时,先做完结构重构并提交,再开一个提交改行为。 这条纪律最难守的地方在于,重构过程中总会看到”这里顺便改一下更好”的地方。正确做法是把它们记到待办清单里,等结构稳定后再处理,而不是当场动手。 先补测试,再动结构 没有测试的重构是盲改。顺序不能反:先为即将被重构的部分补上覆盖其当前行为的测试——注意是当前行为,不是”应该有的行为”。如果现有行为本身就是错的,那属于行为变更,是第二步的事。 补测试时优先覆盖:对外接口契约、边界条件、错误路径。对于没有测试的老模块,一种可行的做法是”特征化测试”:先写一批记录当前实际输出的用例(哪怕输出看起来不合理),把它们固定下来,然后重构,观察哪些用例的输出变了。变了的地方就是需要人工确认的点。 # 特征化测试:先锁住现状,不判断对错 def test_legacy_pricing_current_behavior(): assert legacy_price({"qty": 0, "unit": 10}) == 0 # 现状如此,先记录 assert legacy_price({"qty": -1, "unit": 10}) == -10 # 疑似缺陷,标记待确认 assert legacy_price({"qty": 100, "unit": 10}) == 900 # 折扣逻辑,需确认规则 提交粒度:可评审、可回滚 一个重构提交应该小到评审者能在十分钟内看懂,同时小到出问题可以直接 revert 而不带掉别的东西。判断标准是:这个提交单独 revert 之后,系统是否仍然处于一个可运行、行为一致的状态。 常见的一组拆分方式: 步骤…

1520 66 2
代码重构的安全手法:小步、可回滚、有测试
林见川·2026-10-17
前端状态机实践:用状态机管理复杂交互

一个上传组件里出现五个布尔标志不算稀奇:isLoading、isError、isSuccess、isPaused、isValidating。每个标志单独看都合理,合在一起就埋下了隐患。 布尔标志的组合爆炸 $n$ 个独立的布尔标志,理论上能表示 $2^n$ 种组合。五个标志就是 32 种,而真正合法的 UI 状态可能只有 6 个:待选择、校验中、上传中、已暂停、成功、失败。 剩下那 26 种组合在类型系统里完全合法,在运行时也可能真的出现——比如 isLoading 为 true 的同时 isError 也是 true,界面同时转着圈、弹着错误提示、显示着成功图标。这类 bug 的特征是:不会崩溃,不会报错,只是界面呈现出一种没人预料到的矛盾状态,而且很难复现,因为它往往是两条异步链路交错的结果。 根因在于布尔标志之间互相独立,无法表达互斥和时序约束。而”当前处在哪个状态”本质上是一个互斥的枚举值。用 $k$ 个状态描述,取值就是 $k$ 个;用 $n$ 个标志描述,取值是 $2^n$ 个,其中绝大部分是垃圾。 代码层面的典型表现是散落各处的成对赋值: // 每次状态变化都要记得同步其他标志,漏一个就进入非法组合 setLoading(true); setError(null); setSuccess(false); 这种写法把”状态的一致性”交给了写代码的人去手动维护,而人总会漏。 状态机的三个要素 形式化一点,一个状态机是三元组 $(S, E, delta)$:有限状态集 $S$、事件集 $E$、转移函数 $delta: S times E to…

1351 30 0
前端状态机实践:用状态机管理复杂交互
Elowen·2026-10-17
慢查询优化:从 EXPLAIN 开始排查

慢查询排查是个有固定套路的活:先从慢查询日志里找到嫌疑人,用 EXPLAIN 看它打算怎么执行,判断问题出在索引、写法还是表结构上,改完再验证。整条链路里,EXPLAIN 是承上启下的一步——它不能告诉你语句有多慢,但能告诉你为什么慢。这篇文章把这条链路的每一环拆开讲。 一、慢查询日志:先把嫌疑人找出来 MySQL 默认不记录慢查询,需要显式开启: SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1; -- 超过 1 秒的语句记入日志 SET GLOBAL log_queries_not_using_indexes = OFF; -- 大表上慎开,日志会爆炸 long_query_time 的取值看业务:在线交易类调到几百毫秒,后台批处理可以放宽到几秒。改这个参数只对新连接生效,已经建立的连接要用 SET SESSION 或重连。 日志内容一行一条语句,开头是执行时间、锁时间、返回行数、扫描行数,后面跟完整 SQL。mysqldumpslow 按总耗时、次数、平均耗时排序聚合,能快速找出”执行次数多、总耗时高”的语句,这类语句优化的收益最大。 扫描行数和返回行数的比值值得关注:返回 10 行却扫了 50 万行,基本可以确定索引没起作用或者过滤条件失效。日志里的 Rows_examined 是服务端实际扫描的行数,比 EXPLAIN 的估算值更可信。 开启 log_queries_not_using_indexes 要谨慎。开发环境开着能发现漏建索引的地方,生产环境在大表上开,日志量会很快失控,磁盘写满比慢查询本身更麻烦。 二、EXPLAIN 的关键字段 EXPLAIN SELECT…

334 33 0
慢查询优化:从 EXPLAIN 开始排查
周牧野·2026-10-17
混沌工程入门:用故障注入验证系统韧性

系统在预发环境跑得好好的,到生产上遇到一个依赖超时就全线变慢。这类事发生几次后会让人意识到:正确性和韧性是两件事。前者说没有故障时行为对不对,后者说有故障时行为还能不能接受,而后者几乎不可能靠读代码确认。 实验的前提:先有假设 混沌工程的核心动作不是注入故障,而是验证假设,注入只是手段。 假设需要参照物,即稳态指标:一组代表”系统当前正常”的可观测指标,如请求成功率、P99 延迟、核心接口 QPS。它把”系统还好吗”变成可判定的问题,没有它,注入后看到什么都是模糊的。 一条完整的假设长这样:如果某个下游依赖的响应时间增加到 500ms,那么下单接口的成功率应在 30 秒内保持在 99.9% 以上,因为该依赖有熔断和本地缓存兜底。这句话包含了故障类型、影响范围、指标、阈值,以及机制解释。 对照之下,”随机杀一个 Pod 看看会怎样”不算实验:它没有预期,结果只能是”好像没事”或”好像有事”,无法转为改进项。 假设被证伪也是有价值的结论——它证明了一个被认为存在的兜底实际没生效,报告里最有价值的部分往往就是被证伪的假设。 四个步骤 步骤 做什么 产出 常见遗漏 假设 定义稳态指标与预期影响 一条可判定的陈述 阈值拍脑袋,事后无法判定 注入 选定范围、类型、时长 一次可控的故障 没有明确的撤销路径 观察 采集指标、对比控制组 偏离与否的证据 只看总量不看分位 复盘 判定假设成立与否 改进项与回归实验 发现问题但没有跟进 观察要有控制组:指标波动 3% 可能是注入造成,也可能是流量自然波动,有了对照组才能分开。复盘的输出必须包含回归实验——修复之后重跑同样的实验确认假设成立,没有回归,修复就只是”改了点东西”。 最小爆炸半径 爆炸半径是实验设计里最需要克制的部分,可以从几个维度收紧: 实例数:先注入单个实例,而不是整批。 依赖:先选非关键依赖,而不是核心链路上的数据库。 流量:先用内部账号或 1% 灰度流量,而不是全量用户。 时长:先注入数十秒到数分钟,而不是持续半小时。 地域:先在单可用区,而不是跨区。 时间窗口同样属于爆炸半径:非高峰、有值守人、有变更窗口。值守人的意义不只是”看着”,还包括实验意外扩大时能立刻决策是否中止。 常见的注入类型 注入类型…

193 10 0
混沌工程入门:用故障注入验证系统韧性
韩澈·2026-10-16
日志收集链路:采集、传输、存储与检索

日志链路是一条数据管道:应用产生事件,采集端读取,传输层削峰,存储层建立索引,查询端再按条件检索。任何一段都可能限流或中断。设计重点不是“能看到日志”,而是在业务高峰与局部故障下,日志不反向阻塞业务,并能说明丢失、重复与延迟的边界。 两种 Agent 采集模式 读文件 tail 模式由节点 Agent 监听日志文件,记录文件标识和读取偏移。它适合传统进程、需要本地落盘审计的场景,也便于应用与采集器解耦。难点在于轮转、截断、符号链接、容器短生命周期和磁盘占满。仅按文件名保存偏移会在轮转后读错对象,应同时识别文件系统标识,并处理“旧文件已重命名但仍有未读内容”。 应用写 stdout/stderr 是容器环境的常用方式。应用无需知道后端地址,容器运行时把输出写入节点日志,Agent 再统一采集并补充 Pod、命名空间和容器标签。这里的“直推 stdout”并不代表同步发送到远端;stdout 仍需经过运行时与节点采集链路。若日志产生速度长期超过节点写盘和采集能力,容器日志同样可能占满磁盘。 缓冲、背压与投递语义 采集端到传输层之间需要内存队列吸收短暂抖动,磁盘队列覆盖较长中断。内存缓冲快但进程退出即丢失,磁盘缓冲更耐故障,却要设置容量、刷盘与淘汰策略。队列达到上限时必须明确选择:丢弃低等级日志、采样、暂停读取文件,或把事件转移到隔离区,不能让行为取决于内存耗尽。 业务写日志不应同步阻塞远端传输。若后端变慢,背压应停留在采集链路,不能一路传回业务请求。对本地文件 tail,Agent 可以暂停读取,待后端恢复后按偏移补采;前提是文件在等待期间没有被轮转删除,且磁盘保留足够空间。 常见实现提供至少一次投递:发送后若确认响应丢失,Agent 会重发,于是存储端可能出现重复。可以为事件生成稳定 ID 去重,但全局去重会增加写入成本。多数运行日志接受少量重复,并通过时间、实例、偏移或事件 ID 辅助判断;要求严格一次的场景需要端到端事务语义,成本与复杂度都更高。 buffer: type: disk max_size: 8GiB flush_interval: 1s on_full: drop_debug_then_oldest retry: max_backoff: 30s jitter: true 配置值只是结构示例,容量应根据峰值写入量、可容忍中断时长和节点磁盘预算计算。 多行日志必须在采集端合并 Java 堆栈、解释器异常和某些数据库错误会跨多行输出。若每行作为独立事件,异常类型、调用栈与请求上下文会分散,查询也难以还原。合并规则通常把“符合时间戳开头的行”识别为新事件,其余缩进行追加到当前事件: 新事件开头:^d{4}-d{2}-d{2}Td{2}:d{2}:d{2} 续行示例:^s+ats|^Caused by:|^s*... d+ more 规则应在靠近源头的 Agent…

742 44 4
日志收集链路:采集、传输、存储与检索
码同客·2026-10-16
Redis 数据结构实战:五种基础类型的使用场景

Redis 的价值不在于”快”本身,而在于它把一组精心挑选的数据结构放在了内存里,并且保证了单个命令的原子性。用好 Redis 的关键,是让业务模型尽量贴合这些结构的原生语义——一旦发现需要在客户端把多个命令拼成一个”复合操作”,就该停下来想想选型是不是出了问题。 一、五种基础类型怎么用 String 是最简单的类型,但能力不止存字符串。它可以存整数做计数器,也可以序列化整个对象存成 JSON(慎用,见后文大 key 问题)。 # 页面浏览计数 INCR article:1001:views # 带过期的幂等令牌 SET order:dedupe:20261021:1001 1 NX EX 300 SET key value NX EX seconds 这个组合在分布式锁、防重复提交、幂等去重里出现频率极高:NX 保证只成功一次,EX 保证即使客户端崩溃锁也能自动释放。注意这是”尽力而为”的锁,对正确性要求极高的场景还需要看门狗续期和 fencing token 之类的补强。 Hash 适合存对象的部分字段,读写单个字段不需要序列化整个对象。 HSET user:1001 name "tom" level 3 last_login 1761024000 HINCRBY user:1001 level 1 HGET user:1001 name 用户资料、商品属性这类”字段多但每次只改一两个”的场景,Hash…

274 18 0
Redis 数据结构实战:五种基础类型的使用场景
凛川 Wave·2026-10-16
JavaScript 异步演进:从回调到 async/await

JavaScript 的异步史,某种意义上就是”怎么把回调写得像同步代码”的进化史。从 callback 到 Promise 再到 async/await,每一步都在解决上一阶段留下的可读性和错误处理问题。把事件循环和这三代写法的关系理顺,写异步代码时心里才有底。 一、事件循环:异步为什么”不卡” JS 是单线程执行的,但宿主环境(浏览器或 Node)提供了事件循环。粗略地说:调用栈清空后,事件循环从任务队列里取任务继续执行。定时器、网络响应、用户点击,本质上都是”稍后往队列里塞一个任务”。 队列其实分两类,优先级不同: 宏任务(macrotask):setTimeout、I/O、UI 事件、setInterval,一次循环取一个。 微任务(microtask):Promise 的 .then/.catch/.finally、queueMicrotask、MutationObserver,在当前宏任务结束后、下一个宏任务开始前全部清空。 console.log("a"); setTimeout(() => console.log("b"), 0); Promise.resolve().then(() => console.log("c")); console.log("d"); // 输出:a d c b setTimeout(fn, 0) 不是”立即”,而是”下一个宏任务”;Promise 的回调则在当前宏任务收尾时就跑。这个顺序解释了很多”为什么我的回调晚了一步”的困惑。 二、回调时代的问题 Node 早期的接口几乎全是回调风格: readFile(path, (err, data) => { parse(data, (err, parsed) => { save(parsed, (err) => {…

807 73 0
JavaScript 异步演进:从回调到 async/await
Moon 槐·2026-10-16
备份与恢复演练:RPO/RTO 的落地

备份脚本每天在跑,日志显示成功,监控面板上一片绿色。直到某天需要真正用它恢复数据的时候,才发现文件是坏的、binlog 有断档、或者恢复出来少了几个小时的记录。备份本身不产生价值,能被成功用于恢复的备份才产生价值。 先把两个指标定义清楚 RPO(Recovery Point Objective):发生故障时,允许丢失多少时间窗口内的数据。回答的是”能丢多少”。 RTO(Recovery Time Objective):从故障发生到服务恢复可用,允许用多久。回答的是”要多久”。 这两个数字必须由业务方给出,而不是技术团队拍。定 RPO 时问:”最近两小时的订单全部丢失,业务影响是什么,人工补录的成本是多少?”定 RTO 时问:”服务中断三十分钟和一小时的差别,对业务意味着什么?” 技术团队的任务是回答”在这个 RPO/RTO 下需要什么方案、成本多少”。对应关系大致如下: 目标 需要的手段 RPO 数十分钟,RTO 数小时 每天一次全量备份即可 RPO 数分钟,RTO 数十分钟 全量 + 增量 + binlog 归档(PITR) RPO 秒级 半同步复制到备机,或同步提交的多副本 RTO 分钟级 热备实例常驻运行,随时可切换 把 RPO 写成 0 是不诚实的。即使是同步复制,主库在写入返回前挂掉,客户端收到的是超时而非成功,业务上该事务是否成立本身就存疑。 物理备份与逻辑备份 逻辑备份导出的是 SQL 语句或数据行。典型工具是 mysqldump: mysqldump --single-transaction --master-data=2 --routines…

1836 69 0
备份与恢复演练:RPO/RTO 的落地