1. 程式人生 > >初識Redis,看這一篇就夠了

初識Redis,看這一篇就夠了

環境的搭建和安裝網上有很多教程,在這裡就不再重複了。

1. Redis是什麼?

Redis(全稱:Remote Dictionary Server 遠端字典服務)是一個開源的使用ANSI C語言編寫、支援網路、可基於記憶體亦可持久化的日誌型、Key-Value資料庫。

大家可能知道Redis是做快取用的,它實際上也是一種資料庫,可以對經常使用到的資料進行儲存,也就是大家所說的快取。

官方給出的資料是,Redis能達到10w+的QPS(每秒查詢速度)。

為什麼Redis的速度比Mysql等這種資料快呢?

因為Redis儲存的是key-values格式的資料,時間複雜度是O(1),即直接通過key查詢對應的value。而如Mysql資料庫,底層的實現是B+樹,時間複雜度是O(logn)。

最重要的一點是,資料庫的資料是儲存在磁碟中的,而Redis是儲存在記憶體當中的,它們的速度差距不言而喻。但Redis也支援持久化儲存,這個後面的常見問題裡會提到。

 

 

 

2. Redis資料型別

 

Redis支援5種資料型別:string(字串)、hash(雜湊)、list(列表,有序可重複)、set(集合,無序不可重複)、zset(有序集合,有序不可重複)。

Redis中所有資料都是字串,key是區分大小寫的。

 

1.string是最基本的型別,可以包含任何資料,但是string型別的值最大能儲存512MB。

 

2.hash的value相當於一個map,value裡面也有對應的key-value,特別適合儲存物件。一個hash可以儲存2^32-1個鍵值對,基本用不完。並且可以修改某一個屬性值,所以一般用於儲存使用者或其他實體類的值。

 

3.list中的value按照插入順序排序,可以在列表的頭部和尾部新增新元素。一般用於最新訊息的排行或訊息佇列。

 

4.set存放的是不重複值的集合,是無序的。並提供了求交集、並集、差集等操作,所以一般用於統計等功能。

 

5.與set不同的是,zset是通過分數(score)從小到達進行排序的,我們可以指定每個值的分數,分數可以重複。一般用於排行等功能。

 

3.Redis常用命令

基於對上面5種資料型別的瞭解,接著學習一下Redis常用命令。更多了命令學習,推薦大家看一看官方文件http://www.redis.cn/commands.html


1.對stirng的操作

redis命令不區分大小寫。

下面命令中,str就是key,hello就是value,append為追加命令,如果原來沒有str,就新建一個。

append str hello    //對key為str的鍵追加hello字串
append str redis  //str的value變為helloredis
set str1 1  //set命令設定一個key的value值 str1是key,1是value
get str1   //get命令,獲取一個key的值
incr str1  //incr命令,執行加1操作,比如str1的值會變成2,如果指定的key的value不能表示一個整數,就會返回一個錯誤
decr str1  //減一操作

 

2.對hash的操作

上面說到過,hash的value相當於一個map,所以只設置值的時候myhash是key,h1是value裡面的key,hello是h1的value

hset myhash h1 hello  //設定一個key的value值
hget myhash h1  //返回hello,myhash為key,h1是value裡面的key,兩個都需要指定
hlen myhash  //獲取myhash的欄位數量,這裡返回1
hkeys myhash  //獲取myhash所以欄位,這裡返回h1

3.對list的操作

mylst是key,a,b,c,d都是value,並且有順序,所以實際存進去後是d,c,b,a

lpush mylist a b c d  //lpush,從佇列左邊入隊一個或多個元素
lrange mylist 0 -1  //獲取指定範圍的值,從0開始,-1代表全部,注意這裡返回d,c,b,a。
rpush mylist 1 2 3  //從右邊入隊,再次lrange的話就是d,c,b,a,1,2,3
lpop mylist  //從左邊彈出一個元素,這裡彈出d,此時的mylist就沒有d了

4.對set的操作

