把DDIA读厚(八):数据库复制的“冰与火之歌”

导语:你是否遇到过这样的场景:明明在后台看到了“操作成功”的提示,刷新后数据却“穿越”回了修改前的状态?这并非灵异事件,而是分布式系统中一个经典而残酷的现实。本文将带你深入《设计数据密集型应用》的核心章节,从一个诡异的“数据丢失”谜题开始,揭开数据库复制的两种核心模式——物理复制与逻辑复制的神秘面纱,并最终探索它们如何驱动了现代实时数据架构的脉搏。 序幕——一个“幽灵”数据引发的血案 想象一下,你正在为一个电商系统负责。在一次大促活动前,运营同事紧急修改一件爆款商品的价格,从 ¥ 100 降为 ¥ 80。他在后台点击“保存”,系统弹出“修改成功!”的绿色提示。他长舒一口气。 几分钟后,主数据库服务器因为机房电源抖动而宕机。幸运的是,你们的架构有高可用设计,系统在半分钟内自动将一个从库提升为新的主库,服务恢复。但这时,用户和运营同时发现,那件商品的价格依然是 ¥ 100。那个“成功”的修改,如同幽灵一般,消失得无影无踪。 “我明明存上了!数据库的持久性(Durability)承诺呢?” 运营同事的质问,直击了问题的核心。 要解开这个谜题,我们必须潜入数据库的“复制”世界。复制,即在不同的机器上保存相同数据的副本,其目的无外乎三点: 高可用性:一台挂了,另一台顶上。 读扩展性:更多副本分担读请求。 降低延迟:在用户附近部署副本。 而“幽灵数据”问题的根源,在于主从节点间同步数据的方式——尤其是异步复制(Asynchronous Replication)。 在一个异步复制系统中,主节点处理完写操作后,会立刻向客户端返回“成功”,然后才“抽空”将变更发送给从节点。主从之间存在一个微小的时间窗口,我们称之为复制延迟(Replication Lag)。我们的“血案”,正是在这个延迟窗口内,主节点倒下了。新上任的主节点(原从库)根本没来得及收到那次改价操作,导致了数据的“丢失”。 这是为了高性能和高可用性,必须付出的代价。那么,数据库是如何在内部传递这些变更信息的呢?这里,就引出了两条截然不同的技术路线,宛如冰与火之歌,各有千秋——物理复制与逻辑复制。 物理复制的“凛冬”——PostgreSQL 的 WAL 之道 物理复制,顾名思义,它复制的是数据变更的物理痕跡。它不关心变更的业务含义,只关心“在哪个文件的哪个位置,哪些字节发生了改变”。 这方面最经典的代表,就是 PostgreSQL 的预写日志(Write-Ahead Log, WAL)流复制。 什么是 WAL? 首先,WAL 是单机数据库为了保证崩溃安全而设计的。任何对数据文件的修改,都必须先以日志的形式、顺序地写入到 WAL 文件中。如果数据库在修改数据页的半途中崩溃,重启后可以通过回放 WAL 来恢复到一致的状态。 如何用于复制? PostgreSQL 的工程师们想出了一个绝妙的主意:既然主库为了自身安全必须生成 WAL,何不将这份“操作录像带”直接流式地发送给从库呢?从库接收到后,像快进一样回放这份录像带,在自己数据文件的相同位置上,执行完全相同的字节修改。 主库:先写 WAL,再改内存 buffer。 从库:先接收 WAL,再改内存 buffer(相当于 replay)。 所以从库的数据页始终是 由 WAL 驱动的只读重放。 物理复制的“冰”之特性 这种方式如同寒冰一般,极致、高效、但也非常“僵硬”。 优点:性能极致。这是几乎没有额外开销的复制方式。主库生成 WAL 是刚需,从库应用 WAL 就像是 memcpy,速度极快。 缺点:紧密耦合。这是它的致命弱点。 版本锁定:如果主从库的 PostgreSQL 大版本不同,其内部的数据页存储结构可能存在差异。一份来自 v15 的 WAL “录像带”,在 v14 的从库上“回放”,很可能导致数据文件彻底损坏。 架构锁定:无法跨异构数据库或不同硬件架构进行复制。 灵活性差:你无法只复制一个库或一张表,因为 WAL 记录的是整个实例的物理变化。 物理复制就像一位严谨的复印员,能完美克隆,但对复印稿的格式要求极为苛刻。……

