做爲企業級的存儲組件, Redis被用到不少的業務場景。linux
Redis常常被用做作緩存, 一致性要求不高場景,還能夠當作存儲使用。面試
另外, Redis還提供了消息訂閱、事務、索引等特性。 咱們還能夠利用集羣特性搭建分佈式存儲服務,實現非強一致性的分佈式鎖服務。redis
Redis用到上述場景, 都有一個共同的優點, 就是處理速度快(高性能)。算法
面試中,面試官常常會問到單線程的Redis爲何這麼快? 爲了闡明這個問題, 下面將分三部分講解: (1) 第一部分: Redis到底有多快 (2) 第二部分: 詳細講解Redis高性能緣由 (3) 第三部分: 影響Redis性能的因素編程
要了解Reids的到底有多麼快, 首先須要有相應的評估工具。 其次,須要Redis 在一些平臺經驗數據,來評估Redis性能數量級。 幸運的是Redis提供了這樣的工具,並給出了經常使用的硬件平臺一些經驗數據。windows
下面篇幅比較長,核心觀點以下:設計模式
不關心具體數據的小夥伴,能夠直接跳到第二部分,直接瞭解redis性能卓越的緣由。緩存
Redis包含的redis-benchmark實用程序可模擬N個客戶端同時發送M個總查詢的運行命令(相似於Apache的ab實用程序)。可使用redis-benchmark對redis的性能進行評估。bash
支持如下選項:服務器
Usage: redis-benchmark [-h <host>] [-p <port>] [-c <clients>] [-n <requests]> [-k <boolean>]
-h <hostname> 服務器 hostname (默認 127.0.0.1)
-p <port> 服務器 port (默認 6379)
-s <socket> 服務器 socket (覆蓋host和port)
-a <password> 服務器鑑權密鑰
-c <clients> 啓動的客戶端數量(並行度) (默認 50)
-n <requests> 總請求量(默認 100000)
-d <size> GET和SET請求數據大小(默認 2個字節)
--dbnum <db> 選擇的db編號 (默認 0)
-k <boolean> 1=keep alive 0=reconnect (默認 1)
-r <keyspacelen> 在SET/GET/INCR使用隨機的key值, 在SADD使用隨機的va
-P <numreq> 一個Pipeline包含的請求數. 默認值1 (不使用Pipeline).
-q 安靜模式. 僅僅展現QPS值
--csv 以csv格式輸出
-l 生成循環 永久執行測試
-t <tests> 制定測試命令的命令, 命令列表以逗號分隔
-I Idle模式,僅打開N個idle鏈接並等待
複製代碼
啓動基準以前,您須要具備運行中的Redis實例。我在本身工做的筆記本上, 使用默認參數跑了一個例子:
D:\data\soft\redis-windows>redis-benchmark.exe
.....
====== SET ======
100000 requests completed in 0.81 seconds
50 parallel clients
3 bytes payload
keep alive: 1
99.90% <= 1 milliseconds
99.93% <= 2 milliseconds
99.95% <= 78 milliseconds
99.96% <= 79 milliseconds
100.00% <= 79 milliseconds
123609.39 requests per second
====== GET ======
100000 requests completed in 0.70 seconds
50 parallel clients
3 bytes payload
keep alive: 1
100.00% <= 0 milliseconds
142045.45 requests per second
====== INCR ======
100000 requests completed in 0.71 seconds
50 parallel clients
3 bytes payload
keep alive: 1
99.95% <= 1 milliseconds
99.95% <= 2 milliseconds
100.00% <= 2 milliseconds
140252.45 requests per second
.....
複製代碼
上面的例子中,截取了SET/GET/INCR的測試結果。
測試結果包括測試的環境參數(請求量、client數量、有效載荷)以及請求耗時的TP值。
redis-benchmark默認使用10萬請求量, 50個clinet,有效載荷爲3字節進行測試。
返回結果能夠看出SET/GET/INCR命令在10萬的請求量下,總的請求耗時均低於0.1s之內。 以QPS=10W爲例, 計算出來的平均耗時爲2ms左右(1/(10W/50))。
Redis的性能跟不少因素相關, 在第三部分會詳細介紹。好比客戶端網絡情況、是否使用流水,連接的客戶端。爲了說明Redis到底有多快,咱們使用Reidis官網使用redis-benchmark測試的一組數據。
警告:請注意,如下大多數基準測試已有數年曆史,而且是與今天的標準相比使用舊硬件得到的。該頁面應該進行更新,可是在不少狀況下,使用硬硬件狀態,您會指望看到的數字是此數字的兩倍。此外,在許多工做負載中,Redis 4.0比2.6快
測試是由50個同時執行200萬個請求的客戶端完成的。
全部測試在Redis 2.6.14上運行。
使用迴環地址(127.0.0.1)執行了測試。
使用一百萬個鍵的鍵空間執行測試。
使用和不使用流水線(16條命令流水線)執行測試。
Intel(R) Xeon(R) CPU E5520 @ 2.27GHz
複製代碼
$ ./redis-benchmark -r 1000000 -n 2000000 -t get,set,lpush,lpop -q
SET: 122556.53 requests per second
GET: 123601.76 requests per second
LPUSH: 136752.14 requests per second
LPOP: 132424.03 requests per second
複製代碼
$ ./redis-benchmark -r 1000000 -n 2000000 -t get,set,lpush,lpop -q -P 16
SET: 195503.42 requests per second
GET: 250187.64 requests per second
LPUSH: 230547.55 requests per second
LPOP: 250815.16 requests per second
複製代碼
從以上能夠看出Redis做爲key-value系統讀寫負載大體在10W+QPS, 使用流水線技術可以顯著提高讀寫性能。
$ redis-benchmark -n 100000
====== SET ======
100007 requests completed in 0.88 seconds
50 parallel clients
3 bytes payload
keep alive: 1
58.50% <= 0 milliseconds
99.17% <= 1 milliseconds
99.58% <= 2 milliseconds
99.85% <= 3 milliseconds
99.90% <= 6 milliseconds
100.00% <= 9 milliseconds
114293.71 requests per second
====== GET ======
100000 requests completed in 1.23 seconds
50 parallel clients
3 bytes payload
keep alive: 1
43.12% <= 0 milliseconds
96.82% <= 1 milliseconds
98.62% <= 2 milliseconds
100.00% <= 3 milliseconds
81234.77 requests per second
....
複製代碼
全部set操做均在10ms內完成, get操做均在5ms如下。
Redis是一個單線程應用,所說的單線程指的是Redis使用單個線程處理客戶端的請求。 雖然Redis是單線程的應用,可是即使不經過部署多個Redis實例和集羣的方式提高系統吞吐, 從官網給出的數據能夠看出,Redis處理速度很是快。
Redis性能很是高的緣由主要有如下幾點:
下面詳細介紹非阻塞IO和優化的數據結構
在《unix網絡編程 卷I》中詳細講解了unix服務器中的5種IO模型。
一個IO操做通常分爲兩個步驟:
按照兩個步驟是否阻塞線程,分爲阻塞/非阻塞, 同步/異步。
五種IO模型分類:
阻塞 | 非阻塞 | |
---|---|---|
同步 | 阻塞IO | 非阻塞IO,IO多路複用,信號驅動IO |
異步IO | 異步IO |
在linux中,默認狀況下全部的socket都是blocking,一個典型的讀操做流程大概是這樣:
Linux下,能夠經過設置socket使其變爲non-blocking。當對一個non-blocking socket執行讀操做時,流程是這個樣子:
IO multiplexing這個詞可能有點陌生,可是若是我說select/epoll,大概就都能明白了。有些地方也稱這種IO方式爲事件驅動IO(event driven IO)。咱們都知道,select/epoll的好處就在於單個process就能夠同時處理多個網絡鏈接的IO。它的基本原理就是select/epoll這個function會不斷的輪詢所負責的全部socket,當某個socket有數據到達了,就通知用戶進程。它的流程如圖:
Linux下的asynchronous IO其實用得很少,從內核2.6版本纔開始引入。先看一下它的流程:
介紹完unix或者類unix系統IO模型以後, 咱們看下redis怎麼處理客戶端鏈接的?
總的來講Redis使用一種封裝多種(select,epoll, kqueue等)實現的Reactor設計模式多路複用IO處理客戶端的請求。
Reactor設計模式經常用來實現事件驅動。除此以外, Redis還封裝了不一樣平臺多路複用IO的不一樣的庫。處理過程以下:
由於 Redis 須要在多個平臺上運行,同時爲了最大化執行的效率與性能,因此會根據編譯平臺的不一樣選擇不一樣的 I/O 多路複用函數做爲子模塊。
具體選擇過程以下:
Redis 會優先選擇時間複雜度爲 O(1) 的 I/O 多路複用函數做爲底層實現,包括 Solaries 10 中的 evport、Linux 中的 epoll 和 macOS/FreeBSD 中的 kqueue,上述的這些函數都使用了內核內部的結構,而且可以服務幾十萬的文件描述符。
可是若是當前編譯環境沒有上述函數,就會選擇 select 做爲備選方案,因爲其在使用時會掃描所有監聽的描述符,因此其時間複雜度較差 O(n),而且只能同時服務 1024 個文件描述符,因此通常並不會以 select 做爲第一方案使用。
Redis提供了豐富的數據結構,而且不一樣場景下提供不一樣實現。
Redis做爲key-value系統,不一樣類型的key對應不一樣的操做或者操做對應不一樣的實現,相同的key也會有不一樣的實現。Redis對key進行操做時,會進行類型檢查,調用不一樣的實現。
爲了解決以上問題, Redis 構建了本身的類型系統, 這個系統的主要功能包括:
redisObject 對象。 基於 redisObject 對象的類型檢查。 基於 redisObject 對象的顯式多態函數。 對 redisObject 進行分配、共享和銷燬的機制。
redisObject定義:
/*
* Redis 對象
*/
typedef struct redisObject {
// 類型
unsigned type:4;
// 對齊位
unsigned notused:2;
// 編碼方式
unsigned encoding:4;
// LRU 時間(相對於 server.lruclock)
unsigned lru:22;
// 引用計數
int refcount;
// 指向對象的值
void *ptr;
} robj;
複製代碼
type 、 encoding 和 ptr 是最重要的三個屬性。
Redis支持4種type, 8種編碼, 分別爲:
有了redisObject以後, 對於特定key的操做過程就能夠很容易的實現:
Redis除了提供豐富的高效的數據結構外, 還提供瞭如HyperLogLog, Geo索引這樣高效的算法。
篇幅的緣由,影響Redis性能的因素將在另一篇文章中介紹。
參考文檔: