Java·一个请求从浏览器到数据库的完整旅程——全栈日更(2/379)

举报
哈基米 发表于 2026/10/23 20:48:24 2026/10/23
【摘要】 一个下单请求从浏览器到 MySQL 再返回的完整链路:DNS/TCP/TLS 握手、Nginx 转发、Filter 与 Interceptor、事务和连接池、Buffer Pool 与 redo log。附三个真实踩坑:Nginx 摘除故障节点、事务里调外部接口拖垮连接池、HikariCP 默认值不够用。

点下"提交订单"那个瞬间,我盯着屏幕发了三秒呆。按钮转圈,页面白着,脑子里突然冒出一个问题:这个请求,此刻到底走到哪了?后来我把整条链路拆开看了一遍,越拆越觉得有意思。今天这篇就把这条旅程完整走一遍,从浏览器一路讲到 MySQL 的磁盘。

第一站:浏览器先把话组织好浏览器要干的第一件事不是发请求,是先弄清楚"发给谁"。你输入或者代码里写的是域名,网络世界只认 IP。所以先查 DNS——本地缓存、系统缓存、运营商递归服务器,一层层问下去,拿到一串数字才罢休。然后是 TCP 三次握手。客户端发 SYN,服务器回 SYN+ACK,客户端再补一个 ACK。别小看这三个包,它们决定了后面所有数据传输的可靠性基础。如果是 HTTPS,握手之后还得再跑一轮 TLS:协商加密套件、交换密钥,完了才开始传正经内容。握手完毕,HTTP 报文才真正成型。它长这样:POST /api/order/create HTTP/1.1

Host: shop.example.com
Content-Type: application/json
Content-Length: 46

{"skuId":1001,"count":2,"addressId":88}就这几行文本。没有魔法,浏览器做的无非是把你要提交的数据序列化成 JSON,加上几个头,扔出去。

第二站:出网卡之后的黑暗隧道请求从网卡出去后,你对它就失控了。它要穿过家里或机房的路由器、运营商的城域网、骨干网,中间可能有几十跳。每一跳都是查路由表、转发,没有任何一跳关心你包里装的是订单还是聊天记录。最终它会撞上机房入口那台 Nginx。这台机器干两件事:TLS 终结(把加密流量解成明文),然后按配置把请求转给后端某台 Java 服务器:upstream java_backend {

    server 10.0.1.11:8080 max_fails=3 fail_timeout=30s;
    server 10.0.1.12:8080 max_fails=3 fail_timeout=30s;
}

server {
    listen 443 ssl;
    location /api/ {
        proxy_pass http://java_backend;
        proxy_connect_timeout 3s;
    }

}我上个月就栽在这段配置上。有个接口偶发 502,查了半天 Java 日志什么都没有,后来发现是其中一台机器假死,Nginx 还傻乎乎往那儿转发。max_fails 和 fail_timeout 没配,摘除故障节点的动作根本不会发生。教训是:四层没事不代表七层没事,健康检查得配到应用层面。第三站:Java 应用怎么接住它请求进了 Tomcat,先被扔进线程池。一个工作线程从 connector 手里接过这坨字节流,解析成 HttpServletRequest 对象,然后开始过安检。先是 Filter 链。字符编码、跨域、登录态校验,一层层过。接着是 Interceptor,业务上的权限校验一般放这儿。最后才轮到你写的 Controller:@PostMapping("/api/order/create")

public Result<Long> createOrder(@RequestBody @Valid OrderReq req) {
    Long orderId = orderService.create(req);
    return Result.ok(orderId);

}Controller 自己往往很薄,活儿都甩给 Service。Service 里才是重头戏:参数再校验一遍、算价格、扣库存、生成订单记录,外面包一层 @Transactional 保证这几步要么全成、要么全滚。这里有个我踩过的坑必须记一笔。我一开始觉得事务加上了就万事大吉,后来发现一个方法里先调了个外部 HTTP 接口,再写库。外部接口超时那几秒,数据库连接一直被占着没放。并发一上来,连接池直接见底,整个服务卡死。说白了,事务里干慢活等于慢性自杀。后来定了个死规矩:远程调用全部挪到事务外面,事务里只留纯 DB 操作。第四站:SQL 是怎么跑出去的Service 写库走的是 MyBatis。它把 XML 或者注解里的 SQL 模板掏出来,填上参数,通过连接池拿到一条数据库连接,把 SQL 发过去。连接池这东西,默认配置基本等于没配。HikariCP 默认 maximumPoolSize 才 10,一个稍微有点流量的服务就会在高峰期排队等连接。池子太小,请求堆积;池子太大,数据库端内存扛不住。官方给过一个公式,连接数 = CPU 核数 × 2 + 有效磁盘数,别拍脑袋往大了调。扣库存的 SQL 到了 MySQL 那边,日志里能看到完整的执行痕迹:SELECT stock FROM sku WHERE id = 1001 FOR UPDATE;

UPDATE sku SET stock = stock - 2 WHERE id = 1001 AND stock >= 2;
INSERT INTO t_order(order_no, sku_id, count, amount)

VALUES ('R20261023xx', 1001, 2, 5960);第五站:MySQL 内部的一毫秒SQL 进了 MySQL,也不是直接怼磁盘。解析器先做词法语法分析,把文本拆成语法树。优化器再看执行计划:走哪个索引、表怎么连、要不要临时表。你可以用 explain 把这个决策过程翻出来看,rows 列估出来的扫描行数和实际差太多,基本就是索引选错了。执行器真正跑的时候,数据页先从磁盘捞到 Buffer Pool 里。下次再查同一行,内存直接命中,快得飞起。写操作也不直接落盘,先改内存,再顺序写 redo log——这就是常说的 WAL。数据库崩溃了靠 redo log 重放,性能和数据安全都保住了。回滚靠的是另一份东西:undo log。每条改动前的旧值都会被记下来,事务一旦中途翻车,拿旧值倒着放回去就行。MVCC 的多版本读也靠它撑着,读已提交和可重复读两个隔离级别的差别,就藏在 undo 链什么时候释放这条细节里。订单这条链路的最后一步是 binlog 写入,主从复制靠它。提交事务的那一瞬间,这条订单才算在数据库里真正"存在"了。回程:原路返回数据库把受影响的行数传回连接池那条连接,MyBatis 包装成返回值,Service 提交事务,Controller 包装成 Result,一路序列化回 HTTP 响应,沿着来时的路回到浏览器。浏览器拿到 JSON,Vue 里对应的响应式状态更新,虚拟 DOM diff 一轮,真实 DOM 改两处,页面上"提交成功"的提示弹出来。回程也不是原样照搬。Nginx 一般会开 gzip,把 JSON 压到原来三分之一大小再发出去,带宽省一截;HTTP keep-alive 让这条 TCP 连接留下来复用,省掉下一轮握手的时间。一个请求的旅程结束,连接还躺在那儿等下一个。全程结束。快的时候几十毫秒,慢的时候够你泡杯茶。把这条线焊在脑子里链路感是全栈和纯工种最大的差别。排障的时候尤其明显。接口慢,链路感差的人只会说"后端慢",有链路感的人会掏出 DevTools 看是 TTFB 长还是下载长;TTFB 长就进服务器看 Nginx 日志里的 upstream_response_time,再进应用看是线程等连接还是 SQL 慢,SQL 慢就 explain。一层层切下去,问题藏不住。这条链路我建议每个后端都亲手画一遍,画不出来,说明还没真正理解自己每天写的代码。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。