redis 總結——重構版
主要內容
redis 簡介
為什麼要用 redis /為什麼要用快取
為什麼要用 redis 而不用 map/guava 做快取?
redis 和 memcached 的區別
redis 常見資料結構以及使用場景分析
redis 設定過期時間
redis 記憶體淘汰機制
redis 持久化機制(怎麼保證 redis 掛掉之後再重啟資料可以進行恢復)
redis 事務
快取雪崩和快取穿透問題解決方案
如何解決 Redis 的併發競爭 Key 問題
如何保證快取與資料庫雙寫時的資料一致性?
redis 簡介
簡單來說 redis 就是一個數據庫,不過與傳統資料庫不同的是 redis 的資料是存在記憶體中的,所以存寫速度非常快,因此 redis 被廣泛應用於快取方向。另外,redis 也經常用來做分散式鎖。redis 提供了多種資料型別來支援不同的業務場景。除此之外,redis 支援事務 、持久化、LUA指令碼、LRU驅動事件、多種叢集方案。
為什麼要用 redis /為什麼要用快取
主要從“高效能”和“高併發”這兩點來看待這個問題。
高效能:
假如使用者第一次訪問資料庫中的某些資料。這個過程會比較慢,因為是從硬碟上讀取的。將該使用者訪問的資料存在數快取中,這樣下一次再訪問這些資料的時候就可以直接從快取中獲取了。操作快取就是直接操作記憶體,所以速度相當快。如果資料庫中的對應資料改變的之後,同步改變快取中相應的資料即可!

高併發:
直接操作快取能夠承受的請求是遠遠大於直接訪問資料庫的,所以我們可以考慮把資料庫中的部分資料轉移到快取中去,這樣使用者的一部分請求會直接到快取這裡而不用經過資料庫。

為什麼要用 redis 而不用 map/guava 做快取?
快取分為本地快取和分散式快取。以 Java 為例,使用自帶的 map 或者 guava 實現的是本地快取,最主要的特點是輕量以及快速,生命週期隨著 jvm 的銷燬而結束,並且在多例項的情況下,每個例項都需要各自儲存一份快取,快取不具有一致性。
使用 redis 或 memcached 之類的稱為分散式快取,在多例項的情況下,各例項共用一份快取資料,快取具有一致性。缺點是需要保持 redis 或 memcached服務的高可用,整個程式架構上較為複雜。
redis 和 memcached 的區別
對於 redis 和 memcached 我總結了下面四點。現在公司一般都是用 redis 來實現快取,而且 redis 自身也越來越強大了!
redis支援更豐富的資料型別(支援更復雜的應用場景) :Redis不僅僅支援簡單的k/v型別的資料,同時還提供list,set,zset,hash等資料結構的儲存。memcache支援簡單的資料型別,String。
Redis支援資料的持久化,可以將記憶體中的資料保持在磁碟中,重啟的時候可以再次載入進行使用,而Memecache把資料全部存在記憶體之中。
叢集模式 :memcached沒有原生的叢集模式,需要依靠客戶端來實現往叢集中分片寫入資料;但是 redis 目前是原生支援 cluster 模式的.
Memcached是多執行緒,非阻塞IO複用的網路模型;Redis使用單執行緒的多路 IO 複用模型。
來自網路上的一張圖,這裡分享給大家!

