从零手写RPC框架:Java高并发场景下的核心原理与实战案例
目录导读
- RPC框架的本质:一次跨进程方法调用的完整旅程
- 架构拆解:动态代理、序列化、网络传输与服务发现
- Java实现核心代码案例(基于Netty + Zookeeper)
- 性能陷阱与优化:从连接复用到心跳检测
- 高频面试问答:为什么说“RPC是分布式系统的骨架”?
RPC框架的本质:一次跨进程方法调用的完整旅程
在分布式系统中,服务A调用服务B的方法,本质上是通过网络发送“调用意图”,RPC(Remote Procedure Call)框架要解决的三大问题:

- 如何“伪装”成本地调用? → 动态代理
- 如何让数据穿越网络? → 序列化/反序列化
- 如何找到目标服务? → 服务注册与发现
一个鲜活的案例是:当你在订单服务中调用userService.getUserById(1001)时,实际发生的是:
客户端代理 → 序列化请求(方法名+参数) → 网络传输 → 服务端反序列化 → 反射调用真实方法 → 序列化结果 → 回传客户端
架构拆解:四大核心组件
| 组件 | 职责 | 常用技术选型 |
|---|---|---|
| 动态代理 | 拦截本地调用,转换为远程消息 | JDK Proxy / CGLIB |
| 序列化协议 | 将对象转换为字节流 | Protobuf / JSON / Hessian |
| 网络通信 | 高效传输字节流 | Netty(NIO) / 传统BIO |
| 注册中心 | 维护服务地址列表 | Zookeeper / Nacos / Consul |
关键点:Netty的零拷贝和内存池特性,是解决高并发网络I/O的基石,这也是为什么主流RPC框架(如Dubbo)默认选择Netty的原因。
Java实现核心代码案例(基于Netty + Zookeeper)
Step 1:定义RPC请求/响应协议
public class RpcRequest implements Serializable {
private String requestId;
private String className; // 接口全限定名
private String methodName;
private Class<?>[] parameterTypes;
private Object[] parameters;
// getter/setter...
}
public class RpcResponse implements Serializable {
private String requestId;
private Object result; // 返回值
private Throwable error; // 异常信息
}
Step 2:客户端动态代理(核心伪代码)
public class RpcProxyFactory {
public static <T> T create(Class<T> interfaceClass) {
return (T) Proxy.newProxyInstance(
interfaceClass.getClassLoader(),
new Class[]{interfaceClass},
(proxy, method, args) -> {
RpcRequest req = buildRequest(interfaceClass, method, args);
RpcClient client = new RpcClient();
// 从Zookeeper获取服务地址,轮询或随机
String serviceAddr = discovery(interfaceClass.getName());
RpcResponse resp = client.send(req, serviceAddr); // Netty同步等待
return resp.getResult();
});
}
}
Step 3:服务端反射处理(核心逻辑)
public class RpcServerHandler extends SimpleChannelInboundHandler<RpcRequest> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcRequest req) {
// 根据 className 从 Spring 容器获取 Bean
Object serviceBean = applicationContext.getBean(req.getClassName());
Method method = serviceBean.getClass().getMethod(req.getMethodName(), req.getParameterTypes());
Object result = method.invoke(serviceBean, req.getParameters());
// 封装响应并写回
}
}
网络传输层:必须设置ChannelOption.TCP_NODELAY为true,禁用Nagle算法,避免小数据包延迟。
性能陷阱与优化:从连接复用到心跳检测
实战案例教训:一个金融项目初期RPC性能低下,排查后发现:
- 问题1:每次调用都重新建立TCP连接(握手+挥手开销巨大)→ 解决:使用Netty的
ChannelPool连接池,固定数量长连接复用。 - 问题2:死连接未被清理(服务器宕机)→ 解决:实现自定义心跳机制(如每30秒发送Ping包),连续3次未响应则剔除连接。
- 问题3:同步阻塞等待响应 → 解决:使用
CompletableFuture和requestId映射,支持异步回调。
优化后效果:相同硬件条件下,QPS从800提升至5000,RT从120ms降至35ms。
高频面试问答:为什么说“RPC是分布式系统的骨架”?
Q1:RPC和HTTP调用有什么本质区别?
- RPC:面向服务内部,强调高性能、低延迟,通常使用TCP长连接和二进制序列化(如Protobuf)。
- HTTP:面向外部API,强调通用性和跨语言,基于文本协议(JSON/XML)。
- 选择建议:内部服务间用RPC,开放API用HTTP。
Q2:如何保证RPC调用不丢不重?
- 幂等性设计:接口设计时使用唯一业务ID(如订单号),服务端判断是否已处理。
- 超时重试策略:设置合理超时时间(默认3000ms),只对查询类接口开启重试,写操作必须人工确认。
Q3:如果Zookeeper挂了,RPC还能工作吗?
- 短期可以:客户端本地会缓存服务地址列表。
- 长期不可:服务上下线无法感知,需配合健康检查机制(如Dubbo的消费者主动探测)。
- 架构优化:使用Nacos等AP模式注册中心(牺牲强一致性,换高可用)。
Q4:Java中为什么不直接用JDK自带序列化?
- JDK序列化产生冗余数据(类信息+继承链),体积大,且存在反序列化安全漏洞。
- 生产环境推荐Kryo(体积小、速度快)或Protobuf(强类型约束,跨语言)。
从“能用”到“好用”的演进
手写一个RPC框架最难的不是代码,而是对分布式问题的深刻认知,建议读者基于上述案例,逐步加入熔断降级(如Sentinel)、链路追踪(如SkyWalking)和流量控制,这将是你技术深度跃迁的最佳路径,不妨打开你的IDE,从RpcRequest类开始,亲手实现一个只有100行核心代码的迷你RPC——你会发现,所谓的“高级架构”其实由无数精妙的工程细节组成。