消息通知系統詳解2---後端設計

消息通知系統詳解1—通訊方式
消息通知系統詳解2—後端設計
消息通知系統詳解3—Netty
消息通知系統詳解4—整合Netty和WebSocket


上個小節,我們講到前後端通訊方式選型,那這節我們介紹下後端架構如何去設計?

整體設計

用戶獲取新的消息通知有兩種模式

  • 上線登錄後向系統主動索取
  • 在線時系統向接收者主動推送新消息

設想下,用戶的通知消息和新通知提醒數據都放在數據庫中,數據庫的讀寫操作頻繁。如果消息量大,DB壓力較大,可能出現數據瓶頸。

這邊按照上述兩種模式,拆分下設計:

上線登錄後向系統索取

此模式是接受者請求系統,系統將新的消息通知返回給接收者的模式,流程如下:

  1. 接收者向服務端netty請求
  2. WebSocket連接Netty服務把連接放到自己的連接池中
  3. Netty根據接受者信息向RabbitMQ查詢消息
  4. 如果有新消息,返回新消息通知
  5. 使用WebSocket連接向,接收者返回新消息的數量
    在這裏插入圖片描述

在線時系統向接收者主動推送

此模式是系統將新的消息通知返回給接收者的模式,流程如下:

  1. RabbitMQ將新消息數據推送給Netty
  2. Netty從連接池中取出接收者的WebSocket連接
  3. Netty通過接收者的WebSocket連接返回新消息的數量
    在這裏插入圖片描述

Rabbitmq搭建

在虛擬機中啓動RabbitMQ

docker run -id --name=tensquare_rabbit -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 15672:15672 -p 25672:25672 rabbitmq:management

訪問地址:http://192.168.200.128:15672

登錄賬號: guest

登錄密碼: guest

IO編程

上面提到了Netty,在開始瞭解Netty之前,先來實現一個客戶端與服務端通信的程序,使用傳統的IO編程和使用NIO編程有什麼不一樣。

傳統IO編程

每個客戶端連接過來後,服務端都會啓動一個線程去處理該客戶端的請求。阻塞I/O的通信模型示意圖如下:
在這裏插入圖片描述
業務場景:客戶端每隔兩秒發送字符串給服務端,服務端收到之後打印到控制檯。

public class IOServer {
    public static void main(String[] args) throws Exception {
​
        ServerSocket serverSocket = new ServerSocket(8000);while (true) {
            // (1) 阻塞方法獲取新的連接
            Socket socket = serverSocket.accept();new Thread() {
                @Override
                public void run() {
                    String name = Thread.currentThread().getName();try {
                        // (2) 每一個新的連接都創建一個線程,負責讀取數據
                        byte[] data = new byte[1024];
                        InputStream inputStream = socket.getInputStream();
                        while (true) {
                            int len;
                            // (3) 按字節流方式讀取數據
                            while ((len = inputStream.read(data)) != -1) {
                                System.out.println("線程" + name + ":" + new String(data, 0, len));
                            }
                        }
                    } catch (Exception e) {
                    }
                }
            }.start();
        }
    }
}

客戶端實現:

public class MyClient {public static void main(String[] args) {
        //測試使用不同的線程數進行訪問
        for (int i = 0; i < 5; i++) {
            new ClientDemo().start();
        }
    }static class ClientDemo extends Thread {
        @Override
        public void run() {
            try {
                Socket socket = new Socket("127.0.0.1", 8000);
                while (true) {
                    socket.getOutputStream().write(("測試數據").getBytes());
                    socket.getOutputStream().flush();
                    Thread.sleep(2000);
                }
            } catch (Exception e) {
            }
        }
    }
}

從服務端代碼中我們可以看到,在傳統的IO模型中,每個連接創建成功之後都需要一個線程來維護,每個線程包含一個while死循環。

如果在用戶數量較少的情況下運行是沒有問題的,但是對於用戶數量比較多的業務來說,服務端可能需要支撐成千上萬的連接,IO模型可能就不太合適了。
如果有1萬個連接就對應1萬個線程,繼而1萬個while死循環,這種模型存在以下問題:

  • 當客戶端越多,就會創建越多的處理線程。線程是操作系統中非常寶貴的資源,同一時刻有大量的線程處於阻塞狀態是非常嚴重的資源浪費。並且如果務器遭遇洪峯流量衝擊,例如雙十一活動,線程池會瞬間被耗盡,導致服務器癱瘓。
  • 因爲是阻塞式通信,線程爆炸之後操作系統頻繁進行線程切換,應用性能急劇下降。
  • IO編程中數據讀寫是以字節流爲單位,效率不高。

NIO編程

NIO,也叫做new-IO或者non-blocking-IO,可理解爲非阻塞IO。NIO編程模型中,新來一個連接不再創建一個新的線程,而是可以把這條連接直接綁定到某個固定的線程,然後這條連接所有的讀寫都由這個線程來負責,我們用一幅圖來對比一下IO與NIO:
在這裏插入圖片描述
如上圖所示,IO模型中,一個連接都會創建一個線程,對應一個while死循環,死循環的目的就是不斷監測這條連接上是否有數據可以讀。但是在大多數情況下,1萬個連接裏面同一時刻只有少量的連接有數據可讀,因此,很多個while死循環都白白浪費掉了,因爲沒有數據。