如果我們添加了重複的元素,不會報錯,但只會存一個。如a b b,只會存a b

兩個集合之間不受影響,即key為myset和myset2兩個集合裡面都可以有a b

sadd myset a b c d  //新增一個或多個元素到集合裡面
smembers myset  //獲取集合裡所有元素,輸出是無序的,隨機的。這裡可能是b,d,c,a
srem myset a c  //移除myset中的a和c元素,由於不可能重複也沒順序,所以可以直接指定元素值來移除

5.對zset的操作

myzset為key,a b c前面的數字就是score

zadd myzset 2 b 1 a 3 c  //新增一個或多個元素
zrange myzset 0 -1  //獲取指定範圍的值,0開始,-1代表全部。這裡返回a,b,c

更多的命令可以看上面網站中的文件,寫的非常詳細,下面的常見問題中也會提及一些。

4.Redis常見問題

1.在大量的key中查詢某一固定字首的key

在實際的業務當中,key的命名是有規範的,比如快取使用者資訊,key的字首可能會是user。

現在有幾千萬條資料,查詢user為字首的key的話,第一下想到的可能會是keys命令

keys user*  //user*為正則表示式

其時間複雜度為O(n),雖然效能也算可以,但是在查詢幾千萬條資料時明顯太慢了,花上幾分鐘都不稀奇,而且在查詢出來之前,可能會造成服務卡頓,佔用大量記憶體,顯然是不可取的。

那麼這種情況就可以使用scan命令

下面的命令中,math count為可選項,可用可不用,所以需要顯示的寫出來。math意味後面會匹配一個正則表示式。count代表一次查詢10條。

這個10條不是強制的,可能會比10條少。

scan 0 math user*  count 10  //從0開始,查詢user為字首的key,一次查詢10條並返回

執行上面一句話後,會返回兩個東西,一個遊標,代表執行到哪了,比如執行到了14325。返回的另一個就是user為字首的key了。

下次再執行這條語句時,把0換成14325,接著上次的位置繼續查詢。但是遊標不一定是遞增的,也許下次的遊標比這次還小,所以存在重複的隱患。

我們可以在業務程式碼處迴圈查詢,記錄每次返回的遊標,並把查詢的key存入到set當中,起到去重的效果。

scan,實際上就是分批查詢,速度顯然沒有keys快,在查詢大量資料時,不會對伺服器造成壓力。資料量不大時依舊推薦keys

 

2.利用Redis實現分散式鎖

首先了解什麼是分散式鎖。即控制分散式系統訪問共享資源的一種方式。

比如系統(或主機)A和B都需要訪問資源DataA時,當A先訪問到了DataA,這時候就需要分散式鎖來把B擋住,防止A和B彼此干擾,保證資料的一致性。

額外提一點就是,Redis命令的操作是原子性的,原子性在資料庫的事務中有體現,Redis的命令也是原子性的,要麼執行要麼不執行,不會出現一個命令執行到一半失敗了,但還是改變了資料的問題。

實現分散式鎖,需要解決一下幾個問題:

1.互斥性,即任意時刻只能有一個客戶端獲取鎖。

2.安全性,鎖只能有持有它的客戶端刪除,不能由其他客戶端刪除。

3.死鎖,即由於某些原因,一些客戶端出現問題不能及時釋放鎖,導致其他客戶端也不能獲取鎖。

4.容錯,當某些Redis節點出現問題時,客戶端也要能獲取到鎖。

我們可以用setnx實現鎖的功能。語法:setnx key value

僅當key不存在時,才會設定成功。成功返回1,否則返回0。

1.在對應的訪問資源的業務程式碼處,對指定的key設值,如果成功了,則代表沒有其他執行緒執行過這段程式碼,也就是沒有其他執行緒訪問這個資源。

如果設值失敗,就代表有其他執行緒佔用該資源,就一直等待,直到setnx成功。

2.還有個問題就是,這個key是長期有效的,所以還需要用到expire命令,語法:expire key seconds,seconds單位為秒,用以設定對應key的過期時間。

