这篇实习日志是1月9日的。写这些字的时候是晚上十点半,我刚从公司出来,坐在回住处的公交车上,手机备忘录里已经码了满满一屏的草稿。今天一整天几乎没写什么新功能代码,但我觉得是实习三周以来收获最大的一天:上午独立排查了一个线上订单状态异常的事故,下午第一次全程参与了自己的Code Review,晚上又整理了一份以后每次排查问题都会用到的Bug记录模板。如果你也在实习,或者马上要去实习,这篇日志里的排查思路、评审被批的坑、日志写法,大概率能帮你少走几步弯路。1. 晨会接手的"陈年Bug":已支付订单被定时任务误关1.1 事故背景与工单内容先说清楚我们组在做什么。组里维护的是电商后台的订单服务,订单流转大概是这么一条线:用户下单生成了"待支付"订单,用户去支付,支付渠道回调我们更新订单状态,同时有个定时关单任务每天扫描超时未支付的订单,把它们置为"已关闭"并释放库存。这条链路平时很稳,但出问题的从来不是主链路,而是主链路之间的状态同步缝隙。早上晨会,组长把一个工单转到我名下。内容大致是:某用户昨晚22点14分完成支付,凌晨1点订单却被定时关单任务置为"已关闭"。订单被关闭后系统自动退还了优惠券,用户今天一早发现,非常不满。客服已经和支付渠道核实过,扣款成功,钱没丢,问题锁定在我们订单服务的状态判断上。更麻烦的是,这已经是第二次出现了,上一次紧急修复后大家以为解决了,结果今天又复发。1.2 为什么实习生会被点名接这个工单说实话,当时我第一反应是"这不该轮到实习生吧"。入职三周,我写的最复杂的代码也就是改个状态枚举、补两个单元测试,日志平台都是上周才学会用的。但组长说了句话让我印象很深:"上次排查的同事把步骤都写进文档了,你去照着走一遍,走不通再叫人。"这句话的潜台词是,这次事故虽然级别高,但排查路径已经被前人趟平了,让我去跑一遍,既是处理问题,也是借这个真实案例练我。后来我发现,这其实是带实习生最常见的套路:把一条有确定答案的复杂路径交给你走,练的是你走路的姿势,而不是让你从零开路。1.3 接手后先做的三件事我没有直接去翻代码,而是先按组里的规矩做了三件事。第一件,读排查手册。上次同事留下的文档一共有三页,记录了事故发生的时间段、涉及的服务名、日志查询方式,以及他怀疑过的几个方向。虽然里面没写最终结论,但至少帮我圈定了战场。第二件,搭时间线。我打开日志平台,先查了那个订单从创建到今天早上所有的关键事件,按时间顺序列出来:订单创建于20:02,状态"待支付";22:14支付渠道回调成功,数据库订单状态更新为"已支付";22:15系统下发支付成功通知;然后,01:00:03,定时关单任务读取订单状态,判断为"待支付",于是执行关闭操作,释放库存,退还优惠券;01:00:08,关闭动作完成,状态落库为"已关闭"。这条时间线一列出来,问题就非常清晰了:关单任务在01:00读到的是"待支付",可数据库在22:15就已经是"已支付"了。也就是说,任务读到的状态和数据库的真实状态不一致。要么是读取层的缓存出了问题,要么是任务自己有一段本地状态判断,要么是批量任务之间出现了串数据。下一步就是逐层验证这三种可能。2. 三个小时的离线排查:从日志堆里把根因捞出来2.1 第一轮:从关单任务日志入手,确认"读到了旧状态"我先去翻关单任务的代码。任务逻辑不复杂:扫描"待支付"订单,对于每笔订单,先查Redis里有没有"订单状态缓存",有就直接用缓存值判断,没有再查数据库。为什么要加这层缓存?因为关单任务每分钟跑一次,如果每次都对数据库做全量查询,压力很大,所以上个季度的性能优化里引入了状态缓存,缓存命中就直接跳过。查到这段逻辑之后,我立刻去日志平台查01:00:03那一条日志。日志显示:orderId=xxx, cacheHit=true, cachedStatus=待支付, dbStatus=已支付。关键信息来了——缓存命中了,但缓存里的状态是"待支付",和数据库的"已支付"冲突。这就说明不是任务本身写错了,而是任务在读一个过期的、错误的缓存。2.2 第二轮:查支付回调日志,发现"回调根本没碰缓存"那缓存里的"待支付"是怎么来的?我带着这个疑问去查支付回调服务。按理说,支付成功后,服务应该做的事是:更新数据库订单状态、清掉或更新Redis里的订单状态缓存、发消息通知下游。我翻了回调服务的代码,发现它只做了两件事:更新数据库、发消息。Redis里的状态缓存它压根没动。看到这里我大概有数了:缓存的写入只发生在订单创建那一刻,写入值是"待支付",写入时设了一个过期时间。如果这个过期时间足够短,凌晨一点任务执行时缓存早就过期了,就会走数据库查询,读到"已支付",问题就不会发生。可现在缓存不仅没过期,还准确命中了"待支付",说明这个Key的TTL被设得极长,或者过期时间参数本身出了问题。顺着这个思路,我去查了订单创建时的缓存写入代码。2.3 真正的根因:把15分钟的过期时间写成了15天根因在三行代码里。订单创建服务在写完数据库后,有一段这样的逻辑:往Redis写订单状态缓存,注释写着"缓存15分钟,防止关单任务频繁查库"。但实际代码里,调用Redis SDK时传的参数是15配合TimeUnit.DAYS——过期时间单位从分钟被改成了天。等于说,这个缓存一旦写入,15天后才会过期。这个改动是哪来的?我查了提交历史,发现是上周一次"性能优化"提交里,某位同事想"延长缓存时间以提升命中率",把单位改成了天,但注释和文档都没同步更新。更要命的是,支付回调服务不清缓存这件事一直存在,以前缓存15分钟过期,最多只影响15分钟内的状态判断,侥幸没出事;改成15天后,影响窗口被放大到两周,事故就必然爆发了。这次能抓出来,完全是因为我沿用了上一篇排查文档的步骤,把缓存TTL也列进了检查清单。2.4 修复方案与验证过程修复分三步走,每一步我都说下为什么。第一步,先把订单创建服务的缓存过期时间改回15分钟,用常量定义而不是裸数字,比如ORDER_STATUS_CACHE_TTL_MINUTES = 15,并且注释写明"为什么是15分钟而不是更长:关单任务对状态实时性要求高,缓存过长会放大状态不一致的窗口"。第二步,在支付回调服务里补上"删除订单状态缓存"的逻辑。这是治本的一步。缓存存在的意义是加速,但状态一旦变化,缓存就必须失效,否则任何TTL都有可能是错的。第三步,上线后验证。我写了一小段验证脚本:创建一个测试订单,等支付回调完成后,立刻查Redis确认缓存Key不存在;再等关单任务下一轮扫描,确认它读到的是数据库里的"已支付"。两个检查点都过了,我才敢把工单状态改成"已修复"。这一步也让我意识到,修复线上问题不是改完代码就结束,验证闭环才是真正结束的标志。3. 下午的Code Review:我被自己的代码批了三处3.1 第一处:魔法数字,"15分钟"被写成了"15"上午修复完,我提交了一个MR,自认为逻辑清晰、注释也写了。下午组里例行Code Review,我的MR第一次被拉出来完整过了一遍,带教导师和另一个资深同事轮流发言,结果我被批得体无完肤,但心服口服。第一处是魔法数字。我在常量定义里写了ORDER_STATUS_CACHE_TTL = 15,但没写单位。资深同事直接问:15是分钟还是秒还是小时?我解释说是分钟,他继续问:那为什么是15,不是10、不是30?我答不上来,因为我只是照搬之前的参数。正确做法是把单位也写进常量名,比如ORDER_STATUS_CACHE_TTL_MINUTES = 15,再配一段注释说明这个值是从"关单任务扫描频率+状态实时性要求"推导出来的:任务每分钟扫一次,15分钟意味着最多有15分钟的脏读窗口,属于业务可以接受的范围;如果改成1分钟,数据库压力会增大一个量级。没有理由的参数,review时一定会被问。3.2 第二处:日志只有一句"删除缓存",等于没日志我原来的日志长这样:logger.info("delete order status cache success")。带教导师问了我三个问题:哪个订单?删的哪个Key?删的是Redis里的哪个namespace?我一句话都答不上。他说,你这条日志如果上线,下次再出问题,排查的人根本不知道是哪个订单、哪个Key、删没删成功,只能重新查一遍支付流水。正确做法是把关键上下文全部带进日志:订单号、缓存Key、删除前的TTL剩余值、删除结果,甚至可以把根本原因也打进去。我顺手查了一下组里的日志规范,发现我们用的日志框架支持结构化字段,logger.info("delete cache", "orderId", orderId, "key", key, "ttl", ttl, "result", result)这种格式,在日志平台里可以直接按字段筛选。原来我一直把日志当成"证明程序跑过"的东西,这次之后才明白,日志是写给未来的排查者看的,上下文越全,排查时间越短。3.3 第三处:删缓存和更新数据库的顺序与失败兜底第三处批评最扎心。我在支付回调里写的顺序是:先更新数据库,再删除缓存。资深同事问:如果删缓存失败怎么办?程序会报错重试吗?还是直接吞掉异常?我说,我加了try-catch,失败就记日志。他摇摇头:记日志等于没处理,下次问题还会复现,因为你没解决缓存残留。正确的思路是给这个操作设计一个失败兜底。常见做法有两种:第一种,先删缓存、再更新数据库,如果更新失败,下次查询会因为缓存不存在而回源数据库,读到旧状态也没问题,因为旧状态本来就是"待支付",不会产生错误关闭;第二种,如果先更新数据库再删缓存,那删缓存失败就必须走重试机制,或者依赖一个短暂的TTL让脏数据自然过期。结合我们这次事故,最优雅的方案其实是:删除缓存本身失败的概率很低,但为了万无一失,我给删除操作加上重试,同时把缓存TTL保持在一个足够短的范围内,两个手段互为兜底,就算重试也失败了,缓存最迟15分钟后也自然消失。Review结束后,我把这三点全部改掉,重写了版本,重新过了Review。这次没人再挑毛病。4. 从第1篇到第9篇:我的实习日志写法迭代4.1 一套四个板块的日志骨架说实话,今天要不是为了写这篇实习日志,我也不会回头去看之前的日志。但晚上复盘的时候,我把第1篇到第8篇从头翻了一遍,发现自己的写法经历了明显的变化:前几篇全是流水账,第5篇开始有了一点问题描述,从第7篇开始才真正有"认知"。所以我想把现在这套用着最顺的骨架记下来,给大家一个参考。我的日志现在固定分四块:事实、问题、认知、计划。"事实"写今天具体完成了什么事,做了哪几个任务,改了什么文件,必须可量化;"问题"写今天卡住我的技术难点,卡了多久,最后怎么解决的,解决思路是什么;"认知"写今天推翻或建立的一个认知,比如"Cache不是永久正确的东西,TTL就是脏读窗口";"计划"写明天要做什么,以及今天遗留的尾巴。这四个板块的顺序其实是按时间倒排的:晨会立目标时我觉得今天是计划,晚上写日志时事实已成定局,问题是从事实里提炼出来的,认知是从问题里沉淀的。写了一个星期之后,我发现这种结构最大的好处是,每篇日志都能变成一份可以被检索的记录,而不是情绪垃圾桶。4.2 新手写日志最容易犯的三个毛病回看我自己和周围实习生的日志,最容易犯三个毛病。第一个是记流水账。"上午看了文档,下午改了一个Bug,晚上回家"——这种日志写一百天也不会有什么成长,因为你没有强迫自己回答"为什么"。第二个是只写情绪不写证据。"今天被骂了,很难受"或者"今天很有成就感"——情绪是真实体验的一部分,但日志的价值在于把情绪背后的具体事件写清楚,不然第二天你自己都不知道当时为什么难受。第三个是只写结论不写过程。"今天发现缓存过期时间设置错了,已修复"——这是最可惜的一种。排查过程才是最有价值的资产,你是怎么一步步从日志定位到那三行代码的?你怀疑过哪几个方向?哪些方向被排除了?这些一旦不写,下次遇到类似问题你还是得从零开始。我中午刚被Review批的时候,情绪很差,差点在日志里只写"被批评了,以后注意"。但晚上整理的时候,我逼自己把三个批评点逐一写成了可以执行的动作,立刻就觉得今天没有白挨批。4.3 日志的核心价值:写给一个月后的自己最后说一个我到现在才想明白的事:实习日志不是写给带教导师看的,也不是用来证明"我今天在干活"的,它是写给一个月后的自己的。人记性真的很差,上周修过的Bug,一周后看到相似代码可能完全想不起来当初是怎么解决的。我当时把上一篇排查文档当成救命稻草,就是因为写文档的人认真留了过程,我这周才能沿着走。我如果也想让未来的自己受益,现在就该养成"把过程留下来"的习惯。这也是为什么这篇日志写得比前八篇长很多——今天的事件值得完整记录,而记录本身就是一把钥匙,以后类似的故障,我不用再依赖别人,自己翻日志就能快速进入状态。5. 今晚整理出的Bug排查记录模板(附完整示例)5.1 模板字段与填写要点因为今天的排查过程足够完整,我晚上花了一个小时,把之前随手记的排查笔记整理成了一份可复用的模板。以后不管接手什么工单,我都打算按这个格式填,大概长这样:现象(用户看到了什么)复现环境(能不能复现,线上还是测试环境)影响范围(涉及订单数、用户数、时间窗口)关键日志时间线(事件时间、日志内容、涉及服务)根因(直接原因和更深层原因分开写)修复动作(每一步做了什么,为什么这么做)验证闭环(怎么证明修好了)复发性预防(这类问题以后怎么避免再次发生)每条字段为什么重要,我简单说说。"现象"帮你和用户对焦,"复现环境"决定你能不能在线调试,"影响范围"决定处理优先级,"时间线"是破案的主线,"根因"如果不区分直接原因和深层原因,很容易只修表象,"验证闭环"是很多人漏掉的最后一步,"复发性预防"则是把个人经验转化为团队资产的关键。5.2 今天这次事故按模板填出来长什么样拿今天的事故举个例子,我填出来的版本是这样的:现象:用户已支付订单在凌晨被误关闭,优惠券被系统退还。复现环境:无法直接复现,属于定时任务与支付回调时间窗口交叉触发,只能通过日志复盘。影响范围:本次共影响3笔订单,时间窗口为过去两周内支付且在缓存过期前被关单任务扫描到的订单。关键日志时间线:22:14支付回调成功,数据库状态已更新;01:00:03关单任务命中缓存,读到"待支付";01:00:08状态落库为"已关闭"。根因:直接原因是支付回调未清理订单状态缓存;深层原因是订单创建时的缓存TTL单位被误改为天,导致脏缓存存活时间从15分钟放大到15天。修复动作:过期时间改回15分钟并常量化管理;支付回调增加删除缓存逻辑;删除失败增加重试。验证闭环:测试订单支付后缓存Key不存在,关单任务下一轮扫描读到数据库正确状态。复发性预防:在订单状态变更处统一强制清缓存,禁止任何服务绕过;排查手册补充"检查缓存TTL单位"这一项。5.3 明天开始要做的三件事最后,给明天的自己列个清单。第一件,把这份模板同步给同组另外两个实习生,让大家以后统一用这个格式记录工单排查,这样即使休假或转岗,经验也不会断。第二件,和带教导师约一个十分钟的沟通,想请他看看我下午Review重写后的代码,确认我对事务边界和失败兜底的理解没有偏差。这算是我主动要的一次复查机会。第三件,把今天学到的"日志要带上下文"这个原则应用到我所有旧代码里,把之前写的那些山寨日志统一补上关键字段。这不是导师布置的任务,但我知道,日志早晚是要还的,现在不补,出事的时候补的成本更高。写到这儿,公交快到站了。1月9日这一天,我从一个只会按文档操作的实习生,变成了一个大概知道"为什么要这么查"的人。明天还有新的任务,但至少今天,这篇实习日志我可以安心归档了。
这篇实习日志是1月9日的。写这些字的时候是晚上十点半,我刚从公司出来,坐在回住处的公交车上,手机备忘录里已经码了满满一屏的草稿。今天一整天几乎没写什么新功能代码,但我觉得是实习三周以来收获最大的一天:上午独立排查了一个线上订单状态异常的事故,下午第一次全程参与了自己的Code Review,晚上又整理了一份以后每次排查问题都会用到的Bug记录模板。如果你也在实习,或者马上要去实习,这篇日志里的排查思路、评审被批的坑、日志写法,大概率能帮你少走几步弯路。1. 晨会接手的"陈年Bug":已支付订单被定时任务误关1.1 事故背景与工单内容先说清楚我们组在做什么。组里维护的是电商后台的订单服务,订单流转大概是这么一条线:用户下单生成了"待支付"订单,用户去支付,支付渠道回调我们更新订单状态,同时有个定时关单任务每天扫描超时未支付的订单,把它们置为"已关闭"并释放库存。这条链路平时很稳,但出问题的从来不是主链路,而是主链路之间的状态同步缝隙。早上晨会,组长把一个工单转到我名下。内容大致是:某用户昨晚22点14分完成支付,凌晨1点订单却被定时关单任务置为"已关闭"。订单被关闭后系统自动退还了优惠券,用户今天一早发现,非常不满。客服已经和支付渠道核实过,扣款成功,钱没丢,问题锁定在我们订单服务的状态判断上。更麻烦的是,这已经是第二次出现了,上一次紧急修复后大家以为解决了,结果今天又复发。1.2 为什么实习生会被点名接这个工单说实话,当时我第一反应是"这不该轮到实习生吧"。入职三周,我写的最复杂的代码也就是改个状态枚举、补两个单元测试,日志平台都是上周才学会用的。但组长说了句话让我印象很深:"上次排查的同事把步骤都写进文档了,你去照着走一遍,走不通再叫人。"这句话的潜台词是,这次事故虽然级别高,但排查路径已经被前人趟平了,让我去跑一遍,既是处理问题,也是借这个真实案例练我。后来我发现,这其实是带实习生最常见的套路:把一条有确定答案的复杂路径交给你走,练的是你走路的姿势,而不是让你从零开路。1.3 接手后先做的三件事我没有直接去翻代码,而是先按组里的规矩做了三件事。第一件,读排查手册。上次同事留下的文档一共有三页,记录了事故发生的时间段、涉及的服务名、日志查询方式,以及他怀疑过的几个方向。虽然里面没写最终结论,但至少帮我圈定了战场。第二件,搭时间线。我打开日志平台,先查了那个订单从创建到今天早上所有的关键事件,按时间顺序列出来:订单创建于20:02,状态"待支付";22:14支付渠道回调成功,数据库订单状态更新为"已支付";22:15系统下发支付成功通知;然后,01:00:03,定时关单任务读取订单状态,判断为"待支付",于是执行关闭操作,释放库存,退还优惠券;01:00:08,关闭动作完成,状态落库为"已关闭"。这条时间线一列出来,问题就非常清晰了:关单任务在01:00读到的是"待支付",可数据库在22:15就已经是"已支付"了。也就是说,任务读到的状态和数据库的真实状态不一致。要么是读取层的缓存出了问题,要么是任务自己有一段本地状态判断,要么是批量任务之间出现了串数据。下一步就是逐层验证这三种可能。2. 三个小时的离线排查:从日志堆里把根因捞出来2.1 第一轮:从关单任务日志入手,确认"读到了旧状态"我先去翻关单任务的代码。任务逻辑不复杂:扫描"待支付"订单,对于每笔订单,先查Redis里有没有"订单状态缓存",有就直接用缓存值判断,没有再查数据库。为什么要加这层缓存?因为关单任务每分钟跑一次,如果每次都对数据库做全量查询,压力很大,所以上个季度的性能优化里引入了状态缓存,缓存命中就直接跳过。查到这段逻辑之后,我立刻去日志平台查01:00:03那一条日志。日志显示:orderId=xxx, cacheHit=true, cachedStatus=待支付, dbStatus=已支付。关键信息来了——缓存命中了,但缓存里的状态是"待支付",和数据库的"已支付"冲突。这就说明不是任务本身写错了,而是任务在读一个过期的、错误的缓存。2.2 第二轮:查支付回调日志,发现"回调根本没碰缓存"那缓存里的"待支付"是怎么来的?我带着这个疑问去查支付回调服务。按理说,支付成功后,服务应该做的事是:更新数据库订单状态、清掉或更新Redis里的订单状态缓存、发消息通知下游。我翻了回调服务的代码,发现它只做了两件事:更新数据库、发消息。Redis里的状态缓存它压根没动。看到这里我大概有数了:缓存的写入只发生在订单创建那一刻,写入值是"待支付",写入时设了一个过期时间。如果这个过期时间足够短,凌晨一点任务执行时缓存早就过期了,就会走数据库查询,读到"已支付",问题就不会发生。可现在缓存不仅没过期,还准确命中了"待支付",说明这个Key的TTL被设得极长,或者过期时间参数本身出了问题。顺着这个思路,我去查了订单创建时的缓存写入代码。2.3 真正的根因:把15分钟的过期时间写成了15天根因在三行代码里。订单创建服务在写完数据库后,有一段这样的逻辑:往Redis写订单状态缓存,注释写着"缓存15分钟,防止关单任务频繁查库"。但实际代码里,调用Redis SDK时传的参数是15配合TimeUnit.DAYS——过期时间单位从分钟被改成了天。等于说,这个缓存一旦写入,15天后才会过期。这个改动是哪来的?我查了提交历史,发现是上周一次"性能优化"提交里,某位同事想"延长缓存时间以提升命中率",把单位改成了天,但注释和文档都没同步更新。更要命的是,支付回调服务不清缓存这件事一直存在,以前缓存15分钟过期,最多只影响15分钟内的状态判断,侥幸没出事;改成15天后,影响窗口被放大到两周,事故就必然爆发了。这次能抓出来,完全是因为我沿用了上一篇排查文档的步骤,把缓存TTL也列进了检查清单。2.4 修复方案与验证过程修复分三步走,每一步我都说下为什么。第一步,先把订单创建服务的缓存过期时间改回15分钟,用常量定义而不是裸数字,比如ORDER_STATUS_CACHE_TTL_MINUTES = 15,并且注释写明"为什么是15分钟而不是更长:关单任务对状态实时性要求高,缓存过长会放大状态不一致的窗口"。第二步,在支付回调服务里补上"删除订单状态缓存"的逻辑。这是治本的一步。缓存存在的意义是加速,但状态一旦变化,缓存就必须失效,否则任何TTL都有可能是错的。第三步,上线后验证。我写了一小段验证脚本:创建一个测试订单,等支付回调完成后,立刻查Redis确认缓存Key不存在;再等关单任务下一轮扫描,确认它读到的是数据库里的"已支付"。两个检查点都过了,我才敢把工单状态改成"已修复"。这一步也让我意识到,修复线上问题不是改完代码就结束,验证闭环才是真正结束的标志。3. 下午的Code Review:我被自己的代码批了三处3.1 第一处:魔法数字,"15分钟"被写成了"15"上午修复完,我提交了一个MR,自认为逻辑清晰、注释也写了。下午组里例行Code Review,我的MR第一次被拉出来完整过了一遍,带教导师和另一个资深同事轮流发言,结果我被批得体无完肤,但心服口服。第一处是魔法数字。我在常量定义里写了ORDER_STATUS_CACHE_TTL = 15,但没写单位。资深同事直接问:15是分钟还是秒还是小时?我解释说是分钟,他继续问:那为什么是15,不是10、不是30?我答不上来,因为我只是照搬之前的参数。正确做法是把单位也写进常量名,比如ORDER_STATUS_CACHE_TTL_MINUTES = 15,再配一段注释说明这个值是从"关单任务扫描频率+状态实时性要求"推导出来的:任务每分钟扫一次,15分钟意味着最多有15分钟的脏读窗口,属于业务可以接受的范围;如果改成1分钟,数据库压力会增大一个量级。没有理由的参数,review时一定会被问。3.2 第二处:日志只有一句"删除缓存",等于没日志我原来的日志长这样:logger.info("delete order status cache success")。带教导师问了我三个问题:哪个订单?删的哪个Key?删的是Redis里的哪个namespace?我一句话都答不上。他说,你这条日志如果上线,下次再出问题,排查的人根本不知道是哪个订单、哪个Key、删没删成功,只能重新查一遍支付流水。正确做法是把关键上下文全部带进日志:订单号、缓存Key、删除前的TTL剩余值、删除结果,甚至可以把根本原因也打进去。我顺手查了一下组里的日志规范,发现我们用的日志框架支持结构化字段,logger.info("delete cache", "orderId", orderId, "key", key, "ttl", ttl, "result", result)这种格式,在日志平台里可以直接按字段筛选。原来我一直把日志当成"证明程序跑过"的东西,这次之后才明白,日志是写给未来的排查者看的,上下文越全,排查时间越短。3.3 第三处:删缓存和更新数据库的顺序与失败兜底第三处批评最扎心。我在支付回调里写的顺序是:先更新数据库,再删除缓存。资深同事问:如果删缓存失败怎么办?程序会报错重试吗?还是直接吞掉异常?我说,我加了try-catch,失败就记日志。他摇摇头:记日志等于没处理,下次问题还会复现,因为你没解决缓存残留。正确的思路是给这个操作设计一个失败兜底。常见做法有两种:第一种,先删缓存、再更新数据库,如果更新失败,下次查询会因为缓存不存在而回源数据库,读到旧状态也没问题,因为旧状态本来就是"待支付",不会产生错误关闭;第二种,如果先更新数据库再删缓存,那删缓存失败就必须走重试机制,或者依赖一个短暂的TTL让脏数据自然过期。结合我们这次事故,最优雅的方案其实是:删除缓存本身失败的概率很低,但为了万无一失,我给删除操作加上重试,同时把缓存TTL保持在一个足够短的范围内,两个手段互为兜底,就算重试也失败了,缓存最迟15分钟后也自然消失。Review结束后,我把这三点全部改掉,重写了版本,重新过了Review。这次没人再挑毛病。4. 从第1篇到第9篇:我的实习日志写法迭代4.1 一套四个板块的日志骨架说实话,今天要不是为了写这篇实习日志,我也不会回头去看之前的日志。但晚上复盘的时候,我把第1篇到第8篇从头翻了一遍,发现自己的写法经历了明显的变化:前几篇全是流水账,第5篇开始有了一点问题描述,从第7篇开始才真正有"认知"。所以我想把现在这套用着最顺的骨架记下来,给大家一个参考。我的日志现在固定分四块:事实、问题、认知、计划。"事实"写今天具体完成了什么事,做了哪几个任务,改了什么文件,必须可量化;"问题"写今天卡住我的技术难点,卡了多久,最后怎么解决的,解决思路是什么;"认知"写今天推翻或建立的一个认知,比如"Cache不是永久正确的东西,TTL就是脏读窗口";"计划"写明天要做什么,以及今天遗留的尾巴。这四个板块的顺序其实是按时间倒排的:晨会立目标时我觉得今天是计划,晚上写日志时事实已成定局,问题是从事实里提炼出来的,认知是从问题里沉淀的。写了一个星期之后,我发现这种结构最大的好处是,每篇日志都能变成一份可以被检索的记录,而不是情绪垃圾桶。4.2 新手写日志最容易犯的三个毛病回看我自己和周围实习生的日志,最容易犯三个毛病。第一个是记流水账。"上午看了文档,下午改了一个Bug,晚上回家"——这种日志写一百天也不会有什么成长,因为你没有强迫自己回答"为什么"。第二个是只写情绪不写证据。"今天被骂了,很难受"或者"今天很有成就感"——情绪是真实体验的一部分,但日志的价值在于把情绪背后的具体事件写清楚,不然第二天你自己都不知道当时为什么难受。第三个是只写结论不写过程。"今天发现缓存过期时间设置错了,已修复"——这是最可惜的一种。排查过程才是最有价值的资产,你是怎么一步步从日志定位到那三行代码的?你怀疑过哪几个方向?哪些方向被排除了?这些一旦不写,下次遇到类似问题你还是得从零开始。我中午刚被Review批的时候,情绪很差,差点在日志里只写"被批评了,以后注意"。但晚上整理的时候,我逼自己把三个批评点逐一写成了可以执行的动作,立刻就觉得今天没有白挨批。4.3 日志的核心价值:写给一个月后的自己最后说一个我到现在才想明白的事:实习日志不是写给带教导师看的,也不是用来证明"我今天在干活"的,它是写给一个月后的自己的。人记性真的很差,上周修过的Bug,一周后看到相似代码可能完全想不起来当初是怎么解决的。我当时把上一篇排查文档当成救命稻草,就是因为写文档的人认真留了过程,我这周才能沿着走。我如果也想让未来的自己受益,现在就该养成"把过程留下来"的习惯。这也是为什么这篇日志写得比前八篇长很多——今天的事件值得完整记录,而记录本身就是一把钥匙,以后类似的故障,我不用再依赖别人,自己翻日志就能快速进入状态。5. 今晚整理出的Bug排查记录模板(附完整示例)5.1 模板字段与填写要点因为今天的排查过程足够完整,我晚上花了一个小时,把之前随手记的排查笔记整理成了一份可复用的模板。以后不管接手什么工单,我都打算按这个格式填,大概长这样:现象(用户看到了什么)复现环境(能不能复现,线上还是测试环境)影响范围(涉及订单数、用户数、时间窗口)关键日志时间线(事件时间、日志内容、涉及服务)根因(直接原因和更深层原因分开写)修复动作(每一步做了什么,为什么这么做)验证闭环(怎么证明修好了)复发性预防(这类问题以后怎么避免再次发生)每条字段为什么重要,我简单说说。"现象"帮你和用户对焦,"复现环境"决定你能不能在线调试,"影响范围"决定处理优先级,"时间线"是破案的主线,"根因"如果不区分直接原因和深层原因,很容易只修表象,"验证闭环"是很多人漏掉的最后一步,"复发性预防"则是把个人经验转化为团队资产的关键。5.2 今天这次事故按模板填出来长什么样拿今天的事故举个例子,我填出来的版本是这样的:现象:用户已支付订单在凌晨被误关闭,优惠券被系统退还。复现环境:无法直接复现,属于定时任务与支付回调时间窗口交叉触发,只能通过日志复盘。影响范围:本次共影响3笔订单,时间窗口为过去两周内支付且在缓存过期前被关单任务扫描到的订单。关键日志时间线:22:14支付回调成功,数据库状态已更新;01:00:03关单任务命中缓存,读到"待支付";01:00:08状态落库为"已关闭"。根因:直接原因是支付回调未清理订单状态缓存;深层原因是订单创建时的缓存TTL单位被误改为天,导致脏缓存存活时间从15分钟放大到15天。修复动作:过期时间改回15分钟并常量化管理;支付回调增加删除缓存逻辑;删除失败增加重试。验证闭环:测试订单支付后缓存Key不存在,关单任务下一轮扫描读到数据库正确状态。复发性预防:在订单状态变更处统一强制清缓存,禁止任何服务绕过;排查手册补充"检查缓存TTL单位"这一项。5.3 明天开始要做的三件事最后,给明天的自己列个清单。第一件,把这份模板同步给同组另外两个实习生,让大家以后统一用这个格式记录工单排查,这样即使休假或转岗,经验也不会断。第二件,和带教导师约一个十分钟的沟通,想请他看看我下午Review重写后的代码,确认我对事务边界和失败兜底的理解没有偏差。这算是我主动要的一次复查机会。第三件,把今天学到的"日志要带上下文"这个原则应用到我所有旧代码里,把之前写的那些山寨日志统一补上关键字段。这不是导师布置的任务,但我知道,日志早晚是要还的,现在不补,出事的时候补的成本更高。写到这儿,公交快到站了。1月9日这一天,我从一个只会按文档操作的实习生,变成了一个大概知道"为什么要这么查"的人。明天还有新的任务,但至少今天,这篇实习日志我可以安心归档了。