你真的懂嗎?Redis不是隻有get set那麼簡單

我以前還沒接觸Redis的時候,聽到大數據組的小夥伴在討論Redis,覺得這東西好高端,要是哪天我們組也可以使用下Redis就好了,好長一段時間後,我們項目中終於引入了Redis這個技術,我用了幾下,感覺Redis也就那麼回事啊,不就是get set嗎?當我又知道Redis還有自增、自減操作,而且這些操作還是原子性的,秒殺就可以用這個技術,我就覺得我已經熟悉Redis了。相信有不少curd boy是和以前的我一個想法:Redis不就是get set increment嗎?其實不然,Redis遠遠沒有我們想象中的那麼簡單,今天我就在此獻醜來談談Redis。

關於Redis是什麼,如何安裝等問題就不闡述了,我們直接進入正題吧。

你真的懂嗎?Redis不是隻有get set那麼簡單

Redis五種數據類型及應用場景

Redis有五種數據類型,即 string,list,hash,set,zset(sort set),我想這點只要稍微對Redis有點了解的小夥伴都應該清楚。下面,我們就來討論下這五種數據類型的應用場景。

string

這個類型相信是大家最熟悉的了,但是千萬不要小瞧它,它可以做很多事情,也可以牽出一系列的問題。

我們先從最簡單的入手:

<code>localhost:6379> set coderbear hello
OK
localhost:6379> get codebear
"hello"/<code>

這兩個命令相信大家都知道,我就不解釋了,我們再來用下strlen這個命令:

<code>localhost:6379> strlen codebear
(integer) 5/<code>

哦,我明白了strlen這個命令可以獲得Value的長度啊,hello的長度是5,所以輸出就是5。這個解釋對不對呢?不著急,我們慢慢往下看。

我們使用append命令為codebear這個key追加點東西:

<code>APPEND codebear 中國/<code>

如果我們再次使用strlen命令會輸出什麼呢?當然是7啊,雖然我數學不好,但是10以內的數數,我還是no problem的,但是當我們再次執行strlen命令,你會發現一個奇怪的現象:

<code>localhost:6379> strlen codebear
(integer) 11/<code>

納尼,為什麼是11,是不是我們的打開方式不對,要不再試下?不用了,就算再試上三生三世,你看到的輸出還是11。這是為什麼呢?這就牽扯到二進制安全問題了。

二進制安全

所謂的二進制安全就是隻會嚴格的按照二進制的數據存取,不會妄圖以某種特殊格式解析數據。Redis就是二進制安全的,它存取的永遠是二進制數據,也可以說存取的永遠是字節數組。

我們來get下codebear康康:

<code>get codebear
"hello\\\\xe4\\\\xb8\\\\xad\\\\xe5\\\\x9b\\\\xbd"/<code>

你會發現好端端的"hello中國",存儲到Redis竟然變成這樣了,因為我們的Xshell客戶端使用的是UTF-8,在UTF-8下,一箇中文通常是三個字節,兩個中文就是6個字節,所以在Redis內部"hello中國"佔了5+6=11個字節。

如果你還不信,我們把Xshell的編碼改成GBK看看,在GBK的世界裡,一箇中文通常佔兩個字節,所以:

<code>localhost:6379> set codebeargbk 中國
OK
localhost:6379> get codebeargbk
"\\\\xd6\\\\xd0\\\\xb9\\\\xfa"
localhost:6379> strlen codebeargbk
(integer) 4/<code>

所以說,醒醒吧,小夥計,在Redis裡面是不可能存中文的,我們之所以在程序裡面可以輕輕鬆鬆的拿到中文,只是因為API做了解碼的操作。

沒想到一個String還牽出二進制安全的問題,看來真是不能小瞧任何一個知識點啊,這也就是常說的搜索地獄,當你查找一個問題,發現這個問題的答案又出現了一個你不懂的東西,於是你又開始看那個你不懂的東西,然後又冒出另外一個你不懂的概念,於是...說多了都是淚啊。

我們經常用Redis做緩存,用到的就是set get這兩個命令了,我們還可以用Redis做秒殺系統,在絕大部分情況下,用的也是String這個數據類型,讓我們繼續往下看:

<code>localhost:6379> set codebearint 5
OK
localhost:6379> incr codebearint
(integer) 6/<code>

也許你沒用過incr命令,但是可以從結果和命令名稱猜出incr這個命令是幹嘛的把,沒錯,就是自增,既然有自增,還可以做自減:

<code>localhost:6379> decr codebearint 
(integer) 5/<code>

剛才是6,調用了decr 命令後,又變成5了。

好了,又有一個問題,String是字符串啊,它怎麼可以做加法或減法的操作?

我們用type命令來檢查下codebearint 這個key的類型是什麼:

<code>localhost:6379> type  codebearint
string/<code>

沒錯,是如假包換的String類型啊。

我們再來看一個東西:

<code>localhost:6379> object encoding codebear
"raw"
localhost:6379> object encoding codebearint
"int"/<code>

原來在Redis的內部還會為這個key打上一個標記,來標記它是什麼類型的(這個類型可和Redis的五種數據類型不一樣哦)。

bitmap

有這麼一個需求:統計指定用戶一年之中登錄的天數?這還不簡單,我建個登錄表,不就可以了嗎?沒錯,確實可以,但是這代價是不是有點高,如果有100萬個用戶,那麼這登錄表要有多大啊。這個時候,bitmap就橫空出世了,它簡直是解決此類問題的神器。

我們先來看看什麼是bitmap,說穿了,就是二進制數組,我們來畫一張圖說明下:

你真的懂嗎?Redis不是隻有get set那麼簡單

這就是bitmap了,由許許多多的小格子組成,格子裡面只能放1或者0,說的專業點,那一個個小格子就是一個個bit。 String就很好的支持了bitmap,提供了一系列bitmap的命令,讓我們來試試:

<code>setbit codebear 3 1
(integer) 0
localhost:6379> get codebear
"\\\\x10"/<code>

這是什麼意思呢,就是說現在有8個小格子,第四個格子裡面放的是1(索引從0開始嘛),其他都是0,就像這樣的:

你真的懂嗎?Redis不是隻有get set那麼簡單

讓我們計算下,大小應該是多少,1 2 4 8 16 ,沒錯,用十進制表示是16,而我們get codebear輸出的是“\\\\x10”,“\\\\x”代表是十六進制,也就是16進制的10,16進制的10就是十進制的16了。

我們再用下strlen命令看下:

<code>localhost:6379> strlen codebear
(integer) 1/<code>

看來只佔據了一個字節,讓我們繼續:

<code>localhost:6379> setbit codebear 5 1
(integer) 0/<code>

bitmap就變成了下面這個醬紫:

你真的懂嗎?Redis不是隻有get set那麼簡單

大小用十進制表示就是20。

我們繼續看下strlen:

<code>localhost:6379> strlen codebear
(integer) 1/<code>

還是隻佔據了一個字節,我們明明已經存儲了兩個數據了,是不是非常神奇。

讓我們繼續:

<code>localhost:6379> setbit codebear 15 1
(integer) 0
localhost:6379> strlen codebear
(integer) 2/<code>

從這裡可以看出bitmap是可以擴展的,由於現在我在第16個格子裡面放了1,所以bitmap擴展了,現在strlen是2。 那麼我想知道現在16個格子裡面有多少格子是1的,怎麼辦呢?用bitcount命令:

<code>localhost:6379> bitcount codebear
(integer) 3/<code>

到了這一步,是不是豁然開朗了,用bitmap可以輕鬆統計指定用戶一年之中登錄的天數。 我們假設codebear第一天登錄過,第二天登錄過,最後一天登錄過:

