1、Redis是什么?
官方的說法是:Redis是一個(gè)開源的,基于BSD許可的,內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)存儲系統(tǒng),它可以用作數(shù)據(jù)庫、緩存和消息中間件,它支持多種類型的數(shù)據(jù)結(jié)構(gòu),比如字符串(String),散列(hashes),列表(list),集合(set),有序集合(sorted set)與范圍查詢,bitmaps,hyperloglogs,地理空間(geospatial),索引半徑查詢。
Redis內(nèi)置了復(fù)制(replication),LUA腳本(Lua scripting),LRU驅(qū)動(dòng)事件(LRU eviction)事務(wù)(transactions)和不同級別的磁盤持久化(persistence),并可以通過哨兵(Sentinel)和自動(dòng)分區(qū)(Cluster)提供高可用性(high availability)。
什么意思呢?
Redis可以當(dāng)做數(shù)據(jù)庫使用,也可以當(dāng)做緩存和消息隊(duì)列來用,并且,它支持多種類型的數(shù)據(jù)結(jié)構(gòu),并且內(nèi)置了一大堆不明覺厲的功能,而且還可以布置集群來提高可用性。
下面,我們就來逐步分析Redis的作用以及業(yè)務(wù)場景。
2、使用場景
因?yàn)镽edis的數(shù)據(jù)都儲存在內(nèi)存中,當(dāng)然,它也可以持久化,但它的持久化并不能保證數(shù)據(jù)絕對落地,而且還會帶來Redis性能的下降,因?yàn)?,持久化太過頻繁的話,會增大Redis的壓力。
那么,Redis的使用場景就呼之欲出了:用于存儲少量的,訪問頻率較高的數(shù)據(jù)。
當(dāng)然了,少量的這一點(diǎn),是因?yàn)閮?nèi)存很珍貴,如果你的服務(wù)器內(nèi)存極高,自然就不需要太過注重這一點(diǎn)了。
3、緩存的使用方式
Redis有五種最常用的基本類型,string,hash,list,set以及zset(sorted set)。
Redis有很多命令,光String相關(guān)的就是22個(gè)命令,比如set,get,append,getset等等,官方有提供詳細(xì)的命令文檔,有興趣的可以去了解一下。
如果覺得這些命令不夠用的話,redis還提供了lua腳本擴(kuò)展自定義命令的功能,可以編寫原子性的自定義命令。
-
3.1 String
string是Redis最基本的數(shù)據(jù)類型,使用key-value的方式存儲。
此類型是二進(jìn)制安全的,也就是說Redis的string可以存儲任何數(shù)據(jù),比如jpg對象,或者序列化流。
一個(gè)鍵最多可以儲存512M。
如果key已經(jīng)存在,那么,新的value會覆蓋舊的value
127.0.0.1:6379> set testKey Redis
OK
127.0.0.1:6379> get testKey
"Redis"
127.0.0.1:6379>
-
3.2 Hash
hash也是鍵值對集合,只不過存儲的是string類型的field和value的映射表,非常適合存儲對象。
執(zhí)行hmset命令時(shí),如果哈希表不存在的話,就會創(chuàng)建一個(gè)新的哈希表,并插入數(shù)據(jù)
如果field已經(jīng)存在,則新的value會覆蓋舊的value
127.0.0.1:6379> hmset testMap key1 "value1" key2 "value2"
OK
127.0.0.1:6379> hmget testMap key1
1) "value1"
127.0.0.1:6379>
若是想取出key中的所有field的話,應(yīng)該用hgetall命令
127.0.0.1:6379> hgetall testMap
1) "key1"
2) "value1"
3) "key2"
4) "value2"
127.0.0.1:6379>
注意:hmset命令在4.0.0版本之后被廢棄,要使用hset命令
· 3.3 List
Redis中的list是有序列表,按照插入順序排序,可以添加任意一個(gè)元素到列表的頭部(左邊)或者尾部(右邊)
127.0.0.1:6379> lpush testList value1
(integer) 1
127.0.0.1:6379> lpush testList value2
(integer) 2
127.0.0.1:6379> lrange testList 0 10
1) "value2"
2) "value1"
127.0.0.1:6379>
可以看到,value2雖然是后插入的,但因?yàn)槭褂昧?code>lpush命令,所以被插入到了頭部,變成了第一個(gè)
要是想插入到尾部的話,就要使用rpush命令
· 3.4 Set
Redis的Set是無序集合,如果某個(gè)value已經(jīng)存在,執(zhí)行命令會返回0
127.0.0.1:6379> sadd testSet value1
(integer) 1
127.0.0.1:6379> sadd testSet value2
(integer) 1
127.0.0.1:6379> sadd testSet value3
(integer) 1
127.0.0.1:6379> sadd testSet value4
(integer) 1
127.0.0.1:6379> smembers testSet
1) "value3"
2) "value4"
3) "value1"
4) "value2"
127.0.0.1:6379> sadd testSet value4
(integer) 0
-
3.5 zset
zset和set一樣,都是string類型的集合,且都不允許重復(fù)的value,但zset是有序的。
zset中,每個(gè)元素都會關(guān)聯(lián)一個(gè)double類型的score,redis就是通過score來進(jìn)行排序的,zset的value是唯一的,但score是可以重復(fù)的。
127.0.0.1:6379> zadd testZSet 0 value1 0 value2 0 value3 2 value4 1 value5
(integer) 5
127.0.0.1:6379> zrangebyscore testZSet 0 10
1) "value1"
2) "value2"
3) "value3"
4) "value5"
5) "value4"
127.0.0.1:6379>
可以看到,zset的順序和插入順序無關(guān),而是和score的值有關(guān)。
4、事務(wù)
關(guān)系型數(shù)據(jù)庫中事務(wù)的特點(diǎn)就不再贅述了,在Redis中,事務(wù)可以一次執(zhí)行多個(gè)命令,并且?guī)в幸韵聝蓚€(gè)重要保證:
- 事務(wù)是一個(gè)單獨(dú)的隔離操作:事務(wù)中的所有命令都會序列化、按順序地執(zhí)行,事務(wù)在執(zhí)行過程中,不會被其他客戶端發(fā)來的命令請求打斷。
- 事務(wù)是一個(gè)原子操作:要么全不執(zhí)行,要么全部執(zhí)行。
當(dāng)使用AOF做持久化的時(shí)候,Redis會使用write(2)命令將事務(wù)寫入到磁盤中。
但是,如果當(dāng)Redis服務(wù)期意外中斷,或者遇見硬件故障,就可能只有部分事務(wù)被成功寫入磁盤里,而Redis在重啟時(shí)如果發(fā)現(xiàn)AOF文件出現(xiàn)了這樣的問題,就會退出,并且報(bào)錯(cuò)。
使用redis-check-aof程序可以修復(fù)這一問題:它會移除 AOF 文件中不完整事務(wù)的信息,確保服務(wù)器可以順利啟動(dòng)。
multi命令用于開啟事務(wù),它總是返回OK。
multi被執(zhí)行后,客戶端可以繼續(xù)向服務(wù)端發(fā)送任意數(shù)量的命令,這些命令不會被立即執(zhí)行,而是被放到一個(gè)隊(duì)列中,直到調(diào)用exec命令為止。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> rpush testList v1
QUEUED
127.0.0.1:6379> rpush testList v2
QUEUED
127.0.0.1:6379> rpush testList v3
QUEUED
127.0.0.1:6379> exec
1) (integer) 1
2) (integer) 2
3) (integer) 3
127.0.0.1:6379>
exec命令返回的是一個(gè)數(shù)組,數(shù)組中的每個(gè)元素都是執(zhí)行事務(wù)中的命令產(chǎn)生的回復(fù),并且,回復(fù)命令的順序和命令發(fā)送的順序一致。
當(dāng)客戶端處于事務(wù)狀態(tài)時(shí),每一個(gè)命令都會返回 QUEUED的狀態(tài)回復(fù),代表這些命令入隊(duì)成功。
注意:Redis的事務(wù)沒有回滾功能,如果某條命令執(zhí)行失敗了,那么下面的命令依然會繼續(xù)執(zhí)行——Redis不會停止執(zhí)行事務(wù)的命令。
按照Redis官方的說法,之所以不回滾,是因?yàn)槊铄e(cuò)誤只有一種可能:語法錯(cuò)誤,或者命令用在了錯(cuò)誤的key上。
也就是說,這些錯(cuò)誤是因?yàn)槭褂谜叩脑蛟斐傻?,這些問題應(yīng)該在開發(fā)時(shí)就被發(fā)現(xiàn),不應(yīng)該出現(xiàn)在生產(chǎn)環(huán)境中。
而放棄了回滾,Redis才可以保持內(nèi)部的簡潔和迅速。
簡單來說就是:我不能因?yàn)槟愕牡图夊e(cuò)誤而妥協(xié),以至于犧牲我自己的最大優(yōu)點(diǎn)。
discard命令可以放棄事務(wù),并清空隊(duì)列,客戶端也會從事務(wù)狀態(tài)中退出。
127.0.0.1:6379> set testKey 1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr testKey
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> get testKey
"1"
127.0.0.1:6379>
除了上面三個(gè)命令,Redis還提供了watch命令,為事務(wù)提供check-and-set(CAS)行為。
watch命令可以監(jiān)視某個(gè)key,并會發(fā)覺這些key是否被改動(dòng)了,如果至少一個(gè)被監(jiān)視的key在執(zhí)行exec命令之前被改動(dòng),那么整個(gè)事務(wù)都會被取消,exec命令返回nil來表示事務(wù)已經(jīng)失敗。
舉個(gè)例子,我在客戶端1里設(shè)置了key1=1,然后監(jiān)視此key并開啟事務(wù),將key1設(shè)置成2,但是不提交。
127.0.0.1:6379> set key1 1
OK
127.0.0.1:6379> watch key1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set key1 2
QUEUED
然后在客戶端2里,把key1設(shè)置成3,表示在事務(wù)執(zhí)行期間,有另一人修改了watch監(jiān)視的key。
127.0.0.1:6379> set key1 3
OK
127.0.0.1:6379>
這時(shí)候,返過來再去客戶端1里執(zhí)行exec命令。
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get key1
"3"
127.0.0.1:6379>
可以看到,exec命令執(zhí)行失敗了,并返回了nil,而key1的值,依然是客戶端2里設(shè)置的3.
這也就是Redis中的樂觀鎖。
對key的監(jiān)視周期,從執(zhí)行watch命令開始生效,直到調(diào)用exec為止。
而且還可以監(jiān)視多個(gè)key。
127.0.0.1:6379> watch key1 key2 key3
OK
當(dāng)exec被調(diào)用后,不管事務(wù)是否成功,對key的監(jiān)視都會取消。
使用unwatch命令,可以取消對所有key的監(jiān)視。
5、bitmap
什么是bitmap?
所謂bitmap,其實(shí)就是byte數(shù)組,用二進(jìn)制表示,值只有1和0
bitmap最典型的應(yīng)用,我個(gè)人覺得應(yīng)該就是布隆過濾器了,實(shí)際應(yīng)用中,大概是我接觸到的項(xiàng)目比較少,基本用不到bitmap。
從網(wǎng)上說的案例來講,最常用的就是存儲用戶在線狀態(tài),記錄用戶簽到記錄等操作。
比如建立一個(gè)極長的bitmap,以用戶的ID當(dāng)下標(biāo),如果用戶簽到了,就在將此下標(biāo)中的值更新為1,這么做的優(yōu)勢就是,可以極大地節(jié)省空間,并且時(shí)間復(fù)雜度為O(1),速度很快。
6、hyperloglogs
HyperLogLog 是用來做基數(shù)統(tǒng)計(jì)的算法,HyperLogLog 的優(yōu)點(diǎn)是,在輸入元素的數(shù)量或者體積非常非常大時(shí),計(jì)算基數(shù)所需的空間總是固定 的、并且是很小的。
在 Redis 里面,每個(gè) HyperLogLog 鍵只需要花費(fèi) 12 KB 內(nèi)存,就可以計(jì)算接近 2^64 個(gè)不同元素的基數(shù)。這和計(jì)算基數(shù)時(shí),元素越多耗費(fèi)內(nèi)存就越多的集合形成鮮明對比。
但是,因?yàn)?HyperLogLog 只會根據(jù)輸入元素來計(jì)算基數(shù),而不會儲存輸入元素本身,所以 HyperLogLog 不能像集合那樣,返回輸入的各個(gè)元素。
所謂用戶基數(shù)就是指不重復(fù)的數(shù)據(jù),比如數(shù)據(jù)集 {1, 3, 5, 7, 5, 7, 8}, 那么這個(gè)數(shù)據(jù)集的基數(shù)集為 {1, 3, 5 ,7, 8}, 基數(shù)(不重復(fù)元素)為5。 基數(shù)估計(jì)就是在誤差可接受的范圍內(nèi),快速計(jì)算基數(shù)。
7、過期策略
Redis可以使用expire命令來為key設(shè)置過期時(shí)間,設(shè)置的時(shí)間過期后,key會被自動(dòng)刪除。
過期時(shí)間只能使用刪除key或者覆蓋key的命令來清楚,包括del,set,getset和所有*store命令。
對于修改key中的值而不是覆蓋的操作,不會修改過期時(shí)間,比如想list的新增命令lpush,或者修改hash的值hset,這些都不會修改過期時(shí)間。
redis也提供了另一個(gè)命令persist,可以把key改回持久的,以清除過期時(shí)間。
注意:使用rename修改key的名字,新的key會繼承舊key的屬性,包括過期時(shí)間。
知道了redis有過期時(shí)間的概念,并且可以自動(dòng)刪除過期的key,那么,很容易就能想到一個(gè)新的問題。
redis是怎么刪除過期的key的?
redis有兩種過期策略:定期刪除和惰性刪除
先說惰性刪除。
很簡單,當(dāng)客戶端獲取某個(gè)key的時(shí)候,redis會檢查它是否設(shè)置了過期時(shí)間以及是否過期,如果key已經(jīng)過期,則將其刪除。
定期刪除就是,redis會每秒執(zhí)行10次下面的操作
1、從帶有過期時(shí)間的key里隨機(jī)挑選出20個(gè)進(jìn)行檢查
2、刪除所有過期的key
3、如果被刪除的key超過了25%,則立即繼續(xù)執(zhí)行1,直到被刪除的key低于25%為止
這是一個(gè)狹義的概率算法,假設(shè)我們選出的key代表了整個(gè)存儲空間,這個(gè)算法,可以保證過期的key一直低于25%。
但是,這樣一來,還有個(gè)問題。
假如經(jīng)過了兩種刪除策略之后,內(nèi)存依然不足的話,會發(fā)生什么情況?
答案是:內(nèi)存淘汰機(jī)制
所謂內(nèi)存淘汰機(jī)制,就是當(dāng)redis內(nèi)存不足的時(shí)候,會從key中刪除一些數(shù)據(jù),騰出內(nèi)存空間寫入新的數(shù)據(jù)。
而redis的內(nèi)存淘汰機(jī)制,有以下8種
- volatile-lru:從已設(shè)置過期時(shí)間的數(shù)據(jù)集中,移除最久沒有使用的key
- volatile-lfu:從已設(shè)置過期時(shí)間的數(shù)據(jù)集中,移除使用頻率最少的key
- volatile-ttl:從已設(shè)置過期時(shí)間的數(shù)據(jù)集中,移除將要過期的key
- volatile-random:從已設(shè)置過期時(shí)間的數(shù)據(jù)中,隨機(jī)移除某個(gè)key
- allkeys-lru:當(dāng)內(nèi)存不足寫入新數(shù)據(jù)時(shí),從所有數(shù)據(jù)中,移除最久沒有使用的Key
- allkeys-lfu:當(dāng)內(nèi)存不足寫入新數(shù)據(jù)時(shí),從所有數(shù)據(jù)中,移除使用頻率最少的Key
- allkeys-random:當(dāng)內(nèi)存不足寫入新數(shù)據(jù)時(shí),從所有數(shù)據(jù)中,隨機(jī)移除某個(gè)key
- no-eviction:當(dāng)內(nèi)存不足寫入新數(shù)據(jù)時(shí),寫入操作會報(bào)錯(cuò),同時(shí)不刪除數(shù)據(jù)【默認(rèn)】
注意:volatile-lfu和allkeys-lfu,是redis4.0才引入的策略,3.x版本是沒有的。
8、管道(Pipelining)
Redis是一種基于客戶端-服務(wù)端模型及請求/相應(yīng)協(xié)議的TCP服務(wù)。
也就是說,通常情況下,一個(gè)請求,會遵循以下步驟:
- 客戶端向服務(wù)端發(fā)送一個(gè)查詢請求,并監(jiān)聽Socket返回,通常是以阻塞模式等待服務(wù)器相應(yīng)。
- 服務(wù)端處理命令,并將結(jié)果返回客戶端。
也就是說,每個(gè)命令的執(zhí)行時(shí)間=客戶端發(fā)送時(shí)間+服務(wù)端處理時(shí)間+服務(wù)端返回時(shí)間+一個(gè)網(wǎng)絡(luò)的來回時(shí)間。其中,一個(gè)網(wǎng)絡(luò)的來回時(shí)間是不確定的,它的決定因素很多,比如客戶端到服務(wù)端需要多少跳,網(wǎng)絡(luò)是否擁堵等。這個(gè)時(shí)間的量級是最大的,也就是說,一個(gè)命令的執(zhí)行時(shí)間,很大因素上要取決于網(wǎng)絡(luò)開銷。
舉例來說,假如服務(wù)端每秒可以處理1萬條數(shù)據(jù),網(wǎng)絡(luò)開銷是100毫秒,那么實(shí)際上每秒鐘只能處理10個(gè)請求,最簡單的辦法,是把服務(wù)端和客戶端都放在同一服務(wù)器上,這樣可以把網(wǎng)絡(luò)開銷降低到1ms以下,但生產(chǎn)環(huán)境中,我們幾乎不會這樣來使用。
為了解決這一問題,Redis提供了管道技術(shù)。
其實(shí)不光是Redis,管道技術(shù)已經(jīng)非常成熟并且得到廣泛應(yīng)用了,例如POP3協(xié)議由于支持管道技術(shù),從而顯著提高了從服務(wù)器下載郵件的速度。
在Redis中,如果客戶端使用管道發(fā)送了多條命令,那么服務(wù)器會將多條命令放入一個(gè)隊(duì)列中,這一操作會占用一些內(nèi)存,所以管道中的命令不是越多越好,而是要有一個(gè)合理的值,根據(jù)自己的服務(wù)器內(nèi)存來分配的合理的值。
說到這里,其實(shí)可以看出來了,所謂管道技術(shù),就是將數(shù)據(jù)打包處理,通過降低網(wǎng)絡(luò)開銷的方式來減少處理時(shí)間,以提升效率。
其實(shí)Redis還提供了另一種方式來提高效率,即腳本(Scripting),腳本的性能還要優(yōu)于管道,只不過要使用lua來編寫腳本,有些類似于存儲過程,有需要的可以根據(jù)自己的需要來使用。