阅读全文

把DDIA读厚(七):从SSTable到LSM树,再到MySQL的B+树之辩

在深入数据系统的世界时,我们常常惊叹于上层架构的宏伟,但支撑这一切的,往往是那些看似朴素却充满智慧的底层设计。在 DDIA 第三章中,SSTable 与 LSM 树无疑就是这样的基石。 这趟探索之旅,始于一个简单构件——SSTable,它像一块块乐高积木,虽好用但略显呆板。随后,我们将看到 LSM 树这套“动态系统”是如何赋予这些积木生命,搭建出能抵御写入洪流的坚固堡垒。最后,我们将站在更高的视角,探讨一个经典问题:既然 LSM 树如此强大,为何像 MySQL 这样的关系型数据库巨头,却选择坚守 B+ 树的阵地? 这不仅是对一个数据结构的剖析,更是一场关于设计哲学与工程权衡的深度思辨。 第一幕:SSTable——一块有序且不可变的坚实地基 SSTable(Sorted String Table)是 LSM 树在磁盘上的基本组成单位。它的设计法则纯粹而简单,牢记两点即可: 内部有序:文件内的键值对,严格按照键(Key)排序。 不可变(Immutable):一旦写入,永不修改。 一个 SSTable 文件并非一块铁板,而是由多个部分精密组成的,好比一本字典: 数据块 (Data Block):字典的“书页”,存储着一小段连续的、有序的键值对。一个 SST 文件中通常会有成百上千个 Data Block(取决于文件大小和 block size 配置,例如 4MB 数据 flush 下来,block size=16KB,大约会有 256 个 Data Block)。 索引块 (Index Block):字典的“页眉导引词”,记录每个 Data Block 的边界键及其位置,便于快速定位。 元索引块 (Metaindex Block):记录 Bloom Filter、属性信息等元数据块的位置。 文件尾 (Footer):固定在文件末尾,记录索引块和元索引块的位置,并带有魔数,是读取 SSTable 的入口。 这种结构带来了显而易见的好处: 读取高效:在常见缓存命中的场景下,查找通常只需一次磁盘 I/O(目标 Data Block),范围查询更是高效的磁盘顺序读。 合并顺序化:多个 SSTable 的合并过程逻辑上类似归并排序,以顺序读写为主,相对高效。但同时,也会带来 写放大 的问题。 然而,“不可变”的特性,也带来了致命的弱点:无法高效地处理单次写入。为一个新键值对而重写整个几 GB 的文件,无异于天方夜谭。SSTable 是优秀的“只读”构件,但它需要一套动态系统来盘活它。……

阅读全文

把DDIA读厚(六):一次“写入”的奇幻漂流——从应用到磁盘