<code>localhost:6379> setbit codebear 0 1
(integer) 0
localhost:6379> setbit codebear 1 1
(integer) 0
localhost:6379> setbit codebear 364 1
(integer) 0
localhost:6379> bitcount codebear

(integer) 3/<code>

繼續用strlen來看看,記錄了全年登錄過的日子佔據了多少字節:

<code>localhost:6379> strlen codebear
(integer) 46/<code>

僅僅46個字節,就算每年都登錄,也只佔用46個字節,我不知道這樣的數據存在數據庫應該是多大的,但是我想遠遠不止46個字節把,如果有100萬個用戶,也就不到50M,哪怕這100萬個用戶天天登錄佔據的字節也是這些。

我們再把上面的需求改下:統計指定用戶在任意時間窗口內登錄的天數?

bitcount命令後面還可以帶兩個參數,即 開始 和 結束:

<code>localhost:6379> bitcount codebear 0 2
(integer) 2/<code>

我們還可以把第二個參數寫成-1,代表直到最後一位,即:

<code>localhost:6379> bitcount codebear 0 -1
(integer) 3/<code>

bitmap的強大遠遠不止這些,我們再來康康第二個需求:統計任意日期內,所有用戶登錄情況:

  1. 統計任意一天內登錄過的用戶
  2. 統計任意幾天內登錄過的用戶
  3. 統計任意幾天內每天都登錄的用戶

腦闊疼啊,這特麼的是人乾的事情嗎?別急,這一切都可以用bitmap來實現。

第一個需求,很好實現,假設用戶codebear的userId是5,用戶小強的userId是10,我們可以建立一個key為日期的bitmap,其中第四個、第九個小格子是1,代表userId是5、userId是1的用戶在這一天登錄過,然後bitcount下就萬事大吉,如下所示:

<code>localhost:6379> setbit 20200301 4 1
(integer) 0
localhost:6379> setbit 20200301 9 1
(integer) 0
localhost:6379> bitcount 20200301
(integer) 2/<code>

要實現下面兩個需求,得用新的命令了,直接看結果吧:

<code>localhost:6379> setbit 20200229 9 1
(integer) 1
localhost:6379> bitop and andResult 20200301 20200229
(integer) 2
localhost:6379> bitcount andResult
(integer) 1
localhost:6379> bitop or orResult 20200301 20200229
(integer) 2
localhost:6379> bitcount orResult
(integer) 2/<code>

下面來解釋下,首先又創建了一個key為20200229的bitmap,其中第10個小格子為1,代表用戶Id為10的用戶在20200229這一天登錄過,接下來對key為20200301和20200229的bitmap做與運算,結果也是一個bitmap,並且把結果放入了andResult這個key中,下面就是熟悉的bitcount命令了,康康有多少個小格子為1的,結果是1,也就是這兩天,每天都登錄的用戶有一個。

既然有與運算,那麼就有或運算,下面就是或運算的命令,求出了這兩天有兩位用戶登錄。

這樣後面兩個需求就輕鬆搞定了。

還有大名鼎鼎的布隆過濾器也是用bitmap實現的,關於布隆過濾器在以前的博客也介紹過。

看了那麼多的例子,大家有沒有發現一個問題,所有的運算都在Redis內部完成,對於這種情況,有一個很高大上的名詞:計算向數據移動。與之相對的,把數據從某個地方取出來,然後在外部計算,就叫數據向計算移動。

list

Redis的list底層是一個雙向鏈表,我們先來康康幾個命令:

<code>localhost:6379> lpush codebear a b c d e
(integer) 5
localhost:6379> lrange codebear 0 -1
1) "e"
2) "d"
3) "c"
4) "b"
5) "a"/<code>

push,我懂,推嘛,但是前面+個l是什麼意思呢,前面的l代表左邊,lpush就是在左邊推,這樣第一個推進去的,就是在最右邊,lrange是從左開始拿出指定索引範圍內的數據,後面的-1就是代表拿到最後一個為止。

既然可以在左邊推,那麼必須可以在右推啊,我們康康:

<code>localhost:6379> rpush codebear z
(integer) 6
localhost:6379> lrange codebear 0 -1
1) "e"
2) "d"
3) "c"
4) "b"
5) "a"
6) "z"/<code>

還有兩個彈出命令也很常用,我們來使用下:

<code>localhost:6379> lpop codebear
"e"
localhost:6379> lrange codebear 0 -1
1) "d"
2) "c"
3) "b"
4) "a"
5) "z"
localhost:6379> rpop codebear
"z"
localhost:6379> lrange codebear 0 -1
1) "d"
2) "c"
3) "b"
4) "a"/<code>

lpop是彈出左邊第一個元素,rpop就是彈出右邊第一個元素。

如果我們使用lpush,rpop或者rpush,lpop這樣的組合,就是先進先出,就是隊列了;如果我們使用lpush,lpop或者rpush,rpop這樣的組合,就是先進後出,就是棧了,所以Redis還可以作為消息隊列來使用,用到的就是list這個數據類型了。

相信大家一定都玩過論壇,後面發帖的,帖子通常在前面。為了性能,我們可以把帖子的數據放在Redis中的list裡面,但是總不能無限往list裡面扔數據吧,一般前面幾頁的帖子翻看的人會多一些,再往後面的帖子就很少有人看了,所以我們可以把前面幾頁的帖子數據放在list中,然後設定一個規則,定時去list刪數據,我們就可以用到list的ltrim嗎,命令:

<code>localhost:6379> ltrim codebear 1 -1
OK
localhost:6379> lrange codebear 0 -1
1) "c"
2) "b"
3) "a"/<code>

這個ltrim有點奇怪,它是保留索引範圍之內的數據,刪除索引範圍之外的數據,現在給定的第一個參數是1,第二個參數是-1,就是要保留從索引為1到結束的數據,所以索引為0的數據被刪除了。

hash

現在有一個產品詳情頁,裡面有產品介紹,有價格,有溫馨提示,有瀏覽數,有購買人數等等一堆信息,當然我們可以把整個對象都用String來存儲,但是可能有一些地方只需要產品介紹,儘管是這樣,我們還是必須得把整個對象都拿出來,是不是有點不太划算呢?hash就可以解決這樣的問題:

<code>localhost:6379> hset codebear name codebear
(integer) 1
localhost:6379> hset codebear age 18
(integer) 1
localhost:6379> hset codebear sex true
(integer) 1
localhost:6379> hset codebear address suzhou
(integer) 1
localhost:6379> hget codebear address
"suzhou"/<code>

如果我們是存儲整個對象,現在想修改下age,怎麼辦?要把整個對象全部拿出來,然後再賦值,最後又得放回去,但是現在:

<code>localhost:6379> hincrby codebear age 2 
(integer) 20
localhost:6379> hget codebear age
"20"/<code>

set

set是一種無序,且去重的數據結構,我們可以用它去重,比如我現在要存儲所有商品的Id,就可以用set來實現,有什麼場景需要存儲所有商品的Id呢?防止緩存穿透,當然防止緩存穿透有很多實現方案,set方案只是其中的一種。我們來康康它的基本用法:

<code>localhost:6379> sadd codebear 6 1 2 3 3 8 6
(integer) 5
localhost:6379> smembers codebear
1) "1"
2) "2"
3) "3"
4) "6"
5) "8"/<code>

可以很清楚的看到我們存進去的數據被去重了,而且數據被打亂了。

我們再來看看srandmember這個命令有什麼用?

<code>localhost:6379> srandmember codebear 2
1) "6"
2) "3"
localhost:6379> srandmember codebear 2
1) "6"
2) "2"
localhost:6379> srandmember codebear 2
1) "6"
2) "3"
localhost:6379> srandmember codebear 2
1) "6"
2) "2"
localhost:6379> srandmember codebear 2
1) "8"
2) "3"/<code>

