1. 为什么一本讲技术的书值得写第二篇笔记
第一次翻《码农翻身》的时候,我其实是带着一点偏见的。市面上讲编程的书太多了,要么是纯技术手册,翻两页就犯困;要么是鸡汤式的职业规划,读完热血三分钟,合上书该干嘛还干嘛。但这本书不太一样,它用讲故事的方式把计算机底层那些硬核知识串了起来,读起来像看小说,读完却发现脑子里多了不少东西。
这是第二篇笔记,说明第一篇写完以后我还在继续翻。事实上一本好书就是这样,第一遍读的是热闹,第二遍读的是门道。第一篇笔记我主要记的是书里那些有趣的故事和比喻,比如把进程比作工厂、把线程比作工人之类的。但这次重读,我关注的点完全变了——我开始琢磨作者为什么要用这种方式讲,这些比喻背后对应的真实技术细节是什么,以及这些知识在实际工作中到底怎么用。
这篇文章适合几类人看:一是刚入行不久、对计算机底层还比较模糊的开发者;二是工作了两三年,发现光会调API不够、想补补基础的朋友;三是像我一样读过这本书但觉得没读透、想再看看别人怎么理解的人。我会把书里的核心知识点拆开,结合我自己的工作经验和对底层原理的理解,把那些“讲故事”背后的技术真相补全。不是照搬书里的内容,而是把书当做一个引子,把背后的东西挖出来。
2. 书里那些比喻到底在说什么
2.1 从“程序员的进化史”看技术分层
书里有一章讲程序员的进化,从写机器码到汇编再到高级语言,这个脉络其实特别重要。很多人学编程是从Python或者Java开始的,觉得写代码就是敲几个关键字,但对于“为什么会有这些关键字”“为什么不同的语言有不同的设计哲学”这些问题,往往答不上来。
我自己的理解是,编程语言的进化本质上是在解决一个核心矛盾:人脑的思维方式和机器的执行方式之间的鸿沟。机器只认0和1,人脑习惯的是抽象和逻辑。汇编语言做了一层映射,把机器指令变成了助记符,但依然要求你理解寄存器、内存地址这些概念。高级语言又做了一层映射,把内存管理、类型系统这些东西封装起来,让你可以专注于业务逻辑。
这个分层思想在书里反复出现,只是换了很多不同的故事来讲。比如讲操作系统的时候,它把操作系统比作一个“大管家”,管理着CPU、内存、硬盘这些资源,让上层的应用程序不用直接跟硬件打交道。这个比喻很形象,但如果你只记住“大管家”三个字,那就白读了。真正要理解的是:操作系统提供了哪些抽象?进程抽象了什么?虚拟内存抽象了什么?文件系统抽象了什么?这些抽象各自解决了什么问题?搞清楚了这些,你才能明白为什么写代码的时候需要关注这些概念。
2.2 CPU、内存、硬盘的速度差异为什么是程序设计的核心约束
书里有一个比喻我印象特别深,大意是说CPU的速度像火箭,内存像汽车,硬盘像蜗牛。这个比喻看起来简单,但它其实是理解很多技术方案的钥匙。
我查过一些实际的数字,虽然不同硬件差异很大,但数量级上的差距是稳定的。CPU做一次运算大概是纳秒级别,从内存读一次数据大概是几十到上百纳秒,从SSD读一次数据大概是几十微秒,从机械硬盘读一次数据大概是几毫秒到十几毫秒。换算一下,如果CPU的一次运算是1秒,那从内存读数据相当于等了几十秒,从SSD读相当于等了几个小时,从机械硬盘读相当于等了几天甚至几周。
这个差距意味着什么?意味着任何涉及IO的操作都是昂贵的。所以你会看到缓存无处不在:CPU有L1/L2/L3缓存,操作系统有页缓存,数据库有缓冲池,应用层有Redis。每一层缓存的存在都是为了弥合速度差异。理解了这一点,你就能理解为什么数据库索引那么重要(减少磁盘IO)、为什么Redis那么快(数据在内存里)、为什么消息队列能削峰填谷(把同步IO变成异步)。
书里没有展开讲这些数字,但我觉得这是读这本书时最应该自己补上的部分。因为一旦你脑子里有了这个速度差异的直觉,很多技术选型和架构设计就变得顺理成章了。
2.3 从“进程和线程”的故事理解并发模型
书里讲进程和线程的那一章用了工厂和工人的比喻,这个比喻很经典。进程是工厂,线程是工厂里的工人,工厂提供场地和资源,工人负责干活。多个工人可以共享工厂里的资源,但也要协调避免冲突。
这个比喻对应的技术现实是:进程是资源分配的基本单位,线程是CPU调度的基本单位。同一进程内的线程共享内存空间和文件句柄等资源,但每个线程有自己的栈和寄存器状态。这就解释了为什么线程间通信比进程间通信快——因为不用跨地址空间,直接读写同一块内存就行。
但这里有一个容易被忽略的点:共享内存虽然快,但带来了竞态条件。多个线程同时读写同一块数据,如果不加控制,结果就是不可预测的。书里用“两个人同时往同一个账户存钱”的例子讲了这个问题,对应的解决方案就是锁。但锁本身又带来了新问题:死锁、性能开销、优先级反转等等。
我在实际工作中踩过的一个坑是:以为用了线程安全的集合类就万事大吉了,结果发现复合操作依然有并发问题。比如“检查再执行”这种模式,即使每个单独的操作都是原子的,组合起来也不是。这个坑书里没有直接讲,但如果你理解了线程共享内存的本质,就应该能想到这一层。
3. 那些容易被跳过但极其重要的章节
3.1 关于缓存的一切:从CPU缓存到CDN
书里讲缓存的部分分散在好几个章节里,有讲CPU缓存的,有讲浏览器缓存的,有讲CDN的。第一遍读的时候我把它们当成了独立的知识点,第二遍读才发现它们其实是一回事:用更快的存储介质保存热点数据,减少访问慢速介质的次数。
CPU缓存利用的是时间局部性和空间局部性。时间局部性是说,如果一个数据被访问了,它很可能在不久的将来再次被访问;空间局部性是说,如果一个数据被访问了,它相邻的数据很可能也会被访问。所以CPU缓存不是只缓存一个字节,而是缓存一个缓存行(通常是64字节)。这就解释了为什么遍历数组比遍历链表快——数组的元素在内存里是连续的,一次缓存行加载就能拿到多个元素;链表的节点是分散的,每次都要重新加载缓存行。
浏览器缓存和CDN利用的是类似的原理,只是尺度不同。浏览器缓存把静态资源存在本地,避免重复下载;CDN把内容分发到离用户最近的节点,减少网络延迟。这些方案的共同点是:在离使用者最近的地方放一份副本。
我在实际项目里做过一个优化,把一些不常变但访问频繁的配置数据从数据库加载到应用内存里,QPS直接翻了好几倍。这个思路和CPU缓存、CDN是一脉相承的。书里没有直接教你怎么做这个优化,但如果你理解了缓存的本质,就能自己推导出来。
3.2 数据库索引背后的数据结构
书里讲数据库的时候提到了索引,但没有深入讲B+树。我觉得这是一个遗憾,因为不理解B+树就很难真正理解索引的行为。
简单来说,B+树是一种多路平衡查找树,它的特点是:所有数据都存在叶子节点,非叶子节点只存索引;叶子节点之间用指针连起来,方便范围查询;树的高度很低,通常三层就能存几百万条数据。这意味着查一条数据只需要三次磁盘IO,效率非常高。
为什么不用二叉树?因为二叉树的高度太高,磁盘IO次数太多。为什么不用哈希表?因为哈希表不支持范围查询。为什么不用B树?因为B树的非叶子节点也存数据,导致每个节点能存的索引更少,树的高度更高。
这些取舍背后的逻辑,其实和前面说的速度差异是一回事:磁盘IO太贵了,所以要尽量减少IO次数。理解了这一点,你就能理解为什么索引不是越多越好(每个索引都要维护一棵B+树,写入时要更新)、为什么联合索引有最左前缀原则(B+树的排序方式决定的)、为什么模糊查询以%开头用不上索引(无法利用有序性)。
3.3 从“锁”的故事理解并发控制
书里讲锁的章节用了很多生活化的例子,比如上厕所锁门、借书登记之类的。这些例子很好懂,但实际工作中的锁要复杂得多。
首先是锁的粒度问题。锁的粒度太粗,并发度就低;粒度太细,开销就大,而且容易死锁。数据库里的表锁、页锁、行锁就是不同粒度的选择。MySQL的InnoDB默认用行锁,但行锁是基于索引实现的——如果你更新的条件没有用到索引,就会退化成表锁。这个坑我踩过,一条更新语句没走索引,直接把整张表锁住了,线上服务瞬间雪崩。
其次是锁的类型。共享锁和排他锁是最基本的,但还有意向锁、间隙锁、临键锁等等。间隙锁是为了解决幻读问题引入的,它锁的是一个范围而不是具体的行。这就导致了一个反直觉的现象:即使你查的记录不存在,也可能被锁住。
最后是分布式环境下的锁。单机锁靠的是共享内存和CPU的原子指令,分布式锁就得靠外部存储了,比如Redis或者ZooKeeper。但分布式锁又引入了新的问题:锁的超时时间怎么设?持有锁的进程挂了怎么办?网络分区的时候怎么保证一致性?这些问题书里没有展开,但都是实际工作中绕不开的。
4. 把书里的知识串起来:一个完整的案例
4.1 从用户点击到数据落盘的全链路
为了把书里散落的知识点串起来,我试着还原一个完整的请求链路。假设用户在浏览器里点击了一个按钮,触发了一个查询请求,这个请求最终落到数据库上,返回结果并展示给用户。这个过程里发生了什么?
首先,浏览器要解析URL,检查本地缓存。如果有缓存且没过期,直接返回;否则发起网络请求。网络请求要经过DNS解析、TCP握手、TLS握手(如果是HTTPS),然后才能发送HTTP请求。这些步骤里,DNS解析有缓存(浏览器缓存、操作系统缓存、DNS服务器缓存),TCP握手有优化(TCP Fast Open),TLS握手也有优化(会话复用)。
请求到达服务器后,可能先经过负载均衡器,然后到达应用服务器。应用服务器可能先查本地缓存,没有再查分布式缓存,还没有再查数据库。数据库收到查询请求后,先查缓冲池,没有再查磁盘。磁盘上的数据通过B+树索引定位,读取到内存,返回给应用服务器,再逐层返回给用户。
这个链路里的每一步都对应书里的一个知识点:缓存、网络协议、操作系统IO、数据库索引。单独看每个知识点可能觉得没什么,但串起来看就会发现它们都在解决同一个问题:在速度不匹配的组件之间做权衡。
4.2 每一步的瓶颈在哪里
理解了全链路之后,下一步就是定位瓶颈。如果请求变慢了,可能是哪里的问题?
DNS解析慢?可能是DNS服务器响应慢,或者本地没有缓存。TCP握手慢?可能是网络延迟高,或者服务器负载高。应用服务器慢?可能是线程池满了,或者GC频繁。缓存慢?可能是缓存击穿或者缓存雪崩。数据库慢?可能是没走索引,或者锁冲突,或者磁盘IO瓶颈。
我在实际排查问题的时候,习惯从外往里一层层查。先看监控大盘,确认是全局问题还是局部问题;再看日志,确认是哪个环节报错或者超时;然后用工具(比如arthas、pprof)深入分析。这个思路和书里讲的分层思想是一致的:先定位到层,再定位到点。
4.3 如果让你重新设计这个链路
书里有一个很好的思维训练:如果让你重新设计一个系统,你会怎么做?这个问题没有标准答案,但思考的过程很有价值。
比如,如果让我重新设计一个高并发的查询系统,我会怎么考虑?首先,能缓存的尽量缓存,把大部分请求挡在数据库前面。其次,数据库要分库分表,把压力分散开。再次,读写分离,写走主库,读走从库。最后,异步化,能异步的操作不要同步等。
这些方案书里都提到过,但关键不是记住方案本身,而是理解每个方案背后的权衡。缓存会带来一致性问题,分库分表会带来分布式事务问题,读写分离会带来主从延迟问题,异步化会带来复杂度问题。没有银弹,只有取舍。
5. 读这本书的正确姿势
5.1 第一遍读故事,第二遍读原理
我自己的经验是,这本书至少要读两遍。第一遍快速翻,把故事和比喻过一遍,建立感性认识。第二遍慢读,每读到一个比喻就停下来想:这个比喻对应什么技术概念?这个技术概念解决了什么问题?还有什么相关的问题书里没讲?
比如读到“进程是工厂”的时候,我会想:工厂的场地是什么?是内存空间。工厂的设备是什么?是CPU和IO资源。工厂的工人是什么?是线程。工厂之间怎么通信?通过管道、消息队列、共享内存。这些问题想清楚了,进程和线程的知识点就串起来了。
5.2 带着问题读,不要被动接受
被动读书的典型表现是:每个字都认识,但读完不知道讲了什么。主动读书的做法是:读之前先问自己想知道什么,读的时候不断和已有的知识做对比,读完之后试着用自己的话复述一遍。
我读这本书的时候,会在每个章节旁边写批注,有的是补充书里没讲的细节,有的是记录自己的疑问,有的是联想到了工作中的某个场景。这些批注后来就成了我写笔记的素材。
5.3 把书里的知识映射到自己的项目
书里的例子大多是通用的,但每个人的工作场景不同。把书里的知识和自己的项目对应起来,才能真正内化。
比如书里讲缓存的时候,我会想:我的项目里哪些地方用了缓存?为什么用?有没有可以优化的地方?书里讲锁的时候,我会想:我的项目里哪些地方有并发问题?用的什么锁?有没有死锁的风险?这种映射练习做多了,你会发现书里的知识不再是孤立的点,而是和你每天的工作紧密相连的。
6. 几个我踩过的坑和对应的书里知识点
6.1 缓存穿透、击穿、雪崩的区别和应对
这三个词听起来很像,但对应的问题和解决方案完全不同。缓存穿透是查一个不存在的数据,缓存和数据库都没有,导致每次请求都打到数据库。解决方案是缓存空值或者用布隆过滤器。缓存击穿是某个热点key过期了,大量请求同时打到数据库。解决方案是加互斥锁或者热点数据永不过期。缓存雪崩是大量key同时过期,导致数据库压力骤增。解决方案是过期时间加随机值。
书里讲缓存的时候提到了这些概念,但没有展开讲解决方案。我在实际工作中三个都遇到过,最坑的是缓存雪崩——当时设置了一批key的过期时间都是整点,结果整点一到,数据库直接被打挂。后来改成过期时间加随机偏移,问题就解决了。
6.2 数据库死锁的排查和预防
死锁是并发编程里最头疼的问题之一。数据库死锁的表现是:两个事务互相等待对方持有的锁,谁也进行不下去。MySQL会自动检测死锁并回滚其中一个事务,但应用层会收到一个死锁异常。
排查死锁的方法是看死锁日志,里面会显示两个事务分别持有什么锁、等待什么锁。预防死锁的方法有:按固定顺序访问资源、减少事务持有锁的时间、使用较低的隔离级别、避免大事务。
我在项目里遇到过一次死锁,原因是两个事务更新的顺序不一致。一个先更新A表再更新B表,另一个先更新B表再更新A表。改成统一顺序后,死锁就消失了。这个经验书里没有直接讲,但如果你理解了锁的本质,就能自己推导出来。
6.3 线程池参数设置不当导致的线上问题
线程池是并发编程里最常用的工具之一,但参数设置不当会引发严重问题。核心线程数太小,任务排队;核心线程数太大,上下文切换开销大。队列太长,内存溢出;队列太短,任务被拒绝。最大线程数设置不当,可能创建过多线程导致OOM。
书里讲线程的时候提到了线程池的概念,但没有深入讲参数调优。我的经验是:核心线程数根据CPU核数和任务类型来定,CPU密集型任务设为核数+1,IO密集型任务设为核数*2。队列长度要有限制,避免无限堆积。拒绝策略要根据业务场景选择,不能默认用AbortPolicy。
7. 这本书没讲但你应该知道的东西
7.1 分布式系统的CAP理论和BASE理论
书里主要讲的是单机系统的知识,分布式系统的内容相对较少。但现在的系统大多是分布式的,CAP理论和BASE理论是绕不开的。
CAP理论是说,一致性、可用性、分区容错性三者不可兼得,最多只能满足两个。由于网络分区是不可避免的,所以实际上是在一致性和可用性之间做选择。BASE理论是对CAP的延伸,核心思想是基本可用、软状态、最终一致。
理解了这些理论,你就能理解为什么分布式数据库有不同的设计取向,为什么有些系统选择强一致,有些选择最终一致。
7.2 消息队列的作用和选型
书里提到了消息队列,但没有展开讲。消息队列的核心作用有三个:解耦、异步、削峰。解耦是说生产者和消费者不用直接依赖;异步是说生产者不用等消费者处理完;削峰是说把突发流量缓冲起来慢慢处理。
常见的消息队列有Kafka、RabbitMQ、RocketMQ等,各有优劣。Kafka吞吐量高,适合日志和大数据场景;RabbitMQ延迟低,适合业务消息;RocketMQ兼顾吞吐和延迟,适合电商场景。选型的时候要根据业务需求来定,没有最好的,只有最合适的。
7.3 容器化和编排技术
书里讲操作系统的时候提到了虚拟化,但没有讲容器化。容器化和虚拟化的区别是:虚拟化虚拟的是硬件,容器化虚拟的是操作系统。容器更轻量,启动更快,但隔离性不如虚拟机。
容器编排技术(比如Kubernetes)解决了容器的调度、伸缩、服务发现等问题。这些技术现在已经是后端开发的基本技能了,建议每个后端开发者都了解一下。
8. 怎么把这本书的知识用到面试和工作中
8.1 面试中常见的问题和书里的对应知识点
面试的时候,面试官经常会问一些基础问题,比如“进程和线程的区别”“TCP三次握手”“数据库索引的原理”。这些问题书里都有涉及,但书里讲的是故事,面试要的是准确的答案。
我的建议是:把书里的故事当做理解的工具,但回答的时候要用专业的术语。比如书里说“进程是工厂,线程是工人”,面试的时候你要说“进程是资源分配的基本单位,线程是CPU调度的基本单位,同一进程内的线程共享内存空间”。故事帮你理解,术语帮你表达。
8.2 工作中遇到问题怎么用书里的思路排查
工作中遇到问题的时候,书里的分层思想和速度差异的直觉特别有用。比如系统变慢了,你可以按照从外到里的顺序排查:网络有没有问题?应用服务器有没有问题?缓存有没有问题?数据库有没有问题?每一步都用工具去验证,而不是凭感觉猜。
再比如设计一个新系统的时候,你可以先问自己:这个系统的瓶颈可能在哪里?是CPU密集还是IO密集?需要缓存吗?需要异步吗?需要分片吗?这些问题想清楚了,架构方案就出来了。
8.3 持续学习的方向建议
这本书是一个起点,不是终点。读完这本书,你应该知道自己对哪些领域感兴趣,然后深入下去。比如对操作系统感兴趣,可以读《操作系统导论》;对网络感兴趣,可以读《计算机网络:自顶向下方法》;对数据库感兴趣,可以读《数据库系统概念》。
我的经验是,基础知识的价值是复利的。你今天花时间搞懂了B+树,明天就能理解为什么索引要这样设计,后天就能在面试中侃侃而谈。这些知识不会过时,反而会随着你的工作经验越来越值钱。
最后分享一个小技巧:读技术书的时候,不要追求速度,要追求深度。一本好书读三遍,比三本好书读一遍收获更大。《码农翻身》就是这样一本值得反复读的书,每次读都会有新的收获。