而在NIO模型中,可以把這麼多的while死循環變成一個死循環,這個死循環由一個線程控制。這就是NIO模型中選擇器(Selector)的作用,一條連接來了之後,現在不創建一個while死循環去監聽是否有數據可讀了,而是直接把這條連接註冊到選擇器上,通過檢查這個選擇器,就可以批量監測出有數據可讀的連接,進而讀取數據。

NIO的三大核心組件:通道(Channel)、緩衝(Buffer)、選擇器(Selector)

通道(Channel)

是傳統IO中的Stream(流)的升級版。Stream是單向的、讀寫分離(inputstream和outputstream),Channel是雙向的,既可以進行讀操作,又可以進行寫操作。

緩衝(Buffer)

Buffer可以理解爲一塊內存區域,可以寫入數據,並且在之後讀取它。

選擇器(Selector)

選擇器(Selector)可以實現一個單獨的線程來監控多個註冊在她上面的信道(Channel),通過一定的選擇機制,實現多路複用的效果。

NIO相對於IO的優勢:

IO是面向流的,每次都是從操作系統底層一個字節一個字節地讀取數據,並且數據只能從一端讀取到另一端,不能前後移動流中的數據。NIO則是面向緩衝區的,每次可以從這個緩衝區裏面讀取一塊的數據,並且可以在需要時在緩衝區中前後移動。
IO是阻塞的,這意味着,當一個線程讀取數據或寫數據時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入,在此期間該線程不能幹其他任何事情。而NIO是非阻塞的,不需要一直等待操作完成才能幹其他事情,而是在等待的過程中可以同時去做別的事情,所以能最大限度地使用服務器的資源。
NIO引入了IO多路複用器selector。selector是一個提供channel註冊服務的線程,可以同時對接多個Channel,並在線程池中爲channel適配、選擇合適的線程來處理channel。由於NIO模型中線程數量大大降低,線程切換效率因此也大幅度提高。
和前面一樣的場景,使用NIO實現(複製代碼演示效果即可):

public class NIOServer {
    public static void main(String[] args) throws IOException {
        // 負責輪詢是否有新的連接
        Selector serverSelector = Selector.open();
        // 負責輪詢處理連接中的數據
        Selector clientSelector = Selector.open();new Thread() {
            @Override
            public void run() {
                try {
                    // 對應IO編程中服務端啓動
                    ServerSocketChannel listenerChannel = ServerSocketChannel.open();
                    listenerChannel.socket().bind(new InetSocketAddress(8000));
                    listenerChannel.configureBlocking(false);
                    // OP_ACCEPT表示服務器監聽到了客戶連接,服務器可以接收這個連接了
                    listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);while (true) {
                        // 監測是否有新的連接,這裏的1指的是阻塞的時間爲1ms
                        if (serverSelector.select(1) > 0) {
                            Set<SelectionKey> set = serverSelector.selectedKeys();
                            Iterator<SelectionKey> keyIterator = set.iterator();while (keyIterator.hasNext()) {
                                SelectionKey key = keyIterator.next();if (key.isAcceptable()) {
                                    try {
                                        // (1) 每來一個新連接,不需要創建一個線程,而是直接註冊到clientSelector
                                        SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                                        clientChannel.configureBlocking(false);
                                        // OP_READ表示通道中已經有了可讀的數據,可以執行讀操作了(通道目前有數據,可以進行讀操作了)
                                        clientChannel.register(clientSelector, SelectionKey.OP_READ);
                                    } finally {
                                        keyIterator.remove();
                                    }
                                }
                            }
                        }
                    }
                } catch (IOException ignored) {
                }
            }
        }.start();
​
​
        new Thread() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                try {
                    while (true) {
                        // (2) 批量輪詢是否有哪些連接有數據可讀,這裏的1指的是阻塞的時間爲1ms
                        if (clientSelector.select(1) > 0) {
                            Set<SelectionKey> set = clientSelector.selectedKeys();
                            Iterator<SelectionKey> keyIterator = set.iterator();while (keyIterator.hasNext()) {
                                SelectionKey key = keyIterator.next();if (key.isReadable()) {
                                    try {
                                        SocketChannel clientChannel = (SocketChannel) key.channel();
                                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                                        // (3) 讀取數據以塊爲單位批量讀取
                                        clientChannel.read(byteBuffer);
                                        byteBuffer.flip();
                                        System.out.println("線程" + name + ":" + Charset.defaultCharset().newDecoder().decode(byteBuffer)
                                                .toString());
                                    } finally {
                                        keyIterator.remove();
                                        key.interestOps(SelectionKey.OP_READ);
                                    }
                                }
                            }
                        }
                    }
                } catch (IOException ignored) {
                }
            }
        }.start();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章