srandmember 後面可以帶參數,後面跟著2,就代表隨機取出兩個不重複的元素,如果想取出兩個可以重複的元素,怎麼辦呢?

<code>localhost:6379> srandmember codebear -2
1) "6"
2) "6"/<code>

如果後面跟著負數,就代表取出的元素可以是重複的。

如果後面跟的數字大於set元素的個數呢?

<code>localhost:6379> srandmember codebear 100
1) "1"
2) "2"
3) "3"
4) "6"
5) "8"
localhost:6379> srandmember codebear -10
1) "8"
2) "1"
3) "1"
4) "1"

5) "6"
6) "1"
7) "1"
8) "2"
9) "6"
10) "8"/<code>

如果是正數的話,最多把set中所有的元素都返回出來,因為正數是不重複的,再多返回一個出來,就重複了,如果是負數,那麼不影響,後面跟著幾,就返回多少個元素出來。

我們做抽獎系統,就可以用到這個命令了,如果可以重複中獎,後面帶著負數,如果不能重複中獎,後面帶著正數。

set還可以計算差集、並集、交集:

<code>localhost:6379> sadd codebear1 a b c
(integer) 3
localhost:6379> sadd codebear2 a z y
(integer) 3
localhost:6379> sunion codebear1 codebear2
1) "a"
2) "c"
3) "b"
4) "y"
5) "z"
localhost:6379> sdiff codebear1 codebear2
1) "b"
2) "c"
localhost:6379> sinter codebear1 codebear2
1) "a"/<code>

上面的命令就不過多解釋了,這有什麼用呢,我們可以利用它來做一個“騙取融資”的推薦系統:你們的共同好友是誰,你們都在玩的遊戲是哪個,你可能認識的人。

zset

set是無序的,而zset是有序的,其中每個元素都有一個score的概念,score越小排在越前面,還是先來康康它的基本使用把:

<code>localhost:6379> zadd codebear 1 hello 3 world 2 tree
(integer) 3
localhost:6379> zrange codebear 0 -1 withscores
1) "hello"
2) "1"
3) "tree"
4) "2"
5) "world"
6) "3"
localhost:6379> zrange codebear 0 -1
1) "hello"
2) "tree"
3) "world"/<code>

現在我們就創建了一個key為codebear 的zset,往裡面添加了三個元素:hello ,world ,tree,score分別為1,3,2,後面用zrange取出結果,發現已經按照score的大小排好序了,如果後面跟著withscores,就會把score一起取出來。

如果我們想看看tree排在第幾位,我們可以用zrank命令:

<code>localhost:6379> zrank codebear tree
(integer) 1/<code>

因為是從0開始的,所以結果是1。

如果我們想查詢tree的score是多少:

<code>localhost:6379> zscore codebear tree
"2"/<code>

如果我們想取出從大到小的前兩個,怎麼辦:

<code>localhost:6379> zrange codebear -2 -1
1) "tree"
2) "world"/<code>

但是這樣的結果是有些錯誤的,從大到小的前兩個,第一個元素是world,又該如何呢:

<code>localhost:6379> zrevrange codebear 0 1
1) "world"
2) "tree"/<code>

像排行榜,熱點數據,延遲任務隊列都可以用zset來實現,其中延遲任務隊列在我以前的博客有介紹過。

談到szet,可能還會引出一個問題,Redis中的zset是用什麼實現的?跳錶。

關於跳錶,在這裡就不展開了,為什麼要用跳錶實現呢,是因為跳錶的特性: 讀寫均衡。

Redis為什麼那麼快

