分布式存儲-Redis高性能的原理
前面聊了網絡通信,當我們連接Redis的時候,就是一次通信的過程,所以我們講Redis的高性能的根本之一就是,網絡通信。前面有朋友問到我Redis可以同時處理那么多並發的原因是不是和通信中的多路復用有關,我答應他在后續的章節中講講,所以本章聊聊
- 他的底層和多路復用機制(Reactor模型)
- 內存回收策略
Redis6.0之前的單線程reactor模型
Reactor其實不是一種新的技術,而是基於NIO多路復用機制,提出來的一種高性能的設計模式,底層還是咱們之前聊得NIO多路復用。他的想法是把相應事件和咱們的業務進行分離,這樣就可以通過一個或者多個線程處理IO事件。這里有三個部分:
- Reactor:進行IO事件的分發
- Handler : 處理非阻塞的讀和寫(這其實就是真正處理IO的處理器)
- Acceptor:處理客戶端的連接
整體流程:
Reactor
View Codepublic class Reactor implements Runnable { private final Selector selector; private final ServerSocketChannel serverSocketChannel; public Reactor(int port) throws IOException { selector=Selector.open(); serverSocketChannel=ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(port)); serverSocketChannel.configureBlocking(false); //注冊一個連接事件 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT,new Acceptor(selector,serverSocketChannel)); } @Override public void run() { while (!Thread.interrupted()) { try { selector.select(); Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { dispatch(iterator.next()); iterator.remove(); } } catch (IOException e) { e.printStackTrace(); } } } private void dispatch(SelectionKey next){ //這里到時候就可能拿到handler 或者 acceptor Runnable attachment = (Runnable) next.attachment(); if (attachment!=null){ attachment.run(); } } }Acceptor
View Codepublic class Acceptor implements Runnable { private final Selector selector; private final ServerSocketChannel serverSocketChannel; public Acceptor(Selector selector, ServerSocketChannel serverSocketChannel) { this.selector = selector; this.serverSocketChannel = serverSocketChannel; } @Override public void run() { SocketChannel socketChannel; try { socketChannel=serverSocketChannel.accept(); System.out.println("I get a accept from client!!!"+socketChannel.getRemoteAddress()); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ,new Handler(socketChannel)); } catch (IOException e) { e.printStackTrace(); } } }Handler
View Codepublic class Handler implements Runnable { SocketChannel socketChannel; public Handler(SocketChannel socketChannel) { this.socketChannel = socketChannel; } @Override public void run() { ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int len = 0, total = 0; StringBuilder message = new StringBuilder(); try { do { len = socketChannel.read(byteBuffer); //這里表示還有消息沒有讀完 if (len > 0) { total += len; message.append(new String(byteBuffer.array())); } } while (len > byteBuffer.capacity()); System.out.println(total + ":total"); System.out.println("Server receive message from " + socketChannel.getRemoteAddress() + "::" + message); } catch (IOException e) { e.printStackTrace(); } finally { if (socketChannel != null) { try { socketChannel.close(); } catch (IOException e) { e.printStackTrace(); } } } } }Test
View Codepublic class ReactorMain { public static void main(String[] args) throws IOException { new Thread(new Reactor(8080)).start(); } }這個時候我們去鏈接我們的Reactor
Reactor收到請求,注冊accept事件,多路復用器發現,於是調用acceptor
使用客戶端給reactor發送消息:客戶端發送到reactor上,多路復用發現了事件,然后就去調用handler的run方法,handler就可以讀取到發送內容了
Redis6.0之后
redis6.0之后是的流程為:讀socket、解析socket、以及寫入socket是通過多線程完成的而執行操作是通過單線程完成的,簡而言之,它的主線程只需要處理指令,而其他的操作交給其他的線程進行完成,這樣性能就大大的提高了,並且他還是線程安全的,因為它還是只是由主線程進行操作IO.而6.0之前都是通過單線程完成的
Redis內存回收策略
為什么聊它的內存回收策略的?這是因為它的內存空間是有限的,如果我們不給緩存設置過期時間,有可能就會有很多無效的緩存,那這些無效的數據就會占用我們的內存,從而導致IO的性能(因為檢索或0者操縱數據的時間復雜度就會增加)。
聊到內存淘汰策略,大家一定聽過LRU、LFU這樣的算法。那么當內存達到他的上限的時候(默認是沒有上限,我們可以設置他的內存上限),我們就可以設置LRU算法去釋放部分無效的key.redis提供了8種的內存淘汰策略。
- volatile-lru:針對設置了過期時間的key,使用iru算法進行淘汰(移除最少使用的keys)
- allkeys-lru: 針對所有的key使用iru進行淘汰(移除最少使用的)
- volatile-lfu: 針對最不經常(最少)使用的key,使用lfu算法進行淘汰
- allkeys-lfu:針對所有的key使用lfu算法移除最不經常使用的key
- volatile-random:針對設置了過期時間的key中,隨機移除某個key 。
- allkeys-random:針對所有的keys,隨機移除keys
- volatile-ttl:針對設置了過期時間的keys中,移除存活時間最少的key
- no-eviction:不刪除key,直接拋出異常
其實對於上面這些回收策略,我們主要要分析的就是LRU和LFU,因為random以及ttl沒有什么邏輯可言。
LRU(least recently used):從它的英文全名就能看出他的意思是【最近很少使用的】。那他是如何實現的呢?
他的底層用了兩個數據結構【hashmap和雙向鏈表】。它的鏈表中存儲的是很久沒有使用的keys,而hashmap的作用的定位到某個鏈表中的節點。因為鏈表的時間復雜度是o(n)(要從頭/尾遍歷),如果我們有了hashmap那通過key來尋找value就是一個O(1)的操作。
流程為:
- 把沒有使用的key放在雙向鏈表中,並且在hashmap中針對這個key做一個索引
- 當鏈表滿了的情況下,它會把尾部的數據丟掉(取決於是頭插法還是尾插法)
- 如果一個key被命中了,那他就會移動位置,如果使用的是頭插法則把他的位置移到頭部反之亦然-》這是因為要把他放在一個不容易被lru算法淘汰的位置
缺點:
前面的流程說,當一個key被命中他在鏈表中的位置就會移動到不容易被LRU算法淘汰的位置,那么很可能一個不是熱點數據被命中,他只是使用了一次,然而它也被放在了不容易淘汰的位置。
redis中使用LRU:
他維護了一個大小為16的一個候選池,按照空閑時間進行排序,具體邏輯如下:
- 當回收池滿了的情況下,如果我們要添加一個key,這個key的空閑時間如果是最小的,則不進行任何操作
- 如果回收池沒有滿的情況下,redis會比較當前傳遞的key在所有key中的位置(通過空閑時間去進行對比)他會把插入的位置的元素后移一位再進行插入
- 回收池沒有滿的情況下,當前傳遞的key是小的,則直接插入到最后
- 回收池滿的情況下, 當前的key要比部分的元素的空閑時間更大,那他就需要插入到中間位置,那就需要把頭部的數據移除
LFU(least frequently used)【最少頻率使用】:他和LRU的不同點就是在使用次數上,他會根據key最近被訪問的頻率進行淘汰,比較少的就有效淘汰。他的不同點就是他維護了一個橫向和縱向的雙向鏈表,類似於StampedLock一樣。他是按照計數器對key進行排序。他橫向node表示的訪問的頻次,縱向表示的是具有相同頻次的數據,每次獲取或者修改元素的時候都會根據key的訪問頻率去修改key的位置,這樣就解決了可能對比非熱點數據而占據不可刪除的節點的位置的尷尬情況