上面兩步似乎好像是實現了鎖的功能,但是缺陷也非常明顯,如果成功設值後,在我設定時間之前客戶端就出現問題了怎麼辦?用兩個命令實現一個功能有悖於Redis的原子性。

在Redis2.6.12版本開始,set有兩個引數,就是實現了以上兩個功能。雖然上面兩步分開的做法是錯的,但是思路是一樣的。

具體語法:set key value ex 10 nx。ex代表過期時間,這裡設定10秒過期,nx代表key是要唯一的,即一個命令實現了以上兩個步驟。

最後還有一個小問題,如果不同資源同時設定了鎖key,過期時間也是一樣的,到期後Redis同時刪除大量key時,難免會出現卡頓。

解決方法就是在設定過期值時加上隨機值。

 

3.利用Redis實現訊息佇列

訊息佇列,簡稱MQ,即訊息和佇列兩個單詞的首字母縮寫。常見的訊息佇列有RabbitMQ和RocketMQ等,利用Redis實現訊息佇列只是熟悉下其特點,實際當中一般會使用專門的訊息佇列中介軟體。

如果之前沒了解過訊息佇列,建議搜尋一下訊息佇列相關知識進行一下簡單的學習。

簡單地說,訊息佇列的作用就是接受客戶端的請求,然後對這些請求依次處理,一般應用請求量特別大時,比如秒殺搶購等。上面介紹資料型別時就說到了list一般用於訊息佇列。

看一下list的常見操作,雖然叫做列表,但其特點和資料結構的佇列基本一模一樣。所以在用Redis實現訊息佇列時,首先肯定會想到list。

1.利用list的話,彷彿使用rpush生產訊息,lpop消費訊息就行了。但是有一個小問題,lpop不會等待rpush的,當rpush還沒來得及生成資料時,這時lpop會直接返回null的。

2.既然要等待rpush生成資料,難免又會想到一個命令blpop,其語法為:blpop key seconds。和lpop功能一樣,但是會等待指定的時間,這段時間內rpush如果生成資料的話,blpop會及時返回。

3.但是blpop的缺點也很明顯,當然這個缺點也存在於lpop當中,就是blpop執行完後,代表出隊,rpush生成的這條訊息就沒了,而訊息佇列中有的需求是需要多個消費者去接收的。

這時候就可以用上Redis的訂閱者模式,Redis客戶端可以訂閱任意數量的頻道(Topic)

 

在Redis當中用subscribe命令訂閱一個頻道,語法subscribe topic,topic就是自定義的頻道名稱,注意是topic不是key,不需要事先定義,直接訂閱就行了。

然後用publish生產訊息,語法publish topic value,topic就是你想釋出到哪個頻道,value就是資料內容,而訂閱了這個頻道的所有消費者都會接收到訊息。注意是及時收到,不需要你再去手動用命令獲取。

訂閱者模式,的確解決了以上兩種方法的缺點,但是其缺陷也很明顯,就是隻有處於訂閱者模式,也就是監聽狀態下,消費者才會接受到生產者的訊息,也就是及時傳送及時接收的,一旦Redis客戶端下線,就永遠不會接收到這個訊息了。

這就回到了前面說到的一句話,實際當中會使用專門的訊息佇列中介軟體來說實現這些功能,以上三種方法或多或少可以實現訊息佇列的功能,但是缺陷也非常明顯。

 

4.Redis如何做持久化

Redis是基於記憶體當中的,那麼肯定就會有疑問了,當我關閉主機或者關閉了Redis,那Redis的資料是不是就全沒了。

持久化的作用就是,把Redis的資料儲存到磁碟當中,以免Redis的資料丟失。

Redis有兩種持久化機制,預設的一種是RDB,另一種是AOF。

1.RDB(快照)持久化會在某個時間點儲存全量的資料,快照即針對記憶體進行的快速讀取技術。而這個時間點可以由我們的實際業務進行時間策略配置。

