
I got a bad idea.. 这句话如果是从同事嘴里蹦出来的接下来的剧本通常很熟悉代码评审里出现一段绕开框架规则的逻辑或者线上数据库被手工 UPDATE又或者为了赶一个需求选择在业务代码里写满特殊分支。程序员说这句话的时候往往不是真的觉得自己“想法很坏”而是已经意识到它要走近路、要绕规则只是还没准备好为后果买单。更麻烦的是这种想法在当下通常真的能跑通能少写几张表能省一个中间件能绕过安全拦截能让任务今天上线。但技术系统从来不是“跑通一次”就结束的。今天看起来精巧的偷懒会在三个月后变成事故报告里的根因。这篇文章不想喊“不要有坏主意”这种正确废话而是想讲清楚坏主意为什么容易出现、它们到底坏在技术链条的哪个位置、怎么把一个坏主意拆解成有边界的正经方案。读完你会得到一套判断方法在下次说出 “I got a bad idea” 之前先自己过一遍。本文面向的读者是做开发一到五年的工程师、开始参与技术选型和评审的同学以及需要为线上稳定负责的运维或 SRE。如果你的项目已经在用微服务、消息队列、配置中心这类基础设施下面提到的场景你大概率见过其中几个。1. 坏主意到底坏在哪里先看代价结构先说一个容易误解的地方坏主意不等于“蠢主意”。想出坏主意的人往往不是技术差恰恰相反很多坏主意需要足够熟练才能想到——因为你要清楚框架的漏洞在哪里、数据库的连接串存在哪个文件、线上环境能不能绕过审核。坏主意的本质是在约束识别上出现了系统性偏差。它通常表现为用最小代码改动解决眼前问题却把复杂度、风险、维护成本转嫁给未来的自己和团队。要判断一个想法是不是坏主意最直接的方式是看它的代价结构。好的技术方案代价是前置的。也就是说开发期多花时间、多写文档、多做验证运行期反而平稳。坏主意正好相反开发期很爽代码很少上线很快但运行期代价全是暗雷例如某个隐藏全局变量导致服务重启后状态丢失手工修改数据导致对账永远不平定时任务代替消息队列导致数据重复处理跳过安全校验导致接口被遍历调用。坏主意之所以容易被提出来是因为大脑天然更擅长估算“即时收益”不擅长估算“延迟风险”。你看到的是今天少写两百行代码看不到的是下个季度排查数据不一致时烧掉的三个通宵。所以这篇文章后续所有的讨论都围绕一个中心判断评价一个技术想法的好坏不是看它现在能省多少事而是看它为省这些事把风险推给了谁、推到了什么时候。2. 为什么程序员会忍不住说出“I got a bad idea”理解坏主意的成因比单纯记住“这样不行”更重要。因为只有知道它从哪里来你才能在它出现时抓住它。从认知层面看主要有三个心理机制在起作用。第一个是乐观偏差。人们倾向于高估自己解决问题的能力低估意外发生的概率。尤其当你刚写完一个复杂模块自信心处于高位时会更容易觉得“这个简单的绕过方案不会出问题”。但线上环境最大的特点就是变量多磁盘满、网络抖动、并发请求乱序、依赖服务超时任何一个变量都能让“简单方案”翻车。第二个是可用性启发。当一个人最近刚刚学过某个新技术或者刚看到了某个炫酷的解决方案他会不自觉地高估这个方案的适用性。常见表现是因为刚用了 Redis就把所有需要共享状态的场景都往 Redis 里塞因为刚接触消息队列就让所有系统间通信都走一遍 MQ。工具本身没错错的是“手里有锤子看什么都是钉子”。第三个是沉没成本效应。代码已经写了一半接口已经联调完了这时候如果发现方向有问题很多人的第一反应不是“及时止损”而是“再补一个判断把它绕过去”。这种补丁式修复往往就是一个坏主意走向线上事故的最后一步。从工程执行层面看压力同样是坏主意的催化剂。临近 deadline 时人自然会倾向于选择改动最小、最不需要协调的方案。简历导向也会让人倾向于引入“看起来有噱头”的技术而不是最稳妥的技术。这些动机本身没有恶意但它们会悄悄替换掉你的判断标准你不再问“这个方案是否合适”而是问“这个方案是否省事”。更隐蔽的是目标与手段的混淆。比如目标是“保证订单状态最终一致”手段却不一定是“引入消息队列”。但很多人在讨论中会直接把某个手段当成目标为了上消息队列而上消息队列或者为了不用消息队列而硬写轮询。讨论一旦变成手段之争真正的约束清单就被抛到脑后了。3. 高频坏主意盘点看起来聪明实际埋雷下面盘点几类我在各种项目里见过的高频坏主意。每一类都会说明表面收益、真实代价、更优做法。3.1 生产环境手工执行 SQL 修改数据这是最经典的一类。某天线上订单状态不对运营催着要结果。你查了一下发现是之前一次发布把状态字段写错了。最快的方法是什么连上生产数据库执行一条 UPDATE。表面收益几分钟搞定问题不用发版不用走流程。真实代价没有经过评审和测试SQL 的 WHERE 条件可能漏了数据类型或租户维度导致误更新。手工操作不一定会记录在案后续复盘时根本不知道谁改过数据只能靠猜。生产数据库权限一旦放开就会形成习惯越到后来越难以收回。如果涉及核心交易数据一旦更新错误恢复成本极高。更优做法任何数据订正都必须走脚本化、版本化的流程。脚本放在代码仓库里经过评审在测试环境验证再在干跑模式下预览影响行数最后才在生产执行。而且脚本必须支持幂等和回滚。3.2 用定时任务轮询数据库代替消息队列当系统规模还小的时候最怕听到的话是“不就定时扫一下表嘛搞什么消息队列”。确实很多系统中都有“待处理表 定时任务”的简化方案它能工作但边界非常明确低吞吐、能容忍分钟级延迟、没有复杂重试策略。问题出在“先用定时任务后面再改”这个“后面”通常会无限期延后。等到业务增长后定时任务会带来几个连锁问题轮询间隔越压越短数据库压力越来越大处理失败的消息被反复捞起没有退避策略导致死循环多条任务实例同时运行出现重复消费消息量突增时任务处理不过来队列无限积压。更优做法如果你已经在使用消息队列不要让“简化”退化成定时扫描。如果暂时不想引入消息队列也要把任务设计成具备“分批拉取、并发控制、失败重试、死信隔离”四个能力而不是简单遍历一张表。3.3 在业务代码里手工拼权限判断安全相关的逻辑最容易被“业务开发顺手实现”。典型场景是后端直接在前端传来的用户角色字段上做判断或者把登录状态放到一个普通 Cookie 里又或者在每个接口里重复写一段鉴权代码。表面收益不用理解框架的安全机制不用配置拦截器代码量看起来更少。真实代价每个接口的鉴权逻辑不一致有的忘了加有的加错了。前端可以伪造字段导致越权访问。安全漏洞出现在代码层面时很难通过加一个拦截器统一修复必须逐个接口排查。更优做法认证和授权必须下沉到统一层。无论是 Filter、Interceptor 还是框架自带的安全模块都应该在业务代码之外统一处理。业务代码只关心“当前用户是什么角色、具备什么权限”而不应该关心“这个角色是从哪里来的、如何被验证的”。3.4 为了“性能优化”先更新缓存再更新数据库很多人刚接触缓存时都会犯一个错误为了减少数据库压力先把最新数据写进 Redis再异步更新数据库。如果缓存写入成功、数据库更新失败缓存和数据库就产生了永久不一致。表面收益读取速度变快写入看起来也没问题。真实代价缓存一旦和数据库不一致所有依赖这条数据的业务都会出错。比如库存扣减成功但数据库没扣最后超卖比如配置修改成功但数据库没改重启后配置丢失。更优做法默认情况下应该先更新数据库再删除缓存也就是 Cache-Aside 模式。删除缓存失败时也要有补偿机制比如延迟双删、消息队列监听 binlog 等。缓存永远是为读服务的写路径必须以数据库为准。3.5 在内存里做全量排序和分页这个坏主意经常出现在 CSV 导出、报表查询、后台列表等场景。程序员发现写一条 SQL 里复杂的分页和排序条件太难或者数据库查询比较慢于是想到先把所有数据查出来放到内存里用 Java Stream 或 Python 排序再手动截取分页。表面收益简化 SQL逻辑用代码能写得更灵活。真实代价数据量从一万涨到百万时内存直接被打满。全表查询把压力转移到应用程序数据库连接被长时间占用。一旦遇到大结果集系统延迟和 GC 都会明显恶化。更优做法排序和分页尽量下沉到数据库完成。如果单表数据量确实很大应该走索引优化、聚合表或搜索引擎而不是在应用内存里硬扛。需要导出的场景也不建议一次性加载全量数据应该使用分批查询 流式写入。3.6 复制粘贴一个模块然后改几十个字段当你需要给某个新场景写一段与你刚写过的模块功能类似的代码时最省事的做法是复制上一个文件全局替换名字然后修改差异字段。很多人下意识觉得这样“快、稳、不影响老代码”。表面收益不用重新设计改动直观。真实代价复制出来的模块会带上旧模块的历史包袱比如旧逻辑里的 bug、废弃字段、无效分支。公共逻辑一旦需要修改你必须同步改 N 个副本漏掉一个就出问题。代码评审时很难发现副本之间的细小差异因为这些差异往往是业务上的核心差别却被淹没在大段重复代码中。更优做法先识别重复部分的稳定边界抽取公共方法、基类或组合工具类。如果两个场景的差异足够大那也不应该通过复制来扩展应该明确写出新模块的独立设计。4. 坏主意与好方案的边界一套可复用的决策框架前面列了六类常见坏主意但实际工作中判断一个想法是否可行不能只靠经验类比。你需要的是一套能快速执行的评估框架。我建议在动手之前用以下五个维度给想法打分评估维度要回答的问题出现什么情况必须警惕影响范围这个改动会影响哪些系统、哪些数据影响面不明确或者影响面远超预期恢复成本如果失败了系统能否快速回到原状态没有回滚手段或者回滚需要手工处理数据可观测性运行时是否容易判断方案是否正常没有日志、没有监控失败难以被发现安全边界是否绕过了认证、权限、审计等控制需要临时开放权限或者跳过安全校验认知成本团队里的其他人能否理解和维护只有提出者能解释其他人完全看不懂当五个维度里有两个以上亮起红灯这个想法大概率就是坏主意。但坏主意并非只能被丢弃。它之所以能诞生通常是因为它捕捉到了一个真实的痛点。正确做法是保留它试图解决的问题替换掉它有问题的实现手段。具体可以按四步拆解第一步写下你真正想解决的问题。不要写“我不想引入消息队列”而要写“我需要保证订单状态变更在异常情况下也能最终一致”。第二步列出全部约束。比如团队对消息队列不熟、当前服务器资源不足、不能改动数据库表结构、上线窗口只有两小时。约束列得越全方案讨论越容易落地。第三步选择最小可行的替代方案。用少量改造优先满足约束而不是一开始就追求最优雅的架构。第四步给方案加上安全边界。如果一定要采用某个“有点冒险”的思路就必须同时准备测试环境验证、干跑模式、日志监控、回滚脚本、最小权限。另外有几个信号可以帮你早点意识到“这是坏主意”你正在说服自己“应该没事”你需要关闭某个校验或告警才能跑通你把问题归因于其他系统太复杂而不是自己的方案过于简单你无法一句话说清失败后的影响。5. 把一个坏主意改造成正经方案完整示例下面用一个具体场景演示完整改造过程。场景是订单表中有一批历史数据的状态字段需要批量修正将created_at在指定时间之前的pending订单调整为cancelled。5.1 坏主意版本生产环境直接 UPDATE很多人在紧急情况下会直接打开数据库客户端执行类似下面的 SQLUPDATE order_table SET status cancelled WHERE status pending AND created_at 2024-01-01 00:00:00;这段 SQL 从语法上看没有问题但它的风险是隐性的没有备份被修改的行没有限制单次影响行数没有记录变更批次无法回滚如果这段 SQL 是在生产主库执行一旦 WHERE 条件写错影响面是全部的 pending 订单。正确做法是把变更脚本放到代码仓库带批次号和回滚逻辑先在测试环境验证再通过干跑模式查看影响行数最后在生产执行。5.2 坏主意版本Python 循环逐行更新比直接执行 SQL 稍微“稳妥”一点的做法是写一个 Python 脚本循环更新。很多人会把脚本写成这样# 不推荐的示意全表扫描 单条更新 无事务控制 from sqlalchemy import create_engine, text engine create_engine(mysqlpymysql://user:passlocalhost/db) with engine.connect() as conn: rows conn.execute(text( SELECT id FROM order_table WHERE statuspending AND created_at 2024-01-01 )).fetchall() for r in rows: conn.execute(text( UPDATE order_table SET statuscancelled WHERE id:id ), {id: r.id})这个版本比直接 UPDATE 多了一点“可控感”但仍然是坏主意因为它存在如下问题一次性把所有 id 加载到内存数据量大时照样可能导致 OOM不能重复执行如果脚本中途失败重跑会重复更新已经更新的行没有日志执行后只知道“跑完了”不知道处理了多少行、哪些行失败没有干跑模式测试环境验证不方便没有批次维度无法在出问题时快速回滚。5.3 改造后的正式方案改造的目标不是把脚本变复杂而是让它在生产环境可执行、可观察、可回滚。下面是一个结构示意请根据实际项目的数据库驱动、表名和字段名调整。# batch_fix_orders.py import argparse import logging import time from datetime import datetime from sqlalchemy import create_engine, text logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s, ) logger logging.getLogger(batch_fix_orders) def get_engine(dsn: str): return create_engine(dsn, pool_pre_pingTrue) def run_batch(engine, batch_id, status, dry_run, page_size, sleep_seconds): total_affected 0 processed 0 # 以 id 为游标分批拉取避免一次性加载全量数据 last_id 0 while True: rows engine.execute(text( SELECT id FROM order_table WHERE status :status AND created_at 2024-01-01 00:00:00 AND id :last_id ORDER BY id LIMIT :limit ), {status: status, last_id: last_id, limit: page_size}).fetchall() if not rows: break for row in rows: if not dry_run: # 单行更新前先记录日志方便审计 logger.info(batch%s update order_id%s, batch_id, row.id) engine.execute(text( UPDATE order_table SET statuscancelled WHERE id:id AND status:status ), {id: row.id, status: status}) processed 1 total_affected len(rows) last_id rows[-1].id logger.info(batch%s processed%d last_id%d, batch_id, processed, last_id) time.sleep(sleep_seconds) logger.info(batch%s done. total_affected%d dry_run%s, batch_id, total_affected, dry_run) return total_affected def main(): parser argparse.ArgumentParser(description批量修正订单状态) parser.add_argument(--dsn, requiredTrue, helpSQLAlchemy 连接串) parser.add_argument(--status, defaultpending, help要修正的当前状态) parser.add_argument(--dry-run, actionstore_true, help干跑模式只统计不修改) parser.add_argument(--page-size, typeint, default500, help每批处理行数) parser.add_argument(--sleep, typefloat, default0.1, help批次间隔秒数) parser.add_argument(--batch-id, defaultdatetime.now().strftime(%Y%m%d%H%M%S), help批次号) args parser.parse_args() engine get_engine(args.dsn) try: total run_batch( engine, batch_idargs.batch_id, statusargs.status, dry_runargs.dry_run, page_sizeargs.page_size, sleep_secondsargs.sleep, ) print(f处理完成, 共影响 {total} 行) finally: engine.dispose() if __name__ __main__: main()这个脚本相比坏主意版本增加了几个关键能力分批拉取避免一次性加载全部数据每次更新都记录批次号和订单号方便审计支持 dry-run 模式可以先预览影响范围更新语句带AND status:status保证幂等不会重复覆盖新状态。执行和验证流程如下先在测试环境做干跑python batch_fix_orders.py \ --dsn mysqlpymysql://user:passtest-host/db \ --status pending \ --dry-run \ --batch-id 20240101-test确认影响行数符合预期后再在生产环境执行。执行前后都应该做数据校验-- 执行前确认待处理范围 SELECT COUNT(*) AS total_pending, MIN(id) AS min_id, MAX(id) AS max_id FROM order_table WHERE status pending AND created_at 2024-01-01 00:00:00; -- 执行后应该为 0 SELECT COUNT(*) FROM order_table WHERE status pending AND created_at 2024-01-01 00:00:00;如果执行过程中发现异常应该先停脚本再根据批次日志定位到具体订单评估是否需要回滚。回滚本身也应该是一个脚本而不是手工 SQL。所有操作在开始前都需要确认你有变更生产数据和执行脚本的合法授权并且在测试环境完整演练过。6. 开工之前快速识别坏主意的自查清单你当然不可能每次做技术决策都写一份正式文档。但你可以养成一个习惯在写下第一行代码之前花五分钟问自己几个问题。下面是建议的自查清单自查问题判断标准我是否完全理解这次改动的影响范围如果答不上来先停下来补分析改动是否绕过了已有的安全、权限或审计机制任何绕过都必须是临时的而且要有人知道如果失败我能否在十分钟内回滚如果不能你需要先设计回滚方案同样的逻辑能否在测试环境完整验证不能验证的方案上线前风险极高除了我之外团队其他人能否看懂并维护只有你能解释的方案是一种长期负债我是否在为了赶时间而省略某些步骤省略验证和测试通常是事故的开始这个方案是否建立在某个假设上假设必须被明确写出来并逐个确认我是否曾经反复告诉自己“应该没事”出现这种心理时多数情况下是好事多磨你也可以在团队里设置一条简单的“五分钟原则”当有人提出一个听起来很冒险的想法时先用五分钟把约束、影响面和回滚方案讲清楚。讲不清楚说明这个想法还没有成熟到可以动手。7. 常见问题与排查思路在实施这类方案时你可能会遇到下面这些问题。问题现象可能原因排查方式解决方案批量脚本跑完后数据对账不平更新语句缺少幂等条件重复执行覆盖了新状态对比批次日志、导出受影响记录在 UPDATE 中始终保留原状态条件生成新的批次测试环境正常生产环境行为不一致生产环境数据量更大、权限配置更严格、依赖服务超时检查生产日志、慢查询、监控指标先用 dry-run 模式跑一遍抓取真实影响量后再执行代码评审时别人总说我的方案有问题方案只考虑了个人实现路径未覆盖异常场景和职责边界在评审前先自测一遍异常场景主动补充影响面分析、回滚方案和测试用例紧急修复后服务启动失败修改配置时漏掉了关联项或配置中心未同步查看启动日志、检查配置变更记录先回滚配置恢复服务再通过配置中心生效缓存和数据库数据不一致写顺序错误或删除缓存失败后没有补偿对比缓存 key 和数据库时间戳改为先更新数据库再删缓存加入延迟双删或 binlog 监听补偿大多数线上问题都不是“方案看起来完全不可行”而是“方案的失败模式没有被提前考虑”。排查坏主意相关问题时最有效的方式不是马上改代码而是先把完整的时序和影响面画出来。8. 让坏主意安全落地工程实践与团队建议坏主意本身也有价值。它往往代表一个人对现状的思考甚至是在尝试突破流程的冗余。完全禁止“坏主意”的团队会走向另一个极端不敢尝试、不敢提出异议、所有事情都走流程最后变成流程严谨但没有创造力。所以更合理的做法不是消灭坏主意而是让坏主意在可控的范围内被安全地讨论、测试和落地。技术层面有几条实践经验值得固化下来。第一数据库变更必须脚本化、版本化、可回滚。直接在生产环境执行 SQL 应该被视为严重违规。即使是一次性数据订正也要走代码仓库、评审和测试环境验证。第二安全逻辑一律统一封装。认证、授权、操作审计这类横切关注点不能散落在业务代码里。可以把它们封装成公共组件或框架中间件让业务开发无法绕过。第三重要变更必须有可观测性设计。上线前就要想清楚这次变更通过哪个指标判断成功出问题后从哪份日志开始排查是否已经有告警覆盖如果答案都是“没有”说明方案还没有达到上线标准。第四追求可回滚性。很多时候一个方案被判定为“坏主意”不是因为思路不对而是因为失败后没有退路。只要你能在十分钟内安全回滚很多高风险方案本身就变得可以接受。团队层面可以尝试建立“架构决策记录”的轻量文化。不一定要写很长的文档但至少要记录决策背景、当前约束、可选方案、为什么选择这个方案、可能的风险。这份记录的价值不只是让后人看懂更是倒逼提出者在动手前把问题想清楚。对不同阶段的开发者我的建议侧重点也不同。入门和中级开发者重点应该放在“找约束”上。遇到一个看似省事的方案时先问自己这个方案违背了项目里哪些既有规则这些规则是不是为了应对线上问题才建立的只要养成这个习惯就能避开大部分基础坏主意。主力开发同学重点应该放在“影响面判断”上。你已经有能力写出能运行的代码下一步是判断一段代码在线上运行三个月后会不会变成事故。多参加代码评审多读历史故障复盘比看任何新技术教程都有效。架构师或技术负责人重点应该放在“流程设计”上。与其每次去拦截坏主意不如设计一套机制让任何有一定风险的设计变更都能被低成本验证。比如强制要求数据订正脚本进仓库、强制重要接口有权限审计、强制上线前有回滚方案。9. 总结与后续学习方向“I got a bad idea..” 这句话真正危险的地方不是“idea”本身坏而是它出现的时机通常是在你已经说服自己“可以走捷径”之后。技术判断一旦开始偏向“省事”就很难再停下来认真评估风险。这篇文章的核心观点可以浓缩成一句话评价一个技术决策的好坏不取决于它当前省了多少事而取决于它把风险推给了谁、推到了什么时候。坏主意通常是那些风险后置的方案而好方案则会把风险前置到开发、测试和评审阶段尽量让线上阶段保持平稳。如果你现在正在犹豫某个方案是否可行建议先做三件事把方案的目标和手段分开列出你的约束清单然后跑一个最小验证。这三件事做下来大部分坏主意都会自己暴露问题。后续可以继续深入学习的方向包括系统设计与架构权衡、稳定性工程与故障复盘、安全编码与权限设计、代码评审方法与团队协作流程。这些方向不会教你怎么写一个“更高级的坏主意”而是帮你建立一套稳定的判断力。建议把这篇文章收藏备用。下一次你或你的同事说出 “I got a bad idea” 的时候先别急着动手把上面几条自查清单过一遍可能真的会少一次线上事故。