数据库永远是你最后一道防线,但千万别把它当成第一道防线。我维护过一个跑了三年的老系统,最深的体会是:所有能在代码层解决的问题,都不该抛给数据库去扛。比如分页查询,很多人习惯直接count()再查列表,可当数据量涨到千万级,这条SQL能把连接池拖到打喷嚏。后来我改成先查ID,再用ID批量查详情,响应时间从800ms降到120ms。这不是技巧,是态度。
别让实体类变成“万能口袋”
刚开始写后台,我喜欢设计一个超级Entity,字段从用户ID一直堆到最后登录IP,恨不得把整张表所有列都塞进去。前端要什么,我就从里面取什么。结果就是每次需求变更,实体类动一下,全链路跟着重新编译。后来我痛定思痛,实体类的职责边界必须清晰,一个类只对应一种业务语义,查询VO、更新DTO、持久化PO,各管各的。哪怕多写几个类,换来的是改动隔离。
有人觉得这样麻烦,可真正麻烦的是上线后,你发现一个字段的改动影响了十几个接口。后台系统的维护成本,多半不是加功能加出来的,而是改共用代码改出来的。我现在写代码前会先问自己:这个字段是给谁用的?谁允许改它?如果答案是“都有可能”,那它就不该放在公共实体里。
事务要么短,要么死
事务是后台系统最容易踩的坑。我曾经写过一个批量导入功能,一口气读五千条数据,每条都开一个事务插入,跑完花了四十分钟。后来把整个导入包进一个事务,速度快了,可一旦中途有脏数据,全部回滚,用户得重来。事务的粒度不是越小越好,也不是越大越好,而是短到刚好能保证一致性。
真正的经验是:把耗时操作(IO、远程调用、复杂计算)挪到事务外面,事务里只做写库和必要的校验。比如导入场景,先解析文件、清洗数据、做格式校验,最后开一个事务批量插入。如果某条失败,记录错误行号,而不是整体回滚。用“局部失败”代替“全局原子”,在后台系统里往往是更优雅的取舍。
还有一点:别在事务里调用外部接口。一次调用就算只超时三秒,事务就悬空三秒,锁就多占三秒,并发稍微一高,死锁就来了。
异常不是用来“吞”的
我见过不少同事喜欢catch(Exception e)然后log.error一下,代码继续跑。表面上看系统稳定了,实际上数据早就错乱了。异常是业务的一部分,不是系统的噪音。比如扣库存,库存不足抛个BizException,前端可以明确提示;可如果你把异常吞掉,用户看到“成功”,后台库存却变成负数,这比报错可怕一百倍。
我的习惯是:自定义统一异常,携带错误码和上下文信息。在Controller层用全局异常处理器兜底,返回统一格式。任何异常都要有“出口”,要么转成业务提示,要么触发熔断,要么记录审计日志。一个没有异常处理策略的后台系统,就像没有应急预案的消防队,平时看不出问题,出事了就是大事。
日志是系统的“黑匣子”,不是“碎纸机”
每次排查线上问题,我最怕看到两种日志:一种是几乎没有日志,只知道报错了,不知道在哪;另一种是全屏打印,从SQL到HTTP头全输出,但全是一堆无意义的debug。有意义的日志应该能还原一次请求的完整链路:谁调的、参数是什么、报了什么错、耗时多少。
我开发时会在关键节点打点,比如接口入口打印请求参数(注意脱敏),出口打印响应码和耗时;在service层打印业务状态变更;在调用外部服务时打印请求和响应摘要。上线后把这些日志接入监控平台,按traceId关联。真正省时间的不是写代码的速度,而是排错的速度。一个能三分钟定位问题的日志体系,比多写一千行代码都有价值。
缓存是止痛药,不是营养品
Redis用得好能飞,用不好会摔。我早期做缓存喜欢“先删缓存,再更新数据库”,结果并发下缓存击穿,数据库被打爆。后来改成“先更新数据库,再删除缓存”,配合短暂过期,问题解决了大半。但缓存最大的坑不在这儿,而在一致性。
缓存永远无法和数据库强一致,能做的只是降低不一致的概率和窗口。所以我会给缓存设置合理的过期时间,热点数据用双缓存策略,写操作通过消息队列异步刷新。更要命的是缓存穿透——一个不存在的key,每次请求都穿过缓存直击数据库。解决方案是布隆过滤器或缓存空值。记住:缓存是用来挡子弹的防弹衣,不是藏污纳垢的仓库。如果业务数据变更频繁,写并发高,那干脆别用缓存。
异步化:别让后台系统成为“人形同步工具”
后台系统里很多操作根本不需要同步等结果。比如发送通知、更新统计报表、生成历史快照。早期我写代码喜欢线性执行:先做A,再做B,再通知C,客户端等到全部完成才返回。用户体验差,系统吞吐量也低。后台系统的核心能力之一,就是把“必须立即响应”和“可以延后处理”的逻辑拆开。
我用Spring的@Async加上自定义线程池,把那些不重要的任务丢到后台慢慢跑。但线程池必须隔离:写操作一个池,读操作一个池,异步任务又一个池。否则一个慢任务把线程池占满,核心接口全部阻塞。异步化不是把所有东西都扔出去,而是给不同的任性任务划分车道。另外别忘了幂等性——异步任务可能重复执行,接口要做好重复消费的防护。
配置永远比编码灵活
线上问题一半以上是配置不当引起的。连接池大小、超时时间、重试次数、开关阈值,这些参数如果能硬编码,那系统就失去了自我调节的能力。我现在会将关键参数全部外置到配置中心,用@ConfigurationProperties绑定到配置类。任何环境差异,都应该由配置来消化,而不是靠程序员写if-else。
有一回线上应用频繁超时,排查半天发现是连接池默认最大活跃数只有10,一个高峰期就满了。当时如果配置在代码里,还得发版;配置化之后,直接改配置中心,优雅地扩容。配置管理不是锦上添花,而是后台系统的呼吸管道。
前后端分离不是“甩锅”开始
后台系统开发中,接口设计经常成为前后端矛盾的焦点。我踩过的坑是:为了“灵活”,接口返回Map<String, Object>,前端爱取什么取什么。结果前端换个人,没人知道Map里到底有哪些键。接口的首要义务是稳定和自描述,而不是迁就调用方的随意。我更习惯约定明确的DTO,字段类型、命名、是否必填全部体现在API文档里。
还有接口版本管理。曾经有个系统没做版本,前端改了字段名,后端改了返回结构,上线后互相甩锅。后来每条接口都带上版本号,旧版本保留一段时间,兼容期过后再下线。后台系统的优雅迭代,靠的是契约精神,既约束自己,也约束调用方。
监控不是“事后诸葛”
很多系统都是出了问题才去查日志,被用户投诉才想起看监控。真正的监控应该是预防性的。你要能提前看到连接池使用率逼近80%,看到慢SQL数量曲线在上升,看到内存消耗在稳步爬坡。这些指标不会立即让系统挂掉,但预警能给你抢出时间。
我开发时会用Micrometer加Prometheus,把关键指标暴露出来:QPS、RT、错误率、JVM内存、线程池活跃数、数据库连接数。再配上告警规则,比如成功率低于99.5%就通知。上线不是终点,监控才是运维的起点。后台系统的强者,不是不犯错,而是在错误发生前就看到了征兆。
代码评审不只是“找茬”
独自写代码时,很容易陷入“自己的逻辑自己信”的怪圈。代码评审不是为了打压谁,而是借助他人的视角发现盲区。我经历过一次痛苦的代码评审:我写的批量删除接口,没考虑子表外键关联,评审时被指出来,当时觉得小题大做。结果上线一个月后,真有客户删除主数据,关系表里残留了一堆孤儿记录。代码评审查出的每一个问题,都是一次生产事故的预演。
我更看重评审时的“可读性”:变量名是否清晰,方法是否足够短,分支是否复杂到需要注释。能让人一眼看懂的代码,往往比花哨的设计更可靠。所以每次提交MR,我都会自己先当一次reviewer,从“一个月后接手的人”的角度审视自己的代码。
回归测试是最便宜的保险
后台系统改动频繁,往往改一个接口,牵动十几个调用方。如果每次只是“自测通过”就完事,迟早会在不痛不痒的地方翻车。我的经验是:为核心业务路径写自动化回归测试,重点关注状态流转、金额计算、权限控制这三类易错逻辑。测试不是开发之后的仪式,而是开发过程中的一个环节。
我会在写代码前先想好测试用例,至少覆盖正常路径、边界路径、异常路径。但也不必盲目追求覆盖率,把Controller、Service、Repository层层都测一遍,成本太高。用20%的测试代码覆盖80%的崩溃风险,剩下的交给监控和日志。
最后,代码给你写的其实是“未来的自己”
每写一行Java代码,要想的不是今天能跑通,而是半年后的人能不能看懂、半年后改需求时能不能不动筋动骨。我见过太多烂代码,一眼看过去全是技巧,却没有一丝克制。精彩的后台系统,不是炫技的舞台,而是稳健的基座。它要经得起流量冲击,扛得住需求变化,还得让后来者能轻松接手。
技术永远在更新,框架一年换一个,但经验的沉淀是恒久的。你可以不追新版本,但不能没有自己的判断标准。我衡量一段代码的好坏,只有一个朴素的标准:如果明天这个模块出了问题,是否能在半小时内定位到根因。做不到,就是积累还不够。
后台系统开发是一场没有终点的马拉松,摔过的坑、调优过的SQL、救过的线上事故,都会变成你气定神闲的资本。哪怕只是积累了一点点经验,也值得认真对待每一次设计、每一行代码、每一个不眠夜。这就是我为什么热爱Java后台开发的原因——它枯燥,但足够真实。