這是一個經典的面試題,幾乎面試談到Redis,80%都會問這問題,為什麼Redis那麼快呢?主要有以下原因:

  1. 編程語言:Redis是用C語言編寫的,更接近底層,可以直接調用os函數。
  2. 基於內存:因為Redis的數據都是放在內存的,所以更快。如果放在硬盤,性能要看兩個指標:尋址(轉速),吞吐。尋址是毫秒級別的,一般來說,吞吐在500M左右,就算服務器性能再牛逼,也不會有幾個G的吞吐,而放在內存,是納秒級別的。
  3. 單線程:因為Redis是單線程的,所以避免了線程切換的消耗,也不會有競爭,所以更快。
  4. 網絡模型:由於Redis的網絡模型是epool,是多路複用的網絡模型。(關於epool後面會展開討論)
  5. Redis數據結構的優化:Redis中提供了5種數據類型,其中zset用跳錶做了優化,而且整個Redis其實也都用hash做了優化,使其的時間成本是O(1),查找更快。
  6. Redis6.0推出了I/O Threads,所以更快。(關於I/O Threads後面會展開討論)

Redis有什麼缺點

這就是一個開放式的問題了,有很多答案,比如:

  1. 因為Redis是單線程的,所以無法發揮出多核CPU的優勢。
  2. 因為Redis是單線程的,一旦執行了一個複雜的命令,後面所有的命令都被堵在門外了。
  3. 無法做到對hash中的某一項添加過期時間。

Redis為什麼可以保證原子性

因為Redis是單線程的,所以同時只能處理一個讀寫請求,所以可以保證原子性。

Redis是單線程的,到底該如何解釋

我們一直在強調Redis是單線程的,Redis是單線程的,但是Redis真的完全是單線程的嗎?其實不然,我們說的Redis是單線程的,只是Redis的讀寫是單線程的,也就是work thread只有一個。

什麼是I/O Threads

I/O Threads是Redis 6.0推出的新特性,在以前Redis從socket拿到請求、處理、把結果寫到socket是串行化的,即:

你真的懂嗎?Redis不是隻有get set那麼簡單

而Redis6.0推出了I/O Threads後:

你真的懂嗎?Redis不是隻有get set那麼簡單

可以看到I/O Thread有多個,I/O Thread負責從socket讀數據和寫數據到socket,work thread在處理數據的同時,其他I/O Thread可以再從socke讀數據,先準備好,等work thread忙完手中的事情了,立馬可以處理下個請求。 但是work thread只有一個,這點要牢記。


什麼是epoll

epoll是一種多路複用IO模型,在說epoll之前,不得不說下傳統的IO模型,傳統的IO模型是同步阻塞的,什麼意思呢?就是服務端建立的socket會死死的等待客戶端的連接,等客戶端連接上去了,又會死死的等待客戶端的寫請求,一個服務端只能為一個客戶端服務。

後來,程序員們發現可以用多線程來解決這個問題:

  1. 當第一個客戶端連接到服務端後,服務端會啟動第一個線程,以後第一個客戶端和服務端的交互就在第一個線程中進行。
  2. 當第二個客戶端連接到服務端後,服務端又會啟動第二個線程,以後第二個客戶端和服務端的交互就在第二個線程中進行。
  3. 當第三個客戶端連接到服務端後,服務端又會啟動第三個線程,以後第三個客戶端和服務端的交互就在第三個線程中進行。

看起來,很美好,一個服務端可以為N個客戶端服務,但是總不能無限開線程把 ,在Java中,線程是有自己的獨立棧的,一個線程至少消耗1M,而且無限開線程,CPU也會受不鳥啊。

雖然後面還經歷了好幾個時代才慢慢來到了epoll的時代,但是我作為一個curd boy,api boy就不去研究的那麼深了,現在我們跨過中間的時代,直接來到epoll的時代吧。

我們先來認識下epoll的方法,在linux中,可以用man來看看OS函數:

<code>man epoll/<code>

在介紹中有這麼一段話:

<code>       *  epoll_create(2) creates a new epoll instance and returns a file descriptor referring to that instance.  (The more recent epoll_create1(2) extends
the functionality of epoll_create(2).)