这是《把 DDIA 读厚》系列的第六篇文章。在 DDIA 的精读之旅中,我们已经聊过了可靠性、可扩展性等宏大主题。今天,我想和你一起,做一次微观探险。我们将聚焦于一个最基础、最频繁的动作——“写入”,跟踪一次小小的写入请求,从它在应用程序中诞生,到最终在磁盘上安家,看看这段旅程中都发生了哪些惊心动魄、充满权衡的故事。 这一切,都始于 DDIA 第三章中的一句话: “对于极其简单的场景,日志追加式的写入拥有非常好的性能。” 这句话很符合直觉。我们深挖下去,发现无论是对于 HDD 还是 SSD,顺序追加都能完美地顺应硬件的“天性”,获得极佳的性能。不仅如此,我们还了解到,操作系统(OS)的**页缓存(Page Cache)**机制,更是将这个优势发挥到了极致——它将我们应用层无数次微小的写入,在内存中“攒”成大块,再如行云流水般一次性顺序刷入磁盘。 一切看起来如此完美。 但一个致命的疑问也随之浮现:如果 OS 还没来得及把缓存中的数据刷盘,就发生了断电,数据不就丢了吗? 对于任何严肃的业务系统,这都是不可接受的。为了堵上这个漏洞,我们找到了操作系统提供的“保险开关”——fsync系统调用。它能强制命令 OS 将缓存数据刷入磁盘,确保数据“落盘为安”。 然而,当我们尝试为每一次写入都系上fsync这条“安全带”时,一个诡异的现象出现了:我们引以为傲的性能优势不仅消失殆尽,整个写入模式甚至会退化。这正是本次探索的核心谜题: 为什么在一个繁忙的系统里,为一个逻辑上“顺序追加”的文件频繁调用fsync,最终会导致物理上“随机 I/O”的性能表现? 这个看似矛盾的现象背后,隐藏着一场应用、操作系统与硬件之间,关于性能与持久性的复杂博弈。别急,泡杯咖啡,让我们顺着这个疑问,一探究竟。 第一站:理想乡——顺序写入的物理学优势 想象一下最纯粹的写入模型,就像 DDIA 开篇的db_set脚本一样,它只是简单地将数据追加(append)到一个文件末尾。这在物理上意味着什么? 对于机械硬盘(HDD):磁头无需耗费毫秒级的“寻道时间”在盘片上空到处乱飞,只需移动到文件末尾,然后就可以像老式唱片机一样,在盘片旋转时连续不断地刻录数据。 对于固态硬盘(SSD):它避免了 SSD 最头疼的“先擦除再写入”的放大效应。对于一个已有的数据块,原地修改意味着“读取整个块 -> 内存中修改 -> 擦除整个块 -> 写回整个块”的酷刑。而顺序追加,则是轻松地在干净的闪存页上连续写入,轻快而优雅。 无论在哪种介质上,顺序 I/O 都牢牢抓住了硬件的“天性”,是写入操作的“理想态”。 第二站:缓冲区——操作系统的好心与谎言 既然顺序写入这么快,那问题出在哪?答案在我们和硬件之间隔着的第一个“中间商”——操作系统。 当我们调用write()时,应用程序很快就拿到了“写入成功”的回执。但这是一个“善意的谎言”。数据并未抵达磁盘,而是进入了操作系统内核的页缓存(Page Cache)。 这是 OS 的一片苦心: 性能:把成百上千次微小的写入请求,在内存里“攒”成一大块,然后一次性交给磁盘,将零散的写入合并成大块的顺序 I/O,效率倍增。 响应:让应用程序不必等待缓慢的磁盘,立刻就能返回,继续处理其他任务。 这个缓冲机制也顺带解答了一个底层细节问题:我们逻辑上是连续写入,物理上如何保证磁盘在写完扇区 N 后,紧接着就去写 N+1 呢?答案就在于,文件系统在为这“一大块”攒好的数据申请磁盘空间时,会倾向于分配一片连续的物理块(Extent),从而将逻辑上的顺序追加,转化为了物理上的顺序写入。 然而,这个“中间商”在带来效率的同时,也带来了第一个致命风险:断电会丢数据! 内存是易失的,任何在页缓存里还没来得及去磁盘“安家落户”的数据,都会在断电瞬间烟消云散。 第三站:fsync的审判——追求真理的代价 为了对抗这种风险,操作系统给了我们一个“杀手锏”——fsync()系统调用。它像一张神圣的审判令,强制命令 OS:“放下一切缓冲和优化,把我要求的数据,立刻、马上、同步地刷到磁盘上去!直到确认它安全了再回来见我。” 有了fsync,我们似乎拥有了“数据金身”。但回到我们最初的问题,如果我们为每一次微小的写入都调用fsync,为何反而会陷入“同步随机 I/O”的泥潭? 想象一下你的服务器后台,它是一个繁忙的十字路口: 你的应用刚为database文件完成了一次fsync,磁头停在了它的末尾。 就在下几毫秒,操作系统的日志服务syslog抢到了 CPU,要求在/var/log/messages里写一条日志。为此,磁盘磁头不得不长途跋涉,飞到盘片的另一个位置。 紧接着,你的应用又来了一次写入请求,再次调用fsync。此时,磁头必须从syslog文件的位置,再千里迢迢地飞回来。 看到了吗?在多任务环境下,磁盘磁头的所有权在不同进程间被疯狂抢夺。我们逻辑上对单一文件的顺序追加,在物理层面被“插队”的其他进程打碎,磁头的移动轨迹和随机写入毫无二致。每一次fsync都几乎要支付一次完整的“寻道+旋转”的重税。……

阅读全文

把DDIA读厚(五):图数据库实战——手把手带你挖出一个“欺诈团伙”

