redis是個單線程的程序,為什麼會這麼快呢?每秒10000?這個有點不解,具體是快在哪裡呢?EPOLL?內存?

希望牛人幫我解釋一下原因


純內存資料庫,如果只是簡單的 key-value,內存不是瓶頸。一般情況下,hash 查找可以達到每秒數百萬次的數量級。

瓶頸在於網路 IO 上。

根據你測的的 10000/s 來看,客戶端和 redis 應該是部署在兩台不同的機器,並且是使用同步的方式請求 redis. 每次請求需要通過網路把請求發送到 redis 所在的機器,然後等待 redis 返回數據。時間大部分消耗在網路傳輸中。

如果把 redis 和客戶端放在同一台機器,網路延遲會更小,一般情況下可以打到 60000 次每秒甚至更高,取決於機器性能。

鎖不是影響性能的主要因素。線程鎖 (mutex_lock) 只有在遇到衝突的情況下性能會下降,而正常情況下,遇到衝突的概率很低。如果只是簡單的加鎖、釋放鎖速度是非常快的,每秒鐘上千萬次沒問題。memcache 內部用到了大量的鎖,並沒有見到性能降低。

線程也不是影響吞吐量的重要因素。如第一點來說,一般情況下,程序處理內存數據的速度遠高於網卡接收的速度。使用線程好處是可以同時處理多條連接,在極端情況下,可能會提高響應速度。

使用 epoll 或 libevent 等因為非同步非阻塞 IO 編程只能這麼做。與之對應的是同步阻塞 IO 編程,使用多進程或多線程實現多條連接的處理,比如 apache。一般情況下,非同步非阻塞 IO 模型性能是遠高於同步阻塞 IO 模型的,可以參考 nginx 與 apache 性能的對比。

libevent 並不比 redis 自己實現的 ae_event 慢,代碼多是應為 ae_event 只實現了 redis 需要的功能,而 libevent 則具有更多的功能,比如更快的定時器、buffer event 模型,甚至自帶了 DNS、HTTP 協議的處理。並且 libevent 更通用,而 redis 只專註於 linux 平台。

最後回答題主問題,快在哪?

1、純內存操作

2、非同步非阻塞 IO


redis用自己實現的事件分離器,代碼量很短,沒有cas,沒有lock。

那麼memcache為什麼要多線程呢,因為他是一種通用的kv資料庫。

不會因為某個線程慢而導致其他的線程問題,且能夠完全的使用多核的cpu。

這些是redis不足的地方。

兩者都使用epoll,no-blocking io


總體來說快速的原因如下:

1)絕大部分請求是純粹的內存操作(非常快速)

2)採用單線程,避免了不必要的上下文切換和競爭條件

3)非阻塞IO

內部實現採用epoll,採用了epoll+自己實現的簡單的事件框架。epoll中的讀、寫、關閉、連接都轉化成了事件,然後利用epoll的多路復用特性,絕不在io上浪費一點時間

這3個條件不是相互獨立的,特別是第一條,如果請求都是耗時的,採用單線程吞吐量及性能可想而知了。應該說redis為特殊的場景選擇了合適的技術方案。


給大家簡單介紹一下redis資料庫實現,以下摘自文章《 memcached與redis實現的對比 》

redis資料庫實現原理

首先redis資料庫的功能強大一些,因為不像memcached只支持保存字元串,redis支持string, list, set,sorted set,hash table 5種數據結構。例如存儲一個人的信息就可以使用hash table,用人的名字做key,然後name super, age 24, 通過key 和 name,就可以取到名字super,或者通過key和age,就可以取到年齡24。這樣,當只需要取得age的時候,不需要把人的整個信息取回來,然後從裡面找age,直接獲取age即可,高效方便。

