自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

單線程的Redis為什么能支持10w+的QPS?

開發(fā) 前端 Redis
我們經(jīng)常聽到Redis是一個(gè)單線程程序。準(zhǔn)確的說Redis是一個(gè)多線程程序,只不過請(qǐng)求處理的部分是用一個(gè)線程來實(shí)現(xiàn)的。

單線程為什么能支持10w+的QPS?

我們經(jīng)常聽到Redis是一個(gè)單線程程序。準(zhǔn)確的說Redis是一個(gè)多線程程序,只不過請(qǐng)求處理的部分是用一個(gè)線程來實(shí)現(xiàn)的。

阿里云對(duì)Redis QPS的測(cè)試結(jié)果如下所示

「Redis是如何用單線程來實(shí)現(xiàn)每秒10w+的QPS的呢?」

  • 使用IO多路復(fù)用
  • 非CPU密集型任務(wù)
  • 純內(nèi)存操作
  • 高效的數(shù)據(jù)結(jié)構(gòu)

「只用一個(gè)線程怎么來處理多個(gè)客戶端的連接呢?」

這就不得不提IO多路復(fù)用技術(shù),即Java中的NIO。

當(dāng)我們使用阻塞IO(Java中的BIO),調(diào)用read函數(shù),傳入?yún)?shù)n,表示讀取n個(gè)字節(jié)后線程才會(huì)返回,不然就一直阻塞。write方法一般不會(huì)阻塞,除非寫緩沖區(qū)被寫滿,write才會(huì)被阻塞,直到緩沖區(qū)中有空間被釋放出來。

當(dāng)我們使用IO多路復(fù)用技術(shù)時(shí),當(dāng)沒有數(shù)據(jù)可讀或者可寫,客戶端線程會(huì)直接返回,并不會(huì)阻塞。這樣Redis就可以用一個(gè)線程來監(jiān)聽多個(gè)Socket,當(dāng)一個(gè)Socket可讀或可寫的時(shí)候,Redis去讀取請(qǐng)求,操作內(nèi)存中數(shù)據(jù),然后返回。

「當(dāng)采用單線程時(shí),就無法使用多核CPU,但Redis中大部分命令都不是CPU密集型任務(wù),所以CPU并不是Redis的瓶頸」。

高并發(fā)和大數(shù)據(jù)量的請(qǐng)寬下Redis的瓶頸主要體現(xiàn)在內(nèi)存和網(wǎng)絡(luò)帶寬,所以你看Redis為了節(jié)省內(nèi)存,在底層數(shù)據(jù)結(jié)構(gòu)上占用的內(nèi)存能少就少,并且一種類型的數(shù)據(jù)在不同的場(chǎng)景下會(huì)采用不同的數(shù)據(jù)結(jié)構(gòu)。

「所以Redis采用單線程就已經(jīng)能處理海量的請(qǐng)求,因此就沒必要使用多線程」。除此之外,「使用單線程還有如下好處」

  1. 沒有了線程切換的性能開銷
  2. 各種操作不用加鎖(如果采用多線程,則對(duì)共享資源的訪問需要加鎖,增加開銷)
  3. 方便調(diào)試,可維護(hù)性高

「最后Redis是一個(gè)內(nèi)存數(shù)據(jù)庫(kù),各種命令的讀寫操作都是基于內(nèi)存完成的」。大家都知道操作內(nèi)存和操作磁盤效率相差好幾個(gè)數(shù)量級(jí)。雖然Redis的效率很高,但還是有一些慢操作需要大家避免

Redis有哪些慢操作?

Redis的各種命令是在一個(gè)線程中依次執(zhí)行的,如果一個(gè)命令在Redis中執(zhí)行的時(shí)間過長(zhǎng),就會(huì)影響整體的性能,因?yàn)楹竺娴恼?qǐng)求要等到前面的請(qǐng)求被處理完才能被處理,這些耗時(shí)的操作有如下幾個(gè)部分

Redis可以通過日志記錄那些耗時(shí)長(zhǎng)的命令,使用如下配置即可

  1. # 命令執(zhí)行耗時(shí)超過 5 毫秒,記錄慢日志 
  2. CONFIG SET slowlog-log-slower-than 5000 
  3. # 只保留最近 500 條慢日志 
  4. CONFIG SET slowlog-max-len 500 

