Java实现RPC框架案例

wen java案例 2

从零手写RPC框架:Java高并发场景下的核心原理与实战案例


目录导读

  1. RPC框架的本质:一次跨进程方法调用的完整旅程
  2. 架构拆解:动态代理、序列化、网络传输与服务发现
  3. Java实现核心代码案例(基于Netty + Zookeeper)
  4. 性能陷阱与优化:从连接复用到心跳检测
  5. 高频面试问答:为什么说“RPC是分布式系统的骨架”?

RPC框架的本质:一次跨进程方法调用的完整旅程

在分布式系统中,服务A调用服务B的方法,本质上是通过网络发送“调用意图”,RPC(Remote Procedure Call)框架要解决的三大问题:

Java实现RPC框架案例

  • 如何“伪装”成本地调用? → 动态代理
  • 如何让数据穿越网络? → 序列化/反序列化
  • 如何找到目标服务? → 服务注册与发现

一个鲜活的案例是:当你在订单服务中调用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_NODELAYtrue,禁用Nagle算法,避免小数据包延迟。


性能陷阱与优化:从连接复用到心跳检测

实战案例教训:一个金融项目初期RPC性能低下,排查后发现:

  • 问题1:每次调用都重新建立TCP连接(握手+挥手开销巨大)→ 解决:使用Netty的ChannelPool连接池,固定数量长连接复用。
  • 问题2:死连接未被清理(服务器宕机)→ 解决:实现自定义心跳机制(如每30秒发送Ping包),连续3次未响应则剔除连接。
  • 问题3:同步阻塞等待响应 → 解决:使用CompletableFuturerequestId映射,支持异步回调

优化后效果:相同硬件条件下,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——你会发现,所谓的“高级架构”其实由无数精妙的工程细节组成。

抱歉,评论功能暂时关闭!