后端绝不能做的几个危险实践

这些东西我没有一个是从书上看来的,全是踩过坑之后才明白为什么不能这么写。排名不分先后,纯粹是想到哪写到哪。 1. 只给ID,标题让前端自己一个个请求 场景很常见:前端要一个文章列表,后端图省事,GET /articles 只返回一堆 id,然后美其名曰"职责分离"——标题详情是另一个接口的事,你调 GET /articles/{id} 自己拼去。 结果就是列表页一进来,控制台里刷出几十个并发请求,首屏卡成PPT。这不是设计,这是把N+1查询问题原封不动甩给了前端,还顺便多绕了一层网络延迟。列表接口该把列表页需要的字段一次性拼好返回,这不是"过度设计",这是接口的基本职责。 2. 全表返回,不做分页 “反正数据量不大”——这句话是很多线上事故的开场白。今天不大,半年后呢?没有分页参数的接口,就是埋了一颗定时炸弹,炸的时候往往连负责人自己都忘了当初为什么这么写。哪怕现在数据只有几百条,分页也该在设计阶段就加上,代价几乎为零,省下的是未来某次大促时数据库被一个"查全部"接口打满连接池的命。 3. 自增主键直接暴露给前端 /api/order/1001、/api/order/1002……这种URL我见一次难受一次。别人改个数字就能把你家订单量、用户增长速度、甚至竞品的下单节奏摸得一清二楚,运气不好还能顺便遍历出别人的订单详情。对外暴露的标识符该用UUID或者做一层编码,自增ID留在库里自己用就好,它不欠任何人一次直接曝光。 4. 异常堆栈原样甩给前端 线上环境返回500的时候,body里直接是一段完整的Java堆栈,类名、包路径、SQL语句全在里面——这种接口调试起来是挺爽,问题是生产环境不该有这种爽。这相当于把系统架构图直接发给了任何一个愿意多点几次的人。异常该在网关或全局异常处理器那一层被拦下来,转成一句用户能看懂、攻击者看不出细节的提示,详细堆栈老老实实进日志。 5. 事务里塞一个外部HTTP调用 有一次在 @Transactional 方法里调了一个第三方短信接口,那接口偶尔抽风延迟到十几秒。事务没提交完,数据库连接和行锁全程占着不撒手,一个接口卡住,连接池被排队请求瞬间打满,整个服务跟着一起趴下。外部调用和数据库事务从来不是一路人,该放事务外面的就老老实实放外面,实在需要保证一致性,用消息队列和补偿机制去做,别指望一个事务把所有环节焊死。 6. 用GET做有副作用的操作 GET /user/delete?id=1——这种接口写出来的时候可能只是图方便,浏览器插件预加载、爬虫、CDN缓存,随便一个都可能替你把"删除"这个动作提前执行了。GET的语义是幂等和无副作用,这不是规范洁癖,是因为整条链路上一堆中间层都会按这个假设去优化和缓存你的请求。有副作用的操作,POST/DELETE该用哪个用哪个,别为了省事挑软柿子捏。 7. 返回体里带着密码字段 哪怕只是加密后的密码,哪怕前端根本用不上这个字段,只要它出现在响应体里,就是多一次泄露的机会。抓包工具随手一看,一串哈希也可能被撞库反查出来。查询用户信息的DTO,该在序列化层面就把敏感字段挡掉,而不是寄希望于前端"不会去看"。 8. 循环里一条一条操作Redis或数据库 for 循环里挨个 redis.set()、挨个 insert,数据量小的时候看不出毛病,数据量一上来,网络往返的耗时全部线性叠加,接口响应时间跟着数据量一起爆炸。批量场景该用 pipeline、批量插入,这不是性能优化的"进阶技巧",是写批量逻辑时候就该有的默认选项。 这几条共同的毛病,其实都是同一件事:图眼前方便,把成本转嫁给了未来的自己、前端同事,或者压根不认识的用户。后端接口设计里,“能跑"和"该这么设计"从来是两件事,分清楚这两者,大概就能避开清单里一半的坑。

2026-08-04 06:15:40 PM · 1 分钟
危险实践 后端