執(zhí)行如下命令,就可以查詢到最近記錄的慢日志

  1. 127.0.0.1:6379> SLOWLOG get 5 
  2. 1) 1) (integer) 32693       # 慢日志ID 
  3.    2) (integer) 1593763337  # 執(zhí)行時(shí)間戳 
  4.    3) (integer) 5299        # 執(zhí)行耗時(shí)(微秒) 
  5.    4) 1) "LRANGE"           # 具體執(zhí)行的命令和參數(shù) 
  6.       2) "user_list:2000" 
  7.       3) "0" 
  8.       4) "-1" 
  9. 2) 1) (integer) 32692 
  10.    2) (integer) 1593763337 
  11.    3) (integer) 5044 
  12.    4) 1) "GET" 
  13.       2) "user_info:1000" 
  14. ... 

使用復(fù)雜度過高的命令

之前的文章我們已經(jīng)介紹了Redis的底層數(shù)據(jù)結(jié)構(gòu),它們的時(shí)間復(fù)雜度如下表所示

名稱 時(shí)間復(fù)雜度
dict(字典) O(1)
ziplist (壓縮列表) O(n)
zskiplist (跳表) O(logN)
quicklist(快速列表) O(n)
intset(整數(shù)集合) O(n)

「單元素操作」:對(duì)集合中的元素進(jìn)行增刪改查操作和底層數(shù)據(jù)結(jié)構(gòu)相關(guān),如對(duì)字典進(jìn)行增刪改查時(shí)間復(fù)雜度為O(1),對(duì)跳表進(jìn)行增刪查時(shí)間復(fù)雜為O(logN)

「范圍操作」:對(duì)集合進(jìn)行遍歷操作,比如Hash類型的HGETALL,Set類型的SMEMBERS,List類型的LRANGE,ZSet類型的ZRANGE,時(shí)間復(fù)雜度為O(n),避免使用,用SCAN系列命令代替。(hash用hscan,set用sscan,zset用zscan)

「聚合操作」:這類操作的時(shí)間復(fù)雜度通常大于O(n),比如SORT、SUNION、ZUNIONSTORE

「統(tǒng)計(jì)操作」:當(dāng)想獲取集合中的元素個(gè)數(shù)時(shí),如LLEN或者SCARD,時(shí)間復(fù)雜度為O(1),因?yàn)樗鼈兊牡讓訑?shù)據(jù)結(jié)構(gòu)如quicklist,dict,intset保存了元素的個(gè)數(shù)

「邊界操作」:list底層是用quicklist實(shí)現(xiàn)的,quicklist保存了鏈表的頭尾節(jié)點(diǎn),因此對(duì)鏈表的頭尾節(jié)點(diǎn)進(jìn)行操作,時(shí)間復(fù)雜度為O(1),如LPOP、RPOP、LPUSH、RPUSH

「當(dāng)想獲取Redis中的key時(shí),避免使用keys *」 ,Redis中保存的鍵值對(duì)是保存在一個(gè)字典中的(和Java中的HashMap類似,也是通過數(shù)組+鏈表的方式實(shí)現(xiàn)的),key的類型都是string,value的類型可以是string,set,list等

例如當(dāng)我們執(zhí)行如下命令后,redis的字典結(jié)構(gòu)如下

  1. set bookName redis; 
  2. rpush fruits banana apple; 

我們可以用keys命令來查詢Redis中特定的key,如下所示

  1. # 查詢所有的key 
  2. keys * 
  3. # 查詢以book為前綴的key 
  4. keys book* 

keys命令的復(fù)雜度是O(n),它會(huì)遍歷這個(gè)dict中的所有key,如果Redis中存的key非常多,所有讀寫Redis的指令都會(huì)被延遲等待,所以千萬不用在生產(chǎn)環(huán)境用這個(gè)命令(如果你已經(jīng)準(zhǔn)備離職的話,祝你玩的開心)。

「既然不讓你用keys,肯定有替代品,那就是scan」

scan和keys相比,有如下特點(diǎn)

  1. 復(fù)雜雖然也是O(n),但是是通過游標(biāo)分布執(zhí)行的,不會(huì)阻塞線程
  2. 同keys一樣,提供模式匹配功能
  3. 從完整遍歷開始到完整遍歷結(jié)束,一直存在于數(shù)據(jù)集內(nèi)的所有元素都會(huì)被完整遍歷返回,但是同一個(gè)元素可能會(huì)被返回多次
  4. 如果一個(gè)元素是在迭代過程中被添加到數(shù)據(jù)集的,或者在迭代過程中從數(shù)據(jù)集中被刪除的,那么這個(gè)元素可能會(huì)被返回,也可能不會(huì)被返回
  5. 返回結(jié)果為空并不意味著遍歷結(jié)束,而要看返回的游標(biāo)值是否為0

