redis 是不是單線程
redis 單線程指的是命令處理在一個單線程中。
- 主線程
- redis-server:命令處理、網絡事件的監聽。
- 輔助線程
- bio_close_file:異步關閉大文件。
- bio_aof_fsync:異步 aof 刷盤。
- bio_lazy_free:異步清理大塊內存。
- io_thd_*:io 多線程。
- jemalloc_bg_thd:后臺線程,進行內存分配,內存釋放。
- 輔助線程負責處理阻塞的操作,這樣可以不阻塞主線程,讓主線程最大限度地處理命令,優化性能。
命令處理為什么是單線程
- 單線程的局限:不能有耗時的操作,比如 CPU 運算、阻塞的 IO;對于 redis 而言會影響響應性能。
- redis 處理 IO 密集型:
- 磁盤 IO:
- fork 進程,在子進程做持久化。
- 使用 bio_aof_fsync,另起線程做持久化(異步 aof 刷盤)。
- 網絡 IO:
- 服務多個客戶端,造成 IO 密集;數據請求或返回數據量比較大。
- 開啟 IO 多線程。
- 磁盤 IO:
- redis 處理 CPU 密集型
- 數據結構切換:redis 會根據當前數據量的大小,選擇一個數據結構去存儲。
- 漸進式數據遷移:當數據量小的時候,會分配一個小的內存,當數據量大的時候,會分配一個大的內存(翻倍擴容),那么就需要將原來內存中的數據遷移到新的內存中,redis 不會將原數據一次性都挪過去,而是采用一定的策略逐漸挪過去。
- 為什么不采用多線程
- 加鎖復雜、鎖粒度不好控制。
- 頻繁的 CPU 上下文切換,抵消多線程的優勢。
redis 單線程為什么快
- 高效的 reactor 網絡模型
- 數據結構高效
- 在執行效率與空間占用間保持平衡,可以進行數據結構切換。
- redis 是內存數據庫,大部分情況下:操作完內存后會立刻返回給客戶端,不需要關注寫磁盤的問題。特殊情況:使用 aof 持久化方式 + always 策略:每一次操作完內存后,都必須持久化到磁盤中,然后再返回給客戶端。
- 數據組織方式
typedef struct redisDb {dict *dict; // 存儲所有的 key 和 valuedict *expires; // 存儲所有過期的 keydict *blocking_keys; // 存儲阻塞連接的 keydict *ready_keys; dict *watched_keys; // 被檢測的 key ( MULTI/EXEC ) }struct dict {dictType *type;dictEntry **ht_table[2];unsigned long ht_used[2];long rehashidx; // 默認為 -1,記錄遷移的位置int16_t pauserehash;signed char ht_size_exp[2]; }
- redis 支持 16 個 db,默認使用 db0,可以通過
use
選擇某一個 db。 - redis 內部會分配一個指針數組(每一個數組元素都對應一個鏈表)。key 通過 hash 函數會生成一個 64 位的整數,這個整數對該數組的長度取余,得到一個該數組的索引值,然后將 key 和 value 存儲在該索引位置的鏈表中。
- ht_size_exp 記錄指針數組長度 2 n 2^n 2n 中 n n n 的值,指針數組長度為什么是 2 n 2^n 2n ?
- 因為要把取余運算優化為位運算,優化的前提是 s i z e = 2 n size = 2^n size=2n ;當 s i z e = 2 n size = 2^n size=2n 時,
hash(key) % size = hash(key) & (size - 1)
。 - 取余運算中會有除法運算,計算機做除法運算會比較慢,做位運算會很快。 2 n 2^n 2n 又可以優化為 1 < < n 1<<n 1<<n 。
- 因為要把取余運算優化為位運算,優化的前提是 s i z e = 2 n size = 2^n size=2n ;當 s i z e = 2 n size = 2^n size=2n 時,
- 負載因子 = used / / /size,used 是指針數組存儲元素的個數,size 是指針數組的長度。負載因子越小哈希沖突越小,負載因子越大哈希沖突越大。
- 擴容操作
- 第一次分配指針數組空間長度為 4( 1 < < 2 1<<2 1<<2)。
static int _dictExpandIfNeeded(dict *d) {if (dictIsRehashing(d)) return DICT_OK;if(DICTHT_SIZE(d->ht_size_exp[0]) == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE) }
- 當負載因子 ≥ 1 \geq 1 ≥1 時,即
used / size >= 1
,為了減小哈希沖突,會進行翻倍擴容。第二次擴容會準備一個空間長度為 8 的指針數組,然后將原來數組中的元素遷移到擴容后的數組中。如果正在 fork(在 rdb、aof 復寫以及 rdb-aof 混用的情況下),會阻止擴容,但是此時若負載因子 > 5 >5 >5,索引效率大大降低,則會馬上擴容。
- 第一次分配指針數組空間長度為 4( 1 < < 2 1<<2 1<<2)。
- 縮容操作
- 當負載因子 < 0.1 < 0.1 <0.1 時,即
(size > 4) && ((used / size) < 0.1)
,會進行縮容,縮容后的數組長度恰好大于元素個數并且為 2 n ≥ 4 2^n \geq 4 2n≥4 。
- 當負載因子 < 0.1 < 0.1 <0.1 時,即
- 擴容操作和縮容操作都需要 rehash,因為 key-value 對的存儲位置發生了變化。
- 一個 dict 結構包含兩個散列表(散列表 = 哈希函數 + 指針數組),為什么要準備兩個 ht_table ?
- 為了防止遷移元素較多時,遷移任務變為 CPU 密集型。
- 使用兩個 ht_table 可以將原來數組中的元素逐漸遷移到擴容后的數組中,而不是一次性將元素全部挪過去;當原來數組中的元素全部挪過去后,會
free
原數組;如果free
的空間比較大,會使用 bio_lazy_free 另起線程去 free 這塊空間。
- 漸進式 rehash
處于漸進式 rehash 階段時,不會發生擴容縮容
- 當指針數組中的元素過多的時候,不能一次性 rehash 到 ht_table[1],這樣會長期占用 redis,其它命令得不到響應,所以需要使用漸進式 rehash。
- 步驟:將 ht_table[0] 中的元素重新經過 hash 函數生成 64位整數,再對 ht_table[1] 的長度進行取余,從而映射到 ht_table[1]。
- 策略:
- 在每次增刪改查的時候,遷移一個索引單位。
- 在服務器空閑的時候,會遷移 1ms ,以 100 個索引單位為步長。
int dictRehashMilliseconds(dict *d, int ms) {if (d->pauserehash > 0) return 0; long long start = timeInMilliseconds();int rehashes = 0;while(dictRehash(d, 100)) {rehashes += 100;if (timeInMilliseconds() - start > ms) break;}return rehashes; }
- redis 支持 16 個 db,默認使用 db0,可以通過
redis io 多線程工作原理
- redis 采用 reactor 網絡模型。
- redis 配置
# redis.conf io-threads 4 io-threads-do-reads yes