🔥个人主页:代码不加冰(欢迎来访)
🎬作者简介:java后端学习者
❄️个人专栏:LeetCode刷题日记 , 苍穹外卖日记,SSM框架深入,JavaWeb,
✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
前言:
大家好我是代码不加冰,这里给大家分享一下最近在一个项目中学的新知识,也算是自我的一个总结,方便以后复习。
摘要:
本文结合实际项目经验,对 RPC(远程过程调用)的核心原理进行了系统梳理。
从为什么微服务之间不直接使用 HTTP 调用入手,分析了 RPC 在性能、开发体验以及服务治理方面的优势。随后深入讲解了 RPC 的底层调用流程,包括序列化、网络传输、服务执行和结果返回等关键环节,并重点介绍了动态代理在 RPC 框架中的作用,帮助读者理解 Dubbo 如何实现“像调用本地方法一样调用远程服务”。
此外,文章还详细对比了 JDK 动态代理与 CGLIB 动态代理的实现方式,并进一步延伸到 Nginx 反向代理和负载均衡机制,分析其与 Dubbo 客户端负载均衡的区别与协作关系。通过本文,读者可以建立从动态代理、RPC 到微服务通信架构的完整知识体系。
什么是RPC框架:
RPC(Remote Procedure Call),即远程过程调用。
本地调用:你的代码在同一个 JVM(同一个进程)里。你写了一个
userService.getUserById(1),直接在内存里点过去就执行了。远程调用:随着业务变大,你把用户模块拆分成了
User 服务,把订单模块拆分成了Order 服务,它们分别部署在两台不同的服务器上。此时,Order 服务想要调用User 服务的方法,由于内存不共享,普通的本地调用直接失效了。
RPC 的核心目标就是:让调用远程服务的方法时,像调用本地方法一样简单、透明。
1.为什么不用 HTTP / RestTemplate,非要用 RPC
很多刚学完 Spring Boot 的同学会问:“我用 HTTP 请求(比如 RestTemplate 或者是 HttpClient)一样可以跨服务器调用啊,为什么要用 RPC 框架?”
我们可以用一个简单的表格来对比:
| 特性 | HTTP (RESTful 常用) | RPC (以 Dubbo 为例) |
| 传输协议 | 通常基于 HTTP/1.1,文本传输(JSON) | 常用底层 TCP(Dubbo协议)或 HTTP/2(Triple协议),二进制传输 |
| 性能 | 消息体相对较大,序列化/反序列化慢,开销大 | 消息体小,序列化速度极快,吞吐量极高 |
| 开发体验 | 需要手动拼 URL、处理状态码、解析 JSON | 就像调用本地接口一样,强类型约束,支持代码补全 |
| 服务治理 | 需要额外搭配各种组件(如 Ribbon 负载均衡) | 自带服务发现、负载均衡、容错、流量控制等功能 |
大白话总结:HTTP 就像邮寄挂号信,格式通用,但包装重、速度慢;RPC 就像内部专用传送带,专门为了高并发、高性能的分布式系统“量身定制”。
2.RPC 的底层工作流程
一个完整的 RPC 框架在底层到底做了什么其实它的核心流程其实就 4 步:
Stub(桩/代理):消费端(Consumer)通过动态代理,生成一个接口的代理对象。你以为你调用的是方法,其实调用的是代理。
Serialize(序列化):代理对象把你的请求(方法名、参数类型、参数值)打包,转成二进制字节流。
Transport(网络传输):通过网络(比如 Netty/TCP),将字节流发送到服务提供端(Provider)。
Deserialize(反序列化):提供端收到字节流,还原成 Java 对象,通过反射调用真正的业务方法,然后再把结果按原路返回。
动态代理:
正如前面所说,RPC 的目标是让你像调用本地方法一样调用远程服务。但问题是,消费端(Consumer)手里只有接口(比如
UserService.java),并没有实现类(实现类在远端的服务器上)。在 Java 中,接口是不能直接
new出来对象的。那userService.getUserById(1)这行代码为什么能跑通,而且还能把请求发送到网络另一端呢这就是动态代理施展的地方。
1. 什么是动态代理
静态代理:比如你想买海外的商品,你找了一个专业的代购。这个代购(代理类)和你想买的东西(接口)是一一对应的,代购在代码运行前就已经存在了。
动态代理:你去了一个巨大的万能办事处。你走过去说:“我想调用
UserService的getUserById方法。” 办事处当场给你临时变出一个经纪人(代理对象)。这个经纪人根本不知道getUserById怎么具体实现,但他知道把你的请求打包、通过网络发给远端的真实服务器、再把结果拿回来还给你。
动态代理的核心能力:在程序运行期间,根据你传入的接口,动态地在内存中生成一个代理对象。你对这个接口的所有方法调用,都会被拦截并重定向到一个统一的处理器中。
2. 在 RPC 项目中,动态代理用来干什么
如果没有动态代理,每次想调用远程服务
// 极其痛苦:每次调用都要手动写网络请求 byte[] requestData = serialize("getUserById", 1); byte[] responseData = HttpClient.send("http://192.168.1.10:8080/userService", requestData); User user = deserialize(responseData);有了动态代理后,框架在底层帮你把这些脏活累活全封装了:
// 1. 动态代理在内存中生成一个 UserService 的实现类对象 UserService userService = (UserService) Proxy.newProxyInstance(...); // 2. 你像调用本地代码一样调用它 User user = userService.getUserById(1);当你调用userService.getUserById(1)时,JVM 会自动把这个调用转发给动态代理的InvocationHandler(调用处理器)。在这个处理器里面,框架帮你做了 4 件事:
组装报文:把方法名
getUserById、参数类型Long、参数值1封装成一个请求对象(如RpcRequest)。寻找地址:去注册中心问一下
UserService部署在哪台服务器上(服务发现/负载均衡)。网络传输:把请求对象序列化成二进制流,通过 Netty 或 Socket 发送给提供者(Provider)。
获取结果:等待提供者返回结果,反序列化成
User对象,并return给你的业务代码。
对于编写业务代码的你来说,你根本不知道中间发生了一次网络跨越,这就是所谓的“透明化调用”。
3. Java 中常见的动态代理实现
① JDK 动态代理(Dubbo 2.x 及大部分 RPC 默认首选)
原理:利用 JDK 自带的
java.lang.reflect.Proxy类,基于接口生成代理类。要求:目标类必须实现接口。因为 RPC 本身就是面向接口编程的(消费端只有接口),所以 JDK 动态代理完美契合 RPC 场景。
核心代码结构
public class RpcProxyHandler implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 1. 在这里拦截到方法调用 System.out.println("准备调用远程方法: " + method.getName()); // 2. 在这里写你的网络发送逻辑 (Socket/Netty) // ... 发送并等待接收结果 ... return rpcResponse.getResult(); // 返回远程执行的结果 } }
② CGLIB / Javassist 动态代理
原理:通过修改字节码,生成目标类的子类来做代理。
要求:不需要实现接口,只要类和方法不是
final的就行。在 Dubbo 中的应用:Dubbo 为了追求极致的性能,并没有单纯使用 JDK 自带的动态代理,而是默认使用了Javassist(或者在新版本中支持 ByteBuddy)来动态生成字节码。因为 Javassist 生成代理类的速度和执行效率比原生 JDK 更快。
Nginx的反向代理:
我们在苍穹外卖中学过一个词叫反向代理,可能大家都快忘了,包括我也是,只知道有这个词,但是具体的原理早就忘记了,这里顺便提一下。对比一下正向代理
它们最核心的区别在于:它们究竟是在给客户端当代理,还是在给服务端当代理。
正向代理:代理的是客户端(传话筒)。服务端不知道真正发起请求的客户端是谁。
反向代理:代理的是服务端(挡箭牌)。客户端不知道真正提供服务的是哪台后端服务器。
💡 正向代理
正向代理位于客户端和目标服务器之间。客户端非常清楚自己要访问谁,但由于某些原因(比如网络限制、隐藏身份),客户端无法直接访问,或者不想直接访问,于是找了一个代理服务器。
经典场景:科学上网:国内上不了某些国外网站,你找了一台能上该网站的国外代理服务器。你把请求发给代理,代理帮你去该网站拿数据,再传回给你。
公司内网行为管理:公司为了防止员工上班摸鱼,让所有人的电脑都通过一个正向代理服务器上网。这个代理服务器可以限制你不能访问游戏网站、购物网站。
特点:
客户端必须进行配置(比如在浏览器里设置代理 IP 和端口)。
目标服务器只知道代理服务器的 IP,不知道真正的客户端是谁(保护了客户端隐私)。
💡 反向代理
反向代理同样位于客户端和目标服务器之间,但它是服务端的入口。客户端只知道反向代理服务器的地址(比如
nginx.com),直接把请求发过去。反向代理收到后,在后台悄悄把请求分发给真正干活的业务服务器(如 Tomcat)。
经典场景:
Nginx 负载均衡:你的网站访问量巨大,一台服务器扛不住。你用 Nginx 作为反向代理,后面挂了 10 台业务服务器。用户统一访问 Nginx,Nginx 自动把流量分发给这 10 台服务器。
安全防护:保护后端服务器不被黑客直接攻击。黑客只能看到反向代理服务器的 IP,根本碰不到内网真正的数据库和核心业务服务器。
特点:
客户端不需要做任何配置,用户完全感知不到反向代理的存在,觉得这就是普通的网站。
客户端只知道反向代理的 IP,不知道后端真正提供服务的是哪台服务器(保护了服务端隐私)。
为了方便记忆,我们可以通过下面这张表来做对比:
对比维度 正向代理 (Forward Proxy) 反向代理 (Reverse Proxy) 代理的对象 客户端(替客户端发送请求) 服务端(替服务端接收请求) 谁知道真相? 客户端知道(它自己配置的代理) 服务端知道(它自己架构的 Nginx) 谁被隐瞒了? 目标服务端不知道真正的客户端是谁 客户端不知道真正的业务服务器是谁 部署位置 靠近客户端,通常在局域网内 靠近服务端,通常在服务集群前端 核心作用 突破访问限制(翻墙)、隐藏客户端、上网行为审计 负载均衡、保障内网安全、缓存加速 类比一下:
正向代理(找中介):你想租房,但你不想让房东知道你是谁(可能你是个明星)。于是你找了一个房屋中介(正向代理)替你去和房东谈。房东只看到了中介,合同也是和中介签的,房东不知道真正的租客是谁。
反向代理(找前台/物业):你去租某个大集团的公寓,你来到公寓的接待前台(反向代理)。你跟前台说“我要租房”,前台帮你办理了手续,并分给你 302 房间。你自始至终不知道这个公寓背后的老板/管家具体是谁,你只和前台打交道。
负载均衡:
这里想到了负载均衡,也顺便提一下吧,我看的项目也涉及到了:
Nginx 的反向代理负载均衡:就像一个商场的大门保安兼导购。外面的顾客(浏览器/App)想进来买东西,统一先找 Nginx,Nginx 看看哪个收银台(Web 服务器)人少,就把顾客带到哪个收银台。它面对的是外部世界。
Dubbo 的负载均衡:就像商场内部的对讲机调度系统。收银台(Web 服务)在结账时,需要呼叫仓库(Order 服务)或者财务(User 服务)。收银员自己用对讲机问:“仓库 A 和仓库 B 哪个现在有空?”然后直接把请求发过去。它面对的是企业内部服务之间的沟通。
深入细节
| 对比维度 | Nginx 反向代理负载均衡 | Dubbo 负载均衡 |
| 工作位置 | 系统最前端(网关/边缘)。介于客户端(浏览器)和微服务群之间。 | 系统内部(RPC 层)。介于内部服务 A 和服务 B 之间。 |
| 代理对象 | 代理服务器。客户端只知道 Nginx 的 IP,不知道后端真实服务器的 IP。 | 代理接口/方法。通过动态代理,让开发人员感觉在调用本地方法。 |
| 传输协议 | 通常是HTTP / HTTPS。 | 通常是高性能的TCP 协议(如 Dubbo 协议、Triple 协议)。 |
| 负载均衡机制 | 集中式(服务端负载均衡)。请求必须先经过 Nginx 这个“中间商”,由 Nginx 决定转发给谁。 | 客户端负载均衡。消费者(Consumer)本地有服务列表,自己决定调用哪台 Provider,不经过任何中间商,直连过去。 |
| 服务发现 | 需要手动在nginx.conf里配置upstream的 IP 列表(或者结合外部组件静态配置)。 | 动态自动发现。通过注册中心(如 Nacos/ZooKeeper),服务上线下线自动感知,无需人工干预。 |
客户端负载均衡 vs 服务端负载均衡
Nginx:服务端负载均衡 (Server-side LB)
浏览器发送请求给 Nginx。
Nginx 作为一个实体服务器架在中间,由它来选择一台后端的 Tomcat,并把请求转发(Forward)过去。
缺点:Nginx 成了系统的单点瓶颈。如果流量巨大,Nginx 可能会扛不住;而且多了一次网络转发,会有微小的延迟。
Dubbo:客户端负载均衡 (Client-side LB)
Dubbo 的消费端(Consumer)在启动时,会去注册中心(Nacos)把服务提供者(Provider)的 IP 地址列表下载到本地缓存起来。
当代码调用
userService.getUserById()时,Dubbo 在本地运行负载均衡算法(比如随机、轮询),直接选出一个 IP(比如192.168.1.5)。消费端直接发起 Socket 连接调用该 IP,中间没有任何性能损耗和多余的服务器中转。
它们在真实架构中如何协同工作
在一套标准的微服务架构中,它们是各司其职的。流量的完整生命周期通常是这样的:
[ 用户的浏览器/手机App ] │ (HTTP 协议) ▼ ┌───────────────┐ │ Nginx 大门 │ <--- 服务端负载均衡:把外网 HTTP 流量分发给前端 Web 服务器 └───────┬───────┘ │ (HTTP 协议) ▼ ┌──────────────────────────────────────┐ │ Spring Boot 业务网关 / 前端应用集群 │ └───────┬──────────────────────────────┘ │ │ (Dubbo 动态代理拦截,在本地做负载均衡,转为 TCP 协议) ▼ ┌──────────────────────────────────────┐ │ 内部微服务 A (Consumer) │ └───────┬──────────────────────────────┘ │ (TCP 直连,不经过中间商) ▼ ┌──────────────────────────────────────┐ │ 内部微服务 B (Provider) │ └──────────────────────────────────────┘第一步:用户在国内发起一个 HTTP 请求,首先到达Nginx。Nginx 看眼前有 3 台网关服务器,通过轮询,把请求丢给其中一台(Nginx 实现了外网到内网的分流)。
第二步:网关处理完基础验证后,需要调用内部的“商品服务”。此时网关作为 Dubbo 的Consumer,利用动态代理拦截了调用,并在本地计算出“商品服务-实例B”目前最空闲。
第三步:Consumer 直接通过 TCP 协议把请求发给“商品服务-实例B”(Dubbo 实现了内网微服务之间的高性能沟通)。
结语:
如果对你有帮助,请点赞,关注,收藏,你的支持就是我最大的鼓励!