有興趣的小伙伴可以分析一下scan源碼的實(shí)現(xiàn)就能明白這些特性了

「用用zscan遍歷zset,hscan遍歷hash,sscan遍歷set的原理和scan命令類似,因?yàn)閔ash,set,zset的底層實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu)中都有dict?!?/p>

操作bigkey

「如果一個(gè)key對(duì)應(yīng)的value非常大,那么這個(gè)key就被稱為bigkey。寫入bigkey在分配內(nèi)存時(shí)需要消耗更長(zhǎng)的時(shí)間。同樣,刪除bigkey釋放內(nèi)存也需要消耗更長(zhǎng)的時(shí)間」

如果在慢日志中發(fā)現(xiàn)了SET/DEL這種復(fù)雜度不高的命令,此時(shí)你就應(yīng)該排查一下是否是由于寫入bigkey導(dǎo)致的。

「如何定位bigkey?」

Redis提供了掃描bigkey的命令

  1. $ redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01 
  2.  
  3. ... 
  4. -------- summary ------- 
  5.  
  6. Sampled 829675 keys in the keyspace! 
  7. Total key length in bytes is 10059825 (avg len 12.13) 
  8.  
  9. Biggest string found 'key:291880' has 10 bytes 
  10. Biggest   list found 'mylist:004' has 40 items 
  11. Biggest    set found 'myset:2386' has 38 members 
  12. Biggest   hash found 'myhash:3574' has 37 fields 
  13. Biggest   zset found 'myzset:2704' has 42 members 
  14.  
  15. 36313 strings with 363130 bytes (04.38% of keys, avg size 10.00) 
  16. 787393 lists with 896540 items (94.90% of keys, avg size 1.14) 
  17. 1994 sets with 40052 members (00.24% of keys, avg size 20.09) 
  18. 1990 hashs with 39632 fields (00.24% of keys, avg size 19.92) 
  19. 1985 zsets with 39750 members (00.24% of keys, avg size 20.03) 

可以看到命令的輸入有如下3個(gè)部分

  1. 內(nèi)存中key的數(shù)量,已經(jīng)占用的總內(nèi)存,每個(gè)key占用的平均內(nèi)存
  2. 每種類型占用的最大內(nèi)存,已經(jīng)key的名字
  3. 每種數(shù)據(jù)類型的占比,以及平均大小

這個(gè)命令的原理就是redis在內(nèi)部執(zhí)行了scan命令,遍歷實(shí)例中所有的key,然后正對(duì)key的類型,分別執(zhí)行strlen,llen,hlen,scard,zcard命令,來獲取string類型的長(zhǎng)度,容器類型(list,hash,set,zset)的元素個(gè)數(shù)

使用這個(gè)命令需要注意如下兩個(gè)問題

  1. 對(duì)線上實(shí)例進(jìn)行bigkey掃描時(shí),為避免ops(operation per second 每秒操作次數(shù))突增,可以通過-i增加一個(gè)休眠參數(shù),上面的含義為,每隔100條scan指令就會(huì)休眠0.01s
  2. 對(duì)于容器類型(list,hash,set,zset),掃描出的是元素最多的key,但一個(gè)key的元素?cái)?shù)量多,不一定代表占用的內(nèi)存多

「如何解決bigkey帶來的性能問題?」

  1. 盡量避免寫入bigkey
  2. 如果使用的是redis4.0以上版本,可以用unlink命令代替del,此命令可以把釋放key內(nèi)存的操作,放到后臺(tái)線程中去執(zhí)行
  3. 如果使用的是redis6.0以上版本,可以開啟lazy-free機(jī)制(lazyfree-lazy-user-del yes),執(zhí)行del命令的時(shí)候,也會(huì)放到后臺(tái)線程中去執(zhí)行

大量key集中過期

我們可以給Redis中的key設(shè)置過期時(shí)間,那么當(dāng)key過期了,它在什么時(shí)候會(huì)被刪除呢?

「如果讓我們寫Redis過期策略,我們會(huì)想到如下三種方案」

