evolution - chains

14. 需求驱动的演进链:每一代技术都是上一代的病历

0. 本章先解决什么问题

前十三章是横截面:一个思想在各个组件里的复现。本章换成时间轴:一个需求下的技术怎么一代代迭代。读演进链的固定三问,每一环都拿它过一遍:

  1. 它解决了上一环的什么病?
  2. 它自己的原理是什么?
  3. 它又生出了什么新病,怎么催生下一环?

链读多了会得到一个和总图互补的能力:横截面告诉你”这个机制似曾相识”,时间轴告诉你”这个设计为什么长这样”。任何看起来别扭的设计,多半是在治一个你没见过的病。

演进链的通用结构

这张图怎么读

每个环节两行:上行是它的原理,下行是它的病;箭头代表”病催生下一环”。七条链共用这一个形状。

1. 会话与认证:从无状态到”无状态加一点状态”

1.1 起点:HTTP 天生无状态

HTTP 的设计里每个请求自带全部信息、互相独立,服务器处理完就忘。这个选择本身极优秀:服务器不用记任何人,请求可以打到任何一台机器,协议简单到极致。病出在需求变了:购物车、登录态这些东西,本质上要求服务器”记得你是谁”。

1.2 Cookie:让浏览器替协议带状态

原理: 服务器响应头 Set-Cookie: name=value 下发一小段数据,
浏览器存下来,之后对同一域名的每个请求自动带上 Cookie 头
配套属性: Max-Age 有效期、HttpOnly 禁 JS 读取、
Secure 仅 HTTPS 发送、SameSite 限制跨站携带

我一开始把这一环记成”Cookie 自动带着账号密码去访问”,后来想明白了:往 Cookie 里存账号密码恰恰是这一环被淘汰的那种用法,问题不在 Cookie 机制,在存了什么。Cookie 的真实病单:

  1. 存在客户端,用户和插件都能看、能改:
    存明文身份信息(如 uid=1001)等于请求任何人都能伪造
  2. XSS 偷取: 页面被注入脚本就能读走 Cookie(HttpOnly 只是缓解)
  3. 自动携带是双刃剑: 恶意网站发起的跨站请求也会带上你的 Cookie,
    这就是 CSRF 的根源(SameSite 就是后来打的补丁)
  4. 容量约 4 KB,还随每个请求反复传输

结论:敏感状态不能放客户端。于是状态搬回服务端。

1.3 Session:状态回服务端,客户端只留门牌号

  • 原理:登录成功后服务端生成一条会话记录(用户、权限、有效期),
    存在服务端(Tomcat 的 Session 管理器),
    只把一个随机无意义的 sessionid(如 JSESSIONID)
    通过 Cookie 发给浏览器。之后每请求带 id,服务端按 id 取会话
  • 修了什么:客户端手里只有一串随机数,改它毫无意义,
    看它也看不出任何信息,敏感数据全部回到服务端

新病出在服务端自己身上,而且是分布式时代的病:

  1. 会话存在单机内存: 重启即全体掉线
  2. 集群下请求打到另一台,那台没有你的会话。三代解法:
    粘性会话: nginx 按 ip_hash 把你钉在一台上
    (病: 那台挂了会话就没了,扩缩容也难)
    会话复制: 每台都存全量会话
    (病: 广播同步的流量和内存随节点数爆炸)
    集中存储: 会话统一放 Redis(spring-session 一行配置)
    (主流答案,第 3 章的缓存在这里当会话库用)
  3. 集中存储也有账单: 每个请求都多一次 Redis 查询,
    Redis 成了全站登录态的命脉(要按第 6 章做高可用)
  4. 场景病: 原生 APP、小程序、跨域前后端分离,
    Cookie 的自动携带机制要么没有要么受限

1.4 Token/JWT:把状态签名后还给客户端

先修正我自己的一个旧印象:从 Session 走到 Token 的动因在服务端状态和多端场景,sessionid 本身只有几十字节,一点也不大;JWT 反而比它大得多。

一个真实的 JWT 长这样(两个点分隔三段):

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MjM0IiwibmFtZSI6…IjUyNDIifQ.3V3SgDPpNprzPn0tNYvQO-8QZ7qd3siYo27Brzdizjg
|_______ header ______________| payload ____| signature __________________________|