* Interest in particular file descriptors is then registered via epoll_ctl(2). The set of file descriptors currently registered on an epoll
instance is sometimes called an epoll set.

* epoll_wait(2) waits for I/O events, blocking the calling thread if no events are currently available./<code>

雖然我英語實在是爛,但是藉助翻譯,還是可以勉強看懂一些,大概的意思是:

  1. epoll_create創建了一個epoll示例,並且會返回一個文件描述符。
  2. epoll_ctl用於註冊感興趣的事件。
  3. epoll_wait用於等待IO事件,如果當前沒有感興趣的IO事件,則阻塞,言外之意就是如果發生了感興趣的事件,這個方法便會返回。

下面還給出了一個demo,我們來試著看下:

<code>        epollfd = epoll_create1(0);//創建一個epoll實例,返回一個 epoll文件描述符
if (epollfd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}

ev.events = EPOLLIN;
ev.data.fd = listen_sock;
// 註冊感興趣的事件
// 第一個參數為epoll文件描述符
// 第二個參數為動作,現在是要添加感興趣的事件
// 第三個參數為被監聽的文件描述符
// 第四個參數告訴內核需要監聽什麼事件
// 現在監聽的事件是EPOLLIN,表示對應的文件描述符上有可讀數據
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
// 一個死循環
for (;;) {
// 等待IO事件的發生
// 第一個參數是epoll文件描述符
// 第二個參數是發生的事件集合
// 第三個參數不重要
// 第四個參數是等待時間,-1為永遠等待
// 返回值是發生的事件的個數
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}

// 循環
for (n = 0; n < nfds; ++n) {
// 如果發生的事件對應的文件描述符是listen_sock
if (events[n].data.fd == listen_sock) {
// 建立連接
conn_sock = accept(listen_sock,
(struct sockaddr *) &addr, &addrlen);
if (conn_sock == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
setnonblocking(conn_sock);// 設置非阻塞
ev.events = EPOLLIN | EPOLLET;// 設置感興趣的事件
ev.data.fd = conn_sock;
// 添加感興趣的事件,為 EPOLLIN或者EPOLLET
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
&ev) == -1) {
perror("epoll_ctl: conn_sock");
exit(EXIT_FAILURE);
}
} else {
do_use_fd(events[n].data.fd);
}
}
}/<code>

由於本人沒有學習過C語言,有些註釋的不對的地方請多擔待,但是自認為大概就是這麼個意思。

如果學過Java中的NIO的話,就會發現這個模式和Java中的NIO很像,那是因為Java中的NIO最終調用的就是OS的epoll函數。

epool到底是個什麼鬼呢,說的簡單點,就是告訴內核我對哪些事件感興趣,內核就會幫你監聽,當發生了你感興趣的事件後,內核就會主動通知你。

這有什麼優點呢:

  1. 減少用戶態和內核態的切換。
  2. 基於事件就緒通知方式:內核主動通知你,不牢你費心去輪詢、去判斷。
  3. 文件描述符幾乎沒有上限:你想和幾個客戶端交互就和幾個客戶端交互,一個線程可以監聽N個客戶端,並且完成交互。

epool函數是基於OS的,在windows裡面,沒有epool這東西。

好了,關於epoll的介紹就到這裡了,又出現了三個新名詞:用戶態、內核態、文件描述符,就先不解釋了,以後寫NIO的博客再說吧。那時候,會更詳細的介紹epoll。

Redis的過期策略

一般來說,常用的過期策略有三種:

  1. 定時刪除:需要給每個key添加一個定時器,一到期就移除數據。優點是非常精確,缺點是消耗比較大。
  2. 定期刪除:每隔一段時間,就會掃描一定數量的key,發現過期的,就移除數據。優點是消耗比較小,缺點是過期的key無法被及時移除。
  3. 懶刪除:使用某個key的時候,先判斷下這個key是否過期,過期則刪除。優點是消耗小,缺點是過期的key無法被及時移除,只有使用到了,才會被移除。