定時(shí)刪除,在設(shè)置鍵的過期時(shí)間的同時(shí),創(chuàng)建一個(gè)定時(shí)器。當(dāng)鍵的過期時(shí)間來臨時(shí),立即執(zhí)行對(duì)鍵的刪除操作

惰性刪除,每次獲取鍵的時(shí)候,判斷鍵是否過期,如果過期的話,就刪除該鍵,如果沒有過期,則返回該鍵

定期刪除,每隔一段時(shí)間,對(duì)鍵進(jìn)行一次檢查,刪除里面的過期鍵 定時(shí)刪除策略對(duì)CPU不友好,當(dāng)過期鍵比較多的時(shí)候,Redis線程用來刪除過期鍵,會(huì)影響正常請(qǐng)求的響應(yīng)

定時(shí)刪除策略對(duì)CPU不友好,當(dāng)過期鍵比較多的時(shí)候,Redis線程用來刪除過期鍵,會(huì)影響正常請(qǐng)求的響應(yīng)

惰性刪除讀CPU是比較有好的,但是會(huì)浪費(fèi)大量的內(nèi)存。如果一個(gè)key設(shè)置過期時(shí)間放到內(nèi)存中,但是沒有被訪問到,那么它會(huì)一直存在內(nèi)存中

定期刪除策略則對(duì)CPU和內(nèi)存都比較友好

redis過期key的刪除策略選擇了如下兩種

  1. 惰性刪除
  2. 定期刪除

「惰性刪除」客戶端在訪問key的時(shí)候,對(duì)key的過期時(shí)間進(jìn)行校驗(yàn),如果過期了就立即刪除

「定期刪除」Redis會(huì)將設(shè)置了過期時(shí)間的key放在一個(gè)獨(dú)立的字典中,定時(shí)遍歷這個(gè)字典來刪除過期的key,遍歷策略如下

  1. 每秒進(jìn)行10次過期掃描,每次從過期字典中隨機(jī)選出20個(gè)key
  2. 刪除20個(gè)key中已經(jīng)過期的key
  3. 如果過期key的比例超過1/4,則進(jìn)行步驟一
  4. 每次掃描時(shí)間的上限默認(rèn)不超過25ms,避免線程卡死

「因?yàn)镽edis中過期的key是由主線程刪除的,為了不阻塞用戶的請(qǐng)求,所以刪除過期key的時(shí)候是少量多次」。源碼可以參考expire.c中的activeExpireCycle方法

為了避免主線程一直在刪除key,我們可以采用如下兩種方案

  1. 給同時(shí)過期的key增加一個(gè)隨機(jī)數(shù),打散過期時(shí)間,降低清除key的壓力
  2. 如果你使用的是redis4.0版本以上的redis,可以開啟lazy-free機(jī)制(lazyfree-lazy-expire yes),當(dāng)刪除過期key時(shí),把釋放內(nèi)存的操作放到后臺(tái)線程中執(zhí)行

內(nèi)存達(dá)到上限,觸發(fā)淘汰策略

圖片Redis是一個(gè)內(nèi)存數(shù)據(jù)庫(kù),當(dāng)Redis使用的內(nèi)存超過物理內(nèi)存的限制后,內(nèi)存數(shù)據(jù)會(huì)和磁盤產(chǎn)生頻繁的交換,交換會(huì)導(dǎo)致Redis性能急劇下降。所以在生產(chǎn)環(huán)境中我們通過配置參數(shù)maxmemoey來限制使用的內(nèi)存大小。

當(dāng)實(shí)際使用的內(nèi)存超過maxmemoey后,Redis提供了如下幾種可選策略。

noeviction:寫請(qǐng)求返回錯(cuò)誤

volatile-lru:使用lru算法刪除設(shè)置了過期時(shí)間的鍵值對(duì) volatile-lfu:使用lfu算法刪除設(shè)置了過期時(shí)間的鍵值對(duì) volatile-random:在設(shè)置了過期時(shí)間的鍵值對(duì)中隨機(jī)進(jìn)行刪除 volatile-ttl:根據(jù)過期時(shí)間的先后進(jìn)行刪除,越早過期的越先被刪除

allkeys-lru:在所有鍵值對(duì)中,使用lru算法進(jìn)行刪除 allkeys-lfu:在所有鍵值對(duì)中,使用lfu算法進(jìn)行刪除 allkeys-random:所有鍵值對(duì)中隨機(jī)刪除

