Dubbo框架的1個核心設計點

程序员半支烟發表於2024-09-14

Java領域要說讓我最服氣的RPC框架當屬Dubbo,原因有許多,但是最吸引我的還是它把遠端呼叫這個事情設計得很有藝術。

1、Dubbo優點較多,我只鍾情其一

1.1、優點

業內對於微服務之間呼叫的框架選擇較多,主流是Spring Cloud的Rest方式 和 Dubbo方式,我使用Dubbo方式居多。Dubbo工業級可用,穩定又高效,深受各大公司研發同學的喜愛。

Dubbo的優點較多,比如:

  • 高效能:Dubbo 使用的是基於 Netty 的自定義通訊協議,提供了高效的二進位制資料傳輸,使得遠端服務呼叫效能大幅提升。
  • 模組化設計:Dubbo 的架構非常模組化,主要由五大核心模組組成:遠端呼叫模組(RPC)、叢集模組、負載均衡模組、容錯模組和註冊中心模組。
  • 每個部件都支援多協議:每個部件都支援多種協議,比如註冊中心,支援ZK、Redis、Nacos等等。
  • 負載均衡和容錯:Dubbo 提供了多種容錯機制,比如失敗重試、失敗轉移等。還支援多種負載均衡,比如隨機、輪詢、一致性雜湊等。
  • 服務註冊和發現:Dubbo引入了註冊中心的概念,實現了服務的自動註冊和發現。
  • SPI 擴充套件機制:在背八股文場景下,Dubbo被提及最多的就是使用了類似Java的SPI機制,提高了擴充套件性,這一點仁者見仁智者見智吧。

1.2、鍾情其一

但是,Dubbo最吸引人的,半支菸覺得反而倒是它的RPC呼叫。Dubbo的定位是一個RPC框架,這是它的核心和立足之地,所以Dubbo將RPC的呼叫過程透明化,使得開發者可以專注於業務邏輯,而不用關注底層通訊問題。

一個RPC框架只有聚焦於先做好它的RPC呼叫過程這個模組,才會有人關注,其餘的優點都是在這之後,慢慢迭代而來。

作者將RPC呼叫的這個過程,抽象成一種協議訊息的傳輸機制,再透過控制好執行緒的等待和喚醒,來實現遠端方法呼叫。這一設計思路真是美妙,充分體驗了作者的智慧。

2、RPC簡易示例

學Dubbo,首先就是要學習作者這種設計理念和思路。基於此,來實現一個簡易的遠端方法呼叫,將Dubbo的RPC過程簡易化。

2.1、示例步驟

簡易的RPC過程步驟如下,大致分5步,依舊使用Netty作用Socket通訊工具。

  1. 使用2個Java程序來模擬2個系統之間的呼叫,A程序 和 B程序。
  2. A程序的某個方法,使用網路請求呼叫B程序的某個方法。
  3. 然後A程序的方法就處於等待狀態。
  4. 等B程序的方法執行完之後,在利用網路通知到A程序。
  5. 然後A程序的方法被喚醒,繼續往下執行。

2.2、示例程式碼

  • B程序作為服務端,啟動網路服務
public class BProcessServer {
    private final int port;
    public BProcessServer(int port) {
        this.port = port;
    }

    public void start() throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(new BProcessServerHandler());
                        }
                    });

            ChannelFuture future = bootstrap.bind(port).sync();
            System.out.println("B啟動了服務,埠號: " + port);
            future.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new BProcessServer(8088).start();
    }
}
  • B程序接受網路請求引數,反序列化之後,執行對應的方法,再將執行結果返回:
public class BProcessServerHandler extends SimpleChannelInboundHandler<ByteBuf> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        String reqData = msg.toString(CharsetUtil.UTF_8);
        System.out.println("B程序接受到了請求資料: " + reqData);

        executeMethod(ctx);
    }

    /**
     * 執行方法
     *
     * @param ctx
     * @throws InterruptedException
     */
    private void executeMethod(ChannelHandlerContext ctx) throws InterruptedException {
        // TODO 將請求訊息按照某種規則解析成方法名、方法引數等,其實就是反序列化的過程。
        System.out.println("對接受的資料做反序列化,然後開始執行 訊息體裡指定的方法...");

        // 模擬方法執行
        Thread.sleep(2000);
        System.out.println("執行完畢,返回結果...");

        // 將結果 通知給 A 程序
        ByteBuf dataByteBuf = ctx.alloc().buffer().writeBytes("Task completed".getBytes(CharsetUtil.UTF_8));
        ctx.writeAndFlush(dataByteBuf);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}
  • A程序啟動Netty客戶端,建立與B程序的通訊,然後發起遠端呼叫,處於等待狀態。
public class AProcessClient {

    private final String host;
    private final int port;
    private final Object lock = new Object();  // 監視器物件

    public AProcessClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void start() throws InterruptedException {
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline().addLast(new AProcessClientHandler(lock));
                        }
                    });

            ChannelFuture future = bootstrap.connect(host, port).sync();
            System.out.println("A程序與B程序建立了通訊連線");

            Channel channel = future.channel();

            // 發起遠端呼叫
            callRemoteMethod(channel);

            channel.closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }

    /**
     * 執行方法
     *
     * @param channel
     * @throws InterruptedException
     */
    private void callRemoteMethod(Channel channel) throws InterruptedException {
        //TODO 此處需要將呼叫的方法和引數,按照協議進行序列化。這次暫且省去此過程。
        System.out.println("A程序將 請求的方法和引數 進行序列化,然後向B程序發起網路呼叫...");

        ByteBuf dataByteBuf = channel.alloc().buffer().writeBytes("Start call method".getBytes(CharsetUtil.UTF_8));

        channel.writeAndFlush(dataByteBuf);

        // 使用wait等待B程序通知
        synchronized (lock) {
            System.out.println("A程序等待B程序的響應...");
            lock.wait();  // 等待通知
        }

        System.out.println("A程序收到了B程序的響應通知,繼續往下...");
    }

    public static void main(String[] args) throws InterruptedException {
        new AProcessClient("localhost", 8088).start();
    }
}
  • A程序接受B程序的響應,同時被喚醒,然後以上lock.wait()以後的程式碼得以繼續執行。
public class AProcessClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    private final Object lock;

    public AProcessClientHandler(Object lock) {
        this.lock = lock;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        String resData = msg.toString(CharsetUtil.UTF_8);
        System.out.println("A程序接受到了響應資料: " + resData);

        // B 程序任務完成,使用 notify 喚醒等待的執行緒
        synchronized (lock) {
            lock.notify();  // 喚醒 A 程序
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

3、總結

Dubbo的優秀設計思路有許多,我只鍾情其一,那就是RPC的呼叫過程。以上是一個簡易的RPC遠端呼叫的示例,用於理解Dubbo的原理和原始碼,希望對你有幫助!

本篇完結!歡迎 關注、加V(yclxiao)交流、全網可搜(程式設計師半支菸)

原文連結:https://mp.weixin.qq.com/s/J0fzDH-iqGnnnjqaXMLs-A

相關文章