三段的生成过程,逐段拆:

JWT 由三段组成:

  1. Header:明文 JSON 编码成 Base64Url

    代码块JSON · 1 行收起展开
    {"alg": "HS256", "typ": "JWT"}

    Header 声明签名算法。Base64Url 是 Base64 的 URL 安全变体,会处理 +/= 等字符,使 token 可以稳定放进 URL 和 HTTP Header。

  2. Payload:明文 JSON 编码成 Base64Url

    代码块JSON · 1 行收起展开
    {"sub": "4234", "name": "gilbert", "iat": 1722566400, "exp": 1722570000}

    sub 是用户标识,iat 是签发时间,exp 是过期时间,还可以追加自定义字段。Base64 只是编码,任何人都能还原明文,因此 payload 没有保密性,不应存放密码或隐私数据。

  3. Signature:真正提供防篡改能力的部分

    服务端把 base64Url(header) + "." + base64Url(payload) 作为输入,再用私藏的 secret 计算 HMACSHA256,得到第三段签名。攻击者可以读取前两段,却无法在不知道密钥的情况下修改内容并生成有效签名。

验证过程就是把生成过程重演一遍:

服务端收到 token,切三段
用自己手里的 secret 对前两段重算一次 HMAC
重算结果 == 第三段 ? 信任 payload : 拒绝
然后再检查 exp 是否过期

为什么这个机制防篡改、又为什么防不了偷看,从算式上直接读出来:

  • 改不了:客户端把 payload 里的 sub 改成别人,
    签名输入变了,第三段必须跟着变,
    但重算签名需要 secret,客户端没有 -> 伪造不出匹配的第三段
    (HMAC 是单向的,从签名也反推不出 secret)
  • 瞒不住:payload 只是 Base64 编码,任何人解开就能读
    -> 能放用户 id,不能放手机号密码这类敏感字段
  • 一个真实攻击顺便记住:header 里的 alg 是客户端可改的,
    早年有库接受 alg:“none”(不验签),改一下 header 就绕过验证;
    服务端必须强制指定自己的算法,不信 token 自称的

HS256 和 RS256 的选择也在算式里:

HS256(HMAC + 对称密钥): 签发和验证用同一个 secret。
单体或小集群够用;微服务下每个服务都要持有 secret,
任何一个服务泄漏,全网都能伪造 token
RS256(RSA 私钥签名): 认证中心用私钥签发,
其他服务只拿公钥验证,公钥给谁都不怕
-> 签发权和验证权分离,网关和下游服务只握公钥
(第 3 条链马上要讲的数字签名,就是这里的同一个原语)

修了什么:服务端零会话存储;不依赖 Cookie 的自动携带,放在 Authorization 头里显式发送,APP、跨域、小程序通吃,CSRF 顺带免疫(攻击者的页面无法让浏览器自动带上你的 Authorization 头)。

JWT 的病单同样清楚:

  1. 无法主动吊销: 签出去的 token 在过期前永远有效,
    改密码、封号、登出都拦不住一个还没过期的旧 token
  2. 泄漏即裸奔: 拿到 token 就是你,且没法作废它
  3. payload 只是编码没有加密,内容对所有人可读,不能放敏感数据
  4. 体积比 sessionid 大一个数量级,每个请求都背着

1.5 闭环:refresh token 和黑名单,状态又回来了

工程上的收尾方案把这条链画成了螺旋而在原地画圈:

短有效期 access token(分钟级)+ 长有效期 refresh token:
access 泄漏损失被压到分钟级;refresh 存在服务端可以吊销
(注意: refresh token 就是服务端状态,Session 的幽灵回来了)
黑名单: 登出和封号把 token 塞进 Redis 黑名单,验签后多查一次
(又是状态,而且又是每请求一次 Redis)

判词:纯无状态做不到即时吊销,这是原理不是工艺。工程终态是”大部分请求无状态验签 + 一点点状态兜底吊销”。演进链的常态就是这样:不消灭矛盾,只把矛盾挪到代价最小的位置。

2. HTTP 协议版本:队头阻塞的三次搬家

这条链的主角病只有一个,看它被赶着搬了三次家。

