你写的代码能跑但真的“健壮”吗我见过太多项目上线后服务器CPU飙升、半夜数据库连接池爆满、接口返回500却找不到日志。这些事故背后往往不是业务逻辑有多复杂而是踩中了三个极其常见的错误。今天不聊虚的直接开撕。错误一把异常处理当成“面子工程”很多人在Controller或Service层写一个巨大的try-catch Exception然后稀里糊涂丢给前端一句“系统繁忙”。这种异常吞噬是后端代码的第一大杀手。你吞掉的不是异常而是问题的真相。真正致命的不是异常本身而是异常发生后你压根不知道哪里出了什么问题。比如一个空指针异常如果你在最外层直接扑捉Exception并返回那么栈调用链全部丢失排查问题的成本瞬间翻倍。更可怕的是某些业务中如果异常被吞噬但事务提交数据直接错乱。正确的做法是区分可恢复异常和不可恢复异常。对于不可恢复的如空指针、参数校验失败应当立即fail-fast不要试图去“挽救”一个已经崩坏的上下文。用一个全局异常处理器拦截但必须把原始异常栈以结构化日志输出。记住日志里的堆栈信息比任何业务代码都金贵。另外很多人在catch块里打印e.printStackTrace()然后return null。这等于告诉调用方我可能出了错但我不告诉你原因你忍着吧。宁可让接口报错也不要返回一个看起来“正常”但数据缺失的结果。因为下游系统会根据这个“正常”结果做决策最后酿成连锁反应。还有永远不要用return new Result().setCode(-1, 失败)来伪装业务错误。如果SQL执行超时你应该抛出TimeoutException而不是返回一个“自定义错误码”。自定错误码是给前端看的不是给代码逻辑看的。后端代码里异常就是异常不要试图用返回码代替异常机制。错误二把数据库当成“万能对象存储”我看过太多后端代码直接在Service层搞一个ListUser users userDao.findAll()然后在内存里for循环做各种过滤、聚合、计算。这种做法在数据量小的时候没问题一旦用户量增长到十万、百万响应时间直接崩。数据库的核心能力是关系与约束而不是你的业务计算引擎。很多人偷懒把所有数据拉到内存然后用Java的Stream流处理。这不是效率问题是架构问题。当你的业务需要“查询最近一周订单总额大于1000的用户”就老老实实用SQL写聚合查询而不是把所有订单查出来再在Java里算。还有个更隐蔽的错误N1查询。比如查一个订单列表订单里有商品ID你循环订单去查商品。如果订单有100条你就要查101次数据库。这是典型的上线前没问题、上线后爆慢的场景。解决方案很简单批量查询、使用IN子句、或者用Map预组装数据。但很多人就是懒得改觉得“先上线再说”结果成了事故的导火索。另外连接池的配置是一门玄学但很多人只写默认值。比如连接池最大连接数设为50但Tomcat线程池有200个线程那么高并发下200个线程争抢50个连接大量线程阻塞在等待连接上。更糟糕的是如果你没有设置connectionTimeout这些线程会无限期阻塞最终撑爆应用。连接池大小永远要小于线程池大小且必须设置合理的超时。还有事务滥用。很多人习惯在Service方法上一律加Transactional哪怕只有一个查询。事务会持有数据库连接直到方法结束如果方法里有远程调用或者IO操作这条连接就会一直被占用导致其他请求无连接可用。事务要精确控制只给真正需要原子性的操作加且事务内不要做外部调用。错误三在并发环境下写“自以为线程安全”的代码后端系统天生就是多线程的但很多人的代码还是单线程思维。比如用SimpleDateFormat格式化日期或者用一个HashMap做缓存。这些类不是线程安全的在高并发下会出现死循环、数据错乱甚至内存泄漏。最简单的规则无状态的对象天生安全有状态的对象必须 protected。如果你非得用一个全局集合做缓存请不要用HashMap改用ConcurrentHashMap或者带过期策略的缓存框架如Caffeine。而且当你往缓存里放数据时要考虑到多个线程同时写入的情况使用putIfAbsent而不是put或者用computeIfAbsent来处理原子性。更常见的问题是对共享变量没有加锁。比如一个统计在线人数的AtomicInteger你用incrementAndGet()没问题。但如果你用int做count那就是一个经典的检查然后更新check-then-act的竞态条件。count在字节码层面是三步操作读、加、写这三个步骤之间另一个线程可以插入导致结果少加一次。还有双重检查锁模式Double-Checked Locking很多人在单例模式里用但忘记了加volatile。如果不加volatileJVM可能会对new操作进行指令重排导致其他线程拿到了一个未完全初始化的对象。这是老生常谈的坑但每年都有新代码在踩。另外异步编程中的线程安全问题更隐蔽。比如用CompletableFuture或Reactive框架在回调中修改一个外部变量。由于回调可能在不同线程执行你无法保证变量可见性。正确的做法是尽量让函数无状态或者使用不可变对象传递数据。函数式编程的思想就是减少可变状态这本身就是对并发最好的保护。还有一个让运维头疼的问题在代码里直接new Thread().start()。生产环境一定要用线程池而且不能用Executors.newCachedThreadPool()这种无界的线程池因为当请求高峰时它会无限创建线程最终导致OOM。线程池的参数必须根据机器的核心数、任务类型CPU密集型还是IO密集型精心计算。如何“无痛”改造你的代码说了三个错误可能你觉得心累。但修正它们并不需要推倒重来只需要养成三个习惯第一把“防御性编程”刻在骨头里。每个方法的入口先做参数校验对输入的数据范围、非空、合法性做断言在调用数据库或第三方接口前就拒绝错误数据。这能避免大量底层异常。同时对你的代码边界做极限测试如果用户传了一个10GB的JSON会怎样如果缓存突然挂了会怎样如果数据库连接池被耗尽会怎样没有经过混沌工程考验的代码都是纸老虎。第二对每一次IO操作都设时限和熔断。数据库查询、Redis调用、RPC调用都要设置超时时间。不要相信第三方服务永远可用。使用Resilience4j或Hystrix之类框架做熔断降级但更重要的是不要让一个慢请求拖垮整个线程池。要有一个独立的线程池做外部调用并设置拒绝策略。第三保持代码的“可观测性”。不仅仅是输出日志还要埋点接口响应时间分位数、错误计数器、SQL慢查询日志、JVM内存使用率。用MicrometerPrometheus做指标收集用OpenTelemetry做链路追踪。当线上出问题时你不需要登录服务器查日志看一眼监控面板就知道问题在哪。结尾再说一句健壮的代码不一定是性能最好的但一定是故障恢复最快的。减少代码里的潜规则增加显式的错误处理和边界约束这比任何设计模式都管用。现在去检查你的项目看看有没有这三个错误。如果有别犹豫立刻改。线上事故不会等你重构完才来。
避免这3个常见错误,让你的后端代码更健壮
发布时间:2026/7/7 5:05:26
你写的代码能跑但真的“健壮”吗我见过太多项目上线后服务器CPU飙升、半夜数据库连接池爆满、接口返回500却找不到日志。这些事故背后往往不是业务逻辑有多复杂而是踩中了三个极其常见的错误。今天不聊虚的直接开撕。错误一把异常处理当成“面子工程”很多人在Controller或Service层写一个巨大的try-catch Exception然后稀里糊涂丢给前端一句“系统繁忙”。这种异常吞噬是后端代码的第一大杀手。你吞掉的不是异常而是问题的真相。真正致命的不是异常本身而是异常发生后你压根不知道哪里出了什么问题。比如一个空指针异常如果你在最外层直接扑捉Exception并返回那么栈调用链全部丢失排查问题的成本瞬间翻倍。更可怕的是某些业务中如果异常被吞噬但事务提交数据直接错乱。正确的做法是区分可恢复异常和不可恢复异常。对于不可恢复的如空指针、参数校验失败应当立即fail-fast不要试图去“挽救”一个已经崩坏的上下文。用一个全局异常处理器拦截但必须把原始异常栈以结构化日志输出。记住日志里的堆栈信息比任何业务代码都金贵。另外很多人在catch块里打印e.printStackTrace()然后return null。这等于告诉调用方我可能出了错但我不告诉你原因你忍着吧。宁可让接口报错也不要返回一个看起来“正常”但数据缺失的结果。因为下游系统会根据这个“正常”结果做决策最后酿成连锁反应。还有永远不要用return new Result().setCode(-1, 失败)来伪装业务错误。如果SQL执行超时你应该抛出TimeoutException而不是返回一个“自定义错误码”。自定错误码是给前端看的不是给代码逻辑看的。后端代码里异常就是异常不要试图用返回码代替异常机制。错误二把数据库当成“万能对象存储”我看过太多后端代码直接在Service层搞一个ListUser users userDao.findAll()然后在内存里for循环做各种过滤、聚合、计算。这种做法在数据量小的时候没问题一旦用户量增长到十万、百万响应时间直接崩。数据库的核心能力是关系与约束而不是你的业务计算引擎。很多人偷懒把所有数据拉到内存然后用Java的Stream流处理。这不是效率问题是架构问题。当你的业务需要“查询最近一周订单总额大于1000的用户”就老老实实用SQL写聚合查询而不是把所有订单查出来再在Java里算。还有个更隐蔽的错误N1查询。比如查一个订单列表订单里有商品ID你循环订单去查商品。如果订单有100条你就要查101次数据库。这是典型的上线前没问题、上线后爆慢的场景。解决方案很简单批量查询、使用IN子句、或者用Map预组装数据。但很多人就是懒得改觉得“先上线再说”结果成了事故的导火索。另外连接池的配置是一门玄学但很多人只写默认值。比如连接池最大连接数设为50但Tomcat线程池有200个线程那么高并发下200个线程争抢50个连接大量线程阻塞在等待连接上。更糟糕的是如果你没有设置connectionTimeout这些线程会无限期阻塞最终撑爆应用。连接池大小永远要小于线程池大小且必须设置合理的超时。还有事务滥用。很多人习惯在Service方法上一律加Transactional哪怕只有一个查询。事务会持有数据库连接直到方法结束如果方法里有远程调用或者IO操作这条连接就会一直被占用导致其他请求无连接可用。事务要精确控制只给真正需要原子性的操作加且事务内不要做外部调用。错误三在并发环境下写“自以为线程安全”的代码后端系统天生就是多线程的但很多人的代码还是单线程思维。比如用SimpleDateFormat格式化日期或者用一个HashMap做缓存。这些类不是线程安全的在高并发下会出现死循环、数据错乱甚至内存泄漏。最简单的规则无状态的对象天生安全有状态的对象必须 protected。如果你非得用一个全局集合做缓存请不要用HashMap改用ConcurrentHashMap或者带过期策略的缓存框架如Caffeine。而且当你往缓存里放数据时要考虑到多个线程同时写入的情况使用putIfAbsent而不是put或者用computeIfAbsent来处理原子性。更常见的问题是对共享变量没有加锁。比如一个统计在线人数的AtomicInteger你用incrementAndGet()没问题。但如果你用int做count那就是一个经典的检查然后更新check-then-act的竞态条件。count在字节码层面是三步操作读、加、写这三个步骤之间另一个线程可以插入导致结果少加一次。还有双重检查锁模式Double-Checked Locking很多人在单例模式里用但忘记了加volatile。如果不加volatileJVM可能会对new操作进行指令重排导致其他线程拿到了一个未完全初始化的对象。这是老生常谈的坑但每年都有新代码在踩。另外异步编程中的线程安全问题更隐蔽。比如用CompletableFuture或Reactive框架在回调中修改一个外部变量。由于回调可能在不同线程执行你无法保证变量可见性。正确的做法是尽量让函数无状态或者使用不可变对象传递数据。函数式编程的思想就是减少可变状态这本身就是对并发最好的保护。还有一个让运维头疼的问题在代码里直接new Thread().start()。生产环境一定要用线程池而且不能用Executors.newCachedThreadPool()这种无界的线程池因为当请求高峰时它会无限创建线程最终导致OOM。线程池的参数必须根据机器的核心数、任务类型CPU密集型还是IO密集型精心计算。如何“无痛”改造你的代码说了三个错误可能你觉得心累。但修正它们并不需要推倒重来只需要养成三个习惯第一把“防御性编程”刻在骨头里。每个方法的入口先做参数校验对输入的数据范围、非空、合法性做断言在调用数据库或第三方接口前就拒绝错误数据。这能避免大量底层异常。同时对你的代码边界做极限测试如果用户传了一个10GB的JSON会怎样如果缓存突然挂了会怎样如果数据库连接池被耗尽会怎样没有经过混沌工程考验的代码都是纸老虎。第二对每一次IO操作都设时限和熔断。数据库查询、Redis调用、RPC调用都要设置超时时间。不要相信第三方服务永远可用。使用Resilience4j或Hystrix之类框架做熔断降级但更重要的是不要让一个慢请求拖垮整个线程池。要有一个独立的线程池做外部调用并设置拒绝策略。第三保持代码的“可观测性”。不仅仅是输出日志还要埋点接口响应时间分位数、错误计数器、SQL慢查询日志、JVM内存使用率。用MicrometerPrometheus做指标收集用OpenTelemetry做链路追踪。当线上出问题时你不需要登录服务器查日志看一眼监控面板就知道问题在哪。结尾再说一句健壮的代码不一定是性能最好的但一定是故障恢复最快的。减少代码里的潜规则增加显式的错误处理和边界约束这比任何设计模式都管用。现在去检查你的项目看看有没有这三个错误。如果有别犹豫立刻改。线上事故不会等你重构完才来。