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
韩澈·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
日志收集链路:采集、传输、存储与检索
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-15
技术方案汇报的结构化表达

方案被驳回,很多时候不是方案本身有问题,而是听的人在前十分钟没能建立起”这件事大概是什么、要花多少、风险在哪”的框架,后面的细节就全部悬空了。汇报是一种有结构的工程产物,可以拆开练。 结论先行:前三句话要装什么 技术人习惯按推导顺序讲:背景→调研→候选→权衡→结论。这个顺序适合写论文,不适合汇报。听汇报的人在前几分钟注意力最集中,而这时他最需要的是三件事: 要做什么:一句话说清动作,例如”把订单创建链路中的三个下游改成异步”。 要多少资源:几个人、几周、要不要别的团队配合、有没有外部成本。 要对方做什么决定:是批准、是选 A 还是 B、还是只需要知会。 错误开场:"我先讲一下背景,订单服务现在的调用链路是这样的……"(5 分钟后还没说到要做什么) 正确开场:"我们打算把订单创建里的积分、通知、风控三个下游改为异步, 预计 2 人 3 周,不需要其他团队配合,需要你确认的一点是: 风控允许异步后的一致性窗口,业务侧是否接受。" 结论先行不等于只讲结论。它的含义是:先给骨架,再按需填肉。听的人如果只关心排期,他可以在第一分钟就得到答案然后决定要不要继续听细节。 同一份方案,三种读法 一份方案通常会被三类人看,他们的关注点差异很大: 角色 主要关注 他要的那句话 常见追问 技术负责人/架构师 风险、边界、与现有系统的一致性 “失败时会怎样,怎么回退” 兼容性、降级、监控 项目管理/业务方 排期、成本、收益 “什么时候能用上,值不值” 里程碑、能不能砍范围 一线执行者 能不能落地、依赖谁 “我要改哪些模块、找谁联调” 接口、灰度步骤 做法不是写三份方案,而是同一份方案里把这三个问题各自放在显眼位置。经验上最简单有效的安排是:摘要页回答前两类人的问题,正文后半部分的”实施步骤”回答第三类人的问题。 汇报时还有一条:不要给不同角色讲同一份材料。给架构评审讲时可以跳过成本明细,给业务方讲时可以跳过接口设计,但摘要页必须保留——它是共同语境。 对比矩阵怎么用 候选方案的对比不要用一段段散文描述,用矩阵: 维度 A:全量重构 B:绞杀者模式逐步替换 C:维持现状 + 局部优化 首版交付 12 周 3 周出第一个能力…

1246 30 2
技术方案汇报的结构化表达
Corin·2026-10-15
C++ 智能指针入门:unique_ptr、shared_ptr 与所有权

C++ 的内存管理争议了三十年,现代 C++ 给出的答案不是”再小心一点”,而是把所有权写进类型里。智能指针就是这套答案的载体:谁负责释放、什么时候释放,不再靠注释和约定,而是靠编译器检查。这篇文章把三种智能指针的语义、边界和取舍讲清楚。 一、所有权为什么值得单独说 先看没有所有权语义的代码长什么样: Widget* w = new Widget(); // ... 三十行业务逻辑 ... delete w; 三十行中间如果 return、抛异常、或者后人加了个分支提前退出,delete 就被跳过了。更糟的是,这段代码没有回答几个关键问题:w 能不能被别人 delete?函数返回后调用者要不要接管释放?把 w 塞进容器算谁的? 所有权语义要回答的正是这些问题。当类型本身就能表达”独占”还是”共享”,写错的机会就少了一大截。C++ 里裸指针的传统用法默认表达”非拥有观察”,即只是看着,不负责释放;负责释放的那一方应该用智能指针或者容器表达。 二、unique_ptr:独占且可移动 std::unique_ptr<T> 表达”这块资源归我,只有我能释放”。它没有引用计数,开销通常就是裸指针本身,析构时自动 delete。 auto w = std::make_unique<Widget>(); use(*w); // 用法和裸指针几乎一致,解引用即可 auto w2 = std::move(w); // 所有权转移 // w 现在是 nullptr,使用它之前必须检查或重新赋值 关键性质是可移动、不可复制。拷贝被 = delete 掉了,试图复制会直接编译失败——这正是想要的:独占语义必须由编译器强制,否则就退化成了口头约定。函数传参时同样如此,按值传 unique_ptr…

498 38 2
C++ 智能指针入门:unique_ptr、shared_ptr 与所有权
Talen·2026-10-14
前后端分离架构演进:从模板渲染到 API 优先