為了實現這些數據結構,redis定義了抽象的對象redis object,如下圖。每一個對象有類型,一共5種:字元串,鏈表,集合,有序集合,哈希表。 同時,為了提高效率,redis為每種類型準備了多種實現方式,根據特定的場景來選擇合適的實現方式,encoding就是表示對象的實現方式的。然後還有記錄了對象的lru,即上次被訪問的時間,同時在redis 伺服器中會記錄一個當前的時間(近似值,因為這個時間只是每隔一定時間,伺服器進行自動維護的時候才更新),它們兩個只差就可以計算出對象多久沒有被訪問了。 然後redis object中還有引用計數,這是為了共享對象,然後確定對象的刪除時間用的。最後使用一個void*指針來指向對象的真正內容。正式由於使用了抽象redis object,使得資料庫操作數據時方便很多,全部統一使用redis object對象即可,需要區分對象類型的時候,再根據type來判斷。而且正式由於採用了這種面向對象的方法,讓redis的代碼看起來很像c++代碼,其實全是用c寫的。

//#define REDIS_STRING 0 // 字元串類型
//#define REDIS_LIST 1 // 鏈表類型
//#define REDIS_SET 2 // 集合類型(無序的),可以求差集,並集等
//#define REDIS_ZSET 3 // 有序的集合類型
//#define REDIS_HASH 4 // 哈希類型

//#define REDIS_ENCODING_RAW 0 /* Raw representation */ //raw 未加工
//#define REDIS_ENCODING_INT 1 /* Encoded as integer */
//#define REDIS_ENCODING_HT 2 /* Encoded as hash table */
//#define REDIS_ENCODING_ZIPMAP 3 /* Encoded as zipmap */
//#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
//#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
//#define REDIS_ENCODING_INTSET 6 /* Encoded as intset */
//#define REDIS_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
//#define REDIS_ENCODING_EMBSTR 8 /* Embedded sds
string encoding */

typedef struct redisObject {
unsigned type:4; // 對象的類型,包括 /* Object types */
unsigned encoding:4; // 底部為了節省空間,一種type的數據,
// 可 以採用不同的存儲方式
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
int refcount; // 引用計數
void *ptr;
} robj;

說到底redis還是一個key-value的資料庫,不管它支持多少種數據結構,最終存儲的還是以key-value的方式,只不過value可以是鏈表,set,sorted set,hash table等。和memcached一樣,所有的key都是string,而set,sorted set,hash table等具體存儲的時候也用到了string。 而c沒有現成的string,所以redis的首要任務就是實現一個string,取名叫sds(simple dynamic string),如下的代碼, 非常簡單的一個結構體,len存儲改string的內存總長度,free表示還有多少位元組沒有使用,而buf存儲具體的數據,顯然len-free就是目前字元串的長度。

struct sdshdr {
int len;
int free;
char buf[];
};

字元串解決了,所有的key都存成sds就行了,那麼key和value怎麼關聯呢?key-value的格式在腳本語言中很好處理,直接使用字典即可,C沒有字典,怎麼辦呢?自己寫一個唄(redis十分熱衷於造輪子)。看下面的代碼,privdata存額外信息,用的很少,至少我們發現。 dictht是具體的哈希表,一個dict對應兩張哈希表,這是為了擴容(包括rehashidx也是為了擴容)。dictType存儲了哈希表的屬性。redis還為dict實現了迭代器(所以說看起來像c++代碼)。

哈希表的具體實現是和mc類似的做法,也是使用開鏈法來解決衝突,不過裡面用到了一些小技巧。比如使用dictType存儲函數指針,可以動態配置桶裡面元素的操作方法。又比如dictht中保存的sizemask取size(桶的數量)-1,用它與key做操作來代替取余運算,加快速度等等。總的來看,dict裡面有兩個哈希表,每個哈希表的桶裡面存儲dictEntry鏈表,dictEntry存儲具體的key和value。

