翻回以前的 Java 笔记,会发现主题已经不少了:HashMap、ConcurrentHashMap、JVM、MySQL、Redis、Spring、MQ,还有项目设计。
这些知识点单独看都很重要。但如果只是把答案一条条背下来,遇到“讲讲你的项目”时,依然容易变成一串组件名称。
可以换一个复习角度:沿着一笔订单走一遍,看这些知识分别解决哪一段问题。
第一站:请求进来了
一个用户点击下单,服务先接收到请求。重复点击、网络重试和客户端超时重发,都可能让同一项业务操作出现多次请求。
这时“幂等”就从一个定义变成了实际需求:哪些请求属于同一次操作?第一次执行的结果在哪里?重试时如何返回已有结果?
这里还会牵出集合和锁的问题。如果用本地 Map 保存请求状态,要考虑线程安全;如果服务有多个实例,仅靠单实例里的 Map 或锁,又无法覆盖全部请求。
复习 ConcurrentHashMap 时,可以讨论它如何保证自身操作的并发行为,再追问:业务里的“先查,再写”是否因此变成了一个原子操作?容器的线程安全和业务步骤的原子性,需要分开判断。
第二站:任务要排队吗
如果请求线程等待数据库或外部服务,线程池很快就进入讨论范围。
线程池参数不应只有一份背诵清单。对下单任务来说,更有意义的问题是:任务平均要执行多久,下游能承受多少并发,排队多久以后结果就失去价值,队列满时要怎样向用户说明?
原来的线程池笔记可以作为 API 和原理索引,再用一个小实验补上任务耗时、队列增长和拒绝行为。实验需要写清机器、参数、输入和观察结果,避免把一次本地结果写成通用参数建议。
第三站:库存和订单落库
到了数据库,重点变成两件事:并发请求能否正确争抢库存,扣库存和建订单能否一起成功或一起失败。
乐观并发控制讨论的是竞争更新;事务讨论的是一组数据库操作的边界。解决了其中一个,不代表另一个自然成立。
这时再回头看 MySQL 的索引、隔离级别和锁,问题就具体了:更新语句命中了什么索引,竞争发生在哪里,失败时如何判断是否需要重试?
同样,Spring 的事务问题也不能只停在注解名称。应沿调用路径看代理是否生效、事务管理器是否覆盖这些操作,以及异常被捕获后还会发生什么。
第四站:消息队列与缓存加入进来
为了减少请求同步等待,可以把订单处理交给消费者。但接口响应的含义随之改变:它可能只表示请求已经受理,还不能表示订单已经创建。
可以沿着三个问题往下查:消息是否被接收,消费者是否完成业务处理,结果如何回到用户手里。消息重复时要考虑幂等,处理失败时要考虑重试和最终失败状态。
Redis 在这里可能用于库存提示或结果查询。缓存查询快,但写入缓存和提交数据库不是同一个动作。缓存没有结果时,究竟代表没处理、处理失败,还是数据库已经成功而缓存更新失败?状态设计决定了用户能否看懂结果。
第五站:线上变慢了
如果用户说下单慢,先沿路径确认时间花在哪里,再决定使用哪些工具。
请求是否在入口排队?线程是否等待连接?SQL 是否变慢?消费者是否积压?GC 是否影响响应?这些问题分别对应不同的观测证据。
JVM 和项目排障笔记可以作为工具箱,但“拿出哪个工具”要由现象和证据推动。看到 CPU 高就直接归因 GC,或者看到缓存命中低就认定数据库是唯一瓶颈,都容易跳过真正的问题。
最后,把笔记写成可以验证的小文章
每篇文章可以围绕一个问题保留四样东西:业务条件、最小例子、失败场景、验证结果。
旧代码和摘录不必全部重写。把版本、出处和实验条件补齐,区分学习示例与生产经历,再用新的实验修正旧结论,积累就能继续生长。
本文整理入口是 interview-Java、interview-Java-code 和 secKill。这些仓库保存了笔记与示例,其中也有外部资料摘录;后续专题会按具体章节保留来源,并注明实现版本。