2.1 HTTP/1.0:一次请求一条连接

每个请求新建一条 TCP:三次握手、慢启动爬坡、传完就关。一个页面几十个资源就是几十次握手和几十次从零爬坡,RTT 成本刷屏(第 11 章算过往返的账)。

2.2 HTTP/1.1:连接复用了,响应还得排队

  • :keep-alive 成为默认,一条连接串行跑多个请求
  • 再修:管线化 pipelining,请求可以连发不等回
  • 新病:响应必须按请求顺序返回,第一个响应慢,
    后面全部干等,这就是应用层的队头阻塞。
    管线化因此在浏览器里默认关闭,等于修了个寂寞
  • 民间偏方:浏览器每域名开 6 条并行连接、域名分片撑更多连接、
    雪碧图和文件合并减少请求数
    (所有偏方都在绕”一条连接同时只能跑一件事”这个根)

2.3 HTTP/2:应用层修好了,病搬进 TCP

  • :二进制分帧。报文切成带流编号的帧,
    多个流(stream)的帧在一条 TCP 上交错传输、各自组装
    -> 一条连接真正并发,队头阻塞在应用层消失
    顺手修: HPACK 头压缩。原理三件套:
    静态表(61 个最常见头字段预编号,method: GET 传一个数字就行)、
    动态表(连接内出现过的头字段增量编号,第二次出现只传编号,
    cookie 这种每请求重复的大头从几百字节缩成 1 字节)、
    哈夫曼编码(没进表的字面量按频率变长编码)
    1.1 时代每个请求全量明文重发全部头的浪费就此了结
  • 新病:TCP 只认字节流不认流编号。丢一个包,
    TCP 必须等它重传补齐才肯上交后面的字节,
    哪怕后面字节属于毫不相干的流
    -> 队头阻塞下移到传输层,弱网下 HTTP/2 可能反而在 1.1 之下
    (1.1 的 6 条连接坏一条不影响其他五条)

2.4 HTTP/3:换掉 TCP,病在传输层被治掉

修: QUIC 协议,跑在 UDP 上,把可靠传输做进用户态:
流级重传: 每个流独立确认重传,丢包只挡自己的流
连接迁移: 连接标识用 connection id 而非四元组,
WiFi 切流量 IP 变了,连接不断
内置 TLS 1.3: 传输握手和加密握手合并(下一条链在此汇合)
为什么绕开 TCP 而修 TCP: TCP 在内核和中间设备里,
升级要等全世界换设备换系统;UDP 之上自己实现,
发个应用版本就迭代了。协议僵化逼出的绕行

三级跳收拢:应用层排队(1.1)、传输层排队(2)、流级独立(3)。完整协议细节在 HTTP/1.1、HTTP/2、HTTP/3

3. HTTP 到 HTTPS:三宗病,一环环地补

3.1 明文的三宗病

  • 窃听:报文一路明文,同网段、网关、运营商全能看
  • 篡改:中间任何一跳能改内容(运营商往页面里插广告是真实历史)
  • 冒充:你以为在跟银行说话,其实是个假网站,协议层无从验证

三宗病对应三味药:加密、完整性校验、身份认证。往下每一环都在配药过程中踩出新坑。

3.2 对称加密和它的死结

双方用同一把钥匙加解密(AES),快,适合大流量。死结在钥匙怎么送过去:明文送等于没加密,这就是密钥分发问题,对称加密自己解不了。

3.3 非对称加密:解了分发,背上两个新包袱

  • 原理:公钥加密的只有私钥能解。服务器公开公钥,
    客户端用它加密数据发回,只有握着私钥的服务器能读
  • 包袱一:慢。非对称运算比对称慢两个数量级,扛不动正文流量
  • 包袱二:你拿到的”服务器公钥”真是它的吗?
    中间人把公钥换成自己的,两头分别加密通信,全程透明转发,
    加密形同虚设(调包问题)

3.4 混合加密:各干各的擅长

包袱一的解法直白:非对称只干一件小事,安全地送一把随机生成的对称密钥;之后全程对称加密跑正文。TLS 的骨架就是这个分工。

3.5 证书与 CA:调包问题靠信任链压住