“前后端分离”在国内技术圈流行了十年以上,如今已经很少有人把它当成新概念讨论。但有意思的是,近几年随着服务端组件、岛屿架构等形态的出现,渲染逻辑又开始往服务端回流。回头看这段演进,驱动变化的从来不是某种风格的优劣,而是页面复杂度、团队协作方式和终端数量这三件事的实际变化。 服务端渲染:老而弥坚的形态 模板渲染的流程很朴素:请求进来,服务端查数据、填模板、吐 HTML,浏览器直接展示。它的优势在今天依然成立——首屏快(HTML 直出)、SEO 友好、一套渲染逻辑、没有跨域和接口契约问题。对内容型站点、后台管理系统、对交互要求不高的页面,模板渲染依然是成本最低的方案。 它的极限出现在交互密度上升之后。页面里嵌入大量局部刷新、拖拽、实时校验、复杂表单状态时,模板引擎拼字符串的方式会让代码迅速变成意大利面;前后端职责纠缠在模板里,改一个按钮的展示逻辑可能要动后端代码并重新部署。团队变大后,前端和后端抢同一个文件、发布互相等待,协作摩擦指数上升。 分离的收益:并行与多端复用 前后端分离的本质是把”页面”拆成”数据接口 + 客户端渲染”两部分。后端只负责吐 JSON(或 gRPC、GraphQL 等形态的数据),前端在浏览器里拿数据自己组装界面。 最直接的收益是并行开发。接口契约定下来之后,前端用 mock 数据开发界面,后端同步实现逻辑,两边互不阻塞。联调不再是项目尾声的冲刺,而是贯穿全程的例行公事。前端也因此拿回了界面的完整控制权:路由、状态管理、组件复用、动效、离线能力,都变成客户端代码的一部分,可以独立测试、独立发布。 多端复用是另一个推力。同一套接口要喂给网页、App、小程序、开放平台,把渲染留在客户端、把数据抽成接口,是最自然的划分。后端从”为某个页面服务”变成”为多种终端服务”,接口的抽象层次被迫提高,反倒让业务逻辑更清晰。 分离的代价:契约、跨域与鉴权分散 分离没有免费午餐,代价同样具体。 接口契约成为新的协作边界。字段改名、类型变更、语义漂移,过去在模板里改一行就好,现在要动两端。没有契约管理(接口文档、类型定义、契约测试)的团队,联调阶段的”字段对不上”会反复消耗时间。契约一旦对外发布,就变成了承诺,废弃字段要走版本策略,不能想删就删。 跨域是浏览器给分离架构的见面礼。前后端不同源就要处理 CORS 预检、凭证传递、cookie 策略,网关层要为跨域配置付出额外的运维成本。开发环境还要处理代理转发,配置不对就出现”本地能跑、线上 401″的经典问题。 鉴权从”服务端模板里判一下”变成”每个接口都要鉴权 + 前端路由也要守卫”的两层体系。Token 放哪、怎么刷新、跨标签页怎么同步、登出怎么让所有端失效,每一项都是分布式会话的现实问题。过去一个 session 中间件解决的事,现在散落在网关、服务端校验、前端拦截器三处。 REST 还是 RPC:风格背后的关注点 分离之后,接口风格的选择浮上水面。REST 以资源为中心,URL 表示名词、HTTP 方法表示动作,天然适合 CRUD 密集、对外暴露、需要通用缓存语义的场景。它的优势是标准化程度高、任何语言任何工具都能消费。 RPC 风格以动作为中心,一个方法一个调用,参数返回值有强类型定义,适合内部服务间通信、对性能和类型安全敏感的链路。序列化更紧凑、调用更接近”函数调用”的心智模型,代价是生态偏内部、对外暴露需要额外的网关转换。 取舍的关键不是”哪个更先进”,而是消费方是谁。面向第三方、面向多端、需要缓存和可读性的场景偏向 REST;内部微服务、高频调用、对延迟敏感的场景偏向 RPC。一个系统里两者并存是很常见的现实,没必要强求统一。 接口版本管理:承诺的成本 接口一旦对外,改起来就是社会事件。常见的版本策略有 URL 版本(/v1/users)、请求头版本、以及”只加不改”的平滑演进。 URL 版本直白、好路由、好监控,代价是版本会无限繁殖;请求头版本…

717 51 2
前后端分离架构演进:从模板渲染到 API 优先
AsyncLab·2026-10-15
Python 装饰器详解:从语法糖到面向切面

装饰器是 Python 里最容易被”用会了却讲不清”的特性之一。很多人写得出 @login_required,但被问到 @deco 到底等价于什么、为什么必须写 functools.wraps、装饰顺序谁先谁后时,答案就开始含糊。这篇文章从函数对象这个起点把整条链路走一遍。 一、前置:函数是对象,名字只是绑定 Python 里函数是普通对象,可以赋值、传参、放进容器、作为返回值: def greet(name): return f"hi, {name}" hello = greet # hello 与 greet 指向同一个函数对象 print(hello("world")) # hi, world def greet 只是把函数对象绑定到名字 greet 上。既然函数能被赋值,那”把一个函数替换成另一个函数”就完全合法——这正是装饰器的全部机制。 二、闭包:装饰器的载体 装饰器返回的新函数需要”记住”原函数,这就得靠闭包: def make_logger(func): def wrapper(*args, **kwargs): print(f"call {func.__name__}") return func(*args, **kwargs) return wrapper wrapper 引用了外层的 func,即使 make_logger 已经返回,这个绑定依然存活。闭包捕获的是变量绑定而不是值的快照,所以对可变对象的修改能被观察到,这一点在后面讲坑的时候还会用到。 三、functools:先认识两个工具…

733 73 1
Python 装饰器详解:从语法糖到面向切面
雾梨 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 的作用机制
林见川·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 开始排查