
1. 先把三个原则的靶心认清楚前年接手一个订单模块一个文件两千三百多行里面有下单、算价、发券、调支付、写对账、发通知还有一段谁也不敢删的历史兼容逻辑注释写着“勿动动了会出事”。有次改一个优惠券叠加规则上线第二天退款流程全挂了查了大半天才发现退款复用了算价里的一段私有方法而那段方法被我在改优惠券时顺手改了返回值结构。这件事之后我把“原则性”这三个字重新理解了一遍——它不是面试时背的概念而是给代码和系统划的那几条不能越的线单一职责原则管的是“边界在哪”第一性原则管的是“从哪开始想”ACID原则管的是“什么状态不能破”。三条线合起来最终落到三个很实在的地方安全、学习、节约——系统更不容易出事故自己成长路径更清晰长期的人力和机器成本更低。这篇内容适合谁看如果你写业务代码一两年感觉项目越写越乱、改一处崩三处如果你开始做一点模块设计或者带一两个新人需要一套能讲清楚、能落地的判断标准如果你是做技术方案评审的经常被“别人都这么做”这种理由堵回来。下面的东西不玩概念全是能直接抄的做法、能直接套的算例以及我踩过的坑。2. 三个原则各自盯住的是什么问题很多人把这三个原则当成三个孤立的知识点考完就忘。真正有用的是把它们看成一套成本控制工具单一职责控制的是“变更成本”第一性原则控制的是“认知成本”ACID控制的是“故障成本”。三样东西压下去项目才可能长期活着。2.1 单一职责原则判断标准是“变更的理由”不是“代码行数”先纠正一个特别常见的误解。SRPSingle Responsibility Principle里的“单一职责”不是“一个类只干一件事”也不是“一个方法只写十行”。它的原始表述是“一个模块应该有且只有一个变更的理由”。关键词是理由——理由来自人来自需求方。换句话说如果两个需求来自不同的角色、不同的业务方向、不同的上线节奏却都要改同一个文件那这个文件就违反了SRP。举个具体的一个OrderService里同时放着“下单”“退款”“对账出报表”三块逻辑。下单的变更来自产品经理退款的变更来自客服和财务对账的变更来自财务结算周期。三个来源三种节奏三种验收方式却挤在一个类里——这就是典型的职责过载。反过来一个PriceCalculator里有一百五十行算价逻辑包含了阶梯价、会员价、区域价看着很长但它们全都只因为“定价策略变化”这一个理由而改那它反而是合规的。所以行数不是判据变更源的个数才是。我习惯用一个很土的自测问题这个模块如果要在周会上汇报我能不能用一句话说清它负责什么并且这句话里不出现“和”“以及”“顺便”如果说不清基本就该拆了。2.2 第一性原则把“结论”还原成“事实”再从事实重新组装第一性原则First Principles说白了就一句话别从别人给的结论出发从不可再分的事实出发。它的对立面是“类比推理”——因为别人这么做所以我也这么做因为上次这么做成了所以这次还这么做。类比推理省事但它会把别人的历史包袱一起继承过来。做过饭的人有体会照着菜谱一步步抄能做出一盘能吃的菜但只有搞明白“为什么先炒后炖”“为什么这个要大火快炒那个要小火慢煨”你才能在缺一味调料、换一口锅的时候照样做出来。写系统也一样。上来就问“我们要不要上消息队列”“缓存用哪个”这是从结论出发先问“这个链路的读写比例是多少”“数据能容忍丢吗”“延迟预算是多少毫秒”这是从事实出发。我见过最典型的一次争论是某团队要做一个配置下发功能会上直接开吵“用推还是用拉”。吵了四十分钟没结果。后来有人把问题拉回事实配置一共多少条不到两千条、多久变一次平均一天两次、客户端多少台三千台左右、能容忍多长延迟一分钟内都行。四个数字摆出来答案自己就出来了——三千台客户端一天拉两次QPS 不到 0.1随便搞个定时轮询就完事根本不需要长连接推送。算一遍比吵一小时管用。2.3 ACID原则它不是数据库的专属品是“状态变更”的底线ACID 四个字母——原子性、一致性、隔离性、持久性——很多人只在学数据库时背过工作后觉得那是 DBA 的事。但只要你的系统里存在“一次操作要改多处状态”ACID 就是绕不开的底线。用转账这个老例子说清楚四件事。原子性指的是“扣钱和加钱要么都成要么都不成”不能出现扣了没加一致性指的是“转账前后总金额不变”这是业务规则层面的约束隔离性指的是“两个人同时转不会互相看到中间状态导致算错”持久性指的是“银行说转成功了哪怕下一秒机房断电这笔记录也不能消失”。关键在于这四个性质在业务层同样成立。一次下单要扣库存、生成订单、发券、记流水这四步如果只成了两步就是原子性被破坏两个用户抢最后一件库存都显示成功就是隔离性没做好接口返回成功但消息没发出去用户看不到订单就是持久性在业务语义上失守。把 ACID 当成思维模型而不是数据库参数你对“这次操作到底该放在一个事务里还是该拆开补偿”的判断会清晰很多。2.4 三条线合起来看它们都在给“不确定性”上锁单独看SRP 是模块级的约束第一性是思考方法ACID 是数据层的约束好像不搭。但放到一个真实项目里它们其实是在给三种不同的不确定性上锁需求会变所以要用 SRP 把变化圈在一个小范围里别让一次改动扩散出去、信息不完整所以要用第一性把已知事实摆清楚别在假设上盖楼、系统会出故障所以要用 ACID 保证出错的时候状态是可解释、可恢复的。这三件事有一个共同特征它们都是“事前”的工作做完之后看不见摸不着但省下的是“事后”的巨额成本。改一处崩三处的项目排查一次线上资损事故的成本折算成人力往往够重写半个模块了。3. 单一职责怎么落地从“上帝类”到能改得动的模块道理都懂难的是手上这个两千行的类怎么动手。我这几年拆过七八个类似的模块有几个做法是可以直接复用的。3.1 先识别职责过载的四个信号别凭感觉判断看信号。我总结下来有四个命中两个以上基本就该动手了。第一个信号是测试成本异常。你想给某个方法写个单元测试结果要 mock 掉五个依赖、准备三份测试数据写完发现测试代码比被测代码还长。这说明这个方法牵连的东西太多职责边界早就糊了。第二个信号是命名里出现连接词。OrderManager、DataUtil、CommonHelper、XxxAndYyyService这些名字本身就是投降声明——作者已经说不清它到底负责什么了。名字里带“Manager”“Util”“Common”的类十个里有八个是垃圾桶。第三个信号是不同角色在改同一个文件。去看提交记录如果同一个文件最近的提交者里有做订单的、做财务的、做营销的而且改的是不同区块那这就是职责过载的铁证。代码的变更历史比代码本身更能说明问题。第四个信号是改一个需求要动多个不相关的地方。比如“会员折扣规则调整”这么一个需求你要改算价、改下单校验、改退款重算、改报表口径四个地方分散在三个文件里改完还得祈祷没漏。这种情况说明“定价”这个职责被切碎撒到各处了跟职责过载是一体两面。3.2 按“变更来源”切不按“技术分层”切拆的时候最容易犯的错是按技术分层拆把数据库操作抽到 Repository把校验抽到 Validator把组装抽到 Converter。拆完之后文件是变小了但一次业务变更仍然要横跨五个文件改一遍因为这些层都是同一条业务链路的切片它们共享同一个变更理由。正确的切法是按变更来源切。还是拿订单举例我先画一张表把这个模块所有逻辑列出来标上“谁会来改它”逻辑块变更来源变更频率是否独立上线下单校验、创建订单交易产品高是算价、优惠叠加营销产品很高是支付回调处理支付渠道对接中是退款、售后客服与财务中是对账、报表口径财务结算低但影响大是表格一拉出来答案就明摆着了这五块是五个独立的变更源应该拆成五个模块各自有独立的接口、独立的数据访问、独立的上线节奏。拆完之后算价规则再变也碰不到退款那段代码我开头遇到的那种事故从结构上就被排除了。3.3 一段拆分前后的对照拆之前大概长这样一个方法包办到底class OrderService: def create_order(self, user_id, items, coupon_id): # 1. 校验 if not items: raise ValueError(empty) user db.query_user(user_id) if user.status ! active: raise ValueError(inactive) # 2. 算价含会员价、券、阶梯价200 行 total 0 for it in items: price db.query_price(it.sku) if user.level vip: price price * 0.95 total price * it.qty if coupon_id: coupon db.query_coupon(coupon_id) total total - coupon.amount # 3. 扣库存 落单 发券 发通知 db.deduct_stock(items) order_id db.insert_order(user_id, items, total) db.mark_coupon_used(coupon_id) mq.send(order_created, order_id) return order_id拆之后每个类只对一个变更源负责class OrderValidator: 只因为『下单准入规则变化』而修改 def check(self, user, items): if not items: raise DomainError(EMPTY_ITEMS) if user.status ! active: raise DomainError(USER_INACTIVE) class PriceCalculator: 只因为『定价与优惠策略变化』而修改 def calc(self, user, items, coupon): ... class OrderRepository: 只因为『存储结构或访问方式变化』而修改 def save(self, order): ... def deduct_stock(self, items): ... class OrderCreatedNotifier: 只因为『通知渠道或文案变化』而修改 def notify(self, order_id): ... class CreateOrderUseCase: 编排只因为『下单流程步骤变化』而修改 def __init__(self, validator, pricer, repo, notifier): self.validator, self.pricer validator, pricer self.repo, self.notifier repo, notifier def execute(self, user, items, coupon): self.validator.check(user, items) amount self.pricer.calc(user, items, coupon) order_id self.repo.save_with_stock(user, items, amount) self.notifier.notify(order_id) return order_id注意CreateOrderUseCase这一层是必要的它负责“流程编排”。很多人拆完之后把编排逻辑也拆没了结果调用方要自己按顺序调五个对象漏一步就出错——那是拆过头了。3.4 拆过头的三个坑我全踩过第一个坑是链路断裂。拆得太碎一个新同事想搞懂“下单到底发生了什么”要跳八个文件每个文件都只看到局部。我的应对是保留一个薄薄的编排层并且在编排层写上完整的流程注释让人从入口一眼看到全貌想深入再看细节。第二个坑是为了单一而造转发层。曾经有个项目里出现了OrderService调OrderManager调OrderHandler调OrderProcessor四层全是透传没一行有实际逻辑。这种拆法只增加了导航成本。判断标准很简单一个类的方法如果全部只有一行转发那它就不该存在。第三个坑是按技术拆而不是按业务拆。我前面说过这里再强调一次因为它太常见了。把校验拆成Validator、把计算拆成Calculator看起来很像 SRP但如果校验规则和计算规则永远同增同减、同一个产品经理提需求那它们其实共享同一个变更理由拆开反而制造了耦合。注意事项拆分粒度用一句话衡量——一个熟悉业务的人能不能在半小时内把这个模块的职责、输入、输出、依赖讲清楚。能就够细了不能继续拆拆完发现要讲半小时才能串起一条链路说明拆错了方向。4. 第一性原则怎么用把拍脑袋换成算一遍第一性原则听着玄其实是个很机械的流程。我把它拆成五步每次做方案评审都按这个走能挡掉大部分“过度设计”和“跟风选型”。4.1 五步倒推目标、约束、假设、推导、验证第一步写清目标。注意是目标不是手段。“让系统支持十万用户”是目标吗不是太模糊。“在峰值每秒两百次请求下读接口 P99 延迟低于 200 毫秒”这才是目标。第二步列出硬约束。硬约束是那些你改不了的事实预算上限、团队现有技术栈、上线时间点、数据合规要求、现有数据库的能力边界。硬约束越早摆出来越好因为方案空间是被约束框出来的。第三步剔除假设。这一步最费劲也最值钱。你要逼自己把“我们一直是这么做的”“行业里一般都用这个”这类句子划掉只留下能被验证的事实。我常用一个提问法这句话如果被证明是错的我们的方案会变吗会变那它就是假设需要验证不会变那它无关紧要可以忽略。第四步从约束推导。有了目标和事实方案通常只剩一两个合理选项。这一步不需要灵感只需要算术。第五步验证最小可行。别一次上全套。先用最小的成本验证核心假设跑通了再加。这一步能救很多命——我见过太多方案在第四步推得头头是道第五步一压测全崩。4.2 一个真实的容量估算比任何争论都有说服力回到前面那个配置下发的例子我把算术过程写完整你能看到第一性是怎么工作的。已知事实配置总量约两千条每条平均 2KB总共约 4MB平均每天变更两次客户端三千台业务能接受的配置生效延迟是 1 分钟。推导三千台客户端每分钟拉一次就是每分钟三千次请求折合50 QPS。每次返回 4MB 的话总带宽是 50 × 4MB 200MB/s这个就有点吓人了。但注意配置大部分时间不变所以可以用版本号做条件请求——客户端带上本地版本号服务端发现没变就返回 304实际响应体接近零。这样带宽压力基本消失50 QPS 对一个普通服务来说毫无压力。最后方案单实例 本地文件 版本号条件请求配一个定时任务做变更推送的唤醒。整个实现不到两百行没有引入任何中间件。如果当初按“配置中心都该用长连接推送”这个结论出发那就要维护连接数、心跳、断线重连、灰度发布工作量翻十倍不止。4.3 成本也要算而且要算全生命周期选型时只看开发工作量是新手最容易掉的坑。我在做第一性推导时会强制加一张全周期成本表把三年内的账一起算成本项方案A自研简单版方案B引入成熟中间件初期开发约 5 人日约 15 人日含部署调试学习曲线无纯业务代码团队 3 人各约 3 天运维负担复用现有服务几乎为零新增集群、监控、备份、升级故障排查自己写的日志齐全需要理解内部机制扩展上限十万级 QPS 以内够用更高但短期用不上三年总成本估算约 8 人日约 30 人日 持续运维这张表不是为了证明“自研一定更好”而是让决策显性化。如果业务确实需要百万级吞吐那方案 B 的额外成本就是必要投入如果只需要 50 QPS方案 B 的那二十多人日就是纯浪费。4.4 什么情况下别用第一性原则这点必须说清楚因为很多人学了第一性之后什么事都想从零推一遍效率反而低了。成熟标准方案直接抄。比如排序、哈希、基础的 HTTP 服务框架、常见的鉴权流程这些是被无数人验证过的最优解你从第一性推一遍大概率推不出更好的还容易推出 bug。强合规、强安全的领域别自作聪明。涉及资金结算、隐私数据处理的部分行业里有一套经过审计的规范流程照着做就行。这时候“我重新想了一遍”不是优点是风险。时间成本高的时候先求有再求好。周一要上线周五你还在推第一性那是自找麻烦。第一性原则适合用在“这个决策会影响三年”的地方不适合用在“这个决策下周就可能改”的地方。判断标准是这个选择错了返工成本有多高高值得推低先跑起来。5. ACID怎么延伸到业务层事务边界是设计出来的数据层的事务数据库帮你兜底业务层的事务得自己设计。这一块做不好出的都是资损级别的事故。5.1 四个字母在业务代码里的具体样子原子性在业务里对应的是“一组操作要么全做要么全不做”。下单场景扣库存、建订单、核销优惠券、记流水。这四步如果在同一个数据库里一个本地事务就解决了如果库存和订单在不同服务里就得靠补偿或者本地消息表。一致性是业务规则层面的不变量。比如“任何时刻所有账户余额之和加上在途资金等于总发行量”这是一条永远要成立的规则。它不靠数据库的约束来保证靠的是你把所有会改余额的路径都收拢到同一个入口并且在那里做校验。这就是 SRP 和 ACID 的交汇点——如果改余额的逻辑散落在五个地方一致性根本无从保证。隔离性在业务里是“并发操作不能互相污染”。最典型的两个人同时下单最后一件库存。数据库层面靠行锁或者乐观锁版本号解决业务层面还要考虑“用户点了两次提交”这种重复请求那是幂等问题不是隔离问题得分开处理。持久性在业务里是“承诺了就要兑现”。接口返回成功意味着后续所有异步流程发消息、发券、发通知都必须最终完成。这里最容易出事主流程提交成功消息发送失败用户看不到订单。解决办法是把“发消息”这个动作也纳入事务——本地消息表就是干这个的消息先写进同一个库的表中跟业务数据一个事务提交再由后台任务扫表投递。5.2 隔离级别的取舍别只记结论这一块面试常考但真正影响线上表现的是这四种隔离级别带来的锁行为和并发度差异隔离级别脏读不可重复读幻读并发度适用场景读未提交可能可能可能最高几乎不用仅用于极端统计场景读已提交不会可能可能高大多数业务查询的默认选择可重复读不会不会视实现而定中同一事务内需要多次读同一批数据串行化不会不会不会最低强一致要求的结算类操作选的时候不要背“默认用可重复读”这种结论问自己两个问题这个事务里会不会多次读同一行数据并依赖结果一致会就要可重复读以上。这个事务的并发量有多大、持锁时间有多长并发高、持锁长就往低级别靠用应用层的乐观锁替代数据库锁。5.3 长事务是性能杀手也是故障源我遇到过的数据库告警一半以上跟长事务有关。长事务的危害是双份的一是长时间持有锁把其他请求堵在门外二是回滚段膨胀拖慢整个实例。常见的长事务来源有这么几个。把外部调用放进事务里——事务里调了一个第三方接口对方超时三十秒锁就持有三十秒。把批量操作放进一个事务——一次性更新一万行看着优雅实际把锁的范围拉得极大。事务里做了重计算——循环里算价格、算积分算了两秒才提交。我的处理原则很直接事务里只放数据库操作而且只放必须原子的那几个。外部调用、消息发送、文件写入全都挪到事务外面用补偿或者消息表保证最终一致。批量操作拆成小批每批五百到一千条分批提交。# 反例事务里做外部调用 def pay_bad(order_id): with db.transaction(): order db.lock_order(order_id) resp http.post(https://payment.example/charge, {...}) # 可能超时 30s order.status paid db.save(order) # 正例外部调用挪出去用状态机 幂等键保证一致 def pay_good(order_id, idempotency_key): order db.get_order(order_id) if order.status paid: return order # 幂等重复请求直接返回 if not db.try_mark_paying(order_id, idempotency_key): return db.get_order(order_id) # 已被其他请求抢占 resp http.post(https://payment.example/charge, {...}) if resp.ok: db.mark_paid(order_id, resp.txn_id) else: db.mark_pay_failed(order_id, resp.code)5.4 幂等做不好前面全白搭只要你的接口可能被重试——不管是用户手抖点两次、客户端超时重发、还是消息队列重复投递——就必须做幂等。三种做法我都用过各有适用面。唯一键约束最省事也最可靠。给业务表加一个幂等键字段比如“订单号 操作类型”加唯一索引重复插入直接报冲突捕获了当成成功返回。缺点是幂等键的生成规则要提前设计好事后加字段很痛苦。状态机前置判断适合有明确生命周期的东西。支付单的状态是“待支付→支付中→已支付”任何一步都先查状态不符合前置条件就拒绝。注意这里必须用UPDATE ... WHERE status 待支付这种带条件的更新让数据库来保证原子性别用“先查后改”的两步走法那在并发下必然出错。去重表适合没有天然唯一键的场景比如消息消费。建一张表记录已处理的消息 ID处理前先插插失败就说明处理过了。这张表要定期清理不然会无限增长。注意事项幂等的有效期要跟业务对齐。支付类操作可能需要永久幂等而“查询类”操作压根不需要幂等。别一刀切给所有接口都加去重表那是给自己找麻烦。6. 三个原则落到安全、学习、节约上是什么样前面讲的是“怎么做”这一节讲“为什么值得做”。这三个维度是我评估一个技术决策值不值的时候最常用的三个尺子。6.1 安全原则是炸不穿的那层壳安全这个词在这里有两层意思。一层是狭义的信息安全权限校验、数据脱敏、越权防护。另一层是广义的系统安全故障不会扩散一次失误不会拖垮全局。单一职责在狭义安全上的价值特别直接。假如权限校验的逻辑散落在二十个接口里你几乎不可能保证每一处都写对了只要漏一处就是越权漏洞。正确的做法是把校验收拢到一个入口网关或者统一拦截器让“校验”成为一个只有单一职责的组件所有请求必须经过它。这样你要审计的只有一个地方。这就是为什么安全规范里总强调“统一入口”——不是官僚主义是把攻击面收窄。ACID 在资金类业务里就是安全本身。原子性保证不会出现“扣了钱没加钱”持久性保证“说成功了就一定成功”。这类系统里为了性能牺牲这些性质的方案我一般直接否掉因为一次事故的代价远超性能收益。第一性原则在安全上的用法稍微绕一点但很有用它能帮你识别“看起来有必要其实没必要”的安全措施。比如有人提议给所有接口加签名校验理由是“更安全”。用第一性拆一下这个接口是内网调用还是公网暴露调用方是不是只有固定的两个服务如果只在内网、调用方固定那签名带来的收益很小反而增加密钥轮转的运维负担。真正的风险点可能在别处——比如这个接口有没有做入参长度限制、会不会被用来做资源耗尽。6.2 学习用第一性原则搭骨架用单一职责管节奏学技术最容易陷入的状态是“收藏了一堆文章一个都用不上”。原因是从结论入手——学了某个框架的用法但不知道它解决什么问题。我的做法是先问“这个东西没出现之前人们怎么解决的痛点在哪”。比如学消息队列先搞清楚没有它的时候服务之间靠什么通信、会遇到什么问题同步调用链太长、高峰期扛不住、一个服务挂了拖垮上游。把痛点想透了再看消息队列提供了什么异步解耦、削峰填谷、广播。这时候它那些概念Topic、分区、消费组、offset就不是名词背诵而是“为了解决某个具体问题引入的机制”记起来毫不费力。单一职责在学习节奏上的用法是一次只攻一个方向。我见过太多人同时学三门语言、两个框架、一套云原生结果每样都停在“会写 Hello World”。按 SRP 的思路你某个阶段的学习目标应该只有一个变更理由——“我要能独立写一个带鉴权的增删改查服务”这个目标达成就停下来复盘再进下一个。学习计划里出现“同时学习 A 和 B”这种表述基本都会失败。ACID 对学习也有一层很妙的类比尤其是持久性。学的东西如果不落盘——不写成笔记、不写成代码、不写成可复现的 demo——它就像没提交的事务随时会丢。我的习惯是每学一个新东西强制产出一个最小可运行的样例放在自己的代码库里标注“解决什么问题、关键配置是什么、踩了什么坑”。这玩意儿攒两年就是自己的知识库比任何收藏夹都值钱。6.3 节约省的是人力、算力、还有注意力节约这个词容易被理解成“抠门”其实它指的是不浪费。三个原则各自省的是不同的东西。单一职责省的是变更成本。这个收益是复利的一个边界清晰的模块改一次的成本是一一个边界模糊的模块改一次要通读两千行、要跑全量回归、要担心副作用成本是十。一年改二十次差距是多少不用算。而且这里还有个隐性收益——新人上手成本。边界清晰的模块新人两天能改不清晰的两周都不敢动。第一性原则省的是过度设计。前面那个配置下发的例子算一遍省掉二十多个人日。这类节省在很多项目里都存在只是没人算过。我建议每个方案评审都强制带一段“容量与成本估算”哪怕只有三行数字。养成这个习惯之后团队里“上来就要集群、就要中间件”的声音会自然消失因为一算就露馅了。ACID省的是故障处理成本。这个最容易被低估。一次数据不一致的事故处理成本包括定位几小时到几天、修数要写脚本、要双人校验、要留审计、用户补偿、后续的信任修复。相比之下把事务边界设计对的成本可能只是多写二十行代码。这个投入产出比在任何项目里都是最高的。6.4 三者冲突的时候怎么排优先级现实里一定会有冲突。为了节约成本少做校验安全就打折为了学习新技术引入复杂方案就违背节约为了赶进度跳过拆分就把变更成本推给未来。我的排序是安全 节约 学习但这个排序有前提——这里的“安全”指的是不可逆的损失比如资金错误、数据泄露、用户权益受损。这类事情发生了没法回滚所以优先级最高。节约排第二是因为它影响的是可持续性。一个长期亏钱的系统技术再优雅也活不下来。学习排最后不是说学习不重要而是学习成本不该由生产系统承担。想试新技术可以在旁路项目、内部工具、灰度环境里试别拿核心链路当试验田。我见过拿主站练手的出了事故之后再也没人敢提“技术升级”这四个字反而把整个团队的技术迭代能力冻住了。7. 常见问题与排查技巧实录下面这些是我和身边同行真遇到过的问题按症状整理成速查表遇到类似情况可以直接对照。症状大概率原因排查动作处理方式改 A 功能导致 B 功能崩职责耦合共用私有方法看 B 的调用链是否穿过 A抽出共用部分明确接口边界一个类改一次要跑全量回归变更影响面不可控用提交记录看该文件的修改者分布按变更来源拆分模块事务超时、连接池被打满长事务持有锁查慢事务日志看事务内是否有外部调用外部调用挪出事务批量拆小用户被重复扣款接口缺幂等查请求日志是否有重复入参加唯一键或状态机前置判断并发下库存超卖隔离性不足看扣减语句是否带条件更新改条件更新或加乐观锁版本号接口返回成功但用户看不到数据主流程与异步流程不一致查消息是否投递成功本地消息表 定时补偿方案越做越复杂工期一再延没做容量估算按结论选型补一份目标与约束清单用最小可行方案先跑通拆完模块反而更难维护按技术分层拆而非按变更源拆数一条业务链路要跨几个文件按变更源重新划分新人一周都不敢改代码缺乏清晰的模块职责说明看模块是否有单一入口收敛入口补流程注释7.1 三个不写在文档里的排查心得第一个先看提交历史再看代码。代码只告诉你“现在是什么”提交历史告诉你“为什么会变成这样”。一个文件如果被五个人反复改而且每次改动都很小很碎那它的问题一定不是代码质量问题是职责划分问题。改代码解决不了得改结构。第二个用“画一条链路”的办法定位耦合。拿一张纸从请求入口开始把经过的每个方法画出来。如果这条线穿过五个以上的类、跨越三个业务概念那就是耦合过重。这个动作我做过很多次画到一半就能看出来问题在哪。第三个修复数据事故之前先想清楚怎么防止它再发生。线上数据错了第一反应是写脚本修数这没错但修完就完事的话下周还会再来一次。修数脚本和防重脚本应该同时写。7.2 我自己的月度自查清单每个月我会花半小时过一遍手上的模块问自己五个问题这个月改得最多的那个文件它的变更来源是几个如果超过两个下个月排期拆。有没有哪个接口的响应时间在悄悄变长如果有是有慢查询还是有外部依赖。有没有哪个流程的事务边界是“顺手加的”而不是设计出来的有没有哪个方案是“因为别人都这么做”而选定的如果有补一份估算。有没有哪个模块我已经不敢在没测试的情况下改了最后一问最灵。当你对某个模块产生“不敢动”的恐惧时那地方一定有问题而且问题通常不是技术难度是结构混乱。注意事项以上所有做法都有一个共同前提——你得先承认自己的代码会烂。抱着“我这套设计能撑十年”的心态什么原则都救不了。原则的作用是让系统烂得慢一点让修补的成本低一点仅此而已。最后分享一个我自己一直在用的笨办法每次做完一个模块我会在文件顶部写三行注释——这个模块负责什么、它在什么情况下会被修改、它依赖谁。三行写不出来说明这个模块我自己都没想清楚那就别急着提交先想明白。这个习惯坚持了几年最大的收获不是代码变好看了而是每次需求来的时候我能在一分钟内判断出这次改动会波及哪里、要不要提前拆。这种“心里有数”的感觉比任何设计模式都值钱。