
写开发随记这个习惯我保持了快八年。早期纯粹是为了记点零碎笔记怕忘后来回头看那些三言两语反而成了最有价值的排查手册和决策依据。这篇随记汇总了最近两三个月里我遇到的几个典型开发场景——有数据同步组件的设计取舍有重构旧系统时的反复考量也有线上问题排查的完整过程。适合正在做后端开发、中间件维护或者手里攥着老项目的朋友参考尤其是那些刚接手没人维护过的代码、一上来就懵圈的场景这篇记录里基本都有对应的心路历程。1. 最近在做什么一个数据同步组件的开发随记1.1 需求其实没有想象中简单这次的项目说起来不复杂把A系统的订单数据同步到B系统的报表库全量加增量每天凌晨跑一遍。需求文档上只有一句话“保持两边订单数据一致”。但真正动手之后才发现这个“一致”的定义本身就值得反复推敲——是最终一致就行还是要实时一致是容忍短暂延迟还是必须严格事务同步失败后怎么补偿重复消息怎么处理这些问题在文档里统统没有答案。我最初的想法很简单写个脚本每天定时查A库查到了就插到B库。后来和业务同事聊了一圈才发现B系统的报表依赖于订单的“状态变更历史”也就是说每次订单从待支付变成已支付、从已支付变成已发货都需要留下一条记录。如果只是单纯同步订单表的当前状态那历史轨迹就全丢了报表里的数据根本没法看。这就引出了第一个关键设计不能只同步结果表还得同步状态流转事件。我最后选择监听A系统的业务日志表每有一条状态变更就记录一条事件再通过单独的任务把事件异步推给B系统。这种方式比直接同步表多了两层复杂度但它保证了B系统拿到的不只是快照而是一串完整的事件流业务上才能真正实现对账和追踪。1.2 技术选型时我纠结的几个点数据量不算大一天几千条订单状态变更消息量完全在普通队列能够承载的范围内。但选型时我还是纠结了一阵子因为这块功能未来要承接更多系统不止A系统一家。一开始我想直接引入一套完整的消息中间件集群理由很直白成熟、生态好、后续扩展方便。但思来想去又觉得为这么点数据量维护一套独立集群运维成本实在不划算。后来同事提醒我既然数据量不大完全可以用数据库表模拟一个简单的队列——事件表加状态字段消费端定期轮询成功就更新状态。这个方案虽然看起来“土”但胜在逻辑简单、部署零依赖、排查问题的时候也直观出问题直接查表就行。最终我选择了折中方案业务库里的本地事件表保证记录不丢配合一个轻量级的调度任务把事件分批推送到B系统提供的接口。B系统收到后先幂等校验再落库更新。整个过程没有引入额外的中间件代码量反而更好控制。选型的经验就是技术方案的复杂度必须跟着业务的真实体量走不要为了炫技给未来埋坑。2. 编码阶段踩过的三个坑2.1 缓存不一致问题不是所有状态都适合放缓存第一个坑来自我对缓存过度乐观的使用。B系统有一个订单列表页展示的时候需要实时显示订单当前状态。为了降低数据库压力我在Redis里给每个订单状态单独建了个key读的时候先查缓存没有再回源数据库。看起来没什么问题但上线第三天就出事了B系统页面显示的订单状态是“待支付”实际订单早已“已支付”。查了半天原因是我在推送事件时只更新了数据库里的订单状态字段却忘了同步删除对应的Redis缓存key导致后续请求全部命中了旧缓存。修复本身不难加上缓存清理逻辑就行但这个问题的根源是“状态字段天然具有时序性而缓存天然倾向于保留旧值”。事后我把所有状态相关字段的读取都改成了穿透查询只有统计类、配置类的数据才允许走缓存。我现在的原则就是缓存可以放读多写少的静态数据但绝不放强一致要求的状态流转数据为了性能牺牲正确性一定是得不偿失的。2.2 并发处理自增ID的教训第二个坑是并发重复消息导致的脏数据。B系统的接口做了一个简单的幂等校验拿订单ID去库里查如果事件序号已经存在就跳过。本来逻辑没什么问题但测试时我用两个线程同时模拟推送同一批事件结果两条事件都通过了校验库里就多了一条重复的状态记录。深入排查后发现校验和插入两个操作之间有间隙。因为订单ID不是唯一索引两个并发请求同时查“不存在”就同时插入成功。解决方案比想象中简单得多给订单ID加一个联合唯一索引冲突时让数据库直接拒绝而不是靠应用层判断。那次之后我对幂等设计的理解深了一层——真正可靠的幂等必须依托于存储层的约束能力应用层判断只是辅助手段。2.3 SQL索引的隐形问题第三个坑发生在增量查询阶段。A系统的事件表数据量累积到几百万行之后我写的一条分页查询性能突然跌到数秒级别。打开执行计划一看排序字段没有和过滤条件匹配到同一个索引数据库只能把所有符合条件的数据先查出来再在内存里做文件排序。这个问题的隐蔽性在于数据量小的时候根本发现不了。我原来的查询条件针对的是事件类型字段排序是针对时间字段两个字段分别建了单列索引但MySQL在优化器看来单列索引无法同时满足过滤和排序需求。解决方式不复杂把事件类型和时间字段做成一个联合索引问题迎刃而解。这件事情给我的教训是SQL调优不能靠感觉执行计划和EXPLAIN的输出才是最可靠的依据尤其在数据量爬坡之后索引设计必须跟着真实查询模式走。3. 重构旧代码时积累的经验3.1 渐进式重构而不是推倒重来手头还有一个老项目是我刚接手时最头疼的。代码写了几年没有测试覆盖模块边界模糊明明是个订单服务里面居然混着库存扣减和优惠券发放的逻辑。最初我的本能反应是大改把整个服务推倒重写但仔细想了一下成本业务逻辑没文档流程极其复杂直接重写风险完全不可控几个月可能都稳定不下来。我最后选择的是渐进式重构分三步走。第一步把核心链路的日志补全确保每一步都有迹可循第二步按照当前业务语义把接口拆成更小的服务方法保持对外契约不变第三步每拆完一个模块就手动跑一遍核心场景的回归用例确认无误再继续。这个过程很磨人速度也不快。但好处是每个阶段的风险都可控坏代码是逐步暴露的不是一下子集中爆发。我最大的体会是重构的本质不是让代码看起来更漂亮而是让后续的每一处修改都变得更加安全。一上来就追求彻底重写多半会让隐性业务规则丢在旧代码里。3.2 边界情况如何识别并保留重构过程中最怕的不是逻辑复杂而是分支路径丢失。我遇到过几次看着一段代码好像没什么用删掉之后某个特殊场景直接报错。后来学乖了动手前先做两件事。第一件把所有异常分支和兜底逻辑全部标注出来哪怕暂时不清楚为什么存在也要先确认它是被某个调用方依赖的。第二件把处理特殊值(比如零值、空值、超长字符串)的地方单独摘出来优先保留这些边界处理代码。大多数真实的线上坑都不是正常流程走崩的而是边界情况被重构掉了。举个具体例子旧代码里有一段对订单金额为负数时直接抛异常的逻辑我当时觉得这条分支业务上不可能出现准备顺手删掉后来查日志发现还真有这种脏数据是线下人工导入订单时不规范导致的。保留边界处理代码本质上就是在给系统的未知风险留退路。4. 线上问题排查实录4.1 一个诡异的全表扫描案例要说最近印象最深的排查经历就是那个把数据库CPU打满的夜晚。现象很简单B系统的报表服务每隔几天就卡死一次重启后恢复过段时间又卡死。监控面板上看不到任何明显的慢查询告警让我一度以为是内存泄漏。后半夜实在没办法开了全量慢查询日志和实时会话监控总算逮到了元凶一条看起来人畜无害的查询没有走索引直接全表扫描。最诡异的是这条SQL的执行计划在测试环境完全正常偏偏线上就是不走索引。对比之后发现测试环境数据量只有几万行线上那个表已经上千万行数据分布早就变了优化器分析的抽样结果直接决定了它选错了路线。解决方案也很直接除了给这个查询建立合适的索引我把原SQL里一个函数包裹条件改成了直接比较字段消除了索引失效的最后一个隐患。那个晚上最大的收获是排查问题永远得先看数据现状而不是依赖经验猜测。环境不同、数据量不同优化器的表现就完全不同任何SQL上线前最好都用目标数据量的样本验证一遍。4.2 排查工具与思路线上问题排查工具链其实不需要太复杂但我用的几个方法分享出来给同样踩坑的朋友参考。慢查询日志是第一个要开的排查数据库问题如果没有它就等于盲人摸象实时会话查询也很关键问题发生的时候通过数据库的进程列表直接抓出当前正在执行的SQL往往能比慢查询更快定位再看执行计划一旦锁定了嫌疑SQL下一步就是用EXPLAIN分析它的扫描行数和索引命中情况。排查有一个顺序上的讲究先确认影响范围是单条SQL还是整个服务再确认时间点问题是从哪次发布、哪个数据量级开始恶化的最后才是定位根因。直接一上来就抓SQL很容易被表面现象带偏。我那次排查之所以困扰了很久就是因为一开始把方向定在了内存和GC上绕了一个大圈才回到SQL层面。4.3 事后复盘的意义问题修复之后我做的第一件事就是写复盘报告。内容不复杂就四个问题故障表现是什么、根因是什么、怎么发现的、后续怎么防止再犯。复盘的过程比修复本身更有价值。写报告时我才发现这个表的索引之所以一直没有建立是因为建表初期数据量小谁也没想过它会成为瓶颈而当初没有设置慢查询阈值是因为默认配置根本不适合当时的业务量级。于是顺着这个问题我把数据库的规范配置从头到尾梳理了一遍补上了必要的监控告警把慢查询阈值调低还顺手给所有核心表加了计划外的巡检任务。复盘能让我把一次偶然的故障变成一套常规的防御机制。这和写开发随记的逻辑是一样的单次经验容易忘写下来、整理成清单后面的人才能真的站在前人的肩膀上。5. 常见问题速查表5.1 开发中的常规问题整理这些年在开发里踩过的坑太多我整理了一个速查表这次一并分享出来。不敢说全面但基本都是高频问题的真实解法。问题场景典型表现排查思路解决方案接口偶发超时时好时坏重启后恢复查看GC日志和数据库连接池状态确定是连接泄漏还是锁等待分别处理缓存不一致数据更新后读到旧值检查是否有删除缓存逻辑遗漏状态字段绕过缓存静态配置才走缓存并发插入重复数据数据库出现重复记录检查应用层幂等设计用唯一索引兜底持久层约束优先SQL突然变慢执行计划变化看数据量分布和索引走没走重写SQL消除函数包裹条件调整索引服务启动失败配置项缺失对比不同环境的配置文件把配置标准化到统一配置中心这张表不是拿来背的而是遇到问题的时候按图索骥用的。排查的快慢很多时候取决于能不能快速定位到正确的排查方向而不是在错误的方向上浪费时间。5.2 排查问题时的几个习惯接着速查表再说说我个人养成的几个习惯。第一日志一定要打全。不要觉得日志浪费时间线上出问题的时候日志是唯一的线索来源。我习惯在关键链路的入口、出口、异常分支都留下结构化日志带上请求号、参数摘要、耗时时间排查的时候按请求号串联起来看效率会高非常多。第二改动尽量小步提交。每次提交只做一件事描述里写清楚为什么改、影响了什么。很多人不爱写提交说明三周之后回看自己提交的代码根本想不起来当时在干嘛这种成本实在太大了。第三线上操作前多做一步验证。哪怕只是改一个配置项也先看看有没有关联方有没有可能引发缓存穿透有没有版本兼容问题。吃过几次亏之后我现在的信条是谨慎不背锅冲动才背锅。5.3 团队协作和代码评审的视角代码评审这件事我以前也觉得是走形式毕竟大家写的代码都能跑。后来有一次评审环节帮我拦下了一个非常隐蔽的并发问题才改变了我的看法。那时候有个同事准备上线一个定时任务逻辑本身没问题但他用的是应用内静态变量做状态标记部署多实例之后不同实例之间状态根本不同步测试环境发现不了上线后一定是事故。代码评审的核心价值就是提供第二双眼睛。尤其对于那些刚接触某个模块的同事评审人往往能从不同角度发现设计和逻辑上的盲区。我的建议是评审不要只盯语法和规范更要关注三件事并发场景是否考虑了失败场景是否做了处理改动是否影响了周边模块的既有链路。这三点帮团队避过的坑远比揪住一个命名规范来得值。现在我再回头想写开发随记这件事它真正改变的不是我记录了多少内容而是逼着我养成了复盘和沉淀的习惯。每次把问题写出来就等于强制自己把当时的判断逻辑重新走一遍很多模糊的地方就是在写的过程中变清晰的。这个内容后续我打算按照技术主题继续扩展下去把缓存、并发、排查这三块单独拆成系列来记录。如果你也在维护一套让人头疼的系统建议从今天开始把每次踩坑的过程简单记几笔一个月后回来翻你会发现这本随记比任何技术文档都更懂你的项目。