| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896 |
- [
- {
- "kind": "review",
- "id": "01",
- "group": "接口与数据",
- "title": "API 服务开发:我真的掌握了吗",
- "summary": "从一次商品请求讲清运行、校验、状态码与等待,再用独立操作证明商品接口、进度流和连接停止符合约定。",
- "sourceModule": "api",
- "intro": {
- "scenario": "一位同学能照着视频点出商品列表,却解释不了重启后商品为何消失,也分不清 422 和服务未启动。现在由你接手这份本机小目录:先闭卷回答,再用自己的请求与观察证明理解。",
- "startingPoint": [
- "已完成 API 服务开发模块,保留 api-lab、对应虚拟环境及可运行的 main.py;先确认 /health 可访问,所有变式只使用本机课堂数据。",
- "每题先独立写下预测、理由和验证办法,再展开参考要点;实战题先做并记录证据,卡住时再回看指定课时。",
- "自评用于发现薄弱处。能认出参考答案不等于能独立完成;没有实际执行的操作明确记为未验证。"
- ],
- "outcome": "交出自己的解释、请求结果与故障定位记录,指出哪些能力已有独立证据、哪些仍需回课练习。"
- },
- "questions": [
- {
- "id": "q1",
- "title": "文件还在,为什么商品却没了?",
- "prompt": "服务启动后访问根地址得到 404,而 /health 正常;创建一件陶瓷杯后保存 main.py,服务重载,商品列表又变空。请分别解释这两个现象,并说明如何核对服务确实用了 api-lab 的 Python。",
- "checkpoints": [
- "/health 正常说明进程能够回应;根路径没有注册路由时返回 404,与端口连接失败不同,不能仅凭根路径判断服务没启动。",
- "路由由方法和路径共同匹配到函数;main:app 指向 main.py 中的 app,磁盘上存在文件本身并不等于服务进程正在运行。",
- "本课商品字典只在当前进程内存在,自动重载重新启动应用会清空它;Python 源文件保留不能证明商品数据持久保存。",
- "读取实际解释器路径,确认位于 api-lab/.venv,再核对安装和启动使用同一个 Python;提示符出现环境名只是辅助线索。"
- ],
- "misconception": "把所有 404 都归因于服务没启动,或认为源码没删所以商品不会丢。前者混淆路由结果与连接失败,后者混淆磁盘代码与进程内数据。",
- "revisit": [
- {
- "lessonId": "01",
- "label": "启动第一个 FastAPI 服务"
- },
- {
- "lessonId": "06",
- "label": "完成内存版商品 CRUD"
- }
- ]
- },
- {
- "id": "q2",
- "title": "三次填错,应该改地址还是改商品字段?",
- "prompt": "读取 /products/abc、列表条件 limit=0、创建商品时填写空名称,分别应从哪里找到错误位置?再解释:正确输入却因服务端漏返回 id 得到 500,为什么不能要求调用方换个商品名称重试?",
- "checkpoints": [
- "非法编号、越界列表数量、空商品名都可触发 422,但错误位置分别指向 path.product_id、query.limit 和 body.name;必须阅读位置,不能只看相同状态码。",
- "路径与查询字符串先是文本,声明的类型和范围在路由执行前参与校验;ProductCreate 则说明 JSON 商品字段及默认值。",
- "价格按整数分接收,输入必须符合该字段的整数、非负和上限要求;省略字段、显式 null 与提交一个合法值不能一概当成相同输入。",
- "ProductOut 要求返回 id,正常请求因服务返回数据缺字段而失败属于服务端输出错误;response_model 还筛选公开字段,不能用修改正确输入掩盖返回代码的问题。"
- ],
- "misconception": "认为只要出现字段问题就一定是调用方 422,或者只在路由函数里捕获所有校验失败。输出错误应修服务端,部分输入失败还发生在路由执行之前。",
- "revisit": [
- {
- "lessonId": "03",
- "label": "从路径、查询参数和 JSON 中接收数据"
- },
- {
- "lessonId": "04",
- "label": "用 Pydantic 定义输入和输出契约"
- }
- ]
- },
- {
- "id": "q3",
- "title": "重复替换与重复创建,结果应一样吗?",
- "prompt": "先创建帆布袋,再对返回的编号执行两次完全相同的 PUT,随后执行两次 DELETE。请预测各阶段的结果和最终状态,并解释为什么把创建请求原样重发两次可能得到两件商品。",
- "checkpoints": [
- "创建成功应得到 201 和实际 id,后续请求使用这个 id,而不是固定假定为 1;再次 GET 才能核实保存结果。",
- "本模块 PUT 是完整替换,name、description、is_active、price_cents 四个可写字段必须明确提交;两次相同 PUT 后内容一致且 id 不变。",
- "第一次 DELETE 成功返回 204 且响应体为空,之后读取为 404;再次删除可以返回 404,但目标始终是不存在,幂等关注最终状态而非响应逐字相同。",
- "本模块普通 POST 没有自动去重协议,重复创建会分别分配编号;不能把 PUT 的重复行为推广到 POST。验收期间不改代码触发内存重载。"
- ],
- "misconception": "认为幂等要求每次状态码完全一样,或拿创建模型的默认值模拟局部更新。前者忽略最终状态,后者会把未提交字段重置而不是保留旧值。",
- "revisit": [
- {
- "lessonId": "05",
- "label": "设计商品资源与 HTTP 行为"
- },
- {
- "lessonId": "06",
- "label": "完成内存版商品 CRUD"
- }
- ]
- },
- {
- "id": "q4",
- "title": "私有入口为什么没有按预期出现或放行?",
- "prompt": "你把 /private 写进 routers/private.py 后,文档里看不到它。接入后,请求分别得到 503、401 和 200。请给出检查顺序,并说明为什么统一错误格式时仍要保留原有状态码和响应头。",
- "checkpoints": [
- "先确认主入口在创建 app 后执行 include_router;仅导入模块或保存文件不会自动将 APIRouter 里的接口加入应用,也不能再新建一个 app 覆盖旧路由。",
- "503 可对应服务没有配置 DEMO_API_KEY;在启动服务的同一个终端设置后重新启动,再验证进程读取的配置,另一窗口改变量不会改变旧进程。",
- "依赖从 X-API-Key 请求头取值,缺失或错误时在路由之前返回 401,正确课堂值才放行到 200;把值放进查询参数不能满足 Header 声明。",
- "统一 error 外层仍保留 HTTP 状态及 Allow、WWW-Authenticate 等必要头。固定错误结构方便调用方定位,不应把失败都变成 200,也不意味着形成完整用户身份系统。"
- ],
- "misconception": "把依赖当成只供文档展示的注释,或通过放宽全部路由来消除错误。依赖实际控制请求是否进入函数,注册、服务配置和来访者输入要分别检查。",
- "revisit": [
- {
- "lessonId": "07",
- "label": "返回可定位、格式一致的错误"
- },
- {
- "lessonId": "08",
- "label": "复用依赖与拆分路由文件"
- }
- ]
- },
- {
- "id": "q5",
- "title": "两个慢请求与一个浏览器错误,是否同一原因?",
- "prompt": "两个各等 1 秒的请求有时总计约 1 秒,有时约 2 秒且拖慢 /health。另一个商品网页用 localhost 打开时失败,但 curl 能成功。请分别提出最小对照实验,说明每个实验能证明什么。",
- "checkpoints": [
- "保持同一单进程服务、相同并发客户端和相同 delay,只比较 await asyncio.sleep 与在 async 路由内直接 time.sleep;前者等待可重叠,后者会占住事件循环。",
- "同时观察 /health 与总耗时,记录环境和合理开销;async 关键字本身不会把阻塞函数变成异步,也不能据此宣称大量 CPU 计算会加速。",
- "浏览器实验比较完整 Origin:协议、主机和端口;localhost 与 127.0.0.1 是不同来源,查看 OPTIONS 预检和授权响应头,而不是先修改商品内容。",
- "curl 与服务端 HTTPX 不执行浏览器 CORS 限制。HTTPX 调用上游超时是另一段链路,本课可返回 504,不能把它与浏览器来源不匹配合并诊断。"
- ],
- "misconception": "认为“curl 成功所以网页一定能读响应”,或“写了 async 所有等待就会并行”。前者漏了浏览器检查,后者漏了实际执行的阻塞调用。",
- "revisit": [
- {
- "lessonId": "09",
- "label": "使用 async / await 处理 I/O 等待"
- },
- {
- "lessonId": "10",
- "label": "使用 HTTPX 调用服务与调试 CORS"
- }
- ]
- },
- {
- "id": "q6",
- "title": "清单进度应该什么时候显示、什么时候结束?",
- "prompt": "商品保存只需一次结果,清单进度要逐段显示,咨询消息需要双方继续发送。请给三种需要各选一种本课通信方式;再解释为什么对 SSE 响应调用完整文本读取,或将每个网络块直接当成一条 JSON,都可能让体验出错。",
- "checkpoints": [
- "普通商品写入可用 HTTP 请求与完整响应;单向持续进度可用 SSE;连接中双方都要主动发消息时可用 WebSocket。只是偶尔查状态时,定期 HTTP 查询也可能足够。",
- "SSE 在同一个 HTTP 响应体里按空行划分事件;本课 token 表示文字片段,done 表示正常完成,不能把连接消失直接当成业务完成。",
- "完整文本或 JSON 读取要等响应体结束;手动读取应连续解码 UTF-8、缓存未完成文本再按事件边界提取,因为字符和事件都可能跨块,一块也可能含多条事件。",
- "EventSource 适合本课 GET 并在 done 后主动关闭;需要 POST 请求体时可用 fetch 并自行解析。主动取消应有明确停止动作和取消结果,不当作一般服务故障。"
- ],
- "misconception": "把一次 read 与一条完整事件画等号,或因为 SSE 使用 HTTP 就认为它不能逐段显示。网络分块和业务事件边界并不重合,读取方法也会决定何时向用户展示。",
- "revisit": [
- {
- "lessonId": "11",
- "label": "完整响应、流式响应、SSE 与 WebSocket"
- },
- {
- "lessonId": "13",
- "label": "在浏览器中读取流,并在中途停止"
- }
- ]
- }
- ],
- "challenges": [
- {
- "id": "c1",
- "title": "独立验收一件新商品的完整生命周期",
- "prompt": "不复制练习答案,为本机目录增加一件“旅行收纳袋”,价格 2590 分,先不上架。独立完成创建、读取、完整替换、筛选和删除,并加入一次非法价格输入。先写出验收顺序及预期,再执行并保存请求记录。",
- "checkpoints": [
- "从创建响应取得实际 id,记录 201,再读取同一商品确认名称、描述、上架状态和整数价格;不依赖现有样品编号,也不在过程中重载服务。",
- "完整替换明确提交四个可写字段,把价格改为 2790 分并上架;再次读取核实 id 不变且四项内容符合新值,同时比较明确 false 筛选与未传筛选的差异。",
- "发送负价格等确定不符合契约的输入,记录 422 和具体字段位置,并重新读取原商品证明失败请求没有改掉合法数据。",
- "删除后核对 204 空体,再次读取得到 404;把核心状态和结果写成可重复断言,说明哪些是实际记录,哪些还未执行。"
- ],
- "misconception": "只保存几张成功响应,便说完整流程已经验证。没有后续读取、失败后的数据检查和删除空体检查,无法证明各操作之间的真实状态变化。",
- "revisit": [
- {
- "lessonId": "06",
- "label": "完成内存版商品 CRUD"
- },
- {
- "lessonId": "16",
- "label": "把能运行的接口变成可重复验收的服务"
- }
- ]
- },
- {
- "id": "c2",
- "title": "连接恢复了,停止按钮还有效吗?",
- "prompt": "使用本机咨询回显服务,独立安排“正常收发、服务中断后恢复、用户主动停止”三段实验。同时检查清单流的完整内容与真实逐段到达。先写出每一段要记录的证据,再操作;不要将自评勾选当作自动测试通过。",
- "checkpoints": [
- "正常连接后先等 OPEN 再发咨询和 ping,记录 echo 与 pong;关闭并恢复服务后观察有上限、逐渐延长的重试以及新连接的真实回信。",
- "在下一次重试前主动停止,等待超过本课最长重试间隔并检查没有新增连接;断线时未发送的业务消息不应在重连后自动出现。",
- "内容测试检查清单 token 拼接结果与完整响应一致且 done 最后出现;真实网络另记首段与后续片段到达时间,不能用 TestClient 的完整结果证明延迟表现。",
- "分别记录正常完成、主动取消和异常断开;解释心跳是本课应用层消息、重连是新连接,恢复通道并不自动恢复消息执行状态。"
- ],
- "misconception": "看到重新出现“已连接”就宣布恢复与停止都可靠。新连接不证明旧消息已送达,停止后还有计时器也可能继续重连,必须验证可观察的后续行为。",
- "revisit": [
- {
- "lessonId": "15",
- "label": "连接断开后:重连、心跳与取消"
- },
- {
- "lessonId": "16",
- "label": "把能运行的接口变成可重复验收的服务"
- }
- ]
- }
- ],
- "mastery": [
- {
- "id": "m1",
- "text": "我能独立解释一次商品请求从地址匹配、输入检查到响应生成的过程。",
- "evidence": "不看要点写出 404、405、422、500 的不同触发位置,并为其中至少两项提供自己复现的状态与错误位置。"
- },
- {
- "id": "m2",
- "text": "我能独立启动正确环境并完成一件商品的创建、替换和删除。",
- "evidence": "交出 .venv 解释器路径、真实商品 id 以及操作后的读取结果,说明重载对内存商品的影响。"
- },
- {
- "id": "m3",
- "text": "我能独立区分内容检查与真实网络、浏览器行为验证。",
- "evidence": "交出一组可重复断言和一次逐段到达时间记录,明确指出 TestClient 未覆盖的浏览器检查。"
- },
- {
- "id": "m4",
- "text": "我能独立为等待、来源错误和连接中断提出有依据的排查步骤。",
- "evidence": "复现一个受控故障,保留对照结果、修复后恢复请求及停止后无新增连接的记录;未执行部分如实标记。"
- }
- ]
- },
- {
- "kind": "review",
- "id": "02",
- "group": "接口与数据",
- "title": "数据库基础:我能设计并验证商品数据吗",
- "summary": "从字段和关系设计开始,用合法与非法写入验证规则,再解释事务、并发、索引、连接与恢复的实际边界。",
- "sourceModule": "database",
- "intro": {
- "scenario": "你要为一个只管理商品名称、价格、上架状态与分类的小目录设计数据库。同学说“表建出来、查询不报错就算学会”,你需要用具体反例、执行结果和恢复证据检查这个判断。",
- "startingPoint": [
- "已完成数据库基础模块,保留 shop-service 与本机 shop_lab;确认练习账号、库名和当前客户端位置,不把本机实验指向业务数据。",
- "每题先独立给出设计或预测,再展开要点对照。操作题使用新建课堂样品并记录实际编号,不复制固定 id,不省略失败后的数据核查。",
- "结构修改、双会话和恢复各有已学前提;开始操作前重查对应课时的实验边界。没有执行的项写未验证,自评不代表客观考试结论。"
- ],
- "outcome": "交出商品字段与关系设计、有效和无效写入结果、并发观察及独立恢复记录,按证据判断自己能否脱离示例完成。"
- },
- "questions": [
- {
- "id": "q1",
- "title": "字段设计能挡住哪些具体错误?",
- "prompt": "一个保温杯可以改名,也允许和其他商品同名;售价 19.90 元,新建时默认下架,免费样品售价为 0。请独立设计商品核心字段,说明类型、主键、非空、默认值与检查约束分别在防什么错误,并说明如何保存创建和修改时间。",
- "checkpoints": [
- "用稳定 id 主键识别商品,名称使用有长度限制的文本且允许重名;名称会变,不适合当身份,也不能用自增编号大小当商品数量。",
- "price_cents 用有范围的整数分,1990 表示 19.90 元,0 表示免费而非未知;NOT NULL 与非负 CHECK 各自保留,应用还应验证整数与范围。",
- "is_active 默认 FALSE,并用约束限制 0/1;名称应非空白,描述有明确空串语义与 16000 字符上限,不能以“数据库接受文本”为由忽略业务边界。",
- "DATETIME(6) 按统一 UTC 约定保存,字段本身不带时区;created_at 保持初次值,修改时显式更新 updated_at 并递增 version,默认值不会自动代劳。"
- ],
- "misconception": "把所有列统一做成可空文本,或认为 CHECK 足以替代 NOT NULL。这样无法表达数值和缺值语义;CHECK 对 NULL 得到的 UNKNOWN 也不能承担非空检查。",
- "revisit": [
- {
- "lessonId": "02",
- "label": "从商品需求设计表结构、字段与数据约束"
- }
- ]
- },
- {
- "id": "q2",
- "title": "一个商品属于两个分类,怎样避免重复与孤儿?",
- "prompt": "桌面收纳盒同时归入“办公”和“桌面”,另一件样品暂时没有分类。请画出三张表的关系,决定主外键和两个查询方向的索引,再预测重复归类、删除被引用分类、下架商品、删除商品的结果。",
- "checkpoints": [
- "products 与 categories 各自保存商品和分类事实,product_categories 一行保存一组归类;两个外键的类型、符号与父表主键一致,不复制分类名或存逗号编号串。",
- "关联表以 product_id、category_id 组成联合主键,拒绝重复归类;以 category_id、product_id 建反向访问路径,分别支持两个查询方向。",
- "分类名按本课规则唯一,默认 ai_ci 比较下只变大小写仍可能冲突;商品名允许重名,不能把两种名称的唯一性规则混为一谈。",
- "下架保留归类;删除商品由 CASCADE 清理关联但保留分类;删除被引用分类由 RESTRICT 拒绝。未分类商品用 LEFT JOIN 保留,统计关联列而非全部连接行才能得到 0。"
- ],
- "misconception": "给关系表随便加一个 id 后就允许重复组合,或把下架等同于删除。多一个编号不能代替组合唯一规则,状态修改也不会自动触发删除级联。",
- "revisit": [
- {
- "lessonId": "04",
- "label": "设计商品分类关系表、主外键与删除策略"
- }
- ]
- },
- {
- "id": "q3",
- "title": "第二条写入失败,第一件商品还在吗?",
- "prompt": "Python 创建一件名称含单引号的技术书,再给它关联一个不存在的分类。请说明如何传入名称,如何获得新编号,以及第二次写入报错后应怎样结束事务。另解释为什么不能把一次建表语句顺手夹进这组业务写入。",
- "checkpoints": [
- "通过驱动参数传商品名,使单引号和看似 SQL 的片段保持为普通值;不使用字符串拼接生成含用户内容的 SQL,列名等标识符另用固定白名单。",
- "从当前连接的插入结果取得生成编号,再按这个 id 查询或归类;不用 MAX(id) 猜刚创建的是哪一条,也不把执行行数当作记录内容。",
- "若商品与分类必须同时成功,外层控制同一事务,异常后整体 rollback,最后关闭或归还连接;外键失败通常只撤销失败语句,不能因还能 SELECT 就继续提交。",
- "CREATE、ALTER 等结构操作有隐式提交边界,不能用普通 ROLLBACK 假定全部撤销;若分类只是可选步骤,须先明确业务规则才考虑保存点。"
- ],
- "misconception": "认为数据库一报错误就已经替你回滚全部业务,或 with 游标退出等于提交。错误范围和客户端资源范围不同,必须主动处理事务结果。",
- "revisit": [
- {
- "lessonId": "03",
- "label": "用PyMySQL参数化完成增删改查"
- },
- {
- "lessonId": "05",
- "label": "明确InnoDB事务、保存点与DDL提交边界"
- }
- ]
- },
- {
- "id": "q4",
- "title": "另一个人已保存,为什么我还看到旧值?",
- "prompt": "A、B 同时读取同一商品,B 把描述改为 v2 并提交。请预测 A 在 RR、RC 下继续普通查询的不同结果,再解释 RR 的 FOR UPDATE 为什么可能看到新值。若两位编辑都带着 version=1 保存,怎样拒绝后来的覆盖?",
- "checkpoints": [
- "RR 的普通一致性读通常沿用事务首次一致性读建立的快照,因此 A 可能仍见旧值;RC 每条一致性读可见期间新提交的值,两者都不读另一事务未提交数据。",
- "FOR UPDATE 使用锁定的当前读规则,不能套用普通查询的旧快照结论;它会取得锁,要及时结束短事务,不在用户编辑页面期间持有。",
- "把旧版本比较、字段更新和版本递增放在同一条条件更新里;首个 version=1 更新成功后版本变为 2,第二个旧版本匹配 0 行,不应宣称保存成功。",
- "锁等待超时与死锁不是相同事件,回滚范围也可能不同;操作后结束失败事务,再核对最终描述、版本以及是否还有活动事务。"
- ],
- "misconception": "认为 RR 会自动给所有旧页面保存返回版本冲突,或者先读版本再无条件更新就没有竞争。读取快照不替代原子版本条件,分开的检查与写入之间仍能插入另一修改。",
- "revisit": [
- {
- "lessonId": "06",
- "label": "用双会话观察RR、RC与当前读"
- },
- {
- "lessonId": "07",
- "label": "观察行锁、间隙锁、死锁与版本冲突"
- }
- ]
- },
- {
- "id": "q5",
- "title": "只返回 20 行,为何还是读了很多数据?",
- "prompt": "上架商品按创建时间和 id 倒序显示,每页 20 行。请提出组合索引候选,并说明如何从计划判断其作用。翻到很深的位置时,为什么 OFFSET 的工作量可能很大?已有上一页末行后,怎样确定下一段范围?",
- "checkpoints": [
- "候选索引围绕等值上架条件和倒序创建时间、id 组织,详情 id 已有主键;索引来自固定查询,不是每个字段都顺手添加。",
- "记录数据量、上架比例和同一查询的前后计划,比较所选 key、扫描行数与排序情况;EXPLAIN ANALYZE 实际执行 SELECT,不能把估计 cost 当毫秒。",
- "OFFSET 即使只输出 20 行,通常仍处理之前被跳过的大量记录;应查看底层节点而不仅看顶层返回数量。",
- "按时间更早或同时间 id 更小的范围继续读取,保留相同筛选和排序;比较性能时单列获取深游标的成本,实际计划仍需验证。索引也增加写入维护和空间成本。"
- ],
- "misconception": "看到索引名字就宣布优化完成,或藏起取得第 40000 行游标的成本。优化需要同条件下的实际工作量对照,查询结果少不等于扫描工作少。",
- "revisit": [
- {
- "lessonId": "08",
- "label": "设计组合索引、覆盖索引与JSON路径索引"
- },
- {
- "lessonId": "09",
- "label": "读懂EXPLAIN ANALYZE并优化深分页"
- }
- ]
- },
- {
- "id": "q6",
- "title": "借不到连接,换 root 能解决吗?",
- "prompt": "两个请求占住小池中的两条连接,第三个请求借用超时。同学建议改成无限连接并让商品程序使用 root。请指出该建议混淆了什么,说明怎样验证连接归还、事务结束和日常账号权限。",
- "checkpoints": [
- "池耗尽是应用等待空闲连接超时,与数据库行锁等待或认证失败不同;先核对借还范围、并发量和查询耗时,不能用换账号掩盖。",
- "每个并发任务用自己的连接和短事务,成功提交、失败回滚,最后归还;池代理 close 可保留底层连接复用,看到 Sleep 不能单独证明泄漏。",
- "验证释放占用后新请求又能借到连接并查询,容量设置有上限与等待预算;不能通过跨线程共用同一活动事务或无限扩容来绕过。",
- "日常账号保留本课商品读写权限,结构维护使用独立身份;实际检查 CURRENT_USER 与授权,做一项允许的写入和一项预期被拒的 ALTER,拒绝不应被当作待修故障。"
- ],
- "misconception": "把容量、锁与权限都归为“数据库连不上”。root 无法修复遗留事务,无限连接还会转移压力;应按失败发生层次寻找证据。",
- "revisit": [
- {
- "lessonId": "10",
- "label": "用有界连接池管理PyMySQL连接与事务"
- },
- {
- "lessonId": "11",
- "label": "按账号host范围拆分权限与连接配置"
- }
- ]
- }
- ],
- "challenges": [
- {
- "id": "c1",
- "title": "从需求写出一套可被反例检验的表设计",
- "prompt": "先不看原表定义,为“旅行收纳袋 25.90 元,可归入户外和收纳分类”写出字段设计表与关系图,再在本机练习环境用新样品验证。已有表只核对和复用;不要为重做题目删除原表。设计中必须写清缺值、重复、删除和排序规则。",
- "checkpoints": [
- "设计材料写清每表一行的含义,商品稳定主键、名称可重名、整数价格分、下架默认值、UTC 时间与版本维护;逐项说明类型、非空、默认和检查约束的用途。",
- "关系材料明确父子类型一致、组合唯一、分类名称唯一、双向查询访问路径,以及本课商品级联、被引用分类限制删除的选择。",
- "用自己创建的实际编号证明合法价格和两条归类能保存;分别提交空白名称、负价格、重复归类与不存在父编号,记录预期拒绝及失败后原合法数据仍在。",
- "读出两个分类和另一个未分类样品的 0,验证下架仍保留关联;只清理自己新增的样品,并以建表实际输出核对设计没有停留在纸面。"
- ],
- "misconception": "只交一张字段清单,不验证规则是否落到真实表;或为快速重做直接删掉贯穿课程的表。设计能力需要实际约束与反例证据,复习也要维护既有实验边界。",
- "revisit": [
- {
- "lessonId": "02",
- "label": "从商品需求设计表结构、字段与数据约束"
- },
- {
- "lessonId": "04",
- "label": "设计商品分类关系表、主外键与删除策略"
- }
- ]
- },
- {
- "id": "c2",
- "title": "证明备份能恢复,再验证一次兼容加列",
- "prompt": "为本机商品库准备一次独立恢复验收。先写出源库、独立目标库、暂停写入的范围与核对项目,再生成备份并恢复到专用验收库。确认数据后,对照课程验证恢复库可写和源库兼容加列;已有相同列或非空目标时先盘点,不重复执行。",
- "checkpoints": [
- "先核对目标数据库和账号,保护本机凭据文件,备份成功后检查文件校验和;不会把导入目标误指回源库,也不用跳过错误的方式把失败导入宣称成功。",
- "在暂停本次练习写入的条件下比较商品数量、编号范围、上架数量与价格分总和,并核对两个方向的外键孤儿计数以及实际主键、CHECK、删除规则。",
- "在恢复库试写新样品,取得实际自动编号后回滚;兼容加列明确目标和算法边界,验证旧查询与新查询均能工作,并说明 DDL 不能依赖普通回滚撤销。",
- "交出实际恢复结果与用时,区分已完成的逻辑恢复和仍需一致坐标、连续 binlog、停止点与隔离演练的时间点恢复;证据缺失处写未验证。"
- ],
- "misconception": "把文件存在、校验和正确或单项行数相同直接等同于恢复成功。文件完整不证明数据关系和后续写入可用,也不能证明已经具备时间点恢复能力。",
- "revisit": [
- {
- "lessonId": "11",
- "label": "按账号host范围拆分权限与连接配置"
- },
- {
- "lessonId": "12",
- "label": "用mysqldump恢复并验收MySQL结构变更"
- }
- ]
- }
- ],
- "mastery": [
- {
- "id": "m1",
- "text": "我能独立从轻量商品需求推导字段、主外键、约束和删除规则。",
- "evidence": "交出一份字段表与关系图,逐项回答规则服务于什么操作,并用真实结构输出和至少四个失败样例印证。"
- },
- {
- "id": "m2",
- "text": "我能独立完成参数化写入并正确结束成功和失败事务。",
- "evidence": "复现同一真实编号的创建、修改和清理,以及第二步失败后整笔回滚;保留合法数据与连接恢复的检查结果。"
- },
- {
- "id": "m3",
- "text": "我能独立用双会话和执行计划验证并发与查询成本。",
- "evidence": "不看答案记录一次旧值/新值对照和一次索引或分页对照,写明顺序、实际行数、最终版本及实验局限。"
- },
- {
- "id": "m4",
- "text": "我能独立区分连接、锁、权限与恢复问题,并核对修复后的状态。",
- "evidence": "完成一个受控故障的恢复请求与一份独立恢复验收记录;清楚标明未执行的项目,不能仅凭没有报错自评通过。"
- }
- ]
- },
- {
- "kind": "review",
- "id": "03",
- "group": "接口与数据",
- "title": "API 与数据库实战:我能守住一次商品操作的边界吗",
- "summary": "把请求、Session、事务与真实表连成一条线,检查数据契约、分页、迁移、版本冲突、重复创建和通知重试是否都有验证依据。",
- "sourceModule": "api-database",
- "intro": {
- "scenario": "商品已经从内存搬进 MySQL,但能返回 201 还不够:两个人可能同时改同一件商品,创建响应可能丢失,通知可能重复。只用本课的小商品目录,检验你能否把这些结果解释清楚并验证到数据库。",
- "startingPoint": [
- "已完成 API 与数据库实战模块,保留 shop-service 的配置、迁移和探针。先确认当前连接的库名:前半模块使用 shop_lab,迁移后实验使用 shop_lab_migration,测试只指向独立 shop_lab_test。",
- "先独立作答,再展开参考要点;操作题只用随机标记的本机样品。并发和提交问题按原课真实连接实验执行,不把多个任务塞进同一个 Session。",
- "自评依据自己能交出的结果;测试输出、手工故障观察与未验证假设分别标记,不将熟悉术语或看懂答案当作客观通过。"
- ],
- "outcome": "用独立解释、HTTP 与 SQL 双向结果、迁移检查和故障恢复证据,判断自己能否维护这条商品数据链路。"
- },
- "questions": [
- {
- "id": "q1",
- "title": "为什么能共享 Engine,却不能共享 Session?",
- "prompt": "商品 API 的 /live 正常、/ready 失败;另一个同学让两个并发查询共用 Session,并认为连接池只需按一个进程计算。请说明三个问题的检查顺序,并计算两个实例、每实例三个 worker、每进程最多五条连接的稳态与新旧发布重叠上限。",
- "checkpoints": [
- "/live 只证明进程能回应,/ready 还要实际借连接并查询数据库;先核对配置、数据库可达性与借用情况,不把存活结果当作数据链路已准备好。",
- "每进程复用 Engine 池,每请求或独立任务建立自己的 Session;Session 含事务状态,并发分支不能共享,物理连接之后可被池重复借用。",
- "例中稳态应用上限为 30,发布新旧完全重叠可到 60;还要加入任务消费者、迁移和诊断连接并留余量,不能只拿单池五条和数据库总限额比较。",
- "分别观察池等待、建连接、行锁与 SELECT 的预算;请求结束后核对活动事务和后续请求,Sleep 连接可能只是空闲复用,pre_ping 或 recycle 不会恢复正在执行的失败事务。"
- ],
- "misconception": "把“复用数据库资源”理解成所有任务共用一个 Session,或将每进程容量当作全服务容量。前者混用事务状态,后者会在扩进程或发布重叠时低估连接量。",
- "revisit": [
- {
- "lessonId": "02",
- "label": "异步 Session 的生命周期"
- },
- {
- "lessonId": "06",
- "label": "连接池预算、超时与多 Worker"
- }
- ]
- },
- {
- "id": "q2",
- "title": "add、flush、commit 与返回 201 是同一件事吗?",
- "prompt": "创建一件 1990 分陶瓷杯时,程序已拿到 id;有人据此立刻返回成功,再在后台提交。另一次 PATCH 没写价格与明确提交 null 被当作相同情况。请说明这两处为什么不符合本课的数据与事务约定,并指出时间输出要核对什么。",
- "checkpoints": [
- "SQL 真实表、ORM 映射与 Pydantic 输入输出要对齐字段、缺值和价格边界;id 与创建时间由服务/数据库产生,不能把任意输入直接当作公开输出。",
- "add 登记对象,flush 才发送 INSERT 并可取得生成 id,refresh 用来显式读取数据库生成值;拿到 id 不证明事务已提交。",
- "在明确事务内形成输出数据,成功离开事务边界后才返回成功;辅助函数不偷偷提交,失败时让异常触发整笔回滚,不能在响应之后隐藏提交结果。",
- "PATCH 省略价格表示保留原值,显式 null 按本课契约拒绝;DATETIME(6) 存储值依既定 UTC 解释,响应带明确 UTC 时区,updated_at 要由写路径维护。"
- ],
- "misconception": "把 flush 当 commit,或把缺字段与空值统一填成默认值。前者会在尚未持久化时宣称成功,后者可能把本来要求保留的价格清空或重置。",
- "revisit": [
- {
- "lessonId": "03",
- "label": "ORM CRUD 与输入输出模型"
- },
- {
- "lessonId": "04",
- "label": "事务边界、约束失败与提交结果未知"
- }
- ]
- },
- {
- "id": "q3",
- "title": "商品分页了,评价读取就一定有上限吗?",
- "prompt": "列表只取 20 件商品,但每次访问商品的评价属性都追加查询;改成批量预加载后,有一件商品仍带出很多评价。请说明两次性能问题的不同原因,并设计本课范围内的商品和评价分页边界。",
- "checkpoints": [
- "逐商品读取未显式加载的评价会形成 1+N 次往返,异步环境的隐式读取还可能报错;先记录实际 SQL 次数,明确加载后再访问关系。",
- "selectinload 可以将受控商品集合的关系读取合并,但限制商品数量不等于限制评价总数;真实接口按商品单独分页评价,区分无商品的 404 与无评价的空列表。",
- "商品列表使用创建时间与 id 的稳定复合倒序,下一页带上上一页末尾游标;保留相同条件并用实际计划确认访问范围,游标非法时在构建查询前拒绝。",
- "评价列表按评价 id 有界继续取,最多额外读取一条判断下一页;游标编码不是授权或加密,各分页请求也不是一个跨请求一致快照。"
- ],
- "misconception": "把减少查询次数等同于限制返回规模,或认为用了 Base64 游标就安全且全程快照一致。批量加载、分页边界、权限和事务范围是不同约定。",
- "revisit": [
- {
- "lessonId": "05",
- "label": "商品评价关系与 N+1 查询"
- },
- {
- "lessonId": "08",
- "label": "Keyset 分页与数据库错误映射"
- }
- ]
- },
- {
- "id": "q4",
- "title": "迁移版本号没前进,表一定没变吗?",
- "prompt": "自动生成的迁移执行中途失败,版本号没有推进;同学准备直接再跑一次,并把同一个迁移放进普通回滚测试中。请说明先查什么,为什么本课从独立空库建立基线,以及新增 version 时为何先扩展、回填再收紧。",
- "checkpoints": [
- "模型是目标结构,revision 是演进步骤,autogenerate 只给待审查差异;先核对实际表结构和已执行动作,版本号未变不保证结构完全未变。",
- "MySQL 结构操作有隐式提交边界,单条原子 DDL 不代表整个多语句迁移能被 ROLLBACK 撤销;普通 DML 保存点测试也不应承担迁移回滚验证。",
- "原 shop_lab 可能已被数据库模块加入 version、索引和约束,所以本课使用独立 shop_lab_migration 建立基线,保留原库,不对已有状态盲目 stamp 或重复加列。",
- "先做与旧代码兼容的扩展,再按主键有界回填并核对剩余量,最后收紧约束与模型;每阶段都审核迁移、查看实际结构并验证旧新代码可用,不宣称所有 DDL 无锁。"
- ],
- "misconception": "只看迁移版本表判断是否可以无脑重跑,或认为外层事务能兜住所有 DDL。结构可能部分改变,重复执行和错误测试隔离都会掩盖真实风险。",
- "revisit": [
- {
- "lessonId": "07",
- "label": "Alembic迁移、DDL边界与兼容发布"
- },
- {
- "lessonId": "12",
- "label": "真实MySQL隔离测试与故障验收"
- }
- ]
- },
- {
- "id": "q5",
- "title": "旧版本冲突与响应丢失重试,为什么需要两种机制?",
- "prompt": "两位同学都读到陶瓷杯 version=1;另一位创建新商品时没收到响应,决定重新发请求。请分别说明应该携带什么信息、数据库如何裁决,以及哪些返回和持久结果能证明机制生效。",
- "checkpoints": [
- "并发编辑把读取到的旧版本作为条件,与字段更新和版本递增放在同一条原子 UPDATE;第一次成功后旧版本请求得到冲突,不通过先 SELECT 再无条件写入实现。",
- "所有修改和删除路径都遵守已启用的版本协议,旧入口或不递增版本的脚本会破坏保护;本课最终删除同样要求当前版本。",
- "一次逻辑创建重试保持同一个 Idempotency-Key,由可信操作作用域和唯一键约束串行化争用,保存状态与响应供重放;网络错误本身无法判断是否已提交。",
- "同键同输入应返回首次结果与相同 id,同键不同输入应冲突;新键意味着新的逻辑创建。幂等表、商品与结果保存在同一事务,不能只在内存记一份键。"
- ],
- "misconception": "把 version 当创建去重键,或丢响应后每次换新幂等键。版本解决旧编辑覆盖,幂等键解决同次创建重复;混用会漏掉各自需要保护的窗口。",
- "revisit": [
- {
- "lessonId": "09",
- "label": "并发更新:版本条件与行锁"
- },
- {
- "lessonId": "10",
- "label": "幂等键与唯一约束"
- }
- ]
- },
- {
- "id": "q6",
- "title": "通知已经被接收,为什么还可能再发一次?",
- "prompt": "商品与待发事件同事务写入后,发布器发送成功,但本地确认没记上。请推演下一轮会发生什么,说明领取租约、确认 token、接收方 event_id 分别保护哪一段,以及最终应查哪些状态。",
- "checkpoints": [
- "商品与 outbox 同一事务保存,避免商品提交后通知记录漏写;发布器在短事务领取并提交后再发 HTTP,不在持锁事务中等待外部服务。",
- "发送成功而本地确认丢失时,接收方可能已完成,待发事件仍可在租约到期后重领并重复发送,因此本课是至少一次交付而非自动恰好一次。",
- "领取 token 防止旧执行者改写不属于自己的本地确认状态,但不能阻止旧执行者继续发网络请求;接收方使用 event_id 与自身事务去重。",
- "核对商品与事件只产生一次、接收表同 event_id 仍一行、发送确认最终落下,并观察尝试次数与积压。幂等创建原键重放也不应再新增 outbox 事件。"
- ],
- "misconception": "以为有租约和 token 就不会重复发送,或把 HTTP 返回成功当作本地 delivered_at 已写入。网络发送、接收提交和本地确认是不同动作,需要分别核对。",
- "revisit": [
- {
- "lessonId": "10",
- "label": "幂等键与唯一约束"
- },
- {
- "lessonId": "11",
- "label": "事务 Outbox 与跨服务一致性"
- }
- ]
- }
- ],
- "challenges": [
- {
- "id": "c1",
- "title": "给一件商品做双端验收,再复现旧页面冲突",
- "prompt": "在本机已完成迁移的练习环境中,使用新标记创建一件 2590 分旅行收纳袋。独立从 HTTP 与 SQL 两边核对,模拟两个客户端读到同一版本,再完成一次成功修改、一次旧版本冲突和带版本删除。先写出每一步的预期,操作后再对照要点。",
- "checkpoints": [
- "先核对当前配置、库名、迁移状态和 /ready,用响应真实 id 定位数据;HTTP 与 SQL 都能确认名称、整数价格、上架状态以及按 UTC 约定的时间。",
- "两个客户端分别保存同一版本快照;A 修改后读取新值和新版本,B 携旧版本被拒绝,SQL 中仍为 A 的结果且版本只增加一次。",
- "加入一次非法价格或显式 null 的 PATCH,记录输入拒绝及原记录未被改写;说明省略字段与 null 的不同,不借冲突操作覆盖正确数据。",
- "用当前版本删除得到 204 空体,再核对实际记录不存在;保留整个请求顺序、状态与数据库结果,解释本模块 PATCH 和版本删除不能照搬基础 API 的无版本 PUT 流程。"
- ],
- "misconception": "只在接口显示冲突就结束,或用错误版本清理失败后直接手工删数据掩盖结果。需要核对未覆盖的持久状态,并遵守最终写入协议完成后续操作。",
- "revisit": [
- {
- "lessonId": "03",
- "label": "ORM CRUD 与输入输出模型"
- },
- {
- "lessonId": "09",
- "label": "并发更新:版本条件与行锁"
- }
- ]
- },
- {
- "id": "c2",
- "title": "重试与通知去重能在真实提交下成立吗?",
- "prompt": "只在独立测试库安排一次同键并发创建,再演练通知接收成功但本地确认丢失。先说明为什么不能把所有并发任务绑定到同一个回滚 fixture,再写出数据核对和恢复计划。独立完成后记录实际结果;未执行的故障分支不标通过。",
- "checkpoints": [
- "先核对测试 URL、库名和权限,运行已有迁移;并发任务各用自己的 Session 与连接,需要真实提交的部分使用相应独立 fixture,不能用共享外层事务证明跨连接可见性。",
- "同一随机幂等键与同输入并发创建,响应状态和内容相同;按随机标记核对只产生一件商品、一份结果和一条 outbox,不只比较两次 HTTP 响应。",
- "发送到本机去重接收端后按原课跳过一次本地确认,等待租约条件满足再恢复发布;接收表仍只有该 event_id 一行,待发记录最终确认,解释为什么重复发送是允许出现的。",
- "记录故障前、中、后的 HTTP、数据库、连接释放与后续恢复请求;清理仅针对本次随机键相关数据,并区分自动断言、手工记录与未验证项。"
- ],
- "misconception": "在同一连接内看见相同结果,就声称并发唯一性和真实提交已经验证。共享事务、保存点与真实独立连接的可见性不同,恰好避开竞争不能证明故障处理正确。",
- "revisit": [
- {
- "lessonId": "11",
- "label": "事务 Outbox 与跨服务一致性"
- },
- {
- "lessonId": "12",
- "label": "真实MySQL隔离测试与故障验收"
- }
- ]
- }
- ],
- "mastery": [
- {
- "id": "m1",
- "text": "我能独立解释请求、Session、flush、commit 与公开响应之间的边界。",
- "evidence": "不看参考要点画出一次创建及第二次写入失败的流程,标出已执行、未提交、回滚和可返回成功的位置,并用一组实测记录对应。"
- },
- {
- "id": "m2",
- "text": "我能独立对齐表、ORM 和输入输出规则,完成带版本的商品操作。",
- "evidence": "交出真实 id 的 HTTP/SQL 双端结果、UTC 与价格检查,以及一次旧版本冲突和按当前版本删除的完整证据。"
- },
- {
- "id": "m3",
- "text": "我能独立验证查询规模、连接预算和迁移后的真实结构。",
- "evidence": "交出 SQL 次数或分页边界记录、包含多进程及发布重叠的连接预算、迁移状态与实际结构对照,不能只交配置值或生成的迁移文件。"
- },
- {
- "id": "m4",
- "text": "我能独立判断提交未知、重复创建与重复通知,并选择合适的验收隔离。",
- "evidence": "在独立测试库完成一组真实提交故障实验,核对幂等结果、事件去重、连接释放与恢复请求;明确说明保存点测试不能证明的部分。"
- }
- ]
- },
- {
- "kind": "review",
- "id": "04",
- "group": "并发、缓存与容器",
- "title": "异步任务:我真的掌握了吗",
- "sourceModule": "tasks",
- "summary": "围绕商品清单导出,检查自己能否区分受理、执行与完成,并用数据库、日志和文件证据判断重复、等待和恢复。",
- "intro": {
- "scenario": "运营提交一份商品清单,页面已经返回任务编号,但稍后仍查不到文件。先不打开参考要点,沿着你认为必要的证据说明任务可能停在哪一段,再完成下面的判断与变式。",
- "startingPoint": [
- "已完成异步任务模块,能找到最终 tasks_lab 的状态存储、worker、publisher、recovery 和终态验证文件;第 01 课内存实验与第 11 课后的持久实现要分开判断。",
- "每题先写自己的结论、理由与验证办法,再展开要点。需要操作时只用原课本机专用实例和新的 job_id,不清空队列、任务表或其他任务的输出。",
- "将“尚未验证”“借助提示完成”和“能独立完成”分开记录;熟悉答案不能替代自己得到的状态、日志与文件证据。"
- ],
- "outcome": "用导出链路说明和有证据的变式记录,找出执行位置、交付窗口、容量与恢复方面仍需回看的课时。"
- },
- "questions": [
- {
- "id": "q1",
- "title": "拿到 202,究竟确认了什么?",
- "prompt": "同学在第 01 课拿到 202 就显示“文件已完成”;另一位给第 03 课后台函数加上 await,认为这样就能跨服务重启恢复。分别判断两种说法,并说明提交响应、后续查询和重启实验应该怎样核对。",
- "checkpoints": [
- "202 表示受理,响应提供 job_id、当前状态与查询地址;文件完成要通过后续状态和结果确认。第 01 课尚未接执行器,任务保持 queued 是正常现象。",
- "第 03 课后台函数能推进状态,却仍属于应用进程;添加 await 改变等待方式,不会把任务持久保存。",
- "正常关闭可能给任务清理时间,强制退出不保证执行 finally。重启后不能假定内存记录与未完成工作会恢复。",
- "保存真实编号,先查状态、完成后查结果,再在本机重启对应实验进程查询原编号;分别记录终态保护和记录消失,不能把响应发出当作完成证据。"
- ],
- "misconception": "把“HTTP 已返回”“后台任务已登记”和“业务已完成”当成同一件事,会提前承诺结果,也会遗漏进程退出后的工作丢失。",
- "revisit": [
- {
- "lessonId": "01",
- "label": "提交导出任务,返回 202 与可查询状态"
- },
- {
- "lessonId": "03",
- "label": "区分 BackgroundTasks 与 create_task 的生命周期"
- }
- ]
- },
- {
- "id": "q2",
- "title": "限了并发,为什么仍会堆积或卡住?",
- "prompt": "商品读取最多允许两个并发。同学一次创建大量 Task,再给读取加 Semaphore;另一份线程版在编号 7 失败时跳过 task_done。请分别预测排队与退出行为,给出只用 20 个编号即可执行的验证方案。",
- "checkpoints": [
- "Semaphore 只限制进入读取区的并发,不限制已创建的 Task 和等待项;有限队列与固定消费者才能同时约束排队量及执行者数量。",
- "每次取出任务都应结清,包括失败项与结束哨兵;遗漏 task_done 会让队列 join 一直等待未归零的计数。",
- "共享成功/失败计数放在短锁区,耗时工作放在锁外;队列计数清零与所有线程退出还需分别检查,不能靠队列暂时为空判断完成。",
- "记录真实读取峰值不超过 2。线程版消费完全部 20 项时核对成功数加失败数为 20,并确认线程退出;协程 TaskGroup 的失败或超时分支会取消其他任务,应核对消费者取消清理、没有误报完整成功,并把未执行项单独记录,不当作已失败的任务凑总数。"
- ],
- "misconception": "“加并发限制就不会排队”混淆执行与提交;“失败项不用结清”会让协调者永远等不到正确的完成计数。",
- "revisit": [
- {
- "lessonId": "02",
- "label": "用 TaskGroup、队列与超时约束 asyncio"
- },
- {
- "lessonId": "04",
- "label": "用 Lock 与 Queue 管理线程共享状态"
- }
- ]
- },
- {
- "id": "q3",
- "title": "等待超时以后,还剩谁在运行?",
- "prompt": "导出的一段调用同步读取库,另一段只做纯 Python 文字计算。为它们选择执行方式,并解释为什么调用者超时了,仍不能宣布所有后台工作已停止。你会记录什么来判断进程池是否值得使用?",
- "checkpoints": [
- "同步阻塞 I/O 可交给受限线程池,并限制提交数量;纯 Python 计算可用小规模进程池比较,不能因为业务叫“异步任务”就把阻塞工作直接放进事件循环。",
- "Future 等待超时只结束调用者的等待;已运行线程不能靠 cancel 安全强杀。同步库的超时及分段检查 Event 才构成协作停止边界。",
- "进程函数应在可导入模块顶层,传可序列化数据,并保留显式 spawn 与入口保护;进程结果等待超时也不证明 CPU 函数已经结束。",
- "保持输入与计时范围一致,先核对串行与进程结果相同,再记录耗时;小任务可能因启动和传输更慢。退出还需工作函数结束和执行器收尾证据。"
- ],
- "misconception": "“TimeoutError 就是函数被终止”会遗漏仍运行的工作;“进程越多一定越快”忽略任务规模、启动成本和资源限制。",
- "revisit": [
- {
- "lessonId": "05",
- "label": "在线程池中调用阻塞 I/O"
- },
- {
- "lessonId": "06",
- "label": "用进程池处理 CPU 密集导出"
- }
- ]
- },
- {
- "id": "q4",
- "title": "同一导出出现两条消息,是失败吗?",
- "prompt": "同一 job_id 和固定商品快照被两条不同消息送到 worker。第一次已写好文件,却在确认消息前退出。解释再次执行是否符合设计,以及怎样判断重复没有形成冲突的业务结果。",
- "checkpoints": [
- "提交、转交、执行与确认是不同阶段;完成后确认仍存在“副作用已发生但确认未送达”的窗口,因此重投不自动说明系统失效。",
- "第 09 课的相同业务编号与不可变快照应生成相同目标路径和内容;比较两次路径、摘要和完整 JSON,而不是只看消息缓存的 SUCCESS。",
- "临时文件后原子替换避免半文件;同一 job_id 如果携带不同内容,它不能决定哪个版本正确,也不能证明跨机器或断电的一致性。",
- "消息 ID 区分交付,job_id 区分业务导出;相同 task_id 本身不保证去重。说明 broker 配置和恢复前提,不把 late ack 或原子替换称为恰好一次。"
- ],
- "misconception": "把重复交付一概当成业务失败,会绕开恢复机制;把相同消息编号当作去重锁,也无法约束实际函数执行或输出版本。",
- "revisit": [
- {
- "lessonId": "08",
- "label": "启动 Celery broker、worker 与结果缓存"
- },
- {
- "lessonId": "09",
- "label": "理解 ack、重投与幂等输出"
- }
- ]
- },
- {
- "id": "q5",
- "title": "次数还没用完,为什么不能再试?",
- "prompt": "一个任务还有重试额度,但总截止时间已过;另一个收到重复消息,next_attempt_at 却仍在未来。分别判断能否读取或领取,并区分暂时错误、永久错误与实际执行次数。",
- "checkpoints": [
- "尝试必须同时满足次数与总时间条件;过了 deadline 应在真实读取前结束,不能因还有次数就重新获得一整份时间预算。",
- "只对已知且有机会恢复的暂时错误设置有限重试,永久输入错误直接失败;退避与抖动分散压力,不能替代单次 I/O 超时。",
- "持久领取需检查状态、取消、截止时间、次数和 next_attempt_at;未来时间未到,重复消息也不能领取,更不能提前增加 attempts。",
- "消息重新交付次数不等于实际业务执行次数。用过期预算、永久错误及未到领取时间的样本分别记录真实调用和领取是否发生,而非只数日志行。"
- ],
- "misconception": "“剩余次数必须用完”让过期工作继续耗资源;只延迟某条消息而不检查持久时间,会被另一条重复消息提前触发。",
- "revisit": [
- {
- "lessonId": "10",
- "label": "为重试设置退避、抖动与总预算"
- },
- {
- "lessonId": "11",
- "label": "用 MySQL 保存任务状态与租约"
- }
- ]
- },
- {
- "id": "q6",
- "title": "任务已落库,消息却没到怎么办?",
- "prompt": "导出记录已提交时 broker 不可用,后来又发生发送成功但 sent_at 没写回。请画出不用代码的状态顺序,说明 outbox 如何补遗漏,以及为何 worker 仍需领取与完成条件。",
- "checkpoints": [
- "任务和待发事件在同一 MySQL 事务内保存,任一步失败一起回滚;提交端在持久保存后才表示受理,不把发送网络消息放进这个事务。",
- "publisher 短事务领取后先提交,再发消息,最后按当前 token 与有效租约确认。broker 不可用时保留未发送事件,等待有预算的恢复。",
- "发送成功却没确认会重复发送;outbox 保存发送意图,并不保证恰好一次。worker 仍按状态、token、租约、deadline 和取消条件决定是否执行及写回。",
- "派发确认也不等于 worker 已领取。长期 queued 的恢复需要检查时间和已有 pending 事件,避免重复堆通知;重新派发不能直接算一次实际执行。"
- ],
- "misconception": "“有 outbox 就能删掉幂等和恢复检查”混淆保存发送意图、重复交付与业务结果写回三种不同问题。",
- "revisit": [
- {
- "lessonId": "12",
- "label": "用事务 outbox 连接状态提交与消息发布"
- },
- {
- "lessonId": "15",
- "label": "实现优雅退出、取消与失败隔离恢复"
- }
- ]
- }
- ],
- "challenges": [
- {
- "id": "c1",
- "title": "短清单排队与周期重复同时出现",
- "prompt": "有一份两件商品的短清单和一份较长清单,又开启了每分钟导出。先独立设计两组小实验:判断队列拆分与容量是否有效;区分同分钟重复与跨分钟重投。只用本机专用任务,写出能够支持和推翻判断的证据。",
- "checkpoints": [
- "将 queue_name 随 outbox 保存,确认 fast/bulk 真有对应且受限的消费者;查看实际路由和等待,不能只改队列名字就宣称隔离了执行容量。",
- "先统计已有 queued/running,所有提交路径使用相同容量门闩和额度;满额拒绝新增并给可重试的 503,不能返回不存在任务的 202。",
- "生成新的实验时间槽,同槽重复只创建一个 job 及事件;另一个明确执行分钟可产生新槽,当前模型因此不保证原始信号的恰好一次。",
- "保存受理结果、队列、任务编号、槽记录和等待年龄,恢复消费者后核对结果;调度器停机的分钟不会自动补跑,不把未实现能力写成结论。"
- ],
- "misconception": "“拆队列等于自动隔离资源”和“同槽唯一等于任何重投都只执行一次”分别忽略消费者容量和时间槽采用的时间语义。",
- "revisit": [
- {
- "lessonId": "13",
- "label": "限制受理量并拆分长短任务队列"
- },
- {
- "lessonId": "14",
- "label": "用持久时间槽合并周期执行,并验证跨分钟重投边界"
- }
- ]
- },
- {
- "id": "c2",
- "title": "一个旧执行者晚回来了",
- "prompt": "先写计划,再在原课专用环境验证:A 持有租约后失效,扫描恢复并等待退避后由 B 接管,A 又晚到写回。追加取消或次数耗尽的一条分支。要保留什么,才有依据记为“能独立完成”?",
- "checkpoints": [
- "只操作明确的新 job_id,记录 A 的 token、attempts 和期限;过期后由扫描转换,确认持久退避,到期才让 B 领取,重复消息不能提前增加次数。",
- "旧 token 完成应失败,B 生成独立真实文件后才能写入有效 result_ref;数据库引用的文件必须等于固定快照,不能仅看 Celery 状态。",
- "取消分支不能发布成功;次数或期限耗尽需保留失败隔离记录,不清零旧任务伪造恢复。修复重放使用带 replay_of 的新任务。",
- "保存阶段日志、业务与消息编号、状态变化、实际恢复耗时和文件验证;未做分支标未验证,超预算要说明等待来自租约、扫描、退避还是队列。"
- ],
- "misconception": "“A 曾拿到锁就仍有资格完成”忽略当前权属;“看懂恢复流程”也不能替代旧令牌被拒绝和结果文件一致的实际观察。",
- "revisit": [
- {
- "lessonId": "15",
- "label": "实现优雅退出、取消与失败隔离恢复"
- },
- {
- "lessonId": "16",
- "label": "完成故障注入与可观测性验收"
- }
- ]
- }
- ],
- "mastery": [
- {
- "id": "m1",
- "text": "我能独立解释受理、消息交付、工作执行与业务完成为何不是同一件事。",
- "evidence": "不看答案画出一次导出的状态顺序,标出两个失败窗口,并说明每阶段该查哪份记录。"
- },
- {
- "id": "m2",
- "text": "我能独立选择并约束协程、线程或进程,并完成一项小导出。",
- "evidence": "自行准备输入和执行、排队上限,保存正常结果、一条失败分支和实际退出证据。"
- },
- {
- "id": "m3",
- "text": "我能独立证明重复交付没有改变本次有效结果。",
- "evidence": "用新 job_id 得到重复消息记录、唯一有效 result_ref 和与固定快照一致的文件,并解释消息数与 attempts 的区别。"
- },
- {
- "id": "m4",
- "text": "我能独立判断任务在等待、失败、失去租约或已终止,并选择对应恢复动作。",
- "evidence": "为一次故障留下状态、当前 token、期限、恢复耗时和最终结果记录,同时明确尚未实测的分支。"
- }
- ]
- },
- {
- "kind": "review",
- "id": "05",
- "group": "并发、缓存与容器",
- "title": "Redis 缓存:我真的掌握了吗",
- "sourceModule": "redis",
- "summary": "从同一商品的两次读取出发,检查自己能否解释命中、旧值、期限与故障,并守住 MySQL 的读取容量。",
- "intro": {
- "scenario": "商品在 MySQL 中已经改名,页面偶尔仍显示旧名。同学建议把 TTL 设得很长,并在 Redis 故障时让所有请求直查数据库。先独立判断这些做法改变了什么,再完成下面的自测。",
- "startingPoint": [
- "已完成 Redis 模块,能找到原课读取、失效、单飞、降级与探针文件,知道本机商品样本及各专用 Redis 实例的用途。",
- "每题先写时序、判断和取证办法,再展开要点。操作时复用受限配置与真实 PRODUCT_ID,只改本次键或原课明确的专用容器。",
- "正常、故障和未执行项目分别记录;自评帮助定位薄弱点,不因看完答案或勾选条目自动证明掌握。"
- ],
- "outcome": "用来源、版本、期限、回源次数和恢复证据,判断自己能否独立维护一条可解释且容量受控的商品缓存读路径。"
- },
- "questions": [
- {
- "id": "q1",
- "title": "第二次更快,是否证明缓存正确?",
- "prompt": "连续读同一商品,第一次来源 database,第二次 cache。有人只覆盖缓存值,TTL 随后为 -1;另一人把缓存 schema 当成商品 version 一起递增。还需验证什么?这两个修改分别有什么问题?",
- "checkpoints": [
- "命中只说明找到键,还要核对真实商品编号、两次内容、整数分价格和 UTC 时间等约定字段;坏 JSON 或不支持的结构应回源修复。",
- "schema 表示缓存编码结构,商品 version 表示业务数据版本;两者不能替代,商品改名不自动意味着编码格式改变。",
- "TTL 为 -1 表示存在但无期限,-2 才是键不存在。普通 SET 覆盖可能清掉原期限,不能假定第一次设置的期限会一直保留。",
- "只对真实样本的明确键做实验,记录两次来源、内容对比与剩余期限;再注入坏结构,确认来源回到 database 并写入合法副本。"
- ],
- "misconception": "“读得快就是读得对”忽略内容与期限;把 schema 当业务版本,也无法说明这份商品是否比数据库旧。",
- "revisit": [
- {
- "lessonId": "01",
- "label": "连接 Redis、选择类型并设置 TTL"
- },
- {
- "lessonId": "02",
- "label": "实现 Cache-Aside 与带版本的 JSON 缓存"
- }
- ]
- },
- {
- "id": "q2",
- "title": "提交后删缓存,旧值为什么还能回来?",
- "prompt": "A 已从 MySQL 拿到旧商品却尚未回填。B 提交新名称并删缓存,随后 A 才填入旧值。请按顺序写出缓存和数据库版本,再说明提交后失效解决了什么、没有解决什么。",
- "checkpoints": [
- "A 回填后缓存可命中旧版本,而 MySQL 已为更高版本;课程通过手工安排顺序就能稳定复现,不必依赖碰巧的线程调度。",
- "先提交再失效可避免新值尚未提交时读者回源旧值,但不能阻止已经拿到旧快照的请求晚到回填。",
- "修改先用版本条件和受影响行数核对,再返回已提交的真实结果;提交应答未知不能盲目更换条件重试。",
- "失效失败需保留可恢复工作,可按课程范围以持久事件补偿,并明确详情及派生键范围。需要强一致读时显式绕过缓存,不能由一次 DEL 或短 TTL 承诺强一致。"
- ],
- "misconception": "“提交后删过一次,之后绝不会读旧值”漏掉在途旧读取;吞掉失效失败还会让更新副本的工作无法追踪或恢复。",
- "revisit": [
- {
- "lessonId": "03",
- "label": "提交后失效缓存,并复现旧值回填竞态"
- }
- ]
- },
- {
- "id": "q3",
- "title": "“没有商品”和“数据库没连上”能一样缓存吗?",
- "prompt": "连续查询合法但不存在的商品编号时,你想减少回源。一次请求却遇到 MySQL 连接错误,稍后同编号商品又可能被创建。分别设计返回含义、短期缓存与写入失效,并说明非法编号应在哪里被拒绝。",
- "checkpoints": [
- "只有数据库成功查询且确定无行,才能缓存明确 found=False 的缺失结论;连接失败不代表不存在,不能存成同一种空结果。",
- "正负结果需可区分的编码,负缓存用课程中更短的期限,并与普通详情缓存使用清楚的命名空间,避免错误解码。",
- "创建、更新和删除提交后都要考虑详情及 lookup 键;漏掉新建编号的负缓存,会让后来真实商品继续被旧缺失结论挡住。",
- "编号先做严格类型及范围检查,拒绝 0、负数、字符串、True 和超出 BIGINT 正范围的值;这些输入不应先访问 Redis 或 MySQL。"
- ],
- "misconception": "“没拿到商品就缓存空值”混淆无记录与依赖故障,既隐藏故障,又可能让真实商品被误判不存在。",
- "revisit": [
- {
- "lessonId": "04",
- "label": "阻止缓存穿透:输入校验与短期负缓存"
- }
- ]
- },
- {
- "id": "q4",
- "title": "锁过期了,原填充者还能删锁吗?",
- "prompt": "A 取得热门商品填充权后变慢,锁过期,B 成为新持有者。A 恢复后准备写缓存并删锁。请说明应该如何判断这两个动作,再设计不靠随机调度的验证。",
- "checkpoints": [
- "单飞合并同一时段的重复填充;取得权利后还要再读缓存,避免别人已填好。没有取得权利的读者只作有限等待。",
- "过期后可能出现新持有者,A 必须比较自己的 token 与当前值;旧 token 不得删除 B 的锁,也不得在失去权属时发布缓存。",
- "比较与删除或发布应在短 Lua 脚本中连续执行,防止检查后到修改前所有权改变;普通先 GET 再 DEL 不能保证这个组合。",
- "用专用键先写 old-owner 再写 new-owner,验证旧 token 释放返回 0 且新锁仍在;另以固定小并发和计数 loader 核对真实回源次数,超时保留为失败证据。"
- ],
- "misconception": "“我曾拿到锁,所以最后必须由我释放”把过去权属当成当前权属;直接 DEL 可能删掉后来者的锁,造成更多并发回源。",
- "revisit": [
- {
- "lessonId": "05",
- "label": "抑制热点击穿:带令牌的单飞回填"
- }
- ]
- },
- {
- "id": "q5",
- "title": "Redis 停了,全部直查 MySQL 就能保住接口吗?",
- "prompt": "先遇到一批商品缓存同时到期,随后模拟整个 Redis 停止。分别说明 TTL 抖动、少量预热和有界回源能解决哪部分,并写出数据库回源名额耗尽时应该出现什么结果。",
- "checkpoints": [
- "TTL 抖动错开一批键的正常过期,减少集中回源;它不能阻止整个 Redis 不可用,也不能证明返回内容一定足够新。",
- "预热只处理有限热点或课程中的最近少量商品,限制数量和节奏,释放取编号的数据库事务后再逐件填充,不制造无界全库压力。",
- "Redis GET/SET 失败可绕过,但回源仍需本进程并发名额与短等待;多进程总预算要相加,不能只看一个 Semaphore 数字。",
- "拿不到名额应出现 SourceBusy 等受控容量失败,由接口明确响应;MySQL 本身错误仍传播。恢复 Redis 后验证来源逐步由 database 转为 cache。"
- ],
- "misconception": "“降级就是取消限制、全查真源”可能把缓存故障扩大为数据库故障;高命中率也不能证明故障期间有足够容量。",
- "revisit": [
- {
- "lessonId": "06",
- "label": "应对缓存雪崩:TTL 抖动、受控预热与降级"
- },
- {
- "lessonId": "09",
- "label": "配置连接池、超时与熔断并记录缓存指标"
- }
- ]
- },
- {
- "id": "q6",
- "title": "少发几次网络请求,等于中间不能被修改吗?",
- "prompt": "商品查询限流需要增加计数,并在首次计数时设期限。同学建议不带事务的 Pipeline,还说网络中断后无限重试即可。比较 Pipeline、WATCH 与本课 Lua 的职责,并给出四次请求的预期观察。",
- "checkpoints": [
- "Pipeline 减少往返且按命令顺序返回,但 transaction=False 不让整批变成原子操作;网络错误不能证明所有命令都没执行。",
- "WATCH 检查键是否在提交前被其他操作改动;冲突后重新读取再计算,并限制尝试次数,不能拿旧计算结果继续提交。",
- "本课短 Lua 先检查参数,再组合 INCR 与首次 PEXPIRE,避免计数成功却漏设期限;脚本不做长循环或外部请求。",
- "固定窗口中前三次允许、第四次拒绝且期限仍为正,到期后允许;这不是精确滑动窗口。超时重试可能重复计数,需承认保守限流边界,不能无限重试来猜结果。"
- ],
- "misconception": "“合并发送就是原子”和“客户端异常就是服务端没执行”混淆网络效率与执行语义,可能留下永久计数器或重复计数。",
- "revisit": [
- {
- "lessonId": "07",
- "label": "使用 Pipeline、WATCH 与 Lua 保证正确的原子操作"
- }
- ]
- }
- ],
- "challenges": [
- {
- "id": "c1",
- "title": "容量和故障延迟一起变差",
- "prompt": "先独立写观测计划,再用原课专用实例验证:容量实例发生淘汰;商品缓存调用连续失败后被短暂绕过。报告必须区分淘汰、过期、正常未命中、连接错误和 bypass,并解释为何不能把 broker 搬进缓存实例的另一个 DB 来省资源。",
- "checkpoints": [
- "只在容量实例有限写入,记录前后 evicted_keys 增量、used_memory、RSS 与 maxmemory,再读取明确样本键计算本批命中率;不把累计值直接当作本次结果。",
- "键可在 TTL 未到时因容量压力被淘汰;自然到期与淘汰分别记录。maxmemory 不等于 Redis 进程或容器全部内存占用。",
- "先以固定故障函数验证三次真实错误后第四次 bypass,冷却探测后恢复;记录实际调用次数、状态和延迟。正常返回 None 是 miss,认证错误也不能用无限探测掩盖。",
- "broker 与可重建缓存需要独立容量和可靠性安排,DB 编号不隔离内存、CPU 或故障;记录写入拒绝如何暴露,尚未执行的项目明确标出。"
- ],
- "misconception": "把所有 None 都计作未命中,会隐藏绕过与错误;另选一个 DB 也保护不了消息免受同实例的容量或故障影响。",
- "revisit": [
- {
- "lessonId": "08",
- "label": "控制缓存容量与淘汰,隔离任务 Broker"
- },
- {
- "lessonId": "09",
- "label": "配置连接池、超时与熔断并记录缓存指标"
- }
- ]
- },
- {
- "id": "c2",
- "title": "重启后读得到,就证明不会丢了吗?",
- "prompt": "先写下三组实验的判断,再只用原课专用容器验证:无持久化缓存重启、独立 AOF 实例正常重启、主节点写入后副本读取。说明每组能证明什么,以及仍不能作出什么承诺。",
- "checkpoints": [
- "无持久化缓存的键可能消失,MySQL 商品应仍完整,首次回到 database、随后 cache;证明的是副本可重建,不是 Redis 保存了真源。",
- "AOF 样本仅首次写入,重启后只读值和剩余 TTL;再次写会掩盖恢复失败。正常重启成功不等于已经验证断电或所有磁盘故障。",
- "主节点写入后在同一连接按课程方式 WAIT,观察副本确认再读取;确认复制不等于磁盘持久化或强一致承诺。",
- "停止专用主节点,核对副本链路 down 且不会自动升主,再恢复并检查。保留值、期限、角色、确认数及时间;不能用缓存实验代替任务 broker 的持久状态与交付恢复验收。"
- ],
- "misconception": "“正常重启保住样本,所以任何故障都不丢且副本自动接管”混淆了持久化、复制与故障切换。",
- "revisit": [
- {
- "lessonId": "10",
- "label": "演练重启、复制与持久化恢复"
- }
- ]
- }
- ],
- "mastery": [
- {
- "id": "m1",
- "text": "我能独立解释商品缓存从缺失、回源、命中到失效的过程。",
- "evidence": "不看答案画出正常读取与旧值晚回填两条时序,并说明 schema、商品 version 和 TTL 各表达什么事实。"
- },
- {
- "id": "m2",
- "text": "我能独立建立有格式验证、期限和明确缺失含义的读取路径。",
- "evidence": "在新终端恢复配置,用真实样本展示 database/cache 对照、坏格式修复与负缓存读取,留下实际输出。"
- },
- {
- "id": "m3",
- "text": "我能独立验证减少回源的机制,而不只是觉得请求变快。",
- "evidence": "交出有限并发下真实 loader 次数、旧 token 释放失败、回源上限及超出等待预算的可见结果。"
- },
- {
- "id": "m4",
- "text": "我能独立区分缓存容量、连接故障、主动绕过和恢复边界。",
- "evidence": "记录命中率、淘汰增量、状态与延迟,并用重启或副本实验说明哪些数据来源及恢复能力已被验证。"
- }
- ]
- },
- {
- "kind": "review",
- "id": "06",
- "group": "并发、缓存与容器",
- "title": "Docker 容器化:我真的掌握了吗",
- "sourceModule": "docker",
- "summary": "检查自己是否能从进程、网络、存储和发布证据出发,独立把商品服务与导出任务放入容器,并判断什么才算可恢复。",
- "intro": {
- "scenario": "商品 API 镜像已构建成功,容器显示 running,但商品读取失败,重建后文件还可能消失。先不看要点,用自己的顺序区分镜像内容、进程、连接与持久数据,再完成发布变式。",
- "startingPoint": [
- "已完成 Docker 模块,知道本机 Compose 项目、镜像、秘密文件与命名卷的位置;操作前先核对 Docker context 为专用练习环境。",
- "先独立回答,再对照要点。实际演练只用原课有界样本和独立恢复容器,不删除业务卷,不把生产 context 当成实验环境。",
- "记录真实镜像标识、状态、日志与恢复结果;未推送、未注入或未恢复的项目写未验证,不用“容器能运行”代替掌握证据。"
- ],
- "outcome": "形成能解释启动失败、验证数据保留并说明兼容回退边界的本机交付记录,据证据确定需要复习的部分。"
- },
- "questions": [
- {
- "id": "q1",
- "title": "同一镜像,重启与重建有什么不同?",
- "prompt": "主进程执行完后容器停止,同学想在外面加无限等待来“常驻服务”。另一位把导出文件写进容器,认为以后重新 run 同一镜像仍能找到。分别判断,并设计两个最小观察。",
- "checkpoints": [
- "容器运行的是主进程,主进程退出时停止是正常生命周期;应用已不服务时加无限等待不会让 API 重新提供服务。",
- "stop 后 start 复用原容器和可写层;删除后重新 run 创建另一容器,同一镜像不包含前一个容器运行时写入的临时文件。",
- "需要保留的数据放入明确持久存储,命名卷与容器生命周期不同;卷仍不是备份,不能由容器重建成功推断所有故障可恢复。",
- "记录一次性进程退出与日志,再用专用标记比较原容器 stop/start 和新容器同一路径;不要把旧卷挂回后的数据误认为可写层被保留。"
- ],
- "misconception": "“容器对象存在就代表应用在服务”和“镜像一样就包含上次写入”分别忽略主进程状态和运行时可写层。",
- "revisit": [
- {
- "lessonId": "01",
- "label": "镜像、容器与进程生命周期"
- },
- {
- "lessonId": "05",
- "label": "MySQL 持久卷与可恢复备份"
- }
- ]
- },
- {
- "id": "q2",
- "title": "依赖层命中缓存,能直接发布吗?",
- "prompt": "只改一行商品 API 源码后,依赖层显示 CACHED。同学说依赖一定最新,基础镜像标签相同也代表两次产物完全一样。你仍需记录和验证哪些输入?为什么还要控制复制范围?",
- "checkpoints": [
- "先复制依赖锁并安装,再复制源码,可在只改源码时复用依赖层;缓存加快构建,不主动更新依赖,也不保证输入是最新的。",
- "标签可能移动,真实基础 digest、源码版本、目标平台和完整依赖锁用于固定及追溯输入;这些稳定也不能单靠 Dockerfile 声称字节级完全一致。",
- "多阶段的 builder 与 runtime 保持基础和架构一致,复制必要环境与应用;运行时实际核对依赖导入、迁移文件和非 root 用户。",
- "明确构建上下文和 COPY 范围,由忽略文件排除本机秘密、备份和虚拟环境;后来删除误复制秘密不能抹去旧镜像层。"
- ],
- "misconception": "“CACHED 意味依赖已更新且安全”把复用当审查;“同标签就是同内容”让发布记录无法对应实际产物。",
- "revisit": [
- {
- "lessonId": "02",
- "label": "为 FastAPI 编写非 Root Dockerfile"
- },
- {
- "lessonId": "03",
- "label": "分层缓存、多阶段构建与可重复输入"
- }
- ]
- },
- {
- "id": "q3",
- "title": "宿主能连数据库,为什么 API 容器不能?",
- "prompt": "宿主在 127.0.0.1:3307 能查商品,API 容器使用相同地址却失败。有人提议公开更多数据库端口,并用 EXPOSE 代替 Compose 端口配置。请按请求所在位置解释地址、端口和验证顺序。",
- "checkpoints": [
- "API 容器中的 127.0.0.1 指 API 自己;本课 Compose 内访问数据库使用 mysql:3306,Redis 使用 redis:6379。",
- "宿主映射 3307 供宿主访问,发布规则左右分别属于宿主和容器;同一 Compose 网络内互通不依赖发布数据库宿主端口。",
- "EXPOSE 是镜像元信息,不自动开放宿主入口。为修复容器内地址而扩大暴露端口并非必要步骤,也不能修正错误的目标位置。",
- "从 API 的网络位置依次检查 DNS、目标 TCP 和 app_user 认证查询;宿主成功、容器 running 或 TCP 通畅均不能替代真实数据库查询。"
- ],
- "misconception": "“localhost 永远是我的电脑”忽略网络位置;增加公开端口不能修复 API 把自己当成数据库的问题。",
- "revisit": [
- {
- "lessonId": "04",
- "label": "Compose 网络、服务 DNS 与端口"
- }
- ]
- },
- {
- "id": "q4",
- "title": "卷还在、备份命令成功,够了吗?",
- "prompt": "重建 MySQL 容器后专用标记仍可查询,备份也生成了非空文件。两份证据各能证明什么?请设计一个不覆盖原 shop_lab 的恢复检查。",
- "checkpoints": [
- "标记保留说明这次正确复用了持久卷;它不能抵御所有磁盘故障、误删或数据写坏,所以卷不能替代备份。",
- "备份退出成功、文件非空只说明生成了转储;仍需检查导入成功、样本与表引擎符合预期,并验证恢复后查询可用。",
- "按原课建立独立恢复容器、卷和恢复库,导入后核对 volume_probe;业务表迁移后生成的新备份还要补业务验证,不能覆盖原库和原卷。",
- "普通复制运行中目录可能没有一致性,同宿主副本也不能覆盖宿主故障;记录恢复范围与实测耗时,不用每日转储自动承诺分钟级数据损失目标。"
- ],
- "misconception": "“持久卷等于备份”和“生成文件等于恢复可用”分别漏掉独立副本和恢复验证,可能在故障时才发现唯一真源被覆盖或转储不可用。",
- "revisit": [
- {
- "lessonId": "05",
- "label": "MySQL 持久卷与可恢复备份"
- }
- ]
- },
- {
- "id": "q5",
- "title": "容器 running 后,还要等哪几道门?",
- "prompt": "初次启动时 MySQL 已 running,但业务表没建立;运行一段时间后数据库又短暂停止。分别说明启动与运行阶段该观察什么,并判断每个 API 副本启动时都执行迁移是否合适。",
- "checkpoints": [
- "数据库探针应完成认证查询;独立迁移作业再确认结构与 revision,成功退出后才让 API 启动并进行自己的就绪检查,running 不能替代这些条件。",
- "/live 说明应用进程可响应,/ready 验证自己的数据库访问;故障时前者正常而后者失败并不矛盾。",
- "条件依赖只控制启动前置,不能保证数据库此后持续可用;运行时仍需处理失效连接与有预算的恢复,恢复后核对既有商品及新操作。",
- "迁移由一个独立作业执行,失败保留日志并阻止发布;每个 worker 各自迁移会竞争锁与版本状态,不能忽略错误后继续接流量。"
- ],
- "misconception": "“depends_on 成功一次就永远健康”误把启动顺序当持续保证;“每个副本自己改表最方便”又引入并发迁移冲突。",
- "revisit": [
- {
- "lessonId": "06",
- "label": "健康检查、启动重试与一次性迁移"
- }
- ]
- },
- {
- "id": "q6",
- "title": "换成非 root,就保护好秘密了吗?",
- "prompt": "镜像使用 UID 10001,却把密码复制进去再在下一层删除;另一份部署挂载秘密文件,却没让应用读取。为什么都可能失败?你会怎样验证收紧权限后业务仍工作?",
- "checkpoints": [
- "非 root 降低运行权限,不会擦除构建时带入的秘密;后层删除不能抹掉旧层,需按课程思路轮换已泄露凭据并重建无该层的镜像。",
- "运行时秘密文件提供受控位置,应用还需明确支持读取;环境变量名称带 FILE 不代表所有程序自动加载对应文件。",
- "本地 Compose 秘密来自宿主文件,宿主权限、备份和 Docker 管理权限仍是边界;不向应用挂 Docker socket,也不通过打印连接串证明秘密可读。",
- "用实际运行用户核对秘密可读、/app 写入被拒绝、受限 /tmp 可写,再验证 /ready 和商品查询;扫描用于定位并更新输入,不能等同安全已经证明。"
- ],
- "misconception": "“非 root 是完整隔离”和“文件挂上应用自然会读取”省略不同层面的配置与验证;只看到写入报错也不能说明业务仍正常。",
- "revisit": [
- {
- "lessonId": "02",
- "label": "为 FastAPI 编写非 Root Dockerfile"
- },
- {
- "lessonId": "08",
- "label": "配置、Secrets 与容器运行权限"
- }
- ]
- }
- ],
- "challenges": [
- {
- "id": "c1",
- "title": "把完整导出链路放进 Compose",
- "prompt": "从原课最终任务工程出发,先画 API、fast/bulk worker、publisher、recovery 与存储的职责和连接,再独立做本机验证:暂停目标 worker 后提交专用导出,恢复它并核对结果。不要把早期快照演示当成最终任务实现。",
- "checkpoints": [
- "采用最终只接收 job_id 的任务与持久状态表,核对注册名称、参数、目录和依赖;API 与同步任务的 URL 驱动入口不同,但都指向约定数据库。",
- "fast/bulk 真正各有对应消费者;publisher/recovery 的单步函数要按本课包装成有间隔、错误退避并响应停止信号的循环,不能把执行一次当成持续维护。",
- "缓存与认证 broker 分实例;输出放在相关 worker 和验证端可访问的明确共享位置。逐角色设置秘密、并发与资源,数据库连接按进程总数预算。",
- "暂停时确认新任务 queued,恢复后按同一 job_id 核对终态、有效 result_ref 与真实快照文件;同时保留实际资源上限和正常退出日志,不只截图容器 running。"
- ],
- "misconception": "“所有后台角色塞进 API 容器最省事”混淆生命周期与资源;“worker SUCCESS”也不能替代持久记录和真实文件的联合验收。",
- "revisit": [
- {
- "lessonId": "09",
- "label": "API、Worker 与队列的 Compose 整合"
- },
- {
- "lessonId": "07",
- "label": "CPU、内存、进程数与优雅退出"
- }
- ]
- },
- {
- "id": "c2",
- "title": "新镜像有问题,怎样证明能退回?",
- "prompt": "准备一次本机小版本发布,只改商品服务表现,不加新业务。先写发布记录与停止条件,再按原课隔离环境验证;若决定回退旧镜像,必须先确认哪些兼容性,最后交出什么证据?",
- "checkpoints": [
- "记录真实旧新镜像标识、源码与基础输入、当前/目标 revision、备份恢复证据及回退窗口;标签不能替代内容标识,未推送不能编造仓库 digest。",
- "唯一迁移作业先成功,再更新应用角色;失败就停止后续切换,查实际结构与日志,不能用重启掩盖迁移失败。",
- "先验证旧镜像可在当前数据库结构和语义下读写,才把回退列为选项;删除、重命名或语义变化可能关闭回退窗口,应考虑课程讨论的前向修复。",
- "回退后检查 /ready、既有商品与一次新导出,保留原卷完整的证据及实际中断时间;不能清空数据库让旧版启动,也不由单机单副本更新承诺零停机。"
- ],
- "misconception": "“标签换回就一定恢复”和“删卷或机械 downgrade 就能撤销发布”忽略数据库与已提交数据的独立演进,可能把应用问题扩大为数据丢失。",
- "revisit": [
- {
- "lessonId": "10",
- "label": "镜像版本、发布回滚与故障验收"
- },
- {
- "lessonId": "03",
- "label": "分层缓存、多阶段构建与可重复输入"
- }
- ]
- }
- ],
- "mastery": [
- {
- "id": "m1",
- "text": "我能独立说明容器化请求经过的进程、网络和存储边界。",
- "evidence": "不看答案画出宿主、API、MySQL、Redis 连接位置,预测主进程退出、stop/start 与重建对临时文件和卷的不同影响。"
- },
- {
- "id": "m2",
- "text": "我能独立构建并运行包含应用与迁移文件的受限镜像。",
- "evidence": "交出真实构建输入和镜像标识、实际运行用户、依赖及文件检查,以及不打印内容的秘密读取验证。"
- },
- {
- "id": "m3",
- "text": "我能独立判断启动成功与业务就绪,并验证商品和导出的完整链路。",
- "evidence": "提供数据库健康、迁移退出、readiness、按 job_id 核对的终态与文件;失败时能指出未过哪一道门。"
- },
- {
- "id": "m4",
- "text": "我能独立验证恢复或兼容回退,而不只写操作计划。",
- "evidence": "在独立恢复库查到原标记,或在新结构上运行已验证旧镜像完成业务探针;保留原卷,并记录实测恢复结果和未验证边界。"
- }
- ]
- },
- {
- "kind": "review",
- "id": "07",
- "group": "部署、排障与设计",
- "title": "Linux 部署:我能独立发布和恢复商品服务吗",
- "summary": "从登录目标、运行身份到入口、发布和备份,用能够复核的操作结果检查部署是否真正掌握。",
- "sourceModule": "linux",
- "intro": {
- "scenario": "陶瓷杯和帆布袋已经能在开发电脑中查询、导出。现在由你把同一服务交给练习服务器,并为下一次更新或故障留下可用的恢复办法。",
- "startingPoint": [
- "已完成 Linux 部署模块,保留独立练习 VM、可用控制台、shop-service 发布目录,以及一次正常商品读写和导出的记录。没有该环境时先完成书面作答,将实操项保留为未验证。",
- "先独立写出每题判断、检查次序和需要的证据,再展开参考要点;有遗漏时回看指定课时,之后合上答案重新作答。",
- "实操仅限已核对的隔离 VM 和新建测试数据;沿用课程的管理入口、回退和秘密保护边界。未配置真实域名或通知渠道的能力如实注明,不能凭自评视为已验收。"
- ],
- "outcome": "完成一份能区分已解释、已实操和未验证内容的部署自查记录,指出还需重做的具体环节。"
- },
- "questions": [
- {
- "id": "q1",
- "title": "旧 SSH 会话还连着,能收紧登录了吗",
- "prompt": "你准备禁用密码登录。旧管理会话仍能执行命令,新建的 deploy 账号也已经放入公钥。列出现在还必须完成的验证,并解释每一项防止什么问题。",
- "checkpoints": [
- "先核对主机名与主机公钥指纹,确认修改的是目标练习 VM;客户端用于登录的私钥与服务器用于证明身份的主机密钥不是同一把钥匙。",
- "保留原会话和控制台,在另一终端真正建立 deploy 密钥登录并验证管理权限;已建立的旧连接不能证明新的认证配置可用。",
- "核对 authorized_keys 所有者与权限,检查 SSH 配置语法和实际生效值;新路径通过后才收紧并重载,不能用关闭主机指纹校验掩盖身份变化。"
- ],
- "misconception": "误以为“旧会话可用,所以新登录一定没问题”。旧连接可能在认证规则改变前就已建立,退出后才会暴露无法重新登录。",
- "revisit": [
- {
- "lessonId": "01",
- "label": "SSH、账户与服务目录"
- },
- {
- "lessonId": "02",
- "label": "监听地址、DNS 与防火墙"
- }
- ]
- },
- {
- "id": "q2",
- "title": "本机能查商品,浏览器却打不开",
- "prompt": "VM 内直连 API 的商品查询成功,浏览器访问站点失败。应用只监听回环地址。请画出浏览器到数据库的路径,并按顺序给出需要核对的地址、端口和观察结果。",
- "checkpoints": [
- "先区分域名解析、到 VM 的网络路径、Nginx 入口与 API 上游;核对实际 A/AAAA 地址、站点选择及监听端口,不能把所有失败都归到应用代码。",
- "保留 API 回环监听,通过本机带正确 Host 的站点请求验证 Nginx 到 API;若此处成功再检查主机防火墙和云侧入口,若返回网关错误则查看该站点上游日志。",
- "宿主进程沿练习配置访问 127.0.0.1:3307,Compose 网络内才使用 mysql:3306;数据库和任务 broker 不因需要网页入口就对公网开放。",
- "先核对实际 SSH 端口,再调整限定规则并保留回退。HTTPS 还需验证真实域名证书,忽略证书校验的请求不能证明 TLS 正常。"
- ],
- "misconception": "误以为把 API、MySQL 和 Redis 全部绑定公网即可解决访问。这样混淆了各层职责,也没有定位 DNS、代理或证书的实际故障。",
- "revisit": [
- {
- "lessonId": "02",
- "label": "监听地址、DNS 与防火墙"
- },
- {
- "lessonId": "05",
- "label": "Nginx HTTP 入口与 TLS"
- }
- ]
- },
- {
- "id": "q3",
- "title": "管理员能运行,为什么服务账号失败",
- "prompt": "管理员手动导入商品代码成功,shop-api 却无法启动;另一台机器复制来的虚拟环境也在发布目录中。说明应从身份、路径、解释器和配置四方面留下什么检查证据。",
- "checkpoints": [
- "使用真实 shop 运行身份核对解释器、依赖导入及目录每一级的进入权限;管理员的成功结果不能替代低权限进程的结果。",
- "使用目标 Linux/Python 对应的环境和已验证依赖锁,不把开发电脑的虚拟环境直接当作可迁移发布物;检查 current 实际指向的版本。",
- "发布源码由管理员维护、运行用户只读,导出结果通过共享 artifacts 目录写入;不要为解决一次权限错误把整个项目开放写权限。",
- "核对 API 与任务各自的环境文件、读取权限和驱动 URL;记录配置来源而不打印密码。主机进程使用实际主机/端口,不沿用仅容器网络能解析的服务名。"
- ],
- "misconception": "误以为管理员运行成功已证明部署正确。systemd 使用的身份、工作目录、解释器和配置可能都与手动终端不同。",
- "revisit": [
- {
- "lessonId": "03",
- "label": "Python 环境、配置与最小权限"
- },
- {
- "lessonId": "04",
- "label": "systemd 管理 API、Worker 与维护进程"
- }
- ]
- },
- {
- "id": "q4",
- "title": "所有单元 active,就能通知用户已恢复吗",
- "prompt": "API、fast/bulk Worker 和维护进程都显示 active,但新提交的商品清单还没有文件。请列出从“进程在运行”到“业务恢复”的验证链,并指出迁移和定时器应放在哪里。",
- "checkpoints": [
- "先检查 API 真实就绪与商品读写,再按 job_id 核对任务、待发事件、执行状态和结果文件;active 仅描述进程状态,不能证明依赖可达或消息有人处理。",
- "派发器负责把已保存的意图交给队列,Worker 负责执行,恢复器处理符合条件的遗漏;它们是不同入口,应能分别定位配置和日志。",
- "迁移使用独立一次性作业,成功并核对实际结构后才推进发布,不能由每个 API 启动时抢着执行。只在已完成周期任务时单独启用一份 beat。",
- "验收应包括一条新任务完成及原未完成任务的去向;读到 result_ref 后还要确认文件存在且对应固定商品快照。"
- ],
- "misconception": "误以为 active 或一条受理响应代表导出成功。受理、派发、执行、发布文件是不同阶段,前一阶段的成功不能代替后一阶段。",
- "revisit": [
- {
- "lessonId": "04",
- "label": "systemd 管理 API、Worker 与维护进程"
- },
- {
- "lessonId": "10",
- "label": "完整发布、回退与故障 Runbook"
- }
- ]
- },
- {
- "id": "q5",
- "title": "直连逐条出现,代理后为什么挤成一批",
- "prompt": "商品导出进度直连 API 时逐条出现,经过 Nginx 后攒成一批;WebSocket 客户端又连不上。你如何分别验证这两种问题,什么结果才说明修复完成?",
- "checkpoints": [
- "先确认实际 /stream 和 /ws 路由已整合,分别建立直连基线;保持请求和客户端读取方式相同,再比较代理后的事件到达情况。",
- "SSE 检查对应 location 的缓冲设置、空闲读取预算与实际心跳;WebSocket 检查 HTTP/1.1 及 Upgrade、Connection 转发,HTTPS 页面使用 wss。",
- "Nginx 配置检查通过后再加载;分别保留逐条到达记录、101 握手和双向消息结果,不能只凭页面能打开判断连接正常。",
- "再验证断线与主动停止。未实现事件编号和续传时,应承认重新连上不能补齐中断期间漏掉的事件。"
- ],
- "misconception": "误以为统一加大超时就能修复所有长连接。缓冲、协议升级、心跳与客户端取消是不同环节,需要各自的证据。",
- "revisit": [
- {
- "lessonId": "06",
- "label": "SSE、WebSocket 与代理超时"
- }
- ]
- },
- {
- "id": "q6",
- "title": "备份文件很大,就代表能恢复吗",
- "prompt": "备份目录里有一份加密 SQL 文件,商品任务表也曾导出成功。如果原 VM 丢失,你还需要哪些内容和验证,才能说明商品查询与清单下载可以恢复?",
- "checkpoints": [
- "确认转储成功退出、使用兼容工具且快照条件满足;校验密文与解密后文件完整性,在独立空恢复库实际导入,不能只看文件大小。",
- "恢复端必须能取得独立保存的解密私钥和受控数据库凭据;备份 VM 仅持有加密公钥的设计不等于丢失私钥后仍可恢复。",
- "核对商品数据、迁移版本、任务状态,以及 result_ref 指向的清单文件;恢复应用使用隔离队列或禁用真实外发,避免旧任务盲目重放。",
- "记录备份恢复点、可能缺失的数据窗口与实际恢复耗时,再判断是否满足约定目标;没有 binlog 等额外恢复证据时,不承诺任意时刻恢复。"
- ],
- "misconception": "误以为数据库备份成功就能还原整项服务。文件、密钥、配置和任务恢复语义缺一项,都可能出现记录还在但无法下载或重复执行。",
- "revisit": [
- {
- "lessonId": "09",
- "label": "数据库备份、异机加密与恢复目标"
- }
- ]
- }
- ],
- "challenges": [
- {
- "id": "c1",
- "title": "独立完成 r002 发布并证明能回退",
- "prompt": "在隔离 VM 为商品服务准备一个可区分的 r002 小修改。先独立写出发布、停止条件和回退次序,再利用已学工具执行。保留 r001;不引入新业务,不删除数据库数据。需要交出哪些结果,才能证明切换与回退都真实有效?",
- "checkpoints": [
- "准备完整 r002、目标平台环境和共享结果路径,核对 shop 身份可读可执行;记录当前版本、revision、备份证据及新旧代码兼容范围。",
- "迁移作业先成功,核对实际结构后才切换;同文件系统原子替换 current 后仍需重启各应用角色,因为软链变化不会自动替换运行中的代码。",
- "用同一件新建测试商品和一条真实导出验收新版本,记录状态码、版本条件、job_id 与可读文件;单副本重启的中断应如实记录。",
- "仅在兼容范围内切回 r001 并重启相关角色,重复业务验收并确认已提交数据保留;若迁移破坏兼容则停止机械回退,说明为何需要其他修复路径。"
- ],
- "misconception": "误以为 current 已指回旧目录就证明回滚完成。旧进程可能尚未替换,而当前数据库结构也可能不再支持旧代码。",
- "revisit": [
- {
- "lessonId": "07",
- "label": "发布目录、原子切换与兼容回退"
- },
- {
- "lessonId": "10",
- "label": "完整发布、回退与故障 Runbook"
- }
- ]
- },
- {
- "id": "c2",
- "title": "让一次派发停顿留下完整恢复证据",
- "prompt": "先为隔离环境写一个你能解释的“最老待发事件等待过久”阈值,再暂停本次 publisher 并提交一条新商品导出,随后恢复。先独立列出要收集的时间点与记录,完成后据实填写;本题不要求接通真实通知渠道。",
- "checkpoints": [
- "注入前证明基线任务可完成,记录具体实例、新 job_id、预期影响及恢复动作;只暂停已核对的 publisher,保持数据库和 Worker 的原状态。",
- "把事件创建时间、最老待发年龄、publisher 单元状态和相关时间段日志对应起来,区分“已保存任务”与“已送入队列”。",
- "恢复 publisher 后记录派发、任务终态与文件验收时间,确认不是仅把积压转移到另一个阶段;日志导出不包含环境文件内容或秘密。",
- "说明告警窗口和日志保留预算,通知未接入时标为手工演练;根据实际发现与恢复用时提出一个有验收条件的改进,而不填写预计成功。"
- ],
- "misconception": "误以为写了一条告警规则就已经具备自动监控。阈值、观测来源、处理动作与真实通知渠道分别需要验证。",
- "revisit": [
- {
- "lessonId": "08",
- "label": "journald、日志轮转与可执行告警"
- },
- {
- "lessonId": "10",
- "label": "完整发布、回退与故障 Runbook"
- }
- ]
- }
- ],
- "mastery": [
- {
- "id": "m1",
- "text": "我能独立解释一条商品请求经过的身份、网络和数据边界。",
- "evidence": "不看参考要点画出客户端、Nginx、API、数据库和任务角色,标出实际监听地址、运行身份、配置来源与可写目录,并解释一处连接失败应先查哪层。"
- },
- {
- "id": "m2",
- "text": "我能独立准备并启动一个可验证的新发布版本。",
- "evidence": "交出一次隔离发布记录:目标路径和解释器、配置权限、迁移结果、运行角色及商品与导出探针;没有照抄他人的成功输出,未执行项单独标明。"
- },
- {
- "id": "m3",
- "text": "我能独立证明业务切换或回退后的结果符合约定。",
- "evidence": "提供同一测试商品的读写记录、新任务编号、结果文件核对和版本切换前后时间;说明旧代码能否在当前表结构上运行,不以 active 代替业务验收。"
- },
- {
- "id": "m4",
- "text": "我能独立限定故障影响并验证恢复范围。",
- "evidence": "交出一项受控故障的实际时间线及恢复结果,并提供一份独立恢复库的数据和文件核对记录;能够区分已测恢复时间、可能损失窗口与尚未验证能力。"
- }
- ]
- },
- {
- "kind": "review",
- "id": "08",
- "group": "部署、排障与设计",
- "title": "故障排查:我能用证据找到问题并确认恢复吗",
- "summary": "面对慢请求、缺失结果和连接异常,检查自己能否先定位阶段,再限定动作并验证真实恢复。",
- "sourceModule": "troubleshooting",
- "intro": {
- "scenario": "商品页面有时变慢,导出记录仍显示处理中。现在没有现成的故障名称,需要你把用户现象变成可检验的判断,并把修复与恢复证据分清。",
- "startingPoint": [
- "已完成故障排查模块,准备已知正常的隔离 shop-service、副本对应的进程或容器信息,以及一组新建商品和导出测试数据。",
- "先写判断、证据和下一步,再展开参考要点。题目中的现象是待分析条件,不是你已经采集的实测结果;未执行的检查保持未验证。",
- "实操沿用课程的请求数、持续时间和资源预算,每次只注入一个故障。SSE/WebSocket 变式需先确认已整合相应路由,否则先标出缺少的前提。"
- ],
- "outcome": "形成可反驳假设、可追踪动作和可复核恢复的自查记录,找到最需要重新练习的诊断阶段。"
- },
- "questions": [
- {
- "id": "q1",
- "title": "只有导出投诉时,先查什么",
- "prompt": "收到“商品清单一直没有下载结果”的反馈,同时有人说商品页面还能打开。你只有进程列表和一条超时报错。请写出最先建立的影响表,以及在动服务前必须补足的证据。",
- "checkpoints": [
- "分别验证存活、依赖就绪、商品读取、持久任务受理和最终文件,不用一条超时推断全站不可用;没有实现的 HTTP 导出入口不能当作既有能力测试。",
- "记录实际实例、UTC 时间、最后正常时间和具体 job_id,分别标明观察事实与候选原因;进程存在不能证明数据库或文件服务可用。",
- "候选动作应有对应假设、预期改变和恢复前值,先只读检查再做限定动作;恢复判定同时追踪新导出完成及旧任务去向。"
- ],
- "misconception": "误以为先重启所有组件总能最快找原因。多个状态同时改变会破坏定位线索,进程恢复后旧任务也可能仍然遗漏。",
- "revisit": [
- {
- "lessonId": "01",
- "label": "先确认影响范围,再选择止损动作"
- }
- ]
- },
- {
- "id": "q2",
- "title": "请求号、图表和追踪各能证明什么",
- "prompt": "用户提供一次失败请求的编号,你又看到“五分钟错误率升高”的图表。一名同学建议给每个 job_id 建一个指标标签,并只保留 trace。请说明你怎样关联证据,以及这个建议会丢掉或增加什么。",
- "checkpoints": [
- "用 request_id 定位本次请求,再经受理记录关联 job_id;message_id、attempt 或租约版本帮助区分投递与执行。trace 展示阶段关系和耗时,不能替代持久业务状态。",
- "指标使用有限的路由模板、方法和状态类别;具体请求或任务编号放日志/追踪,逐编号标签会使时间序列数量随业务增长。",
- "错误比例需要明确有效样本、分母和窗口,无请求时不能报告为零错误率;耗时分位数按课程的直方图方式聚合,不平均各实例 P95。",
- "追踪可能因采样而缺失,仍应保留业务编号的日志定位链;导出或分享记录前检查敏感字段,不把查询串、完整请求或秘密直接附上。"
- ],
- "misconception": "误以为采集越多越完整,或 trace 能取代所有记录。无限编号标签会拖累监控,采样链路也不能保证每个长期任务都能找回。",
- "revisit": [
- {
- "lessonId": "03",
- "label": "定义 SLI、SLO 与低基数指标"
- },
- {
- "lessonId": "04",
- "label": "把 trace 贯穿 HTTP、数据库与 Celery"
- }
- ]
- },
- {
- "id": "q3",
- "title": "CPU 不高,为什么商品请求一起卡住",
- "prompt": "隔离 API 的 CPU 很低,但两次慢请求会串行完成,同一时刻的健康请求也延迟。另一轮实验 CPU 明显升高。你会对比哪些数据,如何一次只改变一个因素来验证原因?",
- "checkpoints": [
- "先核对负责本次请求的真实进程,再记录同一有限负载下的 CPU、事件循环延迟、HTTP 耗时及线程等待;不能拿另一个进程的低 CPU 证明应用没有问题。",
- "异步路由内同步 sleep 可占住事件循环却不忙于计算;计算循环可能同时拉高 CPU 与调度延迟。两种现象相似,但证据组合不同。",
- "分别恢复可让出的等待,或按课程使用受限线程处理同步 I/O、受限进程处理 CPU 工作;比较相同输入,不同时扩 worker 和改代码,结束后关闭实验入口。"
- ],
- "misconception": "误以为函数写了 async 就不会阻塞,或者 CPU 低就只能是网络。异步函数里直接调用同步阻塞操作仍会让其他请求等不到执行机会。",
- "revisit": [
- {
- "lessonId": "05",
- "label": "区分 CPU 忙、事件循环阻塞与线程池等待"
- }
- ]
- },
- {
- "id": "q4",
- "title": "退出码 137 足够判定内存泄漏吗",
- "prompt": "容器一次退出码为 137;另一轮里 Python 分配快照已经下降,进程 RSS 仍较高。请分别写出现在能下的结论、不能下的结论和下一项观测。",
- "checkpoints": [
- "137 可能来自内存超限终止,也可能是其他 SIGKILL;结合 OOMKilled、cgroup 的 oom_kill 增量、限制值及退出时间判断,不凭单个退出码归因。",
- "tracemalloc、RSS 和容器内存统计覆盖范围不同;释放 Python 对象后底层分配器可能暂留内存,快照下降与 RSS 未立刻下降并不矛盾。",
- "对相同有限请求批次保留多轮、同口径数据,观察同一分配位置是否持续增长;若 Python 跟踪稳定而 RSS 增长,再看原生库、缓冲或子进程,不先宣告 Python 对象泄漏。"
- ],
- "misconception": "误以为 RSS 不回落就一定泄漏,或宿主有空闲内存就不会 OOM。分配口径与容器限制分别影响这两个判断。",
- "revisit": [
- {
- "lessonId": "06",
- "label": "区分 Python 分配、RSS 与容器 OOM"
- }
- ]
- },
- {
- "id": "q5",
- "title": "“查询超时”可能发生在哪个阶段",
- "prompt": "三次商品相关请求分别出现:借连接等待超时、连接到未监听端口失败、数据库锁等待超时。请为每种情况选择最直接的证据与恢复检查,说明为什么不能统一扩大超时。",
- "checkpoints": [
- "池等待先看连接上限、占用与获取连接的耗时;连接失败在实际调用的网络环境核对 DNS、地址和监听。一个请求可能尚未接触数据库就耗尽预算。",
- "数据库锁等待需要连接 ID、活跃事务及谁阻塞谁的关系;诊断账号看不到记录可能只是权限不足,不能把空结果当作没有阻塞。",
- "MySQL 锁等待超时通常只结束失败语句,隔离两会话实验应明确回滚各自事务;再核对等待消失和商品请求恢复。Sleep 连接也可能持有长事务。",
- "增加超时可能把错误地址或锁占用转化为更久排队;先定位等待发生处,再在同等负载下检验修复。"
- ],
- "misconception": "误以为所有 timeout 都是同一种“数据库太慢”。借连接、建立连接、执行及锁等待有不同证据和处理边界。",
- "revisit": [
- {
- "lessonId": "07",
- "label": "分开排查 DNS、TCP、TLS 与连接池等待"
- },
- {
- "lessonId": "08",
- "label": "定位慢 SQL、锁等待与连接池耗尽"
- }
- ]
- },
- {
- "id": "q6",
- "title": "Worker 在线,旧任务为什么仍不完成",
- "prompt": "Worker 的检查命令可以响应,但一个商品导出 job 很久仍是 queued。任务消息也出现过重复。请写出排查分支,并说明什么证据才允许你执行恢复。",
- "checkpoints": [
- "先并排查持久 job、未发送 outbox、实际队列路由、Worker 注册和活动队列;queued 只能说明当前业务状态,不能证明消息已经到 broker。",
- "沿同一 job_id 区分事件 ID、消息到达、成功领取与 attempts,核对租约、next_attempt_at 和 deadline;重复消息不应自动等于又执行了一次。",
- "确认下游和容量恢复后再用有限恢复与派发流程;不能清空队列、重置 attempts 或无预算扩大并发来绕过失败保护。",
- "恢复后覆盖相关截止时间和扫描周期观察最老等待、完成速率、重试与下游错误,核对新文件和旧任务去向,而不只看队列长度短暂下降。"
- ],
- "misconception": "误以为消费者能回应就代表整条任务链正常,或清队列后“积压为零”就是恢复。待发事件可能尚未入队,清除消息还可能丢掉工作。",
- "revisit": [
- {
- "lessonId": "10",
- "label": "定位 Celery 积压、重试风暴与租约恢复"
- }
- ]
- }
- ],
- "challenges": [
- {
- "id": "c1",
- "title": "给缓存故障做一次可复查的诊断",
- "prompt": "先独立写好限定影响与恢复步骤,再在隔离环境只暂停缓存 6380,保持任务 broker 和数据库正常。用同一组小规模商品读取比较故障前后,恢复缓存后再次比较。你需要交出怎样的记录,才能支持“回源仍受控”的判断?",
- "checkpoints": [
- "明确容器/实例、时间窗口、请求数量、恢复方式与停止条件;先采集正常基线,只暂停缓存,不能用任务 broker 的凭据或故障动作替代。",
- "比较同长度窗口的命中、未命中、淘汰与数据库请求增量,以及池等待和 HTTP 状态;累计计数应作差,不能把两次总数直接当每秒速率。",
- "确认缓存错误走有并发和时间上限的回源路径,记录真实数据库压力;超预算时明确失败,不编造商品或无限等待,也不把本进程上限当作全局上限。",
- "恢复后证明正常命中和商品读取回来,broker 未受影响;按实际证据写出哪个假设成立、哪个被排除,以及未采集项。"
- ],
- "misconception": "误以为故障期间所有请求仍返回 200 就证明降级成功。数据库可能承受了不可持续的额外压力,需要容量与等待的证据。",
- "revisit": [
- {
- "lessonId": "09",
- "label": "排查 Redis 热点、淘汰与缓存降级"
- },
- {
- "lessonId": "12",
- "label": "完成故障演练、恢复门槛与 runbook"
- }
- ]
- },
- {
- "id": "c2",
- "title": "定位进度显示延后,并验证真的停止",
- "prompt": "在已整合 SSE 和 WebSocket 的隔离副本中,选择一条真实代理路径。先独立设计比较方法:同一进度请求在直连、代理和浏览器中分别何时可见;用户主动停止后哪些资源应消失。使用已有探针完成核对,不新增业务。",
- "checkpoints": [
- "保持测试输入与读取方式一致,记录首条业务 data 的到达时间;响应头可能先到,因此不能用首字节时间替代首条业务内容时间。",
- "直连正常而代理攒批时检查对应缓冲与路径;二者正常而页面延后时检查客户端读取和渲染。只按证据修改本次隔离 location,先验证配置再加载。",
- "WebSocket 单独核对 101、双向消息、关闭码与实际重连;它的升级问题不能直接用 SSE 的缓冲设置解释。",
- "主动停止后等待超过约定的最大重连间隔,确认无新连接并有服务端清理证据;恢复临时配置,最后再完成一次完整事件流与 echo。"
- ],
- "misconception": "误以为停止页面显示或断开一条连接就等于任务停止。浏览器可能自动重连,服务端也可能还在等待或继续占用资源。",
- "revisit": [
- {
- "lessonId": "11",
- "label": "端到端排查 SSE 缓冲与 WebSocket 断开"
- },
- {
- "lessonId": "12",
- "label": "完成故障演练、恢复门槛与 runbook"
- }
- ]
- }
- ],
- "mastery": [
- {
- "id": "m1",
- "text": "我能独立把用户现象拆成可检验的阶段假设。",
- "evidence": "不看答案,为一条商品请求或导出画出影响矩阵,分别写出事实、候选原因和能推翻原因的下一项检查,不先给出未经证实的根因。"
- },
- {
- "id": "m2",
- "text": "我能独立采集足以定位问题且不会误用的证据。",
- "evidence": "提供一次限定故障中已核对的进程/容器、时间窗口、请求或任务编号以及相关指标、日志或状态表记录;标明单位、累计或增量、合成或实测和权限限制。"
- },
- {
- "id": "m3",
- "text": "我能独立用受控操作检验诊断并保留恢复路径。",
- "evidence": "完成一次只改变一个因素的实验,记录原配置、请求与资源预算、停止条件及恢复动作;修复前后使用同样输入,出现意外时停止注入。"
- },
- {
- "id": "m4",
- "text": "我能独立证明恢复涉及业务结果而不只是进程状态。",
- "evidence": "交出商品读写、新导出文件及旧任务去向的恢复证据,覆盖必要观察窗口,并给出一个能通过再次实验验收的改进项;未验证部分保持明确。"
- }
- ]
- },
- {
- "kind": "review",
- "id": "09",
- "group": "部署、排障与设计",
- "title": "分布式设计:我能解释取舍并推演失败吗",
- "summary": "用商品查询与清单导出的短场景,检验状态归属、一致性、重复、容量和恢复的因果判断;全程可以纸面完成。",
- "sourceModule": "distributed",
- "intro": {
- "scenario": "商品目录要由两个 API 副本提供,导出交给独立执行者。增加组件以后,请求会延迟、重复或遇到部分故障;需要你说明每种选择的承诺和代价。",
- "startingPoint": [
- "已完成分布式理论模块,准备纸笔或普通绘图工具;沿用商品名称、描述、整数价格、上架状态和清单导出,不增加支付、库存等业务。",
- "先独立画状态位置、时间线或列计算式,再展开参考要点。每项结论标清题设与假设,不能把纸面预计结果当作实机验证。",
- "本单元只做理论推演,不要求新代码、额外基础设施或真实故障注入;自查标准用于定位理解缺口,不是客观考试通过证明。"
- ],
- "outcome": "给出能说明状态归属、失败窗口、资源预算和恢复边界的简洁设计,并用反例检验自己的承诺。"
- },
- "questions": [
- {
- "id": "q1",
- "title": "多开一个 API,哪些问题真的解决了",
- "prompt": "API-A 在内存字典记录导出进度,下一次查询落到 API-B。两台各能稳定处理 60 次轻请求/秒,平时总流量为 90 次/秒。分别推演查询和 A 故障后的容量,再说明是否必须继续拆成很多微服务。",
- "checkpoints": [
- "B 没有 A 的内存记录,可能错误地回答任务不存在;可跨副本查询的任务状态需要共享的可靠记录,“API 无状态”不代表整个系统没有状态。",
- "一台故障后只剩 60 次/秒能力,对 90 次/秒需求有 30 次/秒缺口;负载均衡和健康检查不会自动创造容量,切换也有发现与在途失败窗口。",
- "把耗时导出放到独立 Worker 可以单独分配资源,商品模块仍可保留在一个应用中;继续拆分应有独立发布、扩容等具体原因,并承担新网络边界成本。"
- ],
- "misconception": "误以为多开副本会自动共享内存、提供足够故障余量,或必须把每个模块做成微服务。副本、状态保存和服务边界是不同选择。",
- "revisit": [
- {
- "lessonId": "01",
- "label": "从模块化单体到微服务与事件驱动"
- },
- {
- "lessonId": "02",
- "label": "高可用与负载均衡:多开一台为什么还不够"
- }
- ]
- },
- {
- "id": "q2",
- "title": "两个区域都能接请求,为什么仍不能随意确认修改",
- "prompt": "两区都保存商品 42 的 version=7,区间网络中断但用户仍可分别访问。东区把名称改成“白杯”,西区改成“透明杯”。比较“只有可联系权威边界的一侧确认”与“两侧都确认”的结果,并说明列表读取可否采用不同约定。",
- "checkpoints": [
- "只允许满足协调条件的一侧确认,可以保护同一旧版本不会在两区被各自成功修改;无法满足条件的一侧需要拒绝或等待,部分请求不能正常完成。",
- "两侧都按本地 version=7 确认后会产生冲突;恢复通信仍需取舍、合并或人工确认,不能回头宣称当时有唯一的跨区域修改顺序。",
- "列表展示可以在明确时效约定下读较近副本,保存修改则使用保护版本条件的权威边界;副本落后时读到旧值本身不能证明写入已丢。",
- "网络分区不是把数据库记录分片;即使正常通信,复制仍会有延迟和成本,CAP 也不是平时随意勾选两个功能的菜单。"
- ],
- "misconception": "误以为两个区域都能被用户访问就不存在分区,或者稍后最终一致能保证此前每次修改都符合单一顺序。缺失的是区域间协调信息。",
- "revisit": [
- {
- "lessonId": "03",
- "label": "复制与分片:多份副本和分成几份有什么不同"
- },
- {
- "lessonId": "04",
- "label": "CAP 与一致性:断网时,谁还能回答"
- }
- ]
- },
- {
- "id": "q3",
- "title": "改价并删缓存,为何旧价仍能回来",
- "prompt": "A 读到商品旧价格 1990、version=7 后暂停;B 写入 2490、version=8 并删除缓存;A 随后回填。写出最终可能看到的库与缓存,再说明延迟再删一次是否已经形成绝对保证。",
- "checkpoints": [
- "数据库可以是 2490/v8,缓存却被 A 回填成 1990/v7;按次序更新数据库再删除缓存仍存在这个并发交错窗口。",
- "再删除一次可能缩小部分窗口,但把 A 的暂停拉长仍可能在第二次删除后回填;固定延迟不能证明所有慢请求已经结束。",
- "应说明该商品展示能容忍多旧的数据,并按课程选择版本条件、可靠失效及受控回源等组合;不能把容忍旧读与必须保护版本的修改混成同一承诺。",
- "另行区分不存在编号、单一热点过期和大批键过期的压力来源,分别选择校验或负缓存、合并回源、打散到期及容量保护,不用双删回答所有缓存问题。"
- ],
- "misconception": "误以为写后删除或延迟双删能无条件保证缓存永不旧。它们的效果受并发时序影响,必须明确可验证的保障范围。",
- "revisit": [
- {
- "lessonId": "05",
- "label": "缓存的四类难题:旧值、穿透、击穿与雪崩"
- }
- ]
- },
- {
- "id": "q4",
- "title": "提交超时之后,究竟该不该再建一份任务",
- "prompt": "商品导出任务和待发送记录已在数据库同次提交,但响应丢失。用户立即重试,发布器也可能重发同一事件;次日用户又主动导出同样商品。请分别判断这三种重复,并指出 outbox 没有替你保证什么。",
- "checkpoints": [
- "第一次提交与响应丢失后的重试属于同一意图,应沿稳定意图编号找回同一任务;次日主动发起的新需求通常使用新编号,不能只按正文哈希吞掉它。",
- "任务与待发送记录同事务保存,使受理事实和派发意图一起保留;发布器发送成功但来不及确认时仍可能重发,因此接收和执行需要自己的业务保护。",
- "两个 API 同时受理同一意图时,先查不存在再写不够,应由权威存储的唯一约束与事务决定创建结果;去重记录保留时间需覆盖接口承诺。",
- "outbox 已发送不等于 Worker 完成,更不保证外部文件写只发生一次;远端结果未知时优先按稳定对象标识或幂等语义核实,不能把超时一律当失败重做。"
- ],
- "misconception": "误以为 outbox 就是恰好执行一次,或内容相同都该去重。持久派发意图、重复交付与用户的新意图属于不同边界。",
- "revisit": [
- {
- "lessonId": "07",
- "label": "跨系统事务:本地事务、Outbox 与 Saga"
- },
- {
- "lessonId": "08",
- "label": "幂等与未知结果:超时以后要不要再来一次"
- }
- ]
- },
- {
- "id": "q5",
- "title": "旧执行者恢复时,新序号能挡住哪些写入",
- "prompt": "A 的租约到期后 B 接管,A 随后从暂停中恢复。A 序号 41,B 序号 42。分别推演资源已接受 42 和资源尚未见过 42 时 A 的写入;再说明释放锁与发布终态各需什么保护。",
- "checkpoints": [
- "租约到期不意味着 A 的进程停止,所以 A、B 可能同时运行;A 释放资格时须原子核对所有者,不能删掉 B 的锁。",
- "资源已经接受并记录 42 后,应拒绝 41;检查序号、更新最大值与写数据需在同一可靠原子边界内,避免检查后被插队。",
- "若资源只拒绝小于已见最大值而尚未见 42,仍可能接受 41;这种 fencing 不自动保证租约一过期就拒绝所有旧写,承诺更强时需额外的权威有效版本验证。",
- "终态可用状态和版本条件限制发布,计算阶段允许重复但控制成本;已发出的远端副作用仍需自己的核实或幂等保护,不能靠旧进程主动停止倒追回去。"
- ],
- "misconception": "误以为锁过期会杀掉旧进程,或者 fencing 能无条件阻止一切过期写。必须说明资源已经见过什么序号,以及真正写入在哪个边界验证。",
- "revisit": [
- {
- "lessonId": "09",
- "label": "分布式锁与租约:过期后旧 Worker 为什么还在写"
- }
- ]
- },
- {
- "id": "q6",
- "title": "队列加长后,用户需要再等多久",
- "prompt": "导出每秒到达 7 个、完成 5 个,持续 30 秒;高峰后仍每秒到达 3 个。先估算积压和清空时间,再比较两层调用各最多尝试 3 次时的最坏下游请求数,说明应怎样限制新增压力。",
- "checkpoints": [
- "高峰净积压为每秒 2 个,30 秒新增约 60 个;假设耗时稳定、无其他瓶颈,高峰后净消化为每秒 2 个,约需 30 秒清空。",
- "不能用积压直接除以总处理能力,因为新任务仍在到达;还应看最老任务年龄、耗时波动和文件字节吞吐,计算值只是带假设的估计。",
- "两层各最多 3 次尝试,最坏下游可被调用 9 次;重试会消耗剩余时间与资源,应选择合适层主导,并包含连接、等待和退避的总预算。",
- "队列只吸收短时突发,超过用户可接受等待时应限制受理或准确降级;给导出独立资源预算可以保留商品查询,恢复后逐步放量。"
- ],
- "misconception": "误以为队列越长越可靠,或每一层自行重试都只增加一点压力。长期供给不足不会因排队消失,多层尝试次数还会相乘。",
- "revisit": [
- {
- "lessonId": "10",
- "label": "超时、重试、熔断与隔离:阻止局部故障扩散"
- },
- {
- "lessonId": "11",
- "label": "限流、背压与容量:队列只能借时间"
- }
- ]
- }
- ],
- "challenges": [
- {
- "id": "c1",
- "title": "为商品清单画一条完整的失败可恢复路径",
- "prompt": "只做纸面设计:用户选择少量商品提交导出,关闭页面后还能回来查询并下载,也能请求取消。先独立画最小方案,给定你自己的教学用数量上限和完成时限,再在消息确认、Worker 执行和文件返回三个位置插入失败。不要新增订单或支付系统。",
- "checkpoints": [
- "图中明确商品与固定快照、任务状态、派发意图、队列、Worker 和结果文件的归属;提交时保存可继续恢复的事实,文件就绪后才发布可下载引用。",
- "分别推演早确认后未执行、完成后确认丢失、文件结果未知等窗口;每个恢复箭头都应有持久记录或可查询证据,不能只标“重试”。",
- "重复投递使用同一业务身份并保护终态,取消必须阻止不符合状态条件的成功发布;同商品逐条动作若要求顺序,应明确局部顺序范围,不以最新版本过滤代替所有动作。",
- "写出输入、并发或受理上限与完成目标,记录最老等待和完成耗时等观测;说明资源不足时用户看到什么,以及哪些承诺需实机验证才能确认。"
- ],
- "misconception": "误以为把所有组件画全就完成设计。没有状态归属、失败后的证据和恢复条件,图上的重试箭头无法说明工作会怎样继续。",
- "revisit": [
- {
- "lessonId": "06",
- "label": "消息队列:可靠交付、重复与顺序"
- },
- {
- "lessonId": "12",
- "label": "可观测性、容灾与一道完整的系统设计题"
- }
- ]
- },
- {
- "id": "c2",
- "title": "区域恢复后,哪一项能力应先开放",
- "prompt": "只做纸面变式:恢复副本只包含某个时间点的任务数据,部分任务显示完成但清单文件尚未找回,另一批显示处理中;可用资源暂时有限。请排出恢复与开放服务次序,说明损失窗口、用户提示和不能立即重放的原因。",
- "checkpoints": [
- "先确认权威数据恢复点、当前写入归属及配置密钥可用性,再恢复入口和任务协调执行;落后副本不等于历史备份,也不保证包含所有已确认变化。",
- "核对已完成记录与 result_ref 文件,处理中记录与外部可能已发生的效果;文件缺失不能显示可下载成功,未知效果不能无条件重做。",
- "资源有限可先恢复商品与任务查询,明确暂缓批量导出;异步重放仍需保留业务标识、版本与恢复规则,防止旧执行结果覆盖或产生重复副作用。",
- "从恢复点估计可能丢失的数据窗口,从恢复到可用所需步骤估计时间,并分别对照 RPO/RTO 目标;题面没有实测,不能把预算或预计时间写成已达到的保证。"
- ],
- "misconception": "误以为任务表恢复完成就可立即开放全部服务并重发所有未完成任务。文件、权威写入位置与外部效果还未核实,重复和虚假成功都可能出现。",
- "revisit": [
- {
- "lessonId": "03",
- "label": "复制与分片:多份副本和分成几份有什么不同"
- },
- {
- "lessonId": "12",
- "label": "可观测性、容灾与一道完整的系统设计题"
- }
- ]
- }
- ],
- "mastery": [
- {
- "id": "m1",
- "text": "我能独立解释增加副本、拆分进程和复制数据分别改变什么。",
- "evidence": "不看答案画出同一商品查询与导出的状态归属,使用一个副本故障或读到旧值的反例说明各自解决的问题、剩余限制及新增网络成本。"
- },
- {
- "id": "m2",
- "text": "我能独立用时间线检验一致性和重复处理承诺。",
- "evidence": "纸面列出缓存旧值回填、响应丢失或旧 Worker 恢复中的至少两种交错,标出每次读写、证据与原子边界,并指出结论依赖的假设。"
- },
- {
- "id": "m3",
- "text": "我能独立计算容量和等待,而不把估计写成保证。",
- "evidence": "用一组不同于课件的到达率、处理率和持续时间,写出净积压、净消化及重试放大计算,注明单位、稳定耗时假设和最大等待超过目标时的处理。"
- },
- {
- "id": "m4",
- "text": "我能独立给出带失败恢复和明确取舍的轻量方案。",
- "evidence": "交出一页商品清单导出设计,含正常路径、两个失败分支、可查询证据、恢复先后与降级提示;能用反例说明不能保证什么,全程无需新代码或真实故障注入。"
- }
- ]
- }
- ]
|