先把数字签名这个原语本身拆开,它是非对称加密的反向用法,后面每一步都建立在它上面:

  • 加密方向(保密):公钥加密,私钥解密。谁都能写,只有我能读

  • 签名方向(认证):私钥”加密”,公钥解开。只有我能写,谁都能验

  • 签名的生成:对内容算哈希(SHA-256),用私钥加密这个哈希值,
    加密结果就是签名,附在内容后面

  • 签名的验证:用签名者的公钥解开签名,得到”当年的哈希”,
    自己对收到的内容重算一次哈希,两个哈希一致
    -> 内容没被改过(哈希会变)且确实是私钥持有者签的(别人加密不出来)

  • 为什么签哈希而签不全文:非对称加密慢(3.3 的包袱一),
    全文几 MB 签不动,32 字节的哈希瞬间完成,
    而哈希的抗碰撞性保证”改内容必改哈希”

证书就是把这个原语用在”公钥的身份证”上:

  • 服务器向 CA 申请:CA 核实域名归属后,
    对”域名 + 服务器公钥 + 有效期”这份内容做数字签名,签出证书
  • 浏览器验证:操作系统和浏览器出厂内置根 CA 的公钥,
    按上面的验证流程验签。验过 = 这个公钥确实属于这个域名
    实际是一条链: 根 CA 签中间 CA 的证书,中间 CA 签网站证书,
    逐级验到根(根自签,信任的锚点出厂预置)
  • 中间人卡死在这:它能调包公钥,但没有 CA 私钥,
    伪造不出能被内置公钥验过的签名;自签证书直接红页警告
  • 新病:信任集中到了 CA。CA 被攻破或误签发整条链塌方
    (真实发生过),补丁是证书透明日志: 所有签发公开可审计

3.6 一次 TLS 1.2 握手的完整流程

零件齐了(对称、非对称、签名、证书),看它们怎么拼成一次真实握手(ECDHE 套件,两个 RTT):

  1. ClientHello: 客户端随机数 A + 我支持的算法套件列表
  2. ServerHello: 服务器随机数 B + 选定的套件 + 证书
    服务器再发一把 ECDHE 临时公钥,并用证书里公钥
    对应的私钥给它签名(证明临时公钥出自证书主人)
  3. 客户端: 验证书链(3.5 的流程)-> 验临时公钥的签名
    -> 生成自己的 ECDHE 临时公钥发给服务器
    此刻双方各持: 自己的临时私钥 + 对方的临时公钥,
    ECDH 数学保证两边独立算出同一个共享秘密(premaster)
    窃听者拿到两把临时公钥也推不出它
  4. 双方: 用 随机数A + 随机数B + premaster 一起
    推导出本次会话的对称密钥(三个随机数缺一不可,
    防止单方随机数被操纵导致密钥可预测)
  5. 互发 Finished: 用刚生成的对称密钥加密”之前全部握手消息的哈希”
    互验,确认握手过程没被中间人改过一个字节
    之后: 全程对称加密跑正文(3.4 的分工兑现)

ECDHE 里那个 E 是 ephemeral(临时):密钥对每条连接现生现弃,这就是前向安全的来源,服务器长期私钥只用来签名证明身份,从不参与密钥运算,泄漏了也解不开历史流量。

3.7 版本线:SSL 到 TLS 1.3,砍历史包袱

  • SSL 2/3:初代,陆续爆出协议级漏洞,全线废弃
  • TLS 1.0/1.1:过渡代,2020 年前后被主流浏览器弃用
  • TLS 1.2:长期主流。自身还迭代了一轮:
    早期 RSA 握手用服务器公钥直接加密传对称密钥,
    病: 没有前向安全,服务器私钥一旦泄漏,
    过去录下的全部流量都能回头解开
    -> ECDHE 临时密钥交换成为标配: 每条连接现场生成临时密钥对,
    用完即弃,私钥泄漏也解不了历史流量
  • TLS 1.3:大扫除。砍掉所有已知不安全的算法套件,
    握手从 2 个 RTT 压到 1 个(0-RTT 复用模式更快,
    但有重放风险,只给幂等请求用,第 13 章的幂等线索又出现)
    HTTP/3 直接把 TLS 1.3 缝进 QUIC,两条链在这里合流

握手逐步图和抓包实操在 TLS/HTTPS