「Redis的淘汰策略也是在主線程中執(zhí)行的。但內(nèi)存超過Redis上限后,每次寫入都需要淘汰一些key,導(dǎo)致請(qǐng)求時(shí)間變長(zhǎng)」

可以通過如下幾個(gè)方式進(jìn)行改善

  1. 增加內(nèi)存或者將數(shù)據(jù)放到多個(gè)實(shí)例中
  2. 淘汰策略改為隨機(jī)淘汰,一般來說隨機(jī)淘汰比lru快很多
  3. 避免存儲(chǔ)bigkey,降低釋放內(nèi)存的耗時(shí)

寫AOF日志的方式為always

Redis的持久化機(jī)制有RDB快照和AOF日志,每次寫命令之后后,Redis提供了如下三種刷盤機(jī)制

always:同步寫回,寫命令執(zhí)行完就同步到磁盤 everysec:每秒寫回,每個(gè)寫命令執(zhí)行完,只是先把日志寫到aof文件的內(nèi)存緩沖區(qū),每隔1秒將緩沖區(qū)的內(nèi)容寫入磁盤 no:操作系統(tǒng)控制寫回,每個(gè)寫命令執(zhí)行完,只是先把日志寫到aof文件的內(nèi)存緩沖區(qū),由操作系統(tǒng)決定何時(shí)將緩沖區(qū)內(nèi)容寫回到磁盤

當(dāng)aof的刷盤機(jī)制為always,redis每處理一次寫命令,都會(huì)把寫命令刷到磁盤中才返回,整個(gè)過程是在Redis主線程中進(jìn)行的,勢(shì)必會(huì)拖慢redis的性能

當(dāng)aof的刷盤機(jī)制為everysec,redis寫完內(nèi)存后就返回,刷盤操作是放到后臺(tái)線程中去執(zhí)行的,后臺(tái)線程每隔1秒把內(nèi)存中的數(shù)據(jù)刷到磁盤中

當(dāng)aof的刷盤機(jī)制為no,宕機(jī)后可能會(huì)造成部分?jǐn)?shù)據(jù)丟失,一般不采用。

「一般情況下,aof刷盤機(jī)制配置為everysec即可」

fork耗時(shí)過長(zhǎng)

在持久化一節(jié)中,我們已經(jīng)提到「Redis生成rdb文件和aof日志重寫,都是通過主線程fork子進(jìn)程的方式,讓子進(jìn)程來執(zhí)行的,主線程的內(nèi)存越大,阻塞時(shí)間越長(zhǎng)?!?/p>

可以通過如下方式優(yōu)化

控制Redis實(shí)例的內(nèi)存大小,盡量控制到10g以內(nèi),因?yàn)閮?nèi)存越大,阻塞時(shí)間越長(zhǎng)

配置合理的持久化策略,如在slave節(jié)點(diǎn)生成rdb快照

本文轉(zhuǎn)載自微信公眾號(hào)「Java識(shí)堂」,作者李立敏。轉(zhuǎn)載本文請(qǐng)聯(lián)系Java識(shí)堂公眾號(hào)。

 

責(zé)任編輯:武曉燕 來源: Java識(shí)堂
相關(guān)推薦

2020-06-11 09:35:39

Redis單線程Java

2023-10-15 12:23:10

單線程Redis

2020-10-30 16:20:38

Redis單線程高并發(fā)

2023-03-21 08:02:36

Redis6.0IO多線程

2025-01-17 08:23:33

2019-05-07 09:44:45

Redis高并發(fā)模型

2019-06-17 14:20:51

Redis數(shù)據(jù)庫(kù)Java

2023-08-17 14:12:17

2019-02-18 08:10:53

2019-05-06 11:12:18

Redis高并發(fā)單線程

2020-10-16 16:00:50

Redis單線程數(shù)據(jù)庫(kù)

2025-04-24 08:15:00

Redis單線程線程

2020-11-17 10:20:53

Redis多線程單線程

2021-12-28 09:50:18

Redis單線程高并發(fā)

2022-01-04 11:11:32

Redis單線程Reactor

2019-04-02 11:20:48

Redis高并發(fā)單線程

2009-07-10 09:05:20

SwingWorker

2022-05-10 08:31:44

RedisQPS單線程

2019-11-25 10:13:52

Redis單線程I

2024-09-27 11:51:33

Redis多線程單線程
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)