1. Spring 启动流程(含 Spring Framework 与 Spring Boot 视角)
(A)Spring 容器 AbstractApplicationContext.refresh()
经典 12 步
prepareRefresh()
:准备环境(启动时间戳、占位属性、校验必需属性)。obtainFreshBeanFactory()
:创建/刷新BeanFactory
,载入 BeanDefinition(XML/注解/JavaConfig)。prepareBeanFactory()
:设置类加载器、EL 解析器、ApplicationContextAware
等常用能力。postProcessBeanFactory()
:给子类扩展点(如GenericApplicationContext
)。invokeBeanFactoryPostProcessors()
:执行BeanFactoryPostProcessor
(关键:ConfigurationClassPostProcessor
解析@Configuration
、@ComponentScan
、@Import
、注册更多 BeanDefinition)。registerBeanPostProcessors()
:注册BeanPostProcessor
(AOP 代理、@Autowired
注入、@PostConstruct
等都依赖它们)。initMessageSource()
:国际化消息源。initApplicationEventMulticaster()
:事件广播器。onRefresh()
:子类扩展点(Web 环境在此创建内置 Web 服务器)。registerListeners()
:注册并触发早期事件。finishBeanFactoryInitialization()
:实例化非懒加载单例 Bean,完成依赖注入与 AOP 代理创建。finishRefresh()
:清理、发布ContextRefreshedEvent
,容器就绪。
(B)Spring Boot 启动要点(SpringApplication.run
)
- 准备阶段:推断应用类型(Servlet/Reactive/None),收集
ApplicationContextInitializer
、ApplicationListener
,推断主类。 - Environment:解析命令行参数、装载配置(
application.yml/properties
,Profiles)、绑定Environment
。 - Banner:打印 Banner(可定制)。
- 创建 ApplicationContext:如
AnnotationConfigServletWebServerApplicationContext
。 - 准备上下文:设置环境、应用
Initializer
、注册主配置类(@SpringBootApplication
元注解 =@Configuration
+@ComponentScan
+@EnableAutoConfiguration
)。 - 自动配置:通过 AutoConfiguration(Boot 2.x 基于
spring.factories
;Boot 3.x 基于META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
)按条件装配。 - 刷新上下文:进入上文 12 步,Web 应用在
onRefresh()
创建并启动内置 Tomcat/Jetty/Undertow,注册DispatcherServlet
。 - 回调与事件:执行
ApplicationRunner
/CommandLineRunner
,发布ApplicationStartedEvent
、ApplicationReadyEvent
(应用可对外服务)。
2. Elasticsearch 与 SQL(关系型数据库)的区别
维度 | Elasticsearch | 关系型数据库(SQL/RDBMS) |
---|---|---|
数据模型 | 文档型(JSON),Schema 灵活;倒排索引 | 行/列+严格 Schema;B-Tree/索引 |
查询语言 | Query DSL(布尔、全文检索、评分);也有 ES-SQL(语法糖) | 标准 SQL(选择、连接、聚合、窗口函数) |
一致性 | 近实时(默认 \~1s refresh),最终一致;写入刷盘 translog + 段合并 | 事务 ACID、强一致 |
事务 | 文档级原子;无跨多文档/多索引强事务 | 多语句事务,隔离级别 |
连接/关联 | 建议反范式,支持 limited 的 nested/parent-child,成本高 | JOIN 一等公民 |
排序/相关性 | TF-IDF/BM25 等相关性打分、模糊搜索 | 精确匹配,排序多基于字段 |
聚合分析 | 分布式聚合、桶/度量聚合、近实时 | GROUP BY 、窗口函数,强一致 |
扩展性 | 天生分片+副本,水平扩展容易 | 纵向为主;水平分片需中间件/应用层 |
适用场景 | 全文检索、日志/可观测性、近实时检索与聚合 | OLTP 事务、强一致业务、复杂关联 |
成本与维护 | 集群调参(分片、副本、刷新、合并)、写放大 | 事务调优、索引设计、主从/分库分表 |
选型建议:检索与日志分析优先 ES;核心交易、强一致与复杂 JOIN 走 RDBMS。它们常互补:数据落地 RDBMS,旁路同步至 ES 做搜索。
3. git merge
vs git rebase
概念
- merge:把另一个分支的变更合并到当前分支,可能产生合并提交(保留分叉历史)。
- rebase:把当前分支提交“挪到”目标分支的最新之后(改写历史,生成新提交,历史线性)。
对比
- 历史形态:merge 保留分叉,rebase 线性干净。
- 冲突处理:merge 一次性解决;rebase 可能多次在每个变基提交处解决。
- 公共分支:禁止对已共享/推送的分支 rebase(会改写他人基础);merge 安全。
- 回溯定位:merge 节点易标记里程碑;rebase 便于
git bisect
/阅读。 -
命令示例:
- 在
main
合并feature
:git checkout main && git merge feature
- 在
feature
上跟进main
:git checkout feature && git rebase main
- 拉取时保持线性:
git pull --rebase
- 在
团队建议
- 私有分支用 rebase 跟进主干,提交整洁;合入主干用 merge –no-ff 保留合并点(或使用 squash merge 统一提交粒度)。
4. 子线程如何继承父线程里的数据(Java / Python)
Java
ThreadLocal
:线程本地变量,不会自动继承。-
InheritableThreadLocal
(新建线程时复制)- 适合直接 new Thread 的场景;线程池复用线程时不生效。
- 示例:
static final InheritableThreadLocal
CTX = new InheritableThreadLocal<>(); public static void main(String[] args) { CTX.set("trace-123"); new Thread(() -> System.out.println(CTX.get())).start(); // 输出 trace-123 } -
线程池中的传递:使用 TransmittableThreadLocal (TTL)(阿里开源)包装
Executor
,解决线程复用导致的上下文不传与“脏数据”问题。TransmittableThreadLocal
ttl = new TransmittableThreadLocal<>(); ExecutorService raw = Executors.newFixedThreadPool(4); ExecutorService exec = TtlExecutors.getTtlExecutorService(raw); ttl.set("trace-xyz"); exec.submit(() -> System.out.println(ttl.get())); // 正确输出 -
实践要点
- 上下文建议封装为不可变对象;用完记得
remove()
防泄漏。 - 记录链路用日志 MDC(
org.slf4j.MDC
),搭配 TTL 更稳妥。 - 能显式传参就显式传参,降低隐式上下文耦合。
- 上下文建议封装为不可变对象;用完记得
Python
threading.local()
:线程本地存储,不会自动继承。-
contextvars
(推荐传递上下文)ContextVar
对线程与协程隔离;默认不继承到新线程,但可用copy_context()
拷贝父上下文并在子线程中运行。
import threading, contextvars trace_id = contextvars.ContextVar("trace_id") def worker(): print("child:", trace_id.get(None)) def main(): trace_id.set("t-123") ctx = contextvars.copy_context() threading.Thread(target=ctx.run, args=(worker,)).start() if __name__ == "__main__": main() # child: t-123
-
在线程池中:为每个
submit
捕获上下文from concurrent.futures import ThreadPoolExecutor from contextvars import copy_context with ThreadPoolExecutor() as ex: ctx = copy_context() ex.submit(ctx.run, worker) # 传播父上下文快照
- 其他:显式参数传递最清晰;全局变量可见但不建议承载“上下文”。
5. 从“浏览器输入 URL”到“页面呈现”的全过程(高频面试版)
0)历史/本地“秒开”快路径(跳过网络)
- 前进/后退缓存(bfcache)
- 场景:用户点浏览器“后退/前进”。
- 行为:浏览器把**上次的整页快照(DOM、JS 堆、样式与滚动位置)**直接复活,几乎不走解析与网络。Chrome DevTools 还能检测页面是否适配 bfcache。
- Service Worker 拦截
- 若页面受 SW 控制,导航/资源请求先触发
fetch
事件;SW 可直接从 Cache Storage 返回离线副本,或自定策略(缓存优先/网络优先/过时-再验证等)。SW 缓存独立于 HTTP 缓存。
- HTTP 浏览器缓存(memory/disk)
- 强缓存命中(
Cache-Control: max-age
/Expires
仍有效)→ 直接用本地副本,不发请求。 - 需要再验证(过期但有
ETag
/Last-Modified
)→ 发条件请求,若 304 则复用本地体;否则拿新体并更新缓存。 - Chromium 同时有 内存缓存 与 磁盘缓存 两层实现。
1)导航与 URL 解析
- 地址栏判定:搜索词还是 URL;必要时做 HSTS 升级到 HTTPS(若命中列表)。
- 解析 协议/主机/端口/路径/查询/片段,非法字符转义。
2)DNS 解析(无本地命中时)
- 查询路径(可能 DoH):浏览器/系统缓存 → hosts → 递归解析(LDNS→根→TLD→权威),返回一个或多 IP(含负载均衡/CDN)。
3)定位链路层目标(ARP / MAC)
- 若目标在同一二层网段:用 ARP 将对端 IP 解析为 MAC;跨网段则解析 默认网关 的 MAC,由网关继续转发。结果缓存于 ARP 表以减少广播。
4)建连与加密:TCP/QUIC & TLS
- TCP(HTTP/1.1、绝大多数 HTTP/2 场景):三次握手,随后 TLS 握手(通常 TLS 1.2/1.3);
- QUIC(HTTP/3):基于 UDP,将 TLS 1.3 集成到握手里,减少往返,弱化 TCP 层队头阻塞问题。
- ALPN 在握手中协商具体 HTTP 版本(
h2
/http/1.1
/h3
等)。 - TLS 握手要点:双方确认算法、验证证书、生成会话密钥;TLS 1.3 往返更少、套件更现代。
5)发起 HTTP 请求(可能经代理/CDN/WAF)
- 组成:方法、路径、请求头(
Host
、Accept-*
、Cookie
、If-None-Match
…)和可选实体。 - CDN 命中则边缘直接响应;未命中回源。
6)服务器响应与缓存语义
- 典型状态:
200
(新内容)、301/302/307/308
(重定向)、304
(协商缓存命中)、4xx/5xx
。 -
缓存控制(关键面试点):
- 响应指令:
Cache-Control: max-age/s-maxage, public/private, no-store, no-cache, must-revalidate
; - 验证器:
ETag/If-None-Match
、Last-Modified/If-Modified-Since
; - 强缓存 vs 协商缓存 的命中与回写策略。
- 响应指令:
7)连接复用与版本差异(性能面)
- HTTP/1.1:文本报文、易受应用层队头阻塞;常见并发=每域 6 连接。
- HTTP/2:二进制分帧 + 单连接多路复用 + HPACK 头压缩,应用层基本无队头阻塞(TCP 层仍有);Server Push 已逐步退场。
- HTTP/3(QUIC):基于 UDP,流级重传与拥塞控制,丢包不牵连其他流,握手更快。
8)Renderer 渲染流水线(以 Chromium 为例)
-
解析构建:
- HTML → DOM(脚本标签默认阻塞解析;
defer/async/module
可缓解); - CSS → CSSOM;
- 合并为 Render Tree。
- HTML → DOM(脚本标签默认阻塞解析;
- 布局(Reflow):计算几何、盒模型。
- 绘制与合成:分层、栅格化、GPU 合成输出到屏幕。
- 子资源获取:解析器预扫描发现外链资源,对每个资源同样走 SW/HTTP 缓存/网络 的“三段式”流程。
- 页面生命周期:事件循环、微/宏任务,后续交互驱动增量布局与绘制。
(Chrome 官方系列对浏览器多进程架构与渲染管线有系统性讲解。)
9)连接关闭(或复用)
- HTTP/1.1 通常 keep-alive 复用;需要时四次挥手释放。HTTP/2/3 在会话层复用更充分,减少频繁建连开销。
10)一张“面试可画”的决策树(简化)
- 历史导航? → bfcache 命中 ⇒ 直接呈现。
- Service Worker 控制? → 走 SW 策略,可能离线直出。
-
HTTP 缓存命中?
- 强缓存有效 ⇒ 本地直接用;
- 需验证 ⇒ 发条件请求,304 用本地体;
- 否则走网络。
- 走网络:DNS → ARP/MAC → TCP/QUIC + TLS(ALPN)→ 请求/响应(含 CDN)→ 回写缓存。
- 渲染流水线:DOM/CSSOM/Render Tree → 布局 → 绘制/合成。
5.1 HTTP/1.1 vs “HTTP/2”
对比项 | HTTP/1.1 | HTTP/2 |
---|---|---|
编码形态 | 文本协议 | 二进制分帧(帧/流/消息) |
连接利用 | 慢启动;管线化几乎停用;常并发 6 条连接/域名 | 单连接多路复用(并行请求,无应用层 HOL 阻塞) |
队头阻塞 | 应用层易 HOL;需多连接来缓解 | TCP 层仍可能 HOL,但应用层已缓解 |
头部 | 重复大、无压缩(或弱压缩) | HPACK 头压缩(动态/静态字典) |
优先级 | 无 | 请求优先级/依赖(实现差异) |
服务器推送 | 无 | Server Push(浏览器已普遍弃用) |
安全/TLS | 可明文/HTTPS | 明文 h2c 存在但少见,实际多为 TLS+h2 |
性能策略 | 雪碧图、域名分片、内联资源 | 多路复用下不再需要这些反模式 |
补充:HTTP/3 基于 QUIC(UDP)解决 TCP HOL 与建连时延,更进一步提升时延与丢包恢复。
5.2 为什么不同地方 ping 同一个域名会解析到不同的 IP?
一句话总结:多数网站把“域名 → IP”的映射交给DNS 调度与 CDN 来做“就近/最优”分配;权威 DNS 会基于解析器所在位置(或 EDNS Client Subnet 提供的客户端前缀)、实时健康检查与时延、权重轮询等策略返回不同的 A/AAAA 记录,于是你在不同地区/不同网络 ping
同一域名时,往往拿到不同 IP。请求真正到达时,还可能走到 Anycast 最近节点或对应的边缘机房。
典型原因拆解
- DNS/GeoDNS 就近解析(基于解析器位置 + 可选 ECS)
- 现代权威 DNS 会根据“发起查询的递归解析器”的地理/网络位置来返回离它“拓扑更近”的边缘节点 IP(常由 CDN 维护)。如果开启 EDNS Client Subnet(ECS),递归解析器还会把你的客户端网段(如
/24
)附带给权威 DNS,从而得到更贴合你实际位置的答案。不同地点/不同 ISP/不同公共解析器(8.8.8.8/1.1.1.1)都会导致不同的解析结果。
- CDN 全局流量调度(健康、时延、容量)
- CDN/权威 DNS 会综合健康检查与实时时延做全局服务负载均衡(GSLB):例如 AWS Route 53 的基于时延路由会把伦敦来的请求指向当前时延更低的新加坡或俄勒冈后端;网络状态变化时,返回值也会随之改变。
- 轮询/加权轮询(多 A/AAAA)
- 很多域名同时配置多条 A/AAAA 记录做 round-robin 或加权轮询,不同解析器/不同时间拿到的顺序或子集可能不同,从而看到不同 IP。
- Anycast(任一播)
-
有的DNS 解析器与CDN 边缘都采用 Anycast:同一个 IP 在全球多个站点对外广播,BGP 会把你的流量路由到网络路径最近的节点。这有两种表现:
- 相同域名 → 不同 IP:由 DNS 调度决定,常见;
- 相同域名 → 相同 Anycast IP,但不同地区会被路由到不同机房(路由“就近”到不同地点),
ping
延迟不同。
- 解析与缓存差异
- 不同递归解析器各自的缓存命中、TTL、本地策略不同,导致短时间内答案不一致;CDN 常把 TTL 设得较低以便快速切流,也会放大这种差异(这点属于实现细节,一般无需惊慌)。
小提示:
ping
用的是系统的 DNS 解析结果;在双栈网络中,ping
(IPv4)与ping -6
(IPv6)对应查询 A 与 AAAA,拿到的 IP 自然不同,这与地理调度并不冲突。
如何自行验证(命令行思路)
- 看不同解析器的答案:
dig www.example.com +short @1.1.1.1
dig www.example.com +short @8.8.8.8
- 观察 CNAME/CDN 映射链:
dig www.example.com
(看是否指向 CDN 域,如*.cdnprovider.net
)。 - 模拟 ECS(若解析器/权威都支持):
dig www.example.com +subnet=1.2.3.0/24 @8.8.8.8
对比+subnet=5.6.7.0/24
的返回差异。
一图心智模型
你(客户端)
│ DNS 查询
▼
本地/公共递归解析器 ──(可带 ECS 客户端前缀)──▶ 域名权威 DNS/CDN GSLB
│ │
│◀──────── 返回“就近/最优”的 A/AAAA ────────┘
│
▼
得到 IP → 发起连接(可能是 Anycast IP,路由到最近节点)
结论:不同地点 ping
同一域名拿到不同 IP 是正常现象,是 GeoDNS + CDN +(可选)Anycast 等机制协同实现的“就近与高可用”。当你换了网络、换了解析器、或过了 TTL,解析答案都可能变化。
6. 「拼手气红包」如何均匀/公平分配(算法与实现要点)
目标:总金额 S
(以分为单位)拆成 n
份,每份 ≥ min
(通常 1 分),期望值相等、结果看起来“有惊喜”。
常见算法
-
二倍均值法(Double-Mean,O(n))
- 思想:剩余金额
R
、剩余人数m
时,当前可领范围为(min, 2 * (R/m) - min)
的均匀分布(再与上界R - (m-1)*min
取最小以安全)。 - 期望:
E[x_i] = R/m
,总体期望均等;极端值概率可控,看起来“有人多有人少”。 -
伪代码(以分为单位,整型随机):
remain = S for i in 1..n-1: m = n - i + 1 max_allow = remain - (m-1)*min max_dm = floor(2 * remain / m) # 二倍均值 hi = min(max_allow, max_dm) amt = rand_int(min, hi) # 均匀取整 give(i, amt) remain -= amt give(n, remain)
- 优化:可设置
max_cap
(单个上限)→hi = min(hi, max_cap)
;最后用“最大余数法”修正四舍五入误差(若使用浮点/元)。
- 思想:剩余金额
-
线段切割法(随机切点/Dirichlet)
- 在
[0, S]
上取n-1
个随机点,排序后求相邻差即每份金额,天然总和S
。 - 若需每份 ≥ min:先预留
n*min
,对S - n*min
切割,再加回min
。 - 连续模型近似 Dirichlet(1,…,1),从统计意义上最“公平随机”。
- 在
-
正态/指数偏好法
- 以均值
S/n
、方差参数控制离散度,从分布采样再做非负与求和约束修正(体验更“刺激”)。 - 需注意最终离散到分后的校正。
- 以均值
正确性与工程细节
- 边界:若
S < n*min
→ 不可分;若n==1
→ 直接给S
。 - 单位:**统一用“分”**计算,避免浮点误差;输出时再转“元”。
- 公平性:定义为期望相等;二倍均值法与切割法均满足;可通过模拟检验均值与分布。
- 风控:加上单包上限、黑名单、幂等(避免并发重复领取)、幂等凭证与发放流水。
- 并发:领取过程使用 CAS/分布式锁/队列,避免超发;可预生成拆分结果进入持久化队列。
7. LeetCode 85. 最大矩形
https://leetcode.cn/problems/maximal-rectangle/description/
个人感受
- 面试官很注重学历(问我为什么没保研/学校为什么这么差)
- 很注重八股(没听到八股答案就上压力)
- 确实是我太菜了