这是《把 DDIA 读厚》系列的第五篇文章。在上一篇,我们深入探讨了关系模型与文档模型的世纪之争,核心在于它们如何处理数据的“关系”。今天,我们要把“关系”这个词推向极致,聊一聊为“关系”而生的数据模型——图。 引子:当 JOIN 遇见了"六度空间" 你跟产品经理说:“这个‘猜你喜欢’的功能,要查用户好友的好友,还得看共同兴趣,SQL 写起来太复杂,跑起来也慢,不好做。” 产品经理两手一摊:“Facebook 不就能做吗?” 这个场景很真实。当我们的业务需求,不再是简单的“查 A 查 B”,而是变成了“探索 A 和 B 之间千丝万缕的、不确定的、多层次的联系”时,我们熟悉的 JOIN 就开始力不从心了。这时,我们需要一件专门为此而生的神兵利器。 本篇锚点:为"关系"而生的数据模型 我们今天的“锚点”,是 DDIA 第二章关于图模型的核心观点: 图数据模型专为“多对多”关系是常态、数据连接的深度和复杂性是核心挑战的场景而设计。 它的世界里,万物皆为顶点(Nodes),万物之间的联系皆为边(Relationships)。我们的任务,就是在这个由点和线构成的宇宙里,探索那些隐藏的路径和模式。 发散深潜:手把手挖出一个"欺诈团伙" 理论总是枯燥的,我们直接开干。下面,我将手把手带你用当今最流行的图数据库 Neo4j,来完成一次真实的反欺诈“案件侦破”。 第一步:环境准备 请您前往 Neo4j 的官方网站下载并安装 Neo4j Desktop。它对个人开发者完全免费,且安装过程非常简单。 安装后,请按以下步骤操作: 打开 Neo4j Desktop,新建一个项目(Project)。 在这个项目里,点击 “Add Database” -> “Create a Local Database”。 给你的数据库起个名字(比如 fraud-detection),设置一个密码(比如 password),然后点击 “Create”。 数据库创建好后,点击旁边的 “Start” 按钮启动它。 启动成功后,点击 “Open”,这会自动在浏览器中打开 Neo4j Browser 操作台。 至此,您的图数据库环境就已经准备就绪了! 第二步:数据建模与导入 在我们的反欺诈场景中,用户、设备、IP地址 都是顶点。它们之间的 使用、来自 等都是关系。现在,请在 Neo4j Browser 的输入框中,一次性地复制并执行以下所有代码。 Cypher……

阅读全文

把DDIA读厚(四):关系模型 vs 文档模型,世纪之争与你的抉择