前面說過,一個dict對於兩個dictht,是為了擴容(其實還有縮容)。正常的時候,dict只使用dictht[0],當dict[0]中已有entry的數量與桶的數量達到一定的比例後,就會觸發擴容和縮容操作,我們統稱為rehash,這時,為dictht[1]申請rehash後的大小的內存,然後把dictht[0]里的數據往dictht[1]裡面移動,並用rehashidx記錄當前已經移動萬的桶的數量,當所有桶都移完後,rehash完成,這時將dictht[1]變成dictht[0], 將原來的dictht[0]變成dictht[1],並變為null即可。不同於memcached,這裡不用開一個後台線程來做,而是就在event loop中完成,並且rehash不是一次性完成,而是分成多次,每次用戶操作dict之前,redis移動一個桶的數據,直到rehash完成。這樣就把移動分成多個小移動完成,把rehash的時間開銷均分到用戶每個操作上,這樣避免了用戶一個請求導致rehash的時候,需要等待很長時間,直到rehash完成才有返回的情況。不過在rehash期間,每個操作都變慢了點,而且用戶還不知道redis在他的請求中間添加了移動數據的操作,感覺redis太賤了 :-D

typedef struct dict {
dictType *type; // 哈希表的相關屬性
void *privdata; // 額外信息
dictht ht[2]; // 兩張哈希表,分主和副,用於擴容
int rehashidx; /* rehashing not in progress if rehashidx == -1 */ // 記錄當前數據遷移的位置,在擴容的時候用的
int iterators; /* number of iterators currently running */ // 目前存在的迭代器的數量
} dict;

typedef struct dictht {
dictEntry **table; // dictEntry是item,多個item組成hash桶裡面的鏈表,table則是多個鏈表頭指針組成的數組的指針
unsigned long size; // 這個就是桶的數量
// sizemask取size - 1, 然後一個數據來的時候,通過計算出的hashkey, 讓hashkey sizemask來確定它要放的桶的位置
// 當size取2^n的時候,sizemask就是1...111,這樣就和hashkey % size有一樣的效果,但是使用會快很多。這就是原因
unsigned long sizemask;
unsigned long used; // 已經數值的dictEntry數量
} dictht;

typedef struct dictType {
unsigned int (*hashFunction)(const void *key); // hash的方法
void *(*keyDup)(void *privdata, const void *key); // key的複製方法
void *(*valDup)(void *privdata, const void *obj); // value的複製方法
int (*keyCompare)(void *privdata, const void *key1, const void *key2); // key之間的比較
void (*keyDestructor)(void *privdata, void *key); // key的析構
void (*valDestructor)(void *privdata, void *obj); // value的析構
} dictType;

typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next;
} dictEntry;

有了dict,資料庫就好實現了。所有數據讀存儲在dict中,key存儲成dictEntry中的key(string),用void* 指向一個redis object,它可以是5種類型中的任何一種。如下圖,結構構造是這樣,不過這個圖已經過時了,有一些與redis3.0不符合的地方。

5中type的對象,每一個都至少有兩種底層實現方式。string有3種:REDIS_ENCODING_RAW, REDIS_ENCIDING_INT, REDIS_ENCODING_EMBSTR, list有:普通雙向鏈表和壓縮鏈表,壓縮鏈表簡單的說,就是講數組改造成鏈表,連續的空間,然後通過存儲字元串的大小信息來模擬鏈表,相對普通鏈表來說可以節省空間,不過有副作用,由於是連續的空間,所以改變內存大小的時候,需要重新分配,並且由於保存了字元串的位元組大小,所有有可能引起連續更新(具體實現請詳細看代碼)。set有dict和intset(全是整數的時候使用它來存儲), sorted set有:skiplist和ziplist, hashtable實現有壓縮列表和dict和ziplist。skiplist就是跳錶,它有接近於紅黑樹的效率,但是實現起來比紅黑樹簡單很多,所以被採用(奇怪,這裡又不造輪子了,難道因為這個輪子有點難?)。 hash table可以使用dict實現,則改dict中,每個dictentry中key保存了key(這是哈希表中的鍵值對的key),而value則保存了value,它們都是string。 而set中的dict,每個dictentry中key保存了set中具體的一個元素的值,value則為null。圖中的zset(有序集合)有誤,zset使用skiplist和ziplist實現,首先skiplist很好理解,就把它當做紅黑樹的替代品就行,和紅黑樹一樣,它也可以排序。怎麼用ziplist存儲zset呢?首先在zset中,每個set中的元素都有一個分值score,用它來排序。所以在ziplist中,按照分值大小,先存元素,再存它的score,再存下一個元素,然後score。這樣連續存儲,所以插入或者刪除的時候,都需要重新分配內存。所以當元素超過一定數量,或者某個元素的字元數超過一定數量,redis就會選擇使用skiplist來實現zset(如果當前使用的是ziplist,會將這個ziplist中的數據取出,存入一個新的skiplist,然後刪除改ziplist,這就是底層實現轉換,其餘類型的redis object也是可以轉換的)。 另外,ziplist如何實現hashtable呢?其實也很簡單,就是存儲一個key,存儲一個value,再存儲一個key,再存儲一個value。還是順序存儲,與zset實現類似,所以當元素超過一定數量,或者某個元素的字元數超過一定數量時,就會轉換成hashtable來實現。各種底層實現方式是可以轉換的,redis可以根據情況選擇最合適的實現方式,這也是這樣使用類似面向對象的實現方式的好處。