Redis使用的是定期刪除+懶刪除的策略。

管道

如果我們有好多命令要交給Redis,第一個方案是一條一條發,缺點不言而喻:每條命令都需要經過網絡,性能比較低下,第二個方案就是用管道。 在介紹管道之前,先要演示一個東西:

<code>[root@localhost ~]# nc localhost 6379
set codebear hello
+OK
get codebear
$5
hello/<code>

我們往Redis發送命令,不一定必須要用Redis的客戶端,只要連接上Redis服務器的端口就可以了,至於get codebear命令後面輸出了$5是什麼意思,就不在這裡討論了。

管道到底怎麼使用呢,有了上面的基礎,其實也很簡單:

<code>[root@localhost ~]# echo -e "set codebear hello1234 \\n incr inttest \\n set haha haha" | nc localhost 6379
+OK
:1
+OK/<code>

把命令與命令之間用\\n分割,然後通過nc發送給Redis。

我們再來康康是否成功了:

<code>[root@localhost ~]# nc localhost 6379
get inttest
$1
1
get codebear
$9
hello1234
get haha
$4
haha/<code>

需要注意的,雖然多條命令是一起發送出去的,但是整體不具有原子性。 各大操作Redis的組件也提供了管道發送的方法,如果下次在項目中需要發送多個命令不妨試下。

發佈訂閱

當我們有個消息需要以廣播的形式推送給各個系統,除了採用消息隊列的方式,還可以採用發佈與訂閱的方式,在Redis中就提供了發佈訂閱的功能,我們來看下如何使用。

首先,我們要創建一個訂閱者,訂閱名稱為hello的channel:

<code>localhost:6379> subscribe hello
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "hello"
3) (integer) 1/<code>

然後,要創建一個發佈者,往名稱為hello的channel發送消息:

<code>localhost:6379> publish hello goodmorning
(integer) 1/<code>

最後,再回到訂閱者,發現接收到了消息:

<code>1) "message"
2) "hello"
3) "goodmorning"/<code>

但是需要注意,如果先發布消息,訂閱者再去訂閱,是收不到歷史消息的。

是不是特別簡單,在我還不知道有ZooKeeper的時候,我覺得可以用Redis的發佈訂閱功能來做配置中心。

內存淘汰

如果Redis內存滿了,再也容納不下新數據了,就會觸發Redis的內存淘汰策略,在redis.conf有一個配置,就是用來配置具體的內存淘汰策略的:

<code>maxmemory-policy volatile-lru/<code>

它有好幾個配置,在講具體的配置前,要先說兩個名詞,如果這兩個名詞不瞭解的話,那麼每個配置的含義真是隻能死記硬背了。

  • LRU:最少使用淘汰算法:如果這個key很少被使用,被淘汰
  • LFU:最近不使用淘汰算法:如果這個key最近沒有被使用過,被淘汰

下面就是具體的配置了,我們一一來看:

  • volatile-lru:在設置了過期時間的key中,移除最近最少使用的key
  • allkeys-lru:在所有的key中,移除最近最少使用的key
  • volatile-lfu:在設置了過期時間的key中,移除最近不使用的key
  • allkeys-lfu:在所有的key中,移除最近不使用的key
  • volatile-random:在設置了過期時間的key中,隨機移除一個key
  • allkeys-random:隨機移除一個key
  • volatile-ttl:在設置了過期時間的鍵空間中,具有更早過期時間的key優先移除
  • noeviction:神馬也不幹,直接拋出異常

在生產環境中,到底應該使用哪個配置呢? 可以說網上的答案千差萬別,但是可以統一的是一般不會選擇noeviction,所以這個問題還是用萬金油的答案,一個完全正確的廢話答案:看場景。


作者:CoderBear
原文鏈接:https://juejin.im/post/5e5e11bce51d4526fe65178b



分享到:


相關文章: