多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

从进程调度到分页:操作系统资源管理的工程实践与避坑指南

从进程调度到分页:操作系统资源管理的工程实践与避坑指南 进程调度、死锁、存储管理、固定分页、分段这五个词摆在一起看着像是操作系统教材某一小节的课后习题清单。但说句实在话真正在项目里被线上故障按在地上摩擦过几次之后你会发现它们根本不是孤立考点而是同一套思维模型在不同资源维度上的展开怎么把有限的东西分给一堆互相竞争的角色分不好会出什么乱子以及出乱子之后怎么兜底。这篇文章我打算把这五块内容串成一条线来讲重点放在“为什么”和“踩过的坑”上而不是只贴教科书结论。无论你是正在准备面试的应届生、写业务代码时偶尔被死锁和分页问题逼疯的后端开发还是想系统补一遍操作系统底层的朋友都能从里面拿点直接能用的东西。1. 进程调度所有并发问题的起点1.1 CPU资源分配的核心逻辑进程调度说白了就是一件事CPU只有一个先不管多核等待执行的进程有一堆到底让谁先上。你别觉得这问题简单实际里面全是博弈。现场里最常见的一种错觉是“反正CPU跑得够快调度随便做做就行”但真到了高并发场景调度策略直接决定你是吞吐拉满还是大面积超时。调度的触发时机可以分为四类进程从运行态进入等待态比如等IO、进程从运行态进入就绪态比如时间片用完、进程从等待态进入就绪态比如IO完成被唤醒需要重新排队、进程退出。前两类是非抢占式调度下新进程被选中的时机后两类则要求调度器能强行把CPU从当前进程中拿回来——这就是“抢占”。这里面有个非常重要的直觉每一次抢占都是一次上下文切换要把当前进程的寄存器、程序计数器、栈指针全部保存下来再加载新进程的完整状态这个操作有真实开销。我见过有人为了追求极致的响应速度把时间片设成1毫秒结果整个系统一半CPU时间都花在切换上吞吐量反而崩了。调度不是越快越好是“恰到好处”最好。1.2 经典调度算法怎么选FCFS先来先服务是最朴素的策略实现简单、公平性直观但一个致命问题叫“护航效应”一个CPU密集型的耗时任务排在前面后面所有短任务全得等它跑完平均等待时间惨不忍睹。SJF短作业优先在理论上能给出最优平均等待时间但问题在于“你凭什么提前知道一个任务要跑多久”。实际系统里要么用历史执行时间做预测要么干脆放弃这个理想化假设。SRTF是SJF的抢占版本一个更短的进程到达后立刻打断当前进程平均周转时间更优但上下文切换更频繁而且可能饿死长任务。优先级调度解决的是“重要任务优先”的需求但优先级太低的任务可能永远等不到CPU这就是饿死。解决办法是老化每次调度时给等待太久的进程临时抬高优先级。RR时间片轮转是我个人最喜欢用来给人讲调度的一种算法因为它的公平性特别好理解一人一下谁也别想多占。但它的问题也很直白时间片选多大前面说过了这是个工程权衡。多级反馈队列MQF是实际系统最常用的方案它把刚才所有思路组合在一起多个就绪队列不同队列时间片不同高优先级队列时间片短低优先级队列时间片长新任务先进最上层时间片用完没结束就降级。IO密集型任务通常很快就能跑完留在上层CPU密集型任务沉到底层用较长时间片慢慢跑。Linux的CFS完全公平调度思路更巧妙它不用时间片打天下而是引入“虚拟运行时间”的概念每个进程的实际运行时间按权重折算成虚拟时间调度器永远挑虚拟时间最小的那个进程运行。红黑树维护这个有序结构插入删除都是对数级复杂度所以即使几千个线程也不会卡。这个思路在工程上参考价值很高目标不是让每个进程跑得一样多而是让每个进程的“加权虚拟时间”尽量接近。算法核心思路最大短板适用场景FCFS按到达顺序排队护航效应批处理、无交互场景SJF/SRTF短任务优先无法预知执行时间理论最优参照优先级调度重要任务先跑低优先级饿死实时系统、任务分级RR时间片轮转时间片大小难取舍分时系统多级反馈队列分级降级老化参数调优复杂通用操作系统CFS虚拟运行时间排序需要额外数据结构维护Linux内核实际写代码的时候调度的很多经验可以迁移到线程池设计上。线程池的核心参数corePoolSize、maximumPoolSize、workQueue容量本质就是在做一次“调度策略设计”线程数设大了上下文切换开销上去了任务队列设长了响应延迟就上去了。多试几次你就知道操作系统教材里那些算法不是考古内容是每天都在发生的真实博弈。2. 死锁并发世界里最经典的翻车现场2.1 死锁四条件的直觉理解死锁这种事情第一次遇到的人会觉得玄学代码看起来完全没问题程序就是不走了CPU占用率掉到0线程全部卡住。死锁之所以难排查是因为它的四个必要条件单独看都很“正常”互斥条件资源同一时刻只能被一个进程占用。这个太常见了一个全局锁本来就只能一个线程持有。持有并等待条件手里攥着一个资源不放手又去申请另一个资源。非抢占条件已经被占用的资源不能被强行抢走只能等持有者主动释放。循环等待条件多个进程之间形成了一条资源申请链A等B、B等C、C等A。有个生活化的类比我觉得特别贴切四个人在单人通过的独木桥上相遇每个人手里都撑着伞谁也不肯先收伞后退谁也无法从别人身边挤过去于是四个人就这么站在桥上晒一整天的太阳。这就是死锁。但你要注意这四个条件不是“并列关系”是“共同充分条件”。只要打破任意一个死锁就不可能形成。这引出了四种处理策略。2.2 预防、避免、检测恢复的实战取舍死锁预防从根上破坏四条件之一代价通常很重。比如破坏“持有并等待”要求进程在整个执行期间一次性申请所有资源。听起来很安全但资源利用率低到离谱进程A同时拿着打印机的锁和数据库连接的锁打印机可能十分钟都用不上一次别人也用不了。破坏“非抢占条件”就是允许系统强行回收资源这对于CPU寄存器还行对于数据库事务这种带状态的资源强行抢占意味着必须要回滚实现复杂度暴涨。破坏“循环等待”办法是给资源编号要求所有进程按编号递增顺序申请但这个约束在实际系统里极难推行因为业务代码很难严格按固定顺序拿多个锁。死锁避免的例子就是银行家算法。它的思想是在每次资源分配前系统先做一次“安全性检查”确认分配之后系统仍处于安全状态所有进程都能按某个顺序完成才真正分配。这个算法逻辑优美但有两个硬伤一是要求系统知道每个进程未来最多需要多少资源这在真实业务里几乎不可能二是每做一次资源分配就要扫描一遍进程表和资源表开销很大。所以银行家算法更多是作为教学范例存在实际操作系统和大型软件很少直接用它来做全局决策。既然预防和避免都太贵工业界普遍走的是第三条路允许死锁发生然后快速检测、快速恢复。检测手段包括资源分配图检测循环、数据库的锁等待超时、JVM的线程转储分析。恢复手段则是直接杀掉某个进程释放它的资源。或者更“操作系”一点的鸵鸟算法死锁概率足够低重启一次就完事懒得管。别笑Windows和Linux某些场景下就是这态度因为死锁检测本身的资源消耗可能比死锁造成的损失还大。这里多说一句数据库的死锁处理。InnoDB有一套自己的锁等待机制当一个事务检测到死锁时会自动回滚代价最小的那个事务同时输出一条错误信息。你翻MySQL的error log时会看到“Deadlock found when trying to get lock; try restarting transaction”这时候多数情况下不是代码逻辑错了而是两个事务以不同顺序锁了同一批数据。解决办法通常很朴素所有事务都按同一顺序访问资源或者引入超时机制。这和第2.1节讲的循环等待条件是一一对应的。2.3 线程死锁定位一次真实的jstack排查复盘前阵子一个Java服务突然出现接口大面积超时CPU占用率很低但请求就是排队下不去。我第一反应是线程池被打满了于是用jstack dump线程栈。结果发现一个很典型的两锁死锁// 线程A synchronized (accountLock) { // 做一些检查... synchronized (orderLock) { // 更新订单状态 } } // 线程B synchronized (orderLock) { synchronized (accountLock) { // 更新账户余额 } }两个线程拿着对方的锁互相等待。jstack输出里最显眼的是Found one Java-level deadlock:这一句后面会明确列出“Thread A waiting to lock ... held by Thread B”以及“Thread B waiting to lock ... held by Thread A”。看到这个输出基本就实锤了。修复方式不是加超时synchronized不支持超时而是调整锁的获取顺序约定不管是账户操作还是订单操作永远先锁accountLock再锁orderLock。这是典型的“打破循环等待条件”的工程实践。另外一个更稳的办法是改用ReentrantLock.tryLock(timeout)申请不到就放弃已有资源从根上破坏“持有并等待”。3. 存储管理内存是怎么被掰开揉碎的3.1 为什么节目从“直接访问内存”变成“地址抽象”早期计算机在无存储管理的情况下进程拿到的就是真实物理地址。你写一条指令MOV AX, [0x1234]那0x1234就是内存条上实打实的物理位置。这种方式的问题在进程数增多后暴露无遗两个程序编译时都把自己的变量放在同一个地址区间同时加载进内存直接互踩一个程序跑飞了还能直接改写操作系统的内存整个机器瞬间崩溃程序想扩容内存也没门因为它不知道自己还能用哪里。解决办法是引入地址抽象CPU看到的“地址”不再是物理地址而是一层逻辑地址由操作系统配合硬件做翻译。80x86体系里的分段机制就是这么来的逻辑地址由段选择子和段内偏移组成段选择子查全局描述符表GDT或局部描述符表LDT得到段的基地址再加偏移得到线性地址。在开启了分页的情况下线性地址继续走页表翻译成物理地址。这里有个非常重要的概念叫重定位。进程执行到一半操作系统把它从内存地址1GB挪到2GB逻辑上不应该有任何感知因为它的指令里存的都是逻辑地址跑的时候才动态翻译成物理地址。实现方式有两类静态重定位依赖加载器在装载时把所有地址改写一遍效率低而且挪不了第二次动态重定位靠基址寄存器加极限地址寄存器每个进程拥有一个Base, Limit对逻辑地址加上基址得到物理地址同时检查是否超出Limit防止越界访问。现代操作系统几乎全是动态方案因为它是进程换入换出的基础。3.2 固定分区和动态分区的碎片困局存储管理最朴素的办法是固定分区开机时把内存切成一堆大小预先定好的区每个区装一个进程。这个方法简单直接但碎得一塌糊涂。假设内存256MB切了4个64MB分区来了一个30MB的进程它只能进入64MB分区剩下34MB闲置这叫内部碎片——分区分大了但进程没用满。如果物理内存里全是40-50MB的进程而分区都是64MB的2个进程就能占满4个分区但真实使用的只有不到100MB其余全是坑。固定分区的另一个问题是用大分区跑小进程太浪费用多个小分区又跑不了大进程根本没办法按需调配。动态分区就是按进程实际大小切一块内存给它彻底消灭内部碎片但又引入新的问题进程频繁创建销毁之后内存上会布满小缝隙每个缝隙都不够塞一个进程加起来却能顶好几个进程这就是外部碎片。解决外部碎片有两个思路紧凑技术把正被占用的内存块整体往低地址挪把碎片拼成一大块连续空间但这是极其昂贵的操作要暂停所有运行中的进程并修正所有地址引用另一种就是不求连续把逻辑上连续的内存映射到物理上分散的多个块上这样外部碎片就被弥合了。后者正是分页的动机。还有一个历史方案值得提一句交换技术。内存不够时就挑一个进程整个搬到磁盘腾出空间等它需要运行再搬回来。这在虚拟内存概念出现之前很常用但交换的代价极高因为磁盘IO比内存慢几个数量级大进程换入换出一次可能要好几秒。所以后来大家才想到既然整个进程搬太贵那就只搬进程正在用的那些页这就是请求分页虚拟内存的雏形。你现在看这故事线会觉得很清晰固定分区 → 动态分区 → 分页 → 请求分页系统每一步都是被前一步的缺陷逼出来的。4. 固定分页与分段两种切法的实现细节4.1 分页是个好办法但页表是隐藏的魔鬼分页的思路是把物理内存切成固定大小的帧把进程的逻辑地址空间切成同样大小的页。任何一页都可以放到任何一个空闲的帧里进程的逻辑地址因此天然连续物理地址则无所谓连续。地址翻译公式非常简单逻辑地址高几位是页号P低几位是页内偏移d查页表得到页P对应的帧号f物理地址 f × 页大小 d。页大小怎么定非常讲究。页太小比如512字节内部碎片少每个进程的页表却长得吓人一个256MB的逻辑空间要有50多万个页表项光页表本身就把内存挤爆了。页太大比如64MB页表薄了但平均每个进程会浪费半个页的空间内部碎片还在其次更麻烦的是最后一项压根没用上整页都在浪费。工程实践里4KB页是多年权衡下来的甜点区现代x86的默认页大小就是4KB同时支持2MB的HugePage做优化。页表的第一个魔鬼是它必须存放在内存里因为页表可能非常大。每个页表项PTE通常包含物理帧号、有效位、保护位、修改位、访问位、缓存禁用位。有效位特别关键它标记“这个页是否在内存中”如果不在访问时触发缺页中断由内核去磁盘加载。这里有个很大的误区很多人以为翻页表就是一次简单的查表实际上完整的分页访问要查一次页表才能拿到物理地址再访一次内存取数据也就是一次指令要两次访问内存性能损失巨大。所以硬件引入了TLB快表把最近常用的页表项缓存起来命中后一次访问搞定。又因为TLB重新加载开销很高进程切换时不能轻易清空TLB所以才有了地址空间标识符ASID这种东西。大页表的问题是分页实践的第二个坑。32位系统下页表就要占4MB如果每个进程都维护一张光页表就把内存吃没了。解决办法是两级页表页目录 页表项线性地址拆成三段页目录索引、页表索引、页内偏移。地址翻译时先查页目录再查二级页表多一次内存访问所以一般需要TLB陪伴。64位系统下两级不够得用四级页表X86-64就是4层的。理解多级页表的核心在于不是把所有页表都放在内存里而是按需分配二级页表逻辑地址空间中没用到的那部分页表根本不存在。4.2 页面置换内存不够时谁去“背锅”缺页中断发生后如果内存里已经没有空闲页帧就必须从现有页帧里挑一个受害者写回磁盘如果被修改过再把新页载入。这个挑选过程就是页面置换算法。最优置换算法是理论标杆置换未来最长时间不会用到的页。现实中无法实现因为没人知道未来但它给所有算法提供了评价基准。FIFO是最容易实现的排队淘汰先来的页但可能把马上要用的页淘汰掉而且会出现Belady异常分配的内存页数增多缺页次数反而增加。LRU最近最少使用是理论上最好的可实现方案它假设“过去没用的未来大概率也不用”实现上需要记录每个页的最后访问时间代价比较高所以实际系统往往用它的近似实现Clock算法也叫二次机会算法。Clock算法的思路值得重点记一下。它维护一个环形链表每个页帧有一个访问位扫描时如果访问位是0就淘汰是1就清零并继续向后走所有页都访问过一遍后淘汰最早那个“给了第二次机会”的页。这个算法只用一个bit就把LRU的“最近”概念近似出来了代价极低。Linux内核在早期用的就是类似Clock算法的近似LRU如今用更复杂的双链LRU和多级回收机制但核心思想仍是“优先淘汰未被访问、未被修改的页”因为干净页不需要写回磁盘淘汰成本最低。页替换在应用层的典型场景是缓存淘汰。你用Redis做缓存时如果内存写满Redis的maxmemory-policy里就有allkeys-lru、volatile-lru、allkeys-random这些选择本质就是在系统页面置换算法的模式上做缓存决策。如果你在业务代码里实现一个LRU缓存面试官大概率会让你手写一个双向链表哈希表的组合复杂度和Clock算法的取舍逻辑一脉相承。4.3 分段从逻辑视角切而不是从地址空间切分页是从“怎么把物理内存用整齐”这个视角出发的切出来的页对程序员完全透明你根本不知道自己的代码被拆成了多少片。分段则完全不同它是从程序的逻辑结构出发把地址空间按内容切成代码段、数据段、堆段、栈段。你写一个C程序编译器会生成立即数CS、DS、SS这些段寄存器这就是分段思想的直接体现。分段的地址翻译是逻辑地址 段号S 段内偏移d查段表得到该段的基地址base和长度limit物理地址 base d同时检查d limit如果越界就触发保护异常。段表项天然包含长度信息和权限位所以分段在“共享代码”和“访问保护”上比分页自然得多两个进程可以共享同一个代码段的基址因为段就是按逻辑划分的。而分页的4KB帧和逻辑结构没有对应关系共享保护做起来别扭。但进程作为整体内存视图是几段每段长度不同操作系统需要为每段分配连续物理空间因此又回到了外部碎片的老问题。段多了之后段与段之间的空隙很难拼成大块连续空间。所以现代架构的答案往往是段页式结合先按段做逻辑划分段内有自己的页表逻辑地址先查段表得到该段的页表基址再查页表得到物理帧号。这样既有分段的逻辑清晰和保护优势又有分页消除碎片、好做换入换出的优势。x86-64虽然没有明确使用“段页式”这个名词但实际上段机制加页机制协同工作只是段被很大程度上弱化分页成为主角。4.4 应用层的“分页”和操作系统的“分页”是两回事网上热搜里“mybatisplus分页失效”“oracle分页”这类问题其实和操作系统的分页不是同一个东西但它们共享一个词容易让初学者混淆。MyBatis-Plus的分页插件本质上是在SQL执行前把原始SQL改写成带LIMIT offset, size的形式再额外执行一条SELECT COUNT(*)统计总数这里面踩的坑通常是分页插件没有生效导致全表查询、offset过大时深分页性能暴跌、count查询代价过高。我在项目里处理过最大的深分页问题一条LIMIT 1000000, 20的SQL直接打满数据库IO因为MySQL需要扫描前面一百万行才能找到那20条目标数据。这个问题的本质和操作系统分页有异曲同工之处你能看到的“逻辑窗口”当前页数据很小但底层的“物理扫描成本”和偏移量成正比。优化方案通常是两种思路一是推迟关联先查出主键ID再回表二是用游标/键集分页把条件从offset换成where id 上次最大值这正是利用了B树索引的有序性让数据库在索引树上跳着走而不是从头扫。把这个问题放大到操作系统的TLB和页表缓存你会发现它们都在回答同一个问题如何在跳过大量“不需要的东西”时依然保持高效的定位能力。系统设计里很多看似不同领域的知识底层的权衡逻辑是共通的。同样地网上那个“合并多个小分片ts分段视频”的热词也很有趣。多个小分片文件合并成一个完整MP4中间缺了一帧、时间戳对不齐、音画不同步之类的坑究其本质就是在处理“分段数据的连续性”问题。和操作系统把分散的物理帧映射成连续的逻辑地址空间是同一个抽象思路在现实文件系统里的复现只是介质从内存页变成了字节流。5. 常见问题与排查技巧实录5.1 死锁和分页故障的现场排查手册下面这张表是我在实际工作里积累的按“问题现象 → 排查路径 → 处理手段”三个维度整理。网络上那些热词一多半都能归到这四类问题里值得保存下来备查。问题现象核心原因排查命令/工具处理手段线程全部卡住、CPU占用量低Java线程死锁jstack、jcmd Thread.print找到“Found one Java-level deadlock”后调整锁顺序或改用tryLockMySQL事务互相等待回滚数据库行锁死锁SHOW ENGINE INNODB STATUS;information_schema.innodb_trx统一事务访问资源的顺序缩短事务时间必要时设置lock_wait_timeout服务大量慢SQL、返回超时深分页LIMIT offset过大explain执行计划改为游标分页WHERE id ? LIMIT ?或延迟关联Windows系统非分页池内存持续增长驱动或组件未释放非分页内存poolmon、!poolmon或利用tag查看定位到对应tag的驱动更新驱动或修复泄漏组件服务器swap大量换入换出、响应卡顿内存不足缺页率过高vmstat、sar -B、free调低缓存占用、增加内存、调整内核vm.swappinessSQL翻页前后数据不一致分页期间数据发生变更无固定工具需结合业务在事务内统一快照或引入一致性条件线程死锁排查我有几个小技巧。首先是不要盲目地去kill进程先抓jstack保存现场jstack pid dump.txt接着搜索deadlock关键字没有的话也要看那些BLOCKED状态的线程从它们的持有锁和等待锁关系里画一个“锁等待图”。如果在Windows服务器上看到poolmon这种工具它的输出信息非分页池增长的tag对应的是具体驱动组件官方有配套的tag数据库可以对照别自己瞎猜。5.2 缺页、非分页看板和分页失效的底层联动网络热词里有一条“非分页缓冲池内存泄漏”这其实是Windows内核内存管理里的经典问题值得单独说。Windows把内核内存分为分页池和非分页池非分页池中的内存永远不会被换出到磁盘这意味着一旦泄漏内存只能越来越少最后系统直接蓝屏或者驱动崩溃。定位手段核心就是poolmon按tag分组统计非分页池的分配与释放找出一个tag持续增长而释放为零的组件基本就是泄漏元凶。这个问题的本质是“程序申请了内核内存却忘记释放”和用户态内存泄漏的逻辑完全一致只是后果更严重因为非分页池根本没有“换页”这个兜底机制。另外如果你怀疑自己的Linux服务器在频繁分页vmstat的si和so两列如果长期不为零说明系统在持续从磁盘换入换出内存页性能必然受影响。这时候用/proc/pressure/memory或者perf看缺页事件会更精细。分清两种情况一种叫强制缺页页数据不在内存中需要从磁盘加载另一种叫次要缺页页映射关系需要更新但页本身还在物理内存里只是还没有指向它。这两种的代价天差地别排查时别一看到缺页计数高就慌。至于“mybatisplus分页失效”现实中我遇到最多的是两种原因一是忘记在配置类里注册分页插件或者分页插件被多个Configuration类重复注册导致拦截器顺序错乱二是SQL里已经带了LIMIT子句拦截器拼接分页条件时SQL语法直接出错。解决办法是把分页依赖版本和MyBatis-Plus主版本调成一致并且统一只用PageInterceptor这一个实现类。你需要知道分页插件原理是用MyBatis的拦截器接口在Executor执行前改写SQL所以凡是能影响Executor代理生成的环节比如自定义插件顺序、多数据源的拦截器链都可能让分页失效。这种“框架层失效”和操作系统的“页表未建立映射”逻辑也很对称——中间任何一层抽象断了下层再结实也没用。5.3 学习与复习建议怎么把这五个概念串起来记如果你是在备考或准备面试千万别把这五块内容当孤立知识点背那样背了很容易忘。我的建议是用一条资源矛盾主线把它们串起来CPU资源怎么分进程调度→资源分配过程中可能出现的群体性僵持死锁→内存资源怎么给多个进程分存储管理→内存拆成页来分分页→按逻辑段来分分段。你把这个故事从头到尾讲一遍讲顺了这五个考点的核心逻辑就在你脑子里了。面试的时候我被问过一个印象深刻的问题“如果给你一个完全没学过操作系统的人你如何解释分页和分段的区别”我当时的回答是分页是物理视角内存是主人它按固定大小把房间切成方格谁进来都能入住不用管住客的私人物品怎么摆放分段是逻辑视角程序是主人它需要客厅、卧室、厨房每个区域大小自己定但住进一个房间必须保证整个客厅都在同一个地方。分页牺牲了一点逻辑连续性换来了极高的物理利用率分段坚持逻辑完整性换来的是更直观的共享与保护。至于哪个更好实际系统通常是先分段分逻辑再分页落物理。另外特别注意死锁和页面置换分别是面试里最喜欢挖坑的两个点。死锁常考“四个必要条件是否充分”“银行家算法的安全性判定”页面置换常考“Belady异常发生的条件”“LRU的近似实现”这两个点如果你能在纸上写清楚一个银行家算法的安全性检查序列、一个Clock算法的指针移动过程基本就稳了。不要只背结论要能在现场推演面试官看中的是你对系统资源博弈的理解深度。5.4 最后一点实操心得我踩过无数次坑之后最想说的其实是操作系统这些底层概念和你的日常开发从来不是两座孤岛。线上一个说不清道不明的卡顿可能就是非分页池泄漏或者页面置换导致的内存颠簸一条你写了无数遍的分页查询SQL底层逻辑居然和TLB的缓存设计有相似之处线程间互相等待锁导致的死锁和数据库事务死锁在原理上更是同一个模型。在你给项目加线程池参数、给SQL加分页条件、处理线上线程卡死的时候脑子里多留一根弦我这是在调度资源我有没有可能造成循环等待我访问的数据窗口是连续的还是离散的我的缓存策略有没有给“换页”留够空间。想清楚这几个问题很多故障都能在发生之前就被设计规避掉。这也是我花大量篇幅讲“为什么”而不是“是什么”的原因——原理通了换一门语言、换一个中间件你依然能一眼看穿问题本质。
返回列表