4. 分布式锁:一条全是补丁的链

4.1 出发点:进程内的锁出不了进程

synchronized 和 ReentrantLock 锁的是 JVM 里的对象,集群一部署,两台机器各锁各的,互不知情。锁必须搬到所有实例都看得见的地方。

4.2 数据库锁:能用,但贵

  • 唯一键方案:抢锁 = INSERT 一条固定主键记录,成功即持锁,
    释放 = DELETE。病: 抢锁失败要自己轮询;
    持锁进程崩了记录没人删,死锁(没有过期机制)
  • for update 方案:行锁排队。病: 连接被长期占着(第 9 章连接池
    的账),高并发下数据库先倒

4.3 Redis:五连补丁把一个 setnx 修到能用

这段链最能体现”每环治一个病”,逐环过:

  • 第 1 环:SETNX lock 1(不存在才设置,天然互斥)
    病: 持锁进程崩了,锁永远在,死锁
  • 第 2 环:SETNX 后补一条 EXPIRE 设过期
    病: 两条命令之间进程崩了,还是死锁(非原子)
  • 第 3 环:SET lock v NX EX 30 一条命令原子完成
    病: 业务超过 30 秒,锁先过期被别人抢走,
    我干完还去 DEL,把别人的锁删了
  • 第 4 环:value 存自己的唯一标识,DEL 前先 GET 校验是自己的
    病: GET 校验和 DEL 是两步,校验通过的瞬间锁恰好过期
    易主,DEL 还是删了别人的(又是非原子)
  • 第 5 环:校验加删除写进 Lua 脚本,单脚本原子执行
    (第 11 章说过 Lua 的定位: 需要”读了再决定”的复合原子操作)

第 5 环的脚本值得看原文,一共三行,锁的正确性最后就押在它上面:

代码块LUA · 4 行收起展开
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])   -- 是我的锁,删
end
return 0                                 -- 已经易主,什么都不做

4.4 续期:watchdog 治”锁比业务先死”

第 3 环留了个没治干净的病:过期时间到底设多长。短了业务没完锁先飞,长了崩溃后死锁时间长。Redisson 的看门狗把静态时长改成动态续命:

默认锁 30 秒,后台线程每 10 秒检查一次,
持锁线程还活着就把过期重置回 30 秒;
进程真崩了,没人续命,锁最多 30 秒自动释放
(他杀和自愈两头都顾上了)

完整实现和重试、锁误删的实战在 xh - 超时释放 & 锁重试 & watch dogxh - Redisson

4.5 最后一个病:Redis 自己会切换

主节点写入锁后异步复制(第 5 章),还没复制到从节点就宕机,哨兵切换(第 6 章),新主上没有这把锁,第二个客户端顺利拿锁,互斥破了。

  • RedLock:向 5 个独立 Redis 实例同时抢锁,过半成功才算持有
    (第 6 章的多数派搬来锁上用;学术圈对它的时钟假设有著名争论,
    工程上多数场景用单实例加 watchdog,接受极小概率窗口)
  • ZooKeeper 方案:抢锁 = 创建临时顺序节点,
    编号最小者持锁;其他人只 watch 自己的前一个节点
    (只盯前驱,避免锁释放时全体惊醒的羊群效应)
    持锁进程崩了,会话超时临时节点自动删除,天然无死锁
    新病: 会话靠心跳维持,一次长 Full GC 会话过期锁被放掉,
    而 GC 醒来的进程还以为自己持锁(第 6 章判死的原理性问题,
    彻底兜底要靠 fencing token)

这条链的判词:每环修一个洞,洞永远修不完,工程决策的最后一问是”剩下的极端窗口,业务能不能容忍”。

5. Java 并发容器:锁越切越细

5.1 Hashtable:一把锁锁全表

每个方法都挂 synchronized,锁的是整个表对象。任何读写互斥,八个线程排成一队。Collections.synchronizedMap 只是把锁从方法签名挪进包装类,粒度分毫未变。

5.2 ConcurrentHashMap JDK7:分段,十六把锁

  • 原理:表切成 16 个 Segment(每段继承 ReentrantLock),
    按 key 的高位路由进段,不同段的操作完全并行
    (第 7 章分片思想的进程内版本: 并发度 = 段数)
  • :并发度定死在构造时;size 这类全表操作要
    连续尝试无锁统计、不行再锁全段,又贵又拧巴

