分散式儲存-Redis高效能的原理
分散式儲存-Redis高效能的原理
前面聊了網路通訊,當我們連線Redis的時候,就是一次通訊的過程,所以我們講Redis的高效能的根本之一就是,網路通訊。前面有朋友問到我Redis可以同時處理那麼多併發的原因是不是和通訊中的多路複用有關,我答應他在後續的章節中講講,所以本章聊聊
- 他的底層和多路複用機制(Reactor模型)
- 記憶體回收策略
Redis6.0之前的單執行緒reactor模型
Reactor其實不是一種新的技術,而是基於NIO多路複用機制,提出來的一種高效能的設計模式,底層還是咱們之前聊得NIO多路複用。他的想法是把相應事件和咱們的業務進行分離,這樣就可以通過一個或者多個執行緒處理IO事件。這裡有三個部分:
- Reactor:進行IO事件的分發
- Handler : 處理非阻塞的讀和寫(這其實就是真正處理IO的處理器)
- Acceptor:處理客戶端的連線
整體流程:
Reactor
public class Reactor implements Runnable { private final Selector selector; private final ServerSocketChannel serverSocketChannel; public Reactor(int port) throws IOException { selectorView Code=Selector.open(); serverSocketChannel=ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(port)); serverSocketChannel.configureBlocking(false); //註冊一個連線事件 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT,new Acceptor(selector,serverSocketChannel)); } @Overridepublic 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
public 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(); } } }View CodeHandler
public 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(); } } } } }View CodeTest
public class ReactorMain { public static void main(String[] args) throws IOException { new Thread(new Reactor(8080)).start(); } }View Code這個時候我們去連結我們的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的位置,這樣就解決了可能對比非熱點資料而佔據不可刪除的節點的位置的尷尬情況