零拷貝原理的文章網上滿天飛,但你知道如何使用零拷貝嗎?

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"零拷貝是中間件相關面試中必考題,本文就和大家一起來總結一下NIO拷貝的原理,並結合Netty代碼,從代碼實現層面近距離觀摩如何使用java實現零拷貝。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#48b378","name":"user"}}],"text":"1、零拷貝實現原理","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"**“零拷貝”**其實包括兩個層面的含義:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"拷貝一份相同的數據從一個地方移動到另外一個地方的過程,叫拷貝。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"零希望在IO讀寫過程中,CPU控制的數據拷貝到次數爲0。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在IO編程領域,當然是拷貝的次數越少越好,逐步優化,將其拷貝次數將爲0,最大化的提高性能。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那接下來我們循序漸進來看一下如何減少數據複製。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"接下來我們將以RocketMQ消息發送、消息讀取場景來闡述IO讀寫過程中可能需要進行的數據複製與上下文切換。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"1.1 傳統的IO讀流程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一次傳統的IO讀序列流程如下所示:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/cd/cd56790a335e5db1e978734afcdd252f.png","alt":"在這裏插入圖片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"java應用中,如果要將從文件中讀取數據,其基本的流程如下所示:","attrs":{}}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"當broker收到拉取請求時發起一次read系統調用,此時操作系統會進行一次上下文的切換,從用態間切換到內核態。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"通過直接存儲訪問器(DMA)從磁盤將數據加載到內核緩存區(DMA Copy,這個階段不需要CPU參與,如果是阻塞型IO,該過程用戶線程會處於阻塞狀態)","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"然後在CPU的控制下,將內核緩存區的數據copy到用戶空間的緩存區(由於這個是操作系統級別的行爲,通常這裏指的內存緩存區,通常使用的是堆外內存),這裏將發生一次CPU複製與一次上下文切換(從內核態切換到用戶態)","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"將堆外內存中的數據複製到應用程序的堆內存,供應用程序使用,本次複製需要經過CPU控制。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"將數據加載到堆空間,需要傳輸到網卡,這個過程又要進入到內核空間,然後複製到sockebuffer,然後進入網卡協議引擎,從而進入到網絡傳輸中。該部分會在接下來會詳細介紹。","attrs":{}}]}]}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#6a737d","name":"user"}}],"text":"溫馨提示:RocketMQ底層的工作機制並不是上述模型,是經過優化後的讀寫模型,本文將循序漸進的介紹優化過程。","attrs":{}}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"1.2 傳統的IO寫流程","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一次傳統的IO寫入流程如下圖所示:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/43/43450a516cc3e95f0034014c9d30c14c.png","alt":"在這裏插入圖片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"核心關鍵步驟如下:","attrs":{}}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"在broker收到消息時首先會在堆空間中創建一個堆緩存區,用於存儲用戶需要寫入的數據,然後需要將jvm堆內存中數據複製到操作系統內存(CPU COPY)","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"發起write系統調用,將用戶空間中的數據複製到內存緩存區,**此過程發生一次上下文切換(用戶態切換到內核態)**並進行一次CPU Copy。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"通過直接存儲訪問器(DMA)將內核空間的數據寫入到磁盤,並返回結果,此過程發生一次DMA Copy 與一次上下文切換(內核態切換到用戶態)","attrs":{}}]}]}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"1.3 讀寫優化技巧","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從上面兩張流程圖,我們不能看出讀寫處理流程中存在太多複製,同樣的數據需要被複制多次,造成性能損耗,故IO讀寫通常的優化方向主要爲:","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"減少複製次數、減少用戶態/內核態切換次數。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"1.3.1 引入堆外內存","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"jvm堆空間中數據要發送到內核緩存區,通常需要先將jvm堆空間中的數據拷貝到系統內存(","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"一個非官方的理解,用C語言實現的本地方法調用中,首先需要將堆空間中數據拷貝到C語言相關的存儲結構","attrs":{}},{"type":"text","text":"),故提高性能的第一個措施:使用堆外內存。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不過堆外內存中的數據,通常還是需要從堆空間中獲取,從這個角度來看,貌似提升的性能有限。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"1.3.2 引入內存映射(MMap與write)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過引入內存映射機制,減少用戶空間與內核空間之間的數據複製,如下圖所示:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7a/7a88eb153b1d9ae4727aaacd2edcc464.png","alt":"在這裏插入圖片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"內存映射的核心思想就是將內核緩存區、用戶空間緩存區映射到同一個物理地址上,可以減少用戶緩存區與內核緩存區之間的","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"數據拷貝","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但由於內存映射機制並不會減少上下文切換次數。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"1.3.3 大名鼎鼎鼎sendfile","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Linux 2.1內核引入了sendfile函數用於將","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"文件通過socket傳送","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#6a737d","name":"user"}},{"type":"strong","attrs":{}}],"text":"注意sendfile的傳播方向:使用於將文件中的內容直接傳播到Socket,通常使用客戶端從服務端文件中讀取數據,在服務端內部實現零拷貝。","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在1.3.1中介紹客戶端從服務端讀取消息的過程中,並沒有展開介紹從服務端寫入到客戶端網絡中的過程,接下來看看sendfile的數據拷貝圖解:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/bc/bc2e696ce2b96ac1fcc43107d7681433.png","alt":"在這裏插入圖片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"sendfile的主要特點是","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"在內核空間中通過DMA將數據從磁盤文件拷貝到內核緩存區,然後可以直接將內核緩存區中的數據在CPU控制下將數據複製到socket緩存區","attrs":{}},{"type":"text","text":",最終在DMA的控制下將socketbufer中拷貝到協議引擎,然後經網卡傳輸到目標端。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"sendfile的優勢(特點):","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"一次sendfile調用會只設計兩次上下文切換,比read+write減少兩次上下文切換。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"一次sendfile會存在3次copy,其中一次CPU拷貝,兩次DMA拷貝。","attrs":{}}]}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"1.3.4 Linux Gather","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Linux2.4內核引入了gather機制,用以消除最後一次CPU拷貝,即不再將內核緩存區中的數據拷貝到socketbuffer,而是將內存緩存區中的內存地址、需要讀取數據的長度寫入到socketbuffer中,然後DMA直接根據socketbuffer中存儲的內存地址,直接從內核緩存區中的數據拷貝到協議引擎(注意,這次拷貝由DMA控制)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"從而實現真正的零拷貝。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#48b378","name":"user"}}],"text":"2、結合Netty談零拷貝實戰","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面講述了“零拷貝”的實現原理,","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"接下來將嘗試從Netty源碼去探究在代碼層面如何使用“零拷貝”","attrs":{}},{"type":"text","text":"。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從網上的資料可以得知,在java nio提供的類庫中真正能運用底層操作系統的零拷貝機制只有FileChannel的transferTo,而在Netty中也不出意料的對這種方式進行了封裝,其類圖如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/0e/0e0dfde664fbec8dc34537927909dd12.png","alt":"在這裏插入圖片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其主要的核心要點是FileRegion的transferTo方法,我們結合該方法再來介紹DefaultFileRegion各個核心屬性的含義。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/68/68945b999ed472587975f8f0618f113d.png","alt":"在這裏插入圖片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述代碼並不複雜,我們不難得出如下觀點:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"首先介紹DefaultFileRegion的核心屬性含義:File f底層抽取數據來源的底層磁盤文件FileChannel file底層文件的文件通道。long position數據從通道中抽取的起始位置long count需要傳遞的總字節數long transfered已傳遞的字節數量。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"核心要點是調用java nio FileChannel的transferTo方法,底層調用的是操作系統的sendfile函數,即真正的零拷貝。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"調用一次transferTo方法並不一定能將需要的數據全部傳輸完成,故該方法返回已傳輸的字節數,是否需要再次調用該方法的判斷方法:已傳遞的字節數是否等於需要傳遞的總字節數(transfered == count)","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來我們看一下FileRegion的transferTo在netty中的調用鏈,從而推斷一下Netty中的零拷貝的觸發要點。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6f/6f228207ca26adb36ed3521f5422071d.png","alt":"在這裏插入圖片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Netty中代表兩個類型的通道:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"EpollSocketChannel基於Epoll機制進行事件的就緒選擇機制。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"NioSocketChannel","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"基於select機制的事件就緒選擇。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Netty中調用通道Channel的flush或writeAndFlush方法,都會最終觸發底層通道的網絡寫事件,如果待寫入的對象是FileRegion,則會觸發零拷貝機制,接下來我們對兩個簡單介紹一下:","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.1 EpollSocketChannel 通道零拷貝","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"寫入的入口函數爲如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/89/89c2e7e395ebe499720eb3ee2a9399d5.jpeg","alt":"在這裏插入圖片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"核心思想爲:如果待寫入的消息是DefaultFileRegion,EpollSocketChannel將直接調用sendfile函數進行數據傳遞;如果是FileRegion類型,則按照約定調用FileRegion的transferTo進行數據傳遞,這種方式是否真正進行零拷貝取決於FileRegion的transferTo中是否調用了FileChannel的transferTo方法。","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"color","attrs":{"color":"#6a737d","name":"user"}}],"text":"溫馨提示:本文並沒有打算詳細分析Epoll機制以及編程實踐。","attrs":{}}]}],"attrs":{}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.2 NioSocketChannel 通道零拷貝實現","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實現入口爲:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f6/f690f262540f48203e6e437761441875.png","alt":"在這裏插入圖片描述","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從這裏可知,NioSocketChannel就是中規中矩的調用FileRegion的transferTo方法,是否真正實現了零拷貝,取決於底層是否調用了FileChannel的transferTo方法。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2.3 零拷貝實踐總結","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從Netty的實現中我們基本可以得出結論:是否是零拷貝,判斷的依據是是否調用了FileChannel的transferTo方法,更準備的表述是底層是否調用了操作系統的sendfile函數,並且操作系統底層還需要支持gather機制,即linux的內核版本不低於2.4。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文章首發於","attrs":{}},{"type":"link","attrs":{"href":"https://www.codingw.net/posts/e587b550.html","title":"","type":null},"content":[{"type":"text","text":"https://www.codingw.net/posts/e587b550.html","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"horizontalrule","attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者簡介:丁威,《RocketMQ 技術內幕》一書作者、RocketMQ 開源社區優秀佈道師,公衆號「中間件興趣圈」維護者,主打成體系剖析 Java 主流中間件,已發佈 Kafka、RocketMQ、Dubbo、Sentinel、Canal、ElasticJob 等中間件 15 個專欄。","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章