redis 常見資料結構以及使用場景分析
1. String
常用命令: set,get,decr,incr,mget 等。
String資料結構是簡單的key-value型別,value其實不僅可以是String,也可以是數字。 常規key-value快取應用; 常規計數:微博數,粉絲數等。
2.Hash
常用命令: hget,hset,hgetall 等。
Hash 是一個 string 型別的 field 和 value 的對映表,hash 特別適合用於儲存物件,後續操作的時候,你可以直接僅僅修改這個物件中的某個欄位的值。 比如我們可以Hash資料結構來儲存使用者資訊,商品資訊等等。比如下面我就用 hash 型別存放了我本人的一些資訊:
key=JavaUser293847
value={
“id”: 1,
“name”: “SnailClimb”,
“age”: 22,
“location”: “Wuhan, Hubei”
}
3.List
常用命令: lpush,rpush,lpop,rpop,lrange等
list 就是連結串列,Redis list 的應用場景非常多,也是Redis最重要的資料結構之一,比如微博的關注列表,粉絲列表,訊息列表等功能都可以用Redis的 list 結構來實現。
Redis list 的實現為一個雙向連結串列,即可以支援反向查詢和遍歷,更方便操作,不過帶來了部分額外的記憶體開銷。
另外可以通過 lrange 命令,就是從某個元素開始讀取多少個元素,可以基於 list 實現分頁查詢,這個很棒的一個功能,基於 redis 實現簡單的高效能分頁,可以做類似微博那種下拉不斷分頁的東西(一頁一頁的往下走),效能高。
4.Set
常用命令: sadd,spop,smembers,sunion 等
set 對外提供的功能與list類似是一個列表的功能,特殊之處在於 set 是可以自動排重的。
當你需要儲存一個列表資料,又不希望出現重複資料時,set是一個很好的選擇,並且set提供了判斷某個成員是否在一個set集合內的重要介面,這個也是list所不能提供的。可以基於 set 輕易實現交集、並集、差集的操作。
比如:在微博應用中,可以將一個使用者所有的關注人存在一個集合中,將其所有粉絲存在一個集合。Redis可以非常方便的實現如共同關注、共同粉絲、共同喜好等功能。這個過程也就是求交集的過程,具體命令如下:
sinterstore key1 key2 key3 將交集存在key1內
5.Sorted Set
常用命令: zadd,zrange,zrem,zcard等
和set相比,sorted set增加了一個權重引數score,使得集合中的元素能夠按score進行有序排列。
舉例: 在直播系統中,實時排行資訊包含直播間線上使用者列表,各種禮物排行榜,彈幕訊息(可以理解為按訊息維度的訊息排行榜)等資訊,適合使用 Redis 中的 SortedSet 結構進行儲存。
redis 設定過期時間
Redis中有個設定時間過期的功能,即對儲存在 redis 資料庫中的值可以設定一個過期時間。作為一個快取資料庫,這是非常實用的。如我們一般專案中的 token 或者一些登入資訊,尤其是簡訊驗證碼都是有時間限制的,按照傳統的資料庫處理方式,一般都是自己判斷過期,這樣無疑會嚴重影響專案效能。
我們 set key 的時候,都可以給一個 expire time,就是過期時間,通過過期時間我們可以指定這個 key 可以存活的時間。
如果假設你設定了一批 key 只能存活1個小時,那麼接下來1小時後,redis是怎麼對這批key進行刪除的?
定期刪除+惰性刪除。
通過名字大概就能猜出這兩個刪除方式的意思了。
定期刪除 :redis預設是每隔 100ms 就 隨機抽取 一些設定了過期時間的key,檢查其是否過期,如果過期就刪除。注意這裡是隨機抽取的。為什麼要隨機呢?你想一想假如 redis 存了幾十萬個 key ,每隔100ms就遍歷所有的設定過期時間的 key 的話,就會給 CPU 帶來很大的負載!
惰性刪除 :定期刪除可能會導致很多過期 key 到了時間並沒有被刪除掉。所以就有了惰性刪除。假如你的過期 key,靠定期刪除沒有被刪除掉,還停留在記憶體裡,除非你的系統去查一下那個 key,才會被redis給刪除掉。這就是所謂的惰性刪除,也是夠懶的哈!
但是僅僅通過設定過期時間還是有問題的。我們想一下:如果定期刪除漏掉了很多過期 key,然後你也沒及時去查,也就沒走惰性刪除,此時會怎麼樣?如果大量過期key堆積在記憶體裡,導致redis記憶體塊耗盡了。怎麼解決這個問題呢?
redis 記憶體淘汰機制。
redis 記憶體淘汰機制(SQL/">MySQL裡有2000w資料,Redis中只存20w的資料,如何保證Redis中的資料都是熱點資料?)
redis 配置檔案 redis.conf 中有相關注釋,我這裡就不貼了,大家可以自行查閱或者通過這個網址檢視: http://download.redis.io/redis-stable/redis.conf
redis 提供 6種資料淘汰策略:
volatile-lru :從已設定過期時間的資料集(server.db[i].expires)中挑選最近最少使用的資料淘汰
volatile-ttl :從已設定過期時間的資料集(server.db[i].expires)中挑選將要過期的資料淘汰
volatile-random :從已設定過期時間的資料集(server.db[i].expires)中任意選擇資料淘汰
allkeys-lru :當記憶體不足以容納新寫入資料時,在鍵空間中,移除最近最少使用的key(這個是最常用的).
allkeys-random :從資料集(server.db[i].dict)中任意選擇資料淘汰
no-enviction :禁止驅逐資料,也就是說當記憶體不足以容納新寫入資料時,新寫入操作會報錯。這個應該沒人使用吧!
備註: 關於 redis 設定過期時間以及記憶體淘汰機制,我這裡只是簡單的總結一下,後面會專門寫一篇文章來總結!
redis 持久化機制(怎麼保證 redis 掛掉之後再重啟資料可以進行恢復)
很多時候我們需要持久化資料也就是將記憶體中的資料寫入到硬盤裡面,大部分原因是為了之後重用資料(比如重啟機器、機器故障之後回覆資料),或者是為了防止系統故障而將資料備份到一個遠端位置。
Redis不同於Memcached的很重一點就是,Redis支援持久化,而且支援兩種不同的持久化操作。 Redis的一種持久化方式叫快照(snapshotting,RDB),另一種方式是隻追加檔案(append-only file,AOF) .這兩種方法各有千秋,下面我會詳細這兩種持久化方法是什麼,怎麼用,如何選擇適合自己的持久化方法。
快照(snapshotting)持久化(RDB)
Redis可以通過建立快照來獲得儲存在記憶體裡面的資料在某個時間點上的副本。Redis建立快照之後,可以對快照進行備份,可以將快照複製到其他伺服器從而建立具有相同資料的伺服器副本(Redis主從結構,主要用來提高Redis效能),還可以將快照留在原地以便重啟伺服器的時候使用。
快照持久化是Redis預設採用的持久化方式,在redis.conf配置檔案中預設有此下配置:
save 900 1 #在900秒(15分鐘)之後,如果至少有1個key發生變化,Redis就會自動觸發BGSAVE命令建立快照。
save 300 10 #在300秒(5分鐘)之後,如果至少有10個key發生變化,Redis就會自動觸發BGSAVE命令建立快照。
save 60 10000 #在60秒(1分鐘)之後,如果至少有10000個key發生變化,Redis就會自動觸發BGSAVE命令建立快照。
AOF(append-only file)持久化
與快照持久化相比,AOF持久化 的實時性更好,因此已成為主流的持久化方案。預設情況下Redis沒有開啟AOF(append only file)方式的持久化,可以通過appendonly引數開啟:
appendonly yes
開啟AOF持久化後每執行一條會更改Redis中的資料的命令,Redis就會將該命令寫入硬碟中的AOF檔案。AOF檔案的儲存位置和RDB檔案的位置相同,都是通過dir引數設定的,預設的檔名是appendonly.aof。
在Redis的配置檔案中存在三種不同的 AOF 持久化方式,它們分別是:
appendfsync always #每次有資料修改發生時都會寫入AOF檔案,這樣會嚴重降低Redis的速度
appendfsync everysec #每秒鐘同步一次,顯示地將多個寫命令同步到硬碟
appendfsync no #讓作業系統決定何時進行同步
為了兼顧資料和寫入效能,使用者可以考慮 appendfsync everysec選項 ,讓Redis每秒同步一次AOF檔案,Redis效能幾乎沒受到任何影響。而且這樣即使出現系統崩潰,使用者最多隻會丟失一秒之內產生的資料。當硬碟忙於執行寫入操作的時候,Redis還會優雅的放慢自己的速度以便適應硬碟的最大寫入速度。
Redis 4.0 對於持久化機制的優化
Redis 4.0 開始支援 RDB 和 AOF 的混合持久化(預設關閉,可以通過配置項aof-use-rdb-preamble開啟)。
如果把混合持久化開啟,AOF 重寫的時候就直接把 RDB 的內容寫到 AOF 檔案開頭。這樣做的好處是可以結合 RDB 和 AOF 的優點, 快速載入同時避免丟失過多的資料。當然缺點也是有的, AOF 裡面的 RDB 部分是壓縮格式不再是 AOF 格式,可讀性較差。
補充內容:AOF 重寫
AOF重寫可以產生一個新的AOF檔案,這個新的AOF檔案和原有的AOF檔案所儲存的資料庫狀態一樣,但體積更小。
AOF重寫是一個有歧義的名字,該功能是通過讀取資料庫中的鍵值對來實現的,程式無須對現有AOF檔案進行任伺讀入、分析或者寫入操作。
在執行 BGREWRITEAOF 命令時,Redis 伺服器會維護一個 AOF 重寫緩衝區,該緩衝區會在子程序建立新AOF檔案期間,記錄伺服器執行的所有寫命令。當子程序完成建立新AOF檔案的工作之後,伺服器會將重寫緩衝區中的所有內容追加到新AOF檔案的末尾,使得新舊兩個AOF檔案所儲存的資料庫狀態一致。最後,伺服器用新的AOF檔案替換舊的AOF檔案,以此來完成AOF檔案重寫操作
redis 事務
Redis 通過 MULTI、EXEC、WATCH 等命令來實現事務(transaction)功能。事務提供了一種將多個命令請求打包,然後一次性、按順序地執行多個命令的機制,並且在事務執行期間,伺服器不會中斷事務而改去執行其他客戶端的命令請求,它會將事務中的所有命令都執行完畢,然後才去處理其他客戶端的命令請求。
在傳統的關係式資料庫中,常常用 ACID 性質來檢驗事務功能的可靠性和安全性。在 Redis 中,事務總是具有原子性(Atomicity)、一致性(Consistency)和隔離性(Isolation),並且當 Redis 執行在某種特定的持久化模式下時,事務也具有永續性(Durability)。
快取雪崩和快取穿透問題解決方案
快取雪崩
簡介:快取同一時間大面積的失效,所以,後面的請求都會落到資料庫上,造成資料庫短時間內承受大量請求而崩掉。
解決辦法(中華石杉老師在他的視訊中提到過,視訊地址在最後一個問題中有提到):
事前:儘量保證整個 redis 叢集的高可用性,發現機器宕機儘快補上。選擇合適的記憶體淘汰策略。
事中:本地ehcache快取 + hystrix限流&降級,避免MySQL崩掉
事後:利用 redis 持久化機制儲存的資料儘快恢復快取

快取穿透
簡介:一般是黑客故意去請求快取中不存在的資料,導致所有的請求都落到資料庫上,造成資料庫短時間內承受大量請求而崩掉。
解決辦法: 有很多種方法可以有效地解決快取穿透問題,最常見的則是採用布隆過濾器,將所有可能存在的資料雜湊到一個足夠大的bitmap中,一個一定不存在的資料會被 這個bitmap攔截掉,從而避免了對底層儲存系統的查詢壓力。另外也有一個更為簡單粗暴的方法(我們採用的就是這種),如果一個查詢返回的資料為空(不管是數 據不存在,還是系統故障),我們仍然把這個空結果進行快取,但它的過期時間會很短,最長不超過五分鐘。
如何解決 Redis 的併發競爭 Key 問題
所謂 Redis 的併發競爭 Key 的問題也就是多個系統同時對一個 key 進行操作,但是最後執行的順序和我們期望的順序不同,這樣也就導致了結果的不同!
推薦一種方案:分散式鎖(zookeeper 和 redis 都可以實現分散式鎖)。(如果不存在 Redis 的併發競爭 Key 問題,不要使用分散式鎖,這樣會影響效能)
基於zookeeper臨時有序節點可以實現的分散式鎖。大致思想為:每個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節點的目錄下,生成一個唯一的瞬時有序節點。 判斷是否獲取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節點刪除即可。同時,其可以避免服務宕機導致的鎖無法釋放,而產生的死鎖問題。完成業務流程後,刪除對應的子節點釋放鎖。
在實踐中,當然是從以可靠性為主。所以首推Zookeeper。
如何保證快取與資料庫雙寫時的資料一致性?
你只要用快取,就可能會涉及到快取與資料庫雙儲存雙寫,你只要是雙寫,就一定會有資料一致性的問題,那麼你如何解決一致性問題?
一般來說,就是如果你的系統不是嚴格要求快取+資料庫必須一致性的話,快取可以稍微的跟資料庫偶爾有不一致的情況,最好不要做這個方案,讀請求和寫請求序列化,串到一個記憶體佇列裡去,這樣就可以保證一定不會出現不一致的情況
序列化之後,就會導致系統的吞吐量會大幅度的降低,用比正常情況下多幾倍的機器去支撐線上的一個請求。