从考研失败到写Spring Boot:一个菜鸟程序员的技术探索实录
去年三月,我坐在自习室里盯着电脑屏幕发呆。考研成绩刚出,差了国家线3分。那一刻,感觉天都塌了——毕竟整整一年没碰代码,简历上除了“熟悉Java基础”和“了解SpringBoot”之外,几乎一片空白。投了两个月简历,面试官问的问题我答得磕磕巴巴,连“Spring Bean的生命周期”这种经典面试题都说不完整。
但生活总得继续。四月底,我咬牙接了一个中小厂的offer,岗位是Java后端开发。入职第一天,组长扔给我一个任务:“下周上线一个新模块,用Spring Boot搭个脚手架,支持配置热更新、日志追踪,还得能扛住促销流量。”我当时心里直打鼓:这不就是我在准备面试题挑战时反复刷过的场景吗?可真上手做,才发现纸上谈兵和实战完全是两码事。
工具链不是堆砌,而是协同
刚进公司那会儿,我天真地以为只要把主流工具全装上就行:Lombok、MyBatis-Plus、Redisson、SkyWalking、Nacos……结果第一天就翻车了。本地启动项目花了快两分钟,IDE卡成PPT,同事路过看了一眼我的pom.xml,笑着说:“兄弟,你这是在搭瑞士军刀,还是在造火箭?”
后来才明白,工具的价值不在数量,而在契合业务节奏。我们团队主打快速迭代,两周一个版本,产品经理动不动就改需求(上周五晚上10点还拉群说“这个按钮颜色要改”)。在这种环境下,过度设计反而拖累效率。
于是我们做了减法:
- 放弃复杂的分布式事务方案,先用本地事务 + 补偿机制兜底;
- 日志追踪只集成SkyWalking核心模块,去掉指标上报等非必要组件;
- 配置中心用Nacos,但只管开关和阈值,不搞动态Bean注入。
# application-prod.yml 精简示例
spring:
application:
name: order-service
cloud:
nacos:
config:
server-addr: ${NACOS_ADDR}
file-extension: yaml
namespace: prod
management:
endpoints:
web:
exposure:
include: health,info,metrics # 只暴露必要端点
这种克制反而让系统更稳定。双11压测时,我们的服务启动时间控制在8秒内,比隔壁组用全套Spring Cloud Alibaba的快了一倍。
面试题 vs 真实场景:那些被忽略的细节
记得有次技术分享会上,新人问:“Spring Boot自动配置是怎么工作的?”我脱口而出:“@EnableAutoConfiguration通过SPI加载META-INF/spring.factories里的配置类……”话没说完,架构师老张打断我:“但你知道为什么我们禁用了DataSourceAutoConfiguration吗?”
原来,公司数据库账号权限严格分级,测试环境不能连生产库。而Spring Boot默认会尝试创建DataSource,导致应用启动时报错Access denied for user 'test'@'%'。后来我们自定义了一个DatabaseAutoConfig,根据环境变量动态决定是否初始化数据源:
@Configuration
@ConditionalOnProperty(name = "db.enabled", havingValue = "true", matchIfMissing = true)
public class DatabaseAutoConfig {
@Bean
@Primary
public DataSource dataSource() {
// 根据 env 动态构造 HikariCP
return new HikariDataSource(buildConfig());
}
private HikariConfig builduckConfig() {
String env = System.getenv("ENV");
if ("prod".equals(env)) {
// 生产库配置
} else {
// 测试库配置
}
}
}
这件事让我意识到:面试题考的是知识点,但工作中考的是上下文理解。很多“标准答案”在真实业务中根本行不通。比如“Redis缓存穿透用布隆过滤器”,听起来很酷,但我们商品ID是UUID,布隆过滤器内存开销太大,最后改用缓存空值+短TTL搞定。
可读性不是妥协,而是长期投资
作为应届生,我一度觉得“能跑就行”。直到上个月线上出了一次P0事故:用户支付成功但订单状态没更新。排查三天才发现,某个异步回调方法里混用了@Async和@Transactional,事务根本不生效。
那次事故后,我彻底转变了观念。现在写代码,我会花30%时间优化可读性:
- 方法名拒绝“handle/do/process”这种模糊词,直接用业务语言,比如
refundExpiredOrders() - 复杂逻辑拆成小函数,配上JavaDoc说明业务规则
- 关键路径加注释,解释“为什么这么做”而不是“做了什么”
/**
* 订单超时未支付自动关闭
* 注意:必须在事务外调用,否则状态变更无法触发MQ消息
*/
public void closeTimeoutOrders() {
List<Order> timeoutOrders = orderRepository.findUnpaidBefore(Instant.now().minus(30, MINUTES));
timeoutOrders.parallelStream().forEach(order -> {
// 先更新状态,再发MQ(顺序不能反!)
order.setStatus(OrderStatus.CLOSED);
orderRepository.save(order);
mqProducer.sendOrderClosedEvent(order.getId());
});
}
组长看到后点赞说:“这才是能传承的代码。”虽然他下一秒就补了句:“不过周五上线前记得删掉parallelStream,上次压测它把GC打爆了。”
技术探索:在deadline夹缝中成长
说实话,每天应付需求已经够累了,哪有精力搞技术探索?但上个月领导突然说:“竞品上线了实时库存功能,我们下个月必须跟上。”这意味着要从MySQL单表库存升级到Redis原子操作。
那周我天天加班到十点,边听Lo-fi Hip Hop边啃《Redis实战》。过程中踩了无数坑:
- 初始用
INCRBY扣库存,但退货时负数导致数据错乱 → 改用Lua脚本保证原子性 - 压测发现Redis连接池耗尽 → 调整lettuce的sharedConnection=false
- 线上出现超卖 → 加了预占库存+定时回滚机制
最终方案虽然不算完美,但扛住了大促流量。最让我自豪的是,我把整个过程整理成内部Wiki,还附上了压测对比数据:
| 方案 | QPS | 错误率 | 库存一致性 |
|---|---|---|---|
| MySQL行锁 | 120 | 0.5% | 强一致 |
| Redis INCRBY | 3500 | 2.1% | 最终一致 |
| Redis Lua脚本 | 2800 | 0.02% | 强一致 |
现在每次看到订单系统平稳运行,就觉得那些熬夜都是值得的。
给同样迷茫的后来者
写这篇文章时,我刚转正一个月。回头看,考研失败反而是塞翁失马——如果考上研,可能还在实验室调参,而不是亲手搭建高并发系统。技术探索从来不是一蹴而就的,它藏在每次修复Bug的深夜、每个被拒的需求评审、每场和运维的“友好交流”中。
如果你也像我一样:
- 被面试题虐得怀疑人生
- 觉得Spring Boot只是“约定大于配置”的黑盒
- 在工具选择上纠结到失眠
别慌。真实的成长,往往始于承认自己不会。我现在还会在群里问“Nacos配置优先级到底是啥顺序”,也会因为看不懂Netty源码而烦躁。但至少,我不再害怕动手试错了。
毕竟,代码不会骗人。你投入多少心思,它就回报多少稳定性。而那些曾经让你想砸电脑的报错,终将成为你简历上最硬核的故事。
(完)
后记:今天产品又提了个需求——“能不能让用户看到库存实时变化?”我默默打开了Redis Streams文档...

评论 0