RDB會按照時間週期策略對資料以快照的方式儲存到磁盤裡,併產生一個dump.rdb的二進位制檔案。我們可以在redis.conf配置檔案中save引數檢視和配置時間策略。

dump.rdb檔案是如何建立的呢?rdb檔案可以通過兩個命令建立,一個是save,一個是bgsave。要注意這裡的save是redis命令,上面提到的save是配置檔案裡面的引數。

save命令會阻塞Redis伺服器程序,直到rdb檔案建立完成,一般很少使用。

bgsave命令會fork出一個子程序來建立rdb檔案,不會阻塞伺服器程序。fork即建立一個與父程序幾乎一樣的子程序。

bgsave的基本原理:當我們使用bgsave命令時,首先會檢查是否存在RDB/AOF子程序正在進行,有的話就返回錯誤,即當我們第一次執行了bgsave,在執行完之前其他的bgsave會被拒絕執行。

如果沒有正在進行的子程序,就會呼叫redis原始碼裡面的rdbSaveBackground這個方法,然後利用fork建立一個子程序。

RDB的缺點:

  1.1.前面提到,在某個時間點會進行全量資料儲存,資料量大的話由於I/O而嚴重影響到效能。

  1.2.由於RDB是根據配置檔案裡面的時間策略進行儲存的,如果發生意外情況,那麼上次儲存到當前時間段內的資料會發生丟失。

 

2.AOF(Append-Only-File)持久化會以追加的方式(append)儲存除了查詢指令以外所有變更的資料,其預設的檔名稱為appendonly.aof。

AOF持久化預設是關閉的,我們可以在配置檔案當中找到appendonly引數,把它的引數內容改為yes。

前面說到AOF檔案會記錄所有非查詢的所有指令,最後肯定難以避免檔案不斷增大的問題,最主要的問題是記錄的很多資料是不必要的。

比如迴圈更新一個數100次,AOF會記錄這100個過程,而我們只需要最終結果就行了。

所以,Redis提供了一個日誌重寫的功能解決檔案不斷增大的問題,可以用BGREWRITEAOF命令手動執行。日誌重寫在服務不中斷的情況下也能執行,其基本原理如下:

1.使用fork建立一個子程序。2.子程序把新的AOF寫道一個臨時檔案裡,並不會依賴現有的AOF檔案,只需要讀取記憶體中的資料。這裡就優化了很多不必要的資料。

3.主程序這時候會依舊將新的變動寫到記憶體裡,也會寫到現有的AOF檔案裡,即使子程序重寫失敗,資料也不會丟失。4.主程序獲取到子程序AOF重寫完成的訊號後,會把新的變動追加到新的AOF檔案裡。

5.最後使用新的AOF檔案替換掉原來的AOF檔案。

如果啟用了AOF持久化,Redis啟動時會先檢查AOF檔案是否存在,如果存在就直接載入AOF檔案,如果不存在就檢查RDB檔案是否存在,如果存在就載入,不存在就直接啟動Redis。

在Redis4.0之後,推出了RDB-AOF混合持久化方式並作為預設方式,RDB全量儲存,AOF增量儲存,集成了它們各自的優點。

 

5.SpringBoot整合Redis

首先在依賴項裡面新增redis啟動器

spring-boot-starter-data-redis

然後在配置檔案裡面進行相關的配置,更多的配置可以看RedisProperties.java原始碼檢視。

spring.redis.host=127.0.0.1    #redis地址
spring.redis.port=6379    #redis服務埠號

 最後注入相關的類

//操作的是複雜型別,比如各種實體類
@Autowired
RedisTemplate redisTemplate

//操作的是字串
@Autowired
StringRedisTemplate stringRedisTemplate

SpringBoot框架下對Redis的操作不像Jedis那樣可以直接使用原生的Redis命令,具體的API大家可以自行搜尋相關的文件。

不過推薦使用一些SpringBoot的Redis工具類,工具類會對RedisTemplate和StringRedisTemplate的方法進行封裝,而封裝後的方法名和Redis原生命令是一樣