这是《把 DDIA 读厚》系列的第四篇文章。今天,咱们不聊那些高大上的分布式共识,而是回到一切开始之前,聊一个每个后端工程师都必须面对的、最朴素也最重要的问题:你的数据,到底应该怎么存? 引子:建表,还是塞个 JSON? 老哥们,拿到一个新需求,是不是脑子里第一反应就是“这数据存哪个库,表怎么建”?紧接着,灵魂拷问就来了: 是一板一眼地遵循三范式,把数据拆分到好几张关联的表里,然后靠 JOIN 过活? 还是图个痛快,直接在表里弄个 TEXT 或 JSON 字段,把整个对象序列化之后“一把梭”塞进去? 这个问题,表面上是“规范”与“便捷”之争,实际上,背后是两种数据模型哲学的激烈碰撞。这个选择,将在你写下第一行代码之前,就深远地影响你整个系统的架构、性能和未来的可维护性。 回顾与衔接:那些年,我们维护过的"屎山" 在上一篇的结尾,我们留下了一个关于“屎山”系统的思考题。很多时候,一个系统之所以变得难以维护,正是源于其早期做出的、看似无伤大雅的数据模型选择。一个不恰当的模型,会像一根歪掉的顶梁柱,让后续所有的添砖加瓦都变得异常痛苦。 本篇锚点:一切始于数据模型 我们今天讨论的“锚点”,是 DDIA 在第二章开篇的核心论断: 数据模型可能是软件开发中最重要的部分,它不仅影响软件的编写方式,更影响我们对问题的思考方式。 你选择用关系模型还是文档模型,这个决定,定义了你的数据世界观。接下来,我们就通过一个每个 Go 工程师都感同身受的例子,来看看这两种世界观的巨大差异。 发散深潜:一个 Go struct 的"坎坷下凡路" 1. 天堂:我们的"完美"Go struct 在我们的代码世界里,业务对象是纯洁无瑕、高度内聚的。比如,我们要为一个求职网站设计一个“用户简历”结构体,在 Go 里它长这样,非常自然: Go type UserProfile struct { ID int64 Name string Summary string Positions []Position // 工作经历 Educations []Education // 教育背景 } type Position struct { JobTitle string Organization string StartDate time.Time EndDate time.Time } type Education struct { SchoolName string Degree string StartDate time.……

阅读全文

把DDIA读厚(三):写给“未来你”的系统设计原则

这是《把 DDIA 读厚》系列关于第一章的最后一篇文章。在开始前,我们先快速回顾一下本系列的创作“心法”:我们以 DDIA 的核心思想为锚点,用一个接地气的深潜案例将其“翻译”成我们的实战经验,最后收束为可供 Go 工程师和准架构师借鉴的行动指南。 回顾与衔接:我们究竟在维护什么? 在上一篇的结尾,我们留下了一个拷问灵魂的问题: 我们都经历过维护“屎山”代码的痛苦。回想一下,你觉得那个系统最让你头疼的地方,是它的运维极其复杂(可操作性差),还是代码逻辑绕来绕去难以理解(简单性差),亦或是牵一发而动全身,难以修改(可演化性差)? 这个问题没有标准答案,因为通常一个“屎山”系统,这三个问题会并发出现,形成一个令人绝望的恶性循环。 比如一个陈旧的订单系统: 可操作性差:当一个订单状态卡住时,没有任何有效的监控和后台工具。唯一的办法就是 SSH 到线上机器,用 grep 在几百 GB 的非结构化日志里大海捞针,祈祷能找到点线索。运维团队视其为“禁区”。 简单性差:核心的 Order 结构体有超过 100 个字段,其中一半你都不知道是干嘛的。核心的 ProcessOrder 函数长达 2000 行,里面是层层嵌套的 if-else。没人敢动它,因为没人能完全理解它。 可演化性差:系统与一个古老的支付网关实现紧密耦合。当业务要求接入一个新的支付渠道(比如微信或支付宝)时,你发现支付相关的逻辑像鬼一样散落在 15 个不同的文件里。每次修改都像在拆炸弹。 这三种痛苦,恰好就是 DDIA 为我们总结的“可维护性”的三大支柱。它们是我们今天讨论的起点。 本篇锚点:软件的真正成本 我们今天的“锚点”,是 DDIA 提出的一个朴素但常被忽视的真理: 软件的大部分成本不在于初始开发,而在于其持续的、长期的维护。 1 这个维护工作包括:修复 Bug、保持系统平稳运行、调查失效、适配新的平台、为新的业务场景修改功能、偿还技术债,以及添加新功能。我们写的每一行代码,都是在给“未来的自己”或“未来的同事”挖坑或铺路。 为了让未来的路更好走,DDIA 提出了可维护性的三大设计原则:可操作性(Operability)、简单性(Simplicity)**和**可演化性(Evolvability)。 发散(一):可维护性的三大支柱 可操作性 (Operability):让运维不再“背锅” 这指的是,我们的系统设计应该让运维团队的生活尽可能轻松 22。一个具有良好可操作性的系统,应该有好的监控、完善的自动化支持、清晰的文档和可预测的行为 3。这不仅仅是运维团队的事,更是我们开发者的责任。 简单性 (Simplicity):用好的抽象对抗复杂度 这里的简单,不是指功能简陋,而是指移除“意外的”复杂度(accidental complexity) 4。这种复杂度并非问题本身所固有的,而是由我们拙劣的实现方式引入的。 对抗复杂度的最强武器,就是好的抽象。一个好的抽象,能将大量的实现细节隐藏在一个干净、易于理解的外观背后 5。DDIA 举了 SQL 的例子:一句简单的 SELECT 查询,背后隐藏了存储引擎、查询优化器、并发控制等极其复杂的实现,但作为使用者,我们无需关心这些细节 6。 可演化性 (Evolvability):让系统拥抱变化 这指的是我们应该让工程师在未来能轻松地对系统进行修改 777。它也被称为可修改性或可塑性。这是实现敏捷开发在系统层面的基石。 发散(二)深潜:从"代码重构"到"架构重构"的视野升级 马丁·福勒的经典著作《重构》是我们每个开发者的必读物,它教会我们如何在代码层面保持整洁、提高可维护性。……

阅读全文

把DDIA读厚(二):从推特时间线,看懂可扩展性的本质

这是《把 DDIA 读厚》系列的第二篇文章。在上一篇,我们聊了“可靠性”,探讨了如何从“凭感觉”的容错,进化到真正的“可靠性工程”。今天,我们来啃下一个更硬、也更容易被误解的骨头:可扩展性(Scalability)。 回顾与衔接:当"加机器"也解决不了问题 在上一篇的结尾,我们留下了一个思考题: 你是否曾遇到过一个性能瓶颈,是简单的水平扩展无法解决的?它背后的“负载模式”是什么? 这是一个非常经典的问题,几乎每个后端工程师的职业生涯里都会遇到。一个常见的例子就是**“热点账户”或“热点数据”**问题。 想象一个在线教育平台,所有学员都需要在晚上 8 点准时参加一场热门直播课。8 点一到,成千上万的学员同时涌入,系统需要为每个人记录登录、签到等行为。即使你的应用服务器可以轻松地水平扩展(加机器),但所有的写请求最终都指向了同一个逻辑实体——比如数据库里代表这门课程的同一行数据,或者需要更新的同一个总签到人数。 这时,无论你加多少台应用服务器,数据库的那一行数据、那一个计数器,就成了整个系统的瓶颈。所有的请求都在排队等待更新它。这就是一个典型的“加机器”也解决不了的问题。它背后的“负载模式”就是:对单一实体的极度写请求集中。 这个问题引出了我们今天要讨论的核心:在思考“扩展性”之前,我们必须先学会如何描述负载。 本篇锚点:究竟什么是可扩展性? 我们今天讨论的“锚点”,源自 DDIA 对“可扩展性”的精辟定义: 可扩展性不是一个简单的“是/否”标签,而是关于“如果系统的负载以某种特定的方式增长,我们有哪些应对方案?”的讨论。 1 换句话说,当有人问你“你的系统能扩展吗?”时,一个专业的回答不是“能”或“不能”,而是反问:“你指的是哪方面的扩展性?是应对并发用户数增长,还是读写比例变化,或者是数据总量的增加?” 所以,要谈扩展性,我们必须先拥有一套描述它的语言。书中给了我们两个关键工具:描述负载和描述性能。 发散(一):扩展性的语言 描述负载 (Describing Load) DDIA 告诉我们,负载不能用单一数字来描述,而应该用一组最能反映系统架构特点的**“负载参数”**来刻画 2。比如: Web 服务器的每秒请求数。 数据库的读写比例。 实时聊天室的同时在线人数。 缓存的命中率。 描述性能 (Describing Performance) 响应时间 vs 延迟 :响应时间是用户感受到的端到端时间,是我们的金标准 3 。而延迟,通常指请求在队列中等待服务的那部分时间 4 。 百分位点的重要性 :别再用“平均响应时间”来衡量性能了!它极具欺骗性。一个耗时 10 秒的请求,会被 99 个耗时 100 毫秒的请求平均得无影无踪。我们必须关注 p95、p99 甚至 p999 的响应时间 。DDIA 引用了亚马逊的例子:那些请求耗时最长的用户,往往是账户里数据最多的“高价值用户” 5 。为了他们的体验,优化长尾延迟至关重要。 发散(二)深潜:Twitter 时间线背后的"读/写放大"之战 掌握了描述负载和性能的语言,我们就可以来解剖一个神级案例了。这个案例几乎是所有后端工程师理解“读/写放大”和架构权衡的必修课。 Twitter 的核心功能之一是展示用户的“主页时间线”(你关注的所有人发的推文列表)。我们来看它的负载参数:发推请求平均 4.6k QPS,而时间线读取请求高达 300k QPS 6。读请求是写请求的近 65 倍。……

阅读全文

把DDIA读厚(一):从“凭感觉”到可靠性工程

这是《把 DDIA 读厚》系列的第一篇文章。在开始前,我想先跟您聊聊这个系列想做什么。市面上解读经典的书不少,但大多是摘要和复述。咱们想玩点不一样的,真正把这本“屠龙宝刀”读厚。 我们的方法很简单,称之为 “锚点-发散-收束”: 锚点 (Anchor):每一篇,我们都从 DDIA 中精炼出一个最核心、最关键的思想作为“锚点”,确保我们的讨论不偏离航道。 发散 (Diverge):我们会围绕这个“锚点”,结合一个你我他在工作中都可能遇到的具体场景,进行深度剖析,把书中的理论“翻译”成看得见、摸得着的工程实践。 收束 (Conclude):最后,我们会把这些讨论“收束”成可以立即应用的经验和教训,既有给一线开发者的代码级清单,也有给准架构师的架构级思考。 好了,交代完毕。现在,让我们正式开始第一次的“读厚”之旅。 引子:你的服务可靠吗?还是只是"没出事"? 干咱们这行的,谁没在半夜三点被电话叫起来过?当一个新服务上线,我们嘴上说着“应该没问题”,心里可能早就开始“烧香拜佛”了。这种“靠天吃饭”的感觉,其实源于我们对“可靠性”的理解还停留在直觉层面。 DDIA 的第一章,正是要帮助我们完成这个转变:从“凭感觉”做设计,到用“工程思维”构建可靠性。 本篇锚点:故障 (Fault) vs. 失效 (Failure) DDIA 开篇就给我们扔出了一个最基础,但 90%的工程师都会混淆的概念模型,这也是我们今天讨论的“锚点”: 一个可靠的系统,其目标不是杜绝故障(Fault),而是防止故障演变成失效(Failure)。 这两个词儿必须掰扯清楚: 故障 (Fault):指的是系统里某个零件出问题了。比如,数据库突然一个慢查询,网络抖了一下丢了几个包,你写的一个 Go 服务因为空指针 panic 了。 失效 (Failure):指的是整个系统拉胯了,没法给用户提供服务了。比如,用户的 API 请求直接收到了 500 错误。 这就好比你感觉有点头晕(这是故障),但你还能继续跟产品经理 battle(系统没失效)。可要是你直接晕倒了(这就是失效),那需求评审会就得黄。 理解了这个区别,我们就明白了,我们的工作不是幻想一个“零故障”的乌托邦,而是设计一个皮实的、能容忍故障的系统。 发散深潜:一个"普通"的重试,如何引发"雪崩"? 聊到容错,咱们的肌肉记忆第一反应就是“加上重试”。调用下游服务超时了?没事,加个重试,再加个几十毫秒的随机延迟,齐活。我知道,你肯定也写过这样的代码。别不好意思,我也写过。 在大多数情况下,这个模式工作得很好。 它能有效地处理网络偶尔的抖-动、下游服务临时的、随机的抖动。这些都属于瞬时性、非系统性的故障。 但是,当故障模式改变时,这个“好”模式就可能变成“帮凶”。 场景还原: 假设你的服务 A 需要处理一个请求,这个请求需要去服务 B 获取一批用户的详细信息。服务 B 是一个稳定的第三方服务,但有速率限制:100 QPS。 一个隐藏的“坑”: 你在服务 A 里写了段逻辑,它需要处理一个包含 100 个用户 ID 的列表。最直观的写法,自然就是 for 循环这个列表,然后挨个去用户服务 B 查询。这种“啰嗦”的调用模式,在低负载下,可能并不会暴露问题。……

阅读全文

如何一键生成前端代码

作为后端程序员,我一直想独立开发一个产品,哪怕只是一个简单的落地页。但由于前端开发对我来说颇有难度,这个愿望一直未能实现。 直到 ChatGPT 发布,我才借助它来共同开发了一个名为“Bulk Delete ChatGPT”的插件,至今已拥有超过1600名用户,并时常收到好评。 由于插件依赖于 ChatGPT 官网的页面样式,而官网经常更新,因此需要频繁更新插件代码。但由于我的能力限制,无法及时发布最新版本,这也导致了一些差评。 在我发布了三次变更后,我就想能不能在插件里加一个通知功能啥的,这样万一官网有升级,插件不能工作了,也有个通知让用户知晓。而且当我发布新版本后,也可以给用户发布一条通知。但是最终感觉这个有点难度,而且需要申请很多权限,审核难度比较大。最终放弃了。 不过因为我经常更新版本的行为感动了自己,所以我加了个乞讨按钮,企图得到国际赞助,然而并没有: 后来,我听说开发网站并吸引流量后可以投放广告、赚取广告费。因此,我决定尝试制作一个落地页,以探索这一流程。 直到最近,几款一键生成前端代码的工具的出现,帮助我成功制作了一个落地页。我尝试了两种工具: tldraw screenshot-to-code 先用了 tldraw,发现只能生成一次,之后想要优化就不知道怎么做了。(也许是我没找到方法) 之后又用了 screenshot-to-code 这个工具,他可以方便地进行迭代: 最终出来的效果是这样的: 网址是:https://chatgpt-bulk-delete.qcrao.com/ 效果非常好,缺点是这两个工具都需要有 OpenAI 的 API key,要花钱的。 整个过程是这样的,希望能给你一点参考。 我先画了一张草图,要求它生成一个初版网站: 初版和草图比较像,都是黑白色: 接着我让它进行调整,根据一个主色做一个渐变调整: 背景色调成黑色: 中间也碰到了一些其他的具体问题,不过可以把代码喂给 ChatGPT 来解决。一些知识性的问题问 ChatGPT 就太合适了。比如我不知道 Tailwind CSS 里代码的作用: 我的这段经历,虽然充满了技术挑战和不断地调整,但它也展示了一个重要的道理:在现代技术的辅助下,即使是非前端专家,也能创造出令人满意的作品。这不仅是对个人能力的一种挑战,更是一次新技术应用的探索。我通过实践学到了很多,也体会到了技术带来的便利。 即使面对看似难以克服的技术障碍,只要我们愿意尝试新方法,就总有解决问题的途径。我的例子或许不是最完美的,但它证明了一个观点:不断学习、适应新技术,是我们在这个快速发展的时代中保持竞争力的关键。 最后,我想说的是,无论你是一名程序员、设计师,还是任何领域的专业人士,都不要害怕技术的快速发展。拥抱变化,利用新兴技术,将你的创意变为现实。……

阅读全文

如何使用 Raycast 一键打开预设工作环境

工作中,你一定遇到过这样的场景:你正在认真写代码,线上突然出现报警。看到报警信息之后,你不得不打开浏览器,点开收藏夹,打开监控页面、告警页面、trace 页面、日志搜索平台……有时,还需要打开特定的文件或者软件,比如你记在本地的一些常用的命令文件、iterm2 等等。 这些网页、文件、软件,很可能每次遇到 报警时都要打开。这种重复的工作有没有可能一键自动完成呢? 可以。借助 Raycast 可以非常方便地做到(本文介绍的方法在 mac 系统下生效)。 Raycast是一个强大的工具,能够提高用户使用电脑的效率。它为用户提供了一种快速和简单的方式来控制他们的设备和各种应用,不论是发邮件,查看日历,还是管理任务。通过设置快捷键,用户可以无缝地在不同应用之间切换,大大提高工作效率。 需要说明的是,一键打开预设工作环境的实现方法可能有很多。本文采用的方法,是在 chatGPT 的帮助下“独立”完成的。如有雷同,纯属巧合。 总共分三步:指定命令的目录;创建 AppleScript 脚本;更改 AppleScript 脚本。 第一步,指定命令的目录。打开 raycast 设置页面,选择 Scripts tab,点击 “+” 号。 第二步,创建 AppleScript 脚本。 这里的 title 就是之后唤起脚本的命令。 顺便介绍一下 AppleScript: AppleScript 是一种基于 Apple 事件的自动化技术,允许用户编写脚本来控制 Mac 操作系统中的各种应用程序。这意味着用户可以通过 AppleScript 自动执行繁琐的任务,比如批量修改文件,整理电子邮件,甚至创建复杂的文档。它支持过程和事件驱动编程,具有语法简洁、易于阅读和学习的特点,使得非程序员也能编写出有效的脚本。总的来说,AppleScript 是一个强大且用户友好的工具,用于增强 Mac 用户的生产力和工作效率。 完成之后,在第一步设置的路径下,就会出现一个 daily.applescript 文件。 第三步,就是修改 AppleScript 脚本,让它来完成打开指定网页、启动指定软件的功能。 虽然 AppleScript 写起来很直观,但是对没写过的人来说,还是有一定的学习成本。所以将编写脚本的工作交给 chatGPT 最为合适,因为打开指定网页和启动指定软件是最基础的功能,利用 chatGPT 可以更加高效。 果不其然,chatGPT 很快就抛出来了代码,没有修改就能工作了,让人直呼内行,效率简直翻 10 倍。 当然,基于此,我还有一些额外的要求。我希望它能新建一个浏览器窗口,打开这些网址,并将浏览器放在屏幕的左半部分。然后,我希望它能打开 Roam Research,并将其放置在屏幕的右半部分,因为我需要记笔记。 下面,我将直接展示最后的 AppleScript 代码,其实它并不复杂: 1#!/usr/bin/osascript 2 3# Required parameters: 4# @raycast.……

阅读全文