聊聊技术探索与实践:一个传统企业后端开发的碎碎念
大家好,我是老张(不是那个卖课的老张),在一家传统制造业干了三年多Java后端。每天早上八点打卡进办公室,咖啡还没喝完,产品经理已经在群里@我:“这个需求今天能上线吗?”——别问,问就是双11压测又崩了,运维兄弟凌晨三点发朋友圈:“谁改的缓存策略?Redis内存打满啦!”
最近其实有点躁动。待了三年多,系统还是那套Spring Boot + MyBatis + Oracle的“祖传三件套”,业务逻辑写得跟意大利面条似的,连DTO和VO都混着用。眼看同届的哥们跳槽去互联网公司拿30K+,我连LeetCode都没刷满50题……于是今年年初,咬咬牙决定:不能只做CRUD Boy了,得搞点技术深度出来。
这篇文章,就是我在“被迫成长”过程中的一些实践总结,也顺便整理些面试时被问到的技术点——毕竟,想跳槽,总得有点能吹的资本不是?
从一次线上事故说起:缓存穿透 vs 缓存雪崩
去年双11前一周,我们的商品详情页突然QPS飙到10万+(平时才3k)。结果呢?数据库CPU直接拉满,页面一片502。查日志发现,大量请求打到了根本不存在的商品ID上——典型的缓存穿透。
我当时第一反应是加布隆过滤器(Bloom Filter),但运维说:“你这玩意儿占内存,而且重启就丢数据,不行。”
行吧,那就先用空值缓存兜底:
public Product getProduct(Long id) {
String key = "product:" + id;
Product product = redisTemplate.opsForValue().get(key);
if (product != null) {
return product; // 命中缓存
}
// 查数据库
product = productMapper.selectById(id);
if (product == null) {
// 空值也缓存,防止穿透,TTL设短一点比如60s
redisTemplate.opsForValue().set(key, EMPTY_PLACEHOLDER, Duration.ofSeconds(60));
return null;
}
redisTemplate.opsForValue().set(key, product, Duration.ofHours(2));
return product;
}
但问题没完。第二天测试同学压测时,故意把Redis干挂了。结果所有流量瞬间涌向DB,连接池爆了——这就是缓存雪崩。
后来我们做了两件事:
- 本地缓存兜底:用Caffeine做二级缓存,即使Redis挂了,还能撑几秒。
- 熔断降级:集成Sentinel,当DB响应时间超过500ms,直接返回兜底数据或友好提示。
面试题来了:缓存穿透、击穿、雪崩的区别是什么?怎么解决?
别背八股文!结合你真实项目说:比如“我们在XX系统用了空值缓存 + 布隆过滤(后来换成Caffeine本地缓存)+ Sentinel熔断,上线后事故率降了90%”。
架构演进:从单体到模块化,再到微服务?
我们公司系统是典型单体架构,一个WAR包部署在WebLogic上(没错,还在用WebLogic!)。新需求一来,改个订单状态,结果把支付模块搞挂了——因为代码全在一个Git仓库里,依赖乱成一锅粥。
领导说:“要不拆微服务?”
我内心OS:“您知道Consul、Nacos、Seata这些组件维护成本有多高吗?咱们团队就5个后端,俩还在学Spring Cloud!”
于是我们折中:先做模块化拆分,不拆部署。用Maven多模块 + 领域驱动设计(DDD)的思想,把代码按业务边界切开:
order-service/
├── order-api // 对外暴露的接口(Feign Client)
├── order-domain // 核心领域模型、业务规则
├── order-infrastructure // DB、MQ、Redis等基础设施
└── order-application // 应用层,协调领域对象
好处立竿见影:
- 新人接手订单模块,不用看整个项目
- 单元测试覆盖率从30%提到70%
- 重构时敢动代码了(以前改一行,提心吊胆)
开发心得:微服务不是银弹。在资源有限的传统企业,先做好代码内聚和解耦,比盲目上微服务更实际。等哪天你们团队有专职SRE、有完善的CI/CD流水线,再考虑拆也不迟。
面试官最爱问的:你怎么保证接口幂等性?
上个月面试某大厂,二面直接甩我一道题:“用户重复点击支付按钮,怎么防止重复扣款?”
我差点脱口而出:“加个唯一请求ID啊!”
但转念一想,这太浅了。于是我说了我们在项目中的做法:
- 前端:按钮点击后置灰 + 请求带
requestId(UUID) - 后端:
- 先查Redis是否存在
pay:dedup:{requestId} - 存在 → 直接返回上次结果
- 不存在 → 执行业务逻辑,完成后写入Redis(带过期时间)
- 先查Redis是否存在
关键代码:
public PayResult pay(PayRequest request) {
String dedupKey = "pay:dedup:" + request.getRequestId();
// 尝试获取锁(原子操作)
Boolean isAbsent = redisTemplate.opsForValue().setIfAbsent(
dedupKey, "processing", Duration.ofMinutes(5)
);
if (!Boolean.TRUE.equals(isAbsent)) {
// 说明正在处理 or 已处理完成
PayResult cached = getFromCache(dedupKey); // 实际项目中会存完整结果
if (cached != null) return cached;
throw new BizException("请勿重复提交");
}
try {
PayResult result = doPay(request); // 执行真实支付
// 成功后更新缓存为最终结果
redisTemplate.opsForValue().set(dedupKey, result, Duration.ofHours(24));
return result;
} catch (Exception e) {
// 失败也要清理?不!保留"processing"状态防止重试风暴
throw e;
}
}
注意:这里故意没在异常时删除key,是为了防止短时间内大量重试把系统打垮——这是我们在一次线上事故中学到的教训。
关于代码质量:那些年我踩过的坑
在传统企业,老板最关心的是“功能能不能跑”,而不是“代码好不好”。但作为有点追求的开发者,我还是坚持了几条底线:
1. 拒绝魔法值
// bad
if (status == 1) { ... }
// good
if (orderStatus == OrderStatus.PAID) { ... }
2. 异常不要吞!
见过太多同事这么写:
try {
sendEmail(...);
} catch (Exception e) {
// 啥也不干
}
结果用户收不到通知邮件,问题排查三天。现在我们团队强制要求:所有catch必须记录日志 or 抛出业务异常。
3. 单元测试不是摆设
用JUnit 5 + Mockito + AssertJ,覆盖核心业务路径。比如订单状态机流转:
@Test
void shouldFailWhenCancelPaidOrder() {
Order order = new Order();
order.setStatus(OrderStatus.PAID);
assertThatThrownBy(() -> order.cancel())
.isInstanceOf(IllegalStateException.class)
.hasMessage("已支付订单不能取消");
}
性能优化:别再用SELECT * 了!
上周五晚上,测试环境慢得像蜗牛。查慢SQL发现:
SELECT * FROM t_large_table WHERE user_id = ? ORDER BY create_time DESC LIMIT 10;
这张表有2亿条数据,而且*包含了几个TEXT字段。光是网络传输就花了800ms。
优化后:
SELECT id, status, amount FROM t_large_table
WHERE user_id = ? AND create_time > '2023-01-01'
ORDER BY create_time DESC LIMIT 10;
并加上联合索引 (user_id, create_time)。
效果对比:
| 优化项 | 响应时间 | CPU使用率 |
|---|---|---|
| 优化前 | 820ms | 75% |
| 优化后 | 45ms | 12% |
开发心得:在传统企业,数据库往往是性能瓶颈。少查字段、加合适索引、避免大事务,这三招能解决80%的性能问题。
最后聊聊跳槽:技术探索的本质是解决问题
写这篇文章时,我已经投了十几份简历。面试官问得最多的是:“你在传统企业,技术栈是不是很老旧?”
我的回答是:技术不分新旧,关键看你怎么用。
- 我们用Oracle,但通过读写分离 + 分库分表(ShardingSphere)扛住了高并发
- 我们没上K8s,但用Docker + Jenkins实现了自动化部署
- 我们代码老旧,但我推动团队引入SonarQube做静态扫描,bug率下降40%
技术探索不是为了追新,而是为了解决手头的问题。当你能把一个看似普通的场景讲清楚“为什么这么做”、“踩了什么坑”、“效果如何”,你就已经超越了90%的候选人。
结语
三年多下来,我最大的感悟是:在传统企业做开发,既要低头写代码,也要抬头看路。
别抱怨技术栈老,老系统里藏着最真实的业务复杂度;
别嫌弃需求杂,正是这些“脏活累活”逼你思考架构和抽象;
更别停止学习——哪怕每天只看半小时源码,一年后也会感谢自己。
下个月我就要去新公司报道了(终于逃离WebLogic!)。但这段经历我会一直带着:好的工程师,不是在完美的环境里写出优雅代码,而是在泥泞中依然坚持把事情做对。
共勉。

评论 0