需要指出的是,使用skiplist來實現zset的時候,其實還用了一個dict,這個dict存儲一樣的鍵值對。為什麼呢?因為skiplist的查找只是lgn的(可能變成n),而dict可以到O(1), 所以使用一個dict來加速查找,由於skiplist和dict可以指向同一個redis object,所以不會浪費太多內存。另外使用ziplist實現zset的時候,為什麼不用dict來加速查找呢?因為ziplist支持的元素個數很少(個數多時就轉換成skiplist了),順序遍歷也很快,所以不用dict了。

這樣看來,上面的dict,dictType,dictHt,dictEntry,redis object都是很有考量的,它們配合實現了一個具有面向對象色彩的靈活、高效資料庫。不得不說,redis資料庫的設計還是很厲害的。

與memcached不同的是,redis的資料庫不止一個,默認就有16個,編號0-15。客戶可以選擇使用哪一個資料庫,默認使用0號資料庫。 不同的資料庫數據不共享,即在不同的資料庫中可以存在同樣的key,但是在同一個資料庫中,key必須是唯一的。

redis也支持expire time的設置,我們看上面的redis object,裡面沒有保存expire的欄位,那redis怎麼記錄數據的expire time呢? redis是為每個資料庫又增加了一個dict,這個dict叫expire dict,它裡面的dict entry裡面的key就是數對的key,而value全是數據為64位int的redis object,這個int就是expire time。這樣,判斷一個key是否過期的時候,去expire dict裡面找到它,取出expire time比對當前時間即可。為什麼這樣做呢? 因為並不是所有的key都會設置過期時間,所以,對於不設置expire time的key來說,保存一個expire time會浪費空間,而是用expire dict來單獨保存的話,可以根據需要靈活使用內存(檢測到key過期時,會把它從expire dict中刪除)。

redis的expire 機制是怎樣的呢? 與memcahed類似,redis也是惰性刪除,即要用到數據時,先檢查key是否過期,過期則刪除,然後返回錯誤。單純的靠惰性刪除,上面說過可能會導致內存浪費,所以redis也有補充方案,redis裡面有個定時執行的函數,叫servercron,它是維護伺服器的函數,在它裡面,會對過期數據進行刪除,注意不是全刪,而是在一定的時間內,對每個資料庫的expire dict裡面的數據隨機選取出來,如果過期,則刪除,否則再選,直到規定的時間到。即隨機選取過期的數據刪除,這個操作的時間分兩種,一種較長,一種較短,一般執行短時間的刪除,每隔一定的時間,執行一次長時間的刪除。這樣可以有效的緩解光採用惰性刪除而導致的內存浪費問題。

以上就是redis的數據的實現,與memcached不同,redis還支持數據持久化,這個在《memcached與redis實現的對比 》有詳細介紹


快不快看跟誰比,總不能拿純內存訪問的Redis和保證可靠的持久化的Database相比,和MemCached還有點可比度,技術上只是細節不同,各有優缺點,多核架構下,我還是看好多線程模型,但Redis的持久化、快照以及豐富的數據結構和運算功能更加強大一些。