5.3 JDK8:锁细到桶,统计学 LongAdder

  • 原理:抛弃 Segment,直接 Node 数组:
    插入空桶: CAS 一次搞定,无锁
    桶里有人: synchronized 锁桶头节点,粒度 = 一个桶
    桶内链表过长: 单桶节点数到 8 且表长到 64 时转红黑树,
    哈希严重倾斜时查找从 O(n) 保底到 O(log n)
    (到 8 才转是因为正常哈希下链长服从泊松分布,
    长到 8 的概率不到千万分之一,真到了说明哈希质量出了问题)
    扩容: 多线程协助迁移,每个线程认领一段桶搬运
    size: 用 CounterCell 分散计数再求和,
    就是第 3 章 LongAdder 的分段思想(含缓存行填充)
  • 锁的粒度三级跳:全表 -> 段 -> 桶,并发度从 1 到 16 到桶数

平行的两条小链同一个方向:synchronized 自身从无差别重量级锁演化出偏向、轻量级、重量级的升级路线(偏向锁后来因维护成本被移除,演进链也会回退);AtomicLong 的单点 CAS 在高竞争下空转,LongAdder 拆成 Cell 分散累加。
源码级展开在 concurrnetHashMap原理synchronized原理LongAdder源码

5.4 微缩样本:ziplist 到 listpack

演进链在数据结构尺度同样成立。ziplist 每项记录前一项的长度(为了倒序遍历),这个字段是变长编码:中间插入一个大元素,下一项的 prevlen 字段可能要从 1 字节扩到 5 字节,它变长又可能触发再下一项扩,最坏整条链级联重写,这就是连锁更新。
listpack 的修法:每项只记自己的长度,倒序遍历改从项尾解析,前后项彻底解耦,连锁更新从结构上消灭。
细节在 listpack

6. GC 收集器:停顿这个病赶了三十年

这条链的主角病自始至终是 STW 停顿,需求侧的变化是堆从几十 MB 涨到几百 GB。

并发收集的根难题要先立起来,CMS 之后的每一代都在跟它缠斗:收集器一边标记存活对象,业务线程一边改引用关系,就可能漏标(把活对象当垃圾收掉,事故级错误)。三色标记把这件事说清楚了:

三色: 黑 = 自己和引用都扫完;灰 = 自己标了引用还没扫完;白 = 未访问
扫描推进 = 灰变黑、白变灰,结束时还是白的就是垃圾
漏标要同时满足两个条件:

  1. 业务线程把一个白对象挂到了黑对象下面(黑不会再被扫)
  2. 同时删掉了灰对象通往这个白对象的全部旧路径
    两种修法各断一个条件:
    增量更新(CMS 用): 黑对象新增引用时记录下来,
    重新标记阶段把这些黑对象重扫(断条件 1)
    原始快照 SATB(G1 用): 删引用时把旧引用记录下来,
    按标记开始时刻的对象图算存活(断条件 2,代价是多留浮动垃圾)
    两种记录都靠写屏障: 给”修改引用”这个动作插一段钩子代码

带着这个根难题看每一代:

  • Serial:单线程收集,全程 STW。堆小(几十 MB)时停顿几十毫秒能忍
    病: 堆一大,停顿随之秒级
  • Parallel:收集本身多线程化,吞吐显著提升(吞吐优先的定位)
    病: 停顿时长照样和堆大小成正比,只是干活的人多了
  • CMS:第一次把”和业务线程并发”做进收集器:
    初始标记(短 STW,只标 GC Roots 直连)-> 并发标记
    -> 重新标记(短 STW,处理增量更新记下的账)-> 并发清除
    病单三条: 并发期间业务还在产生垃圾(浮动垃圾),
代码块JAVA · 2 行收起展开
  预留空间不够就并发失败退化成 Serial Old 全停顿(最痛一刀);
  标记清除不整理,碎片攒多了大对象放不下提前 Full GC;

并发线程抢业务 CPU

  • G1:region 化。堆切成几千个小块(又是第 2 章定长块),
    记录每块回收收益,按用户设定的停顿目标(如 200ms)
    每次只挑收益最高的一批块回收(垃圾优先,Garbage First)
    并发标记换 SATB 路线,浮动垃圾多留一轮但重新标记更快
    -> 停顿从”和堆成正比”变成”可预算”
    病: 跨块引用要维护记忆集,内存开销可观;
    超过半个 region 的大对象单独处理,碎在细节里
  • ZGC:着色指针(利用 64 位地址的空闲位存标记)+ 读屏障,
    连”移动对象”都和业务并发做,停顿进入亚毫秒且和堆大小无关
    病: 读屏障让吞吐略降,换来的是 TB 级堆也能亚毫秒

链的读法收获:每代收集器的”定位”(吞吐优先、低延迟优先)说的就是它治什么病、容忍什么病。选型问题从背参数变成对号入座:批处理任务吃吞吐选 Parallel,在线服务吃延迟按堆大小在 G1 和 ZGC 里挑。完整各论在 垃圾回收器

7. 服务架构:每次拆分都是把进程内问题换成分布式问题

这条链把前面十三章全部串成账单:

  • 单体:一个进程装下全部业务。开发部署最简单
    病: 发布牵一发动全身,单点故障全站死,扩容只能整体复制
  • 集群 + nginx:多实例跑同一份代码,反向代理分流(第 8 章)
    修: 单点和扩容
    新病: 会话不一致(本章第 1 条链的分布式 Session 问题原地爆发)、
    数据库成为新瓶颈
  • 数据层拆:读写分离(第 5 章)、分库分表(第 7 章)、缓存(第 3 章)
    修: 数据库瓶颈
    新病: 复制延迟、跨片查询、缓存一致性,各章讲过的账全来了
  • 微服务:按业务边界把单体拆成独立部署的服务
    修: 团队并行开发、按服务独立扩容、故障隔离
    新病清单直接对应一整套基础设施:
    进程内调用变远程 -> RPC 框架、超时重试、幂等(第 13 章线索)
    服务地址动态变化 -> 注册中心(第 8 章)
    横切逻辑分散 -> 网关统一鉴权限流(第 8 章)
    一个慢服务拖死一片 -> 熔断、隔离、降级
    跨服务数据一致 -> 分布式事务、消息最终一致(第 11 章 MQ)
    排障跨十个进程 -> 全链路追踪

判词:微服务的每一件基础设施,都是拆分时放出来的一个病。所以架构选型的真问题从来是”当前团队和体量,值不值得为扩展性付这套分布式账单”,单体在很多规模下就是正确答案。

8. 怎么用演进链学新技术

三问模板(解决什么、原理是什么、新病是什么)之外,两个实用推论:

  1. 见到别扭的设计先考古: SameSite、doublewrite、
    延迟双删、偏向锁的移除,全是某个病的疤痕。
    查它治什么病,比背它的规则记得牢十倍
  2. 链和总图配合用: 新技术先问”它在哪条链的哪一环”
    (治谁的病),再问”它落在哪几格”(用了哪些思想),
    两个坐标一定,剩下的才是真增量

9. 学完本章你能解决什么问题

  1. Cookie 的四条病各催生了什么补丁,为什么说存账号密码是用法的病?
  2. Session 集群三代解法的取舍,集中存储又背上了什么?
  3. JWT 为什么原理上做不到即时吊销,工程闭环怎么”请回状态”?
  4. 队头阻塞在三代 HTTP 里各卡在哪一层,QUIC 为什么选 UDP?
  5. 混合加密怎么分工,证书链怎么压住调包,前向安全治的什么病?
  6. Redis 锁五连补丁每环治什么,watchdog 和 ZK 方案各自的残余风险是什么?
  7. ConcurrentHashMap 两代的锁粒度怎么切细,size 为什么最后用了 LongAdder 思想?
  8. CMS 的三条病里哪条最痛,G1 的”可预算停顿”靠什么结构实现?
  9. 微服务的每件基础设施各对应拆分放出的哪个病?

核心一句话:技术不是被发明的,是被上一代的病逼出来的;读任何新技术先问它治谁的病、又生了什么病,演进链加十二思想两个坐标一定位,剩下的才是需要真正花力气的增量。

延伸阅读