目前想到的原因有這幾方面。

  • Libevent。和Memcached不同,Redis並沒有選擇libevent。Libevent為了迎合通用性造成代碼龐大(目前Redis代碼還不到libevent的1/3)及犧牲了在特定平台的不少性能。Redis用libevent中兩個文件修改實現了自己的epoll event loop(4)。業界不少開發者也建議Redis使用另外一個libevent高性能替代libev,但是作者還是堅持Redis應該小巧並去依賴的思路。一個印象深刻的細節是編譯Redis之前並不需要執行./configure。
  • CAS問題。CAS是Memcached中比較方便的一種防止競爭修改資源的方法。CAS實現需要為每個cache key設置一個隱藏的cas token,cas相當value版本號,每次set會token需要遞增,因此帶來CPU和內存的雙重開銷,雖然這些開銷很小,但是到單機10G+ cache以及QPS上萬之後這些開銷就會給雙方相對帶來一些細微性能差別(5)。


單線程有時候比多線程 或多進程更快,比需要考慮並發、鎖,也不會增加上下文切換等開銷,也即代碼更加簡潔,執行效率更高~~~

Redis是用內部是快的原因之一,要比較的化,可以同MemCache比較下


服務端編程的3大性能殺手:

1、大量線程導致的線程切換開銷。

2、鎖。

3、非必要的內存拷貝。

redis比memcached避免的更好!


對於redis 這種cache類型的服務(很多人說redis是資料庫,我實在不敢認同)。CPU從來都不是瓶頸。

KV結構內存如果採用Hash存儲。內存部分的訪問速度是千萬級別。

瓶頸是網路和內存。網路對外部請求負載而言,內存是對數據容量而言的。其實如果內存足夠,完全可以可以負擔更多數據,

就現在的頻率的CPU,如果請求量是1W的入出(包數量)壓力級別。epoll + one loop 的處理需要的CPU大改也就10%。10W級別應該30%的CPU也足夠了(其實應該用不到)。剩餘的對付查詢,綽綽有餘。到時Redis這種用字元串當命令字處理的東東,在字元串處理上會有一定的消耗。

至於memcache 為啥要做成多線程……,請去問作者。我個人認為……,完全沒有必要。

CPU這玩意,也只能讓他閑著了。為啥要閑著,因為現在硬體設備遠遠跟不上軟體發展。真正的cache伺服器,內存應該1T(這樣可以部署好幾個redis了),網卡也應該提升級別。但你看現在的設備。128G的都是稀罕玩意。萬M的網卡倒是有了,但其他地方……。所以。。。CPU閑著也就閑著把。沒法子。


是10w級別不是1w


1. 非阻塞socket本身就可以是單線程

2. redis並不是單線程,至少他在做持久化的時候並不是單『線程』

否則1條線程處理網路IO+數據解析+入key-value對內存集 + 磁碟io 。他會卡個稀巴爛

所以, redis 是在可以單線程的地方做了單線程


redis的設計哲學就是業務操作短平快,在保證完全內存運算情況下,有個設計哲學就是局部串列運算,因為大量的內存鎖請求和釋放也需要時間,同時鎖等待更容易把業務線程池完全hang死。

同時它的文件處理模塊,即處理socket請求這塊採用了非阻塞IO操作,一個Eventloop下來基本沒有等待點


純內存操作本身就非常快不是瓶頸,所以redis的內存操作用的是單線程。當然redis的磁碟操作還是用了多線程,畢竟磁碟操作要慢的多。


我研發高性能資料庫haisql_memcache,查詢性能 單核已經比redis單核性能更快,多核比memcache快70%,CPU每增加1個核心性能增加大約10萬QPS, 目前在低端3.4G主頻4核CPU下4K byte 數據大約40萬QPS. 網站地址: http://www.haisql.com

我覺得演算法快是關鍵,redis也有比較快的演算法,原因如下:

我的經驗是:目前實現每個CPU核心平均每個數據包的總體處理時長大約是10微秒,其中4-5微秒是網路層收發的開銷,因此,網路層必須要使用類似epoll這樣的技術,但是每個epoll隊列只能最大提供15萬QPS, 對於多核CPU就必須要為每個CPU準備一個epoll來提供多核下更高的QPS, 即使用多組epoll來提供更高的性能。

1個微秒是hash查詢花費的時間,採用更快的hash演算法可以做到主代碼0.6-0.7微秒的性能。另外就是採用更高性能的spin_lock, 包括spin_lock_shared讀寫鎖,來減少鎖衝突的概率。將hash表分組為 256個來減少和穩定rehash的性能。使用了比std::unordered_map更快,rehash性能抖動小的自主研發的高性能 基於 環形隊列 結構的 circular_hash_map 庫。環形隊列大小自動收縮擴展。

0-2個微秒是數據Copy花費的時間,與數據大小有關,這部分表面上看似乎很容易壓縮掉,但是,我這邊的設計中,網路層代碼與應用層是完全隔離的,所以,這部分開銷無法去掉。

1-2個微秒是parse指令解析花費的時間, 這個時間主要是需要創建一組std::vector&, 盡量採用預分配的對象可以減少一些時間,基本也就這樣了。

如果是set/add/relpace等更新命令那麼將涉及一次內存的申請和一次內存的釋放,這部分開銷由於我的設計是動態申請內存,所以去不掉, 只能是盡量做一個中間層cache部分釋放的對象回收利用,類似java的內存回收機制。另外,使用jemalloc/tcmalloc也可以提高性能。

那麼,我們的資料庫為什麼比redis/memcache快, 就是我的C/C++底層庫快很多。我們從C/C++最底層開始重新設計研發效率更高的C/C++底層庫,隨著時間的增加, 我們已經積累了超過1萬行的C/C++庫代碼,每周我們都有新的C++底層庫研發出來,在速度更快,更好的C++內部庫的助力下, 我們研發各類資料庫產品,每個資料庫產品都將受益於這些底層C++庫所帶來的更高的效率。

也許有人會說 : C/C++庫都是經典, 是不可超越的, 那麼, 這裡也是一個例子,即使是memcpy這樣看上去無法優化的庫代碼,其實也是有辦法優化的。
例如: 怎樣寫出一個更快的 memset/memcpy ? - 知乎用戶的回答 - 知乎 https://www.zhihu.com/question/35172305/answer/61584927

已經替換了一大批C/C++的標準庫中過於陳舊的內容。如果新的模塊上在演算法方面沒有創新,那麼重寫是毫無意義的, -O3優化下,同樣演算法的模塊,性能幾乎不會有變化, 所以,更重要的是演算法上必須要有創新。


1.放在內存里,可以理解成是在內存里處理數據結構

2.單線程但是是非同步不是馬上拿到結果,相當於for循環,電腦CPU是2.xGHz,執行起來自然很快


快和慢是相對來講的,很多人認為是網路瓶頸造成的,你認為1w並發就快了,在我看來,不說單核達到千兆網卡的線速148.8W並發,但在僅訪問內存,採用合適的數據結構(無鎖)下,單核10w並發(考慮內存隨機訪問cache-misses,TCP協議棧開銷,部分請求合併)也是中規中矩的。


單線程是接收連接單線程吧,處理查詢難道和接收線程是一個?


為什麼快,首先是內存操作,另外epoll io非阻塞,沒了


沒具體壓測過,我們使用redis就是看中他可以數據落盤,防止重啟機器數據丟失。在應用層還是使用memcache, 在memcache沒命中的情況下,再去redis取數據並同步mem


真不算快,你是沒有見過一秒2000萬的調度。


推薦閱讀:

Redis服務支持5000萬的QPS,有什麼好的思路?
如何評價360開源的pika項目?
scrapy-redis 和 scrapy 有什麼區別?
隊列是什麼意思?

TAG:Redis | NoSQL | Libevent | liburl庫 |