一篇文章瞭解Redis資料庫
文章原創於公眾號:程式猿周先森。本平臺不定時更新,喜歡我的文章,歡迎關注我的微信公眾號。
redis是一個key-value儲存系統。它支援儲存的value型別相對更多,包括string(字串)、list(連結串列)、set(集合)、zset(sorted set --有序集合)和hash(雜湊型別)。這些資料型別都支援push/pop、add/remove及取交集並集和差集及更豐富的操作,而且這些操作都是原子性的。在此基礎上,redis支援各種不同方式的排序。為了保證效率,資料都是快取在記憶體中。區別的是redis會週期性的把更新的資料寫入磁碟或者把修改操作寫入追加的記錄檔案,並且在此基礎上實現了主從同步。簡單來說 Redis 就是一個數據庫,不過與傳統資料庫不同的是 Redis 的資料是存在記憶體中的,所以存寫速度非常快,因此 Redis 被廣泛應用於快取方向。Redis 也經常用來做分散式鎖。Redis 提供了多種資料型別來支援不同的業務場景。除此之外,Redis 支援事務 、持久化、LUA 指令碼、LRU 驅動事件、多種叢集方案。
本篇文章將從下列幾個方向講解 Redis:
為什麼要用 Redis實現快取?
為什麼要用 Redis 而不用 map/guava 做快取
Redis 和 Memcached 的區別
Redis 常見資料結構以及使用場景分析
Redis 設定過期時間
Redis 記憶體淘汰機制
Redis 持久化機制
Redis 事務
快取雪崩和快取穿透問題解決方案
如何解決 Redis 的併發競爭 Key 問題
如何保證快取與資料庫資料一致性
為什麼要用 Redis 做快取?
第一個問題先丟擲來,既然選擇使用Redis作快取,其實主要從“高效能”和“高併發”來進行理解。
高效能
使用者首次訪問資料,從資料庫讀取,效率較低
將使用者訪問資料儲存快取中,二次讀取則可以直接從快取中讀取,效率更高
資料庫若資料發生改變,則同步更新快取中資料即可。
因為從資料庫讀取資料,是從硬碟中讀取資料,所以效率較低。如果將資料存入快取中,二次讀取從快取讀取,從快取讀取資料是直接操作記憶體,所以效率非常之高。
高併發
直接操作快取能夠承受的請求是遠遠大於直接訪問資料庫的,所以我們可以考慮把資料庫中的部分資料轉移到快取中去,這樣使用者的一部分請求可以不用操作資料庫,提高高併發能力。
為什麼要用 Redis 而不用 map/guava 做快取
快取分為本地快取和分散式快取。以 Java 為例,使用自帶的 map /guava 實現的是本地快取,最主要的特點是輕量以及快速,生命週期隨著 JVM 的銷燬而結束。而且在多例項狀態下快取不具有唯一性。使用 Redis 作快取稱為分散式快取,在多例項狀態下共用一份快取資料,快取具有一致性。
Redis 和 Memcached 的區別
Redis支援常見資料型別:Redis 不僅僅支援簡單的 key/value 型別的資料,同時還提供string(字串)、list(連結串列)、set(集合)、zset(sorted set --有序集合)和hash(雜湊型別)等資料結構的儲存。而Memcache 只支援簡單的資料型別 String。
Redis 支援資料的持久化,可以將記憶體中的資料保持在磁碟中,重啟的時候可以再次載入進行使用,而 Memecache 把資料全部存在記憶體之中。
叢集模式:Memcached 沒有原生的叢集模式,需要依靠客戶端來實現往叢集中分片寫入資料;但是 Redis 目前是原生支援 Cluster 模式的。
Memcached 是多執行緒,非阻塞 IO 複用的網路模型;Redis 使用單執行緒的多路 IO 複用模型。
貼一張對比圖可能看起來更加明顯:
Redis 常見資料結構以及使用場景分析
String
- 常用命令:set、get、decr、incr、mget 等。
String 資料結構是簡單的 Key-Value 型別,Value 可以是string或者數字。常規 Key-Value 快取應用;常規計數:部落格數,閱讀數等。
Hash
- 常用命令:hget、hset、hgetall 等。
Hash 特別適合用於儲存物件。
List
- 常用命令:lpush、rpush、lpop、rpop、lrange 等。
連結串列是 Redis 最重要的資料結構之一,Redis List 為一個雙向連結串列,支援反向查詢和遍歷,更方便操作,不過帶來了額外的記憶體開銷。
Set
- 常用命令:sadd、spop、smembers、sunion 等。
Set 其實和List都是列表的選項,Set 是可以自動去重的。當需要儲存一個不出現重複資料的列表資料,Set 是一個最好的選擇。你可以基於 Set 輕易實現交集、並集、差集的操作。
Sorted Set
- 常用命令:zadd、zrange、zrem、zcard 等。
Sorted Set 相比Set增加了一個權重引數 Score,使得集合中的元素能夠按 Score 進行有序排列。
Redis 設定過期時間
Redis可以對儲存在快取中的資料設定過期時間。作為一個快取資料庫,這是非常實用的功能。之前寫過一篇前後端互動的文章講過,Token 或者一些登入資訊,尤其是簡訊驗證碼都是有時間限制的,按照傳統的資料庫處理方式,一般都是自己判斷過期,這樣無疑會嚴重影響專案效能。而有一個好的方案其實就是將這些驗證資訊存入Redis設定過期時間,如果設定了存活時間30分鐘,那麼半小時之後這些資料就會從Redis中進行刪除。那說到刪除,Redis是如果做到對這些資料進行刪除的呢:
定期刪除:Redis 預設是每隔 100ms 就隨機抽取一些設定了過期時間的 Key,檢查其是否過期,如果過期就刪除。為什麼是隨機抽取而不是檢查所有key?因為你如果設定的key成千上萬,每100毫秒都將所有存在的key檢查一遍,會給CPU帶來比較大的壓力。
惰性刪除 :定期刪除可能會導致很多過期 Key 到了時間並沒有被刪除掉。使用者在獲取key的時候,redis會檢查一下,這個key如果設定過期時間那麼是否過期了,如果過期就刪除這個key。
但是隻是使用定期刪除 + 惰性刪除的刪除機制還是存在一個致命問題:如果定期刪除漏掉了很多過期 Key,而且使用者長時間也沒有使用到這些過期key,就會導致這些過期key堆積在記憶體裡,導致Redis記憶體塊被消耗殆盡。所以有了Redis記憶體淘汰機制的誕生。
Redis 記憶體淘汰機制
Redis 提供 6 種資料淘汰策略:
volatile-lru:從已設定過期時間的資料集中挑選最近最少使用的資料淘汰。
volatile-ttl:從已設定過期時間的資料集中挑選將要過期的資料淘汰。
volatile-random:從已設定過期時間的資料集中任意選擇資料淘汰。
allkeys-lru:當記憶體不足以容納新寫入資料時移除最近最少使用的key。
allkeys-random:從資料集中任意選擇資料淘汰。
no-enviction:當記憶體不足以容納新寫入資料時,新寫入操作會報錯。
Redis 持久化機制
怎麼保證 Redis 宕機之後再重啟Redis後資料可以進行恢復?很多時候我們需要持久化資料也就是將記憶體中的資料寫入到硬盤裡面。Redis持久化支援兩種不同的持久化操作。接下來,我們來簡單聊聊Redis的兩種持久化機制RDB和AOF。
快照持久化(RDB)
RDB持久化是指在指定的時間間隔內將記憶體中的資料集快照寫入磁碟,實際操作過程是fork一個子程序,先將資料集寫入臨時檔案,寫入成功後,再替換之前的檔案,用二進位制壓縮儲存。RDB是Redis預設的持久化方式,會在對應的目錄下生產一個dump.rdb檔案,重啟會通過載入dump.rdb檔案恢復資料。
優點:
只有一個檔案dump.rdb,方便持久化;
容災性好,一個檔案可以儲存到安全的磁碟;
效能最大化,fork子程序來完成寫操作,讓主程序繼續處理命令,所以是IO最大化(使用單獨子程序來進行持久化,主程序不會進行任何IO操作,保證了redis的高效能) ;
如果資料集偏大,RDB的啟動效率會比AOF更高。
缺點:
資料安全性低。
如果當資料集較大時,可能會導致整個伺服器停止服務幾百毫秒,甚至是1秒鐘。
快照持久化是 Redis 預設採用的持久化方式,在 redis.conf 配置檔案中已經進行配置:
save 900 1:在15分鐘內,如果至少有1個key發生變化,Redis就會自動觸發BGSAVE命令建立快照。
save 300 10:在5分鐘內,如果至少有10個key發生變化,Redis就會自動觸發BGSAVE命令建立快照。
save 60 10000:在1分鐘之後,如果至少有10000個key發生變化,Redis就會自動觸發BGSAVE命令建立快照。
AOF持久化
AOF持久化是以日誌的形式記錄伺服器所處理的每一個寫、刪除操作,查詢操作不會記錄,以文字的方式記錄,檔案中可以看到詳細的操作記錄。她的出現是為了彌補RDB的不足(資料的不一致性),所以它採用日誌的形式來記錄每個寫操作,並追加到檔案中。Redis 重啟的會根據日誌檔案的內容將寫指令從前到後執行一次以完成資料的恢復工作。與快照持久化相比,AOF 持久化的實時性更好,因此已成為主流的持久化方案。 預設情況下 Redis 沒有開啟 AOF持久化,可以通過設定 appendonly 引數開啟:
- appendonly yes
開啟 AOF 持久化後每執行一條會更改 Redis 中的資料的命令,Redis 就會將該命令寫入硬碟中的 AOF 檔案。AOF 檔案的儲存位置和 RDB 檔案的位置相同,都是通過 dir 引數設定的,預設的檔名是 appendonly.aof。
在 Redis 的配置檔案中存在三種不同的 AOF 持久化方式,它們分別是:
appendfsync always:每次有資料修改發生時都會寫入AOF檔案
appendfsync everysec:每秒鐘同步一次,將多個寫命令同步到硬碟
appendfsync no:讓作業系統決定何時進行同步
使用者可以使用appendfsync everysec選項 ,讓 Redis 每秒同步一次 AOF 檔案,這樣Redis效能幾乎不會受到影響,而且這樣即使出現宕機,使用者最多隻會丟失一秒之內產生的資料。當硬碟忙於執行寫入操作的時候,Redis 還會優雅的放慢自己的速度以便適應硬碟的最大寫入速度。
優點:
資料安全性更高,AOF持久化可以配置appendfsync屬性
通過append模式寫檔案,即使中途伺服器宕機,可以通過redis-check-aof工具解決資料一致性問題。
AOF機制的rewrite模式。
缺點:
AOF檔案比RDB檔案大,且恢復速度慢;資料集大的時候,比rdb啟動效率低。
根據同步策略的不同,AOF在執行效率上往往會慢於RDB。
Redis 4.0 對於持久化機制的優化
Redis 4.0支援 RDB 和 AOF 的混合持久化,不過預設是關閉狀態。
開啟混合持久化,AOF 重寫的時候就直接把 RDB 的內容寫到 AOF 檔案開頭。
AOF 裡面的 RDB 部分是壓縮格式不再是 AOF 格式,可讀性較差。
Redis 事務
- 命令:MULTI、EXEC、WATCH等。
事務提供了一種按順序地執行多個命令的機制。並且在事務執行期間,伺服器會將事務中的所有命令都執行完畢,然後才去處理其他客戶端的命令請求。事務總是具有原子性、一致性和隔離性,並且當 Redis 執行在某種特定的持久化模式下時,事務也具有永續性。
快取雪崩
快取處理過程:接收到請求請求,先從快取中取資料,取到直接返回結果,取不到時從資料庫中取,資料庫取到更新快取,並返回結果,資料庫也沒取到,那直接返回空結果。
快取雪崩:快取雪崩是指快取中資料大批量到過期時間,而查詢資料量巨大,引起資料庫壓力過大甚至down機。
解決辦法:
快取資料的過期時間設定隨機,防止同一時間大量資料過期現象發生。
如果快取資料庫是分散式部署,將熱點資料均勻分佈在不同搞得快取資料庫中。
設定熱點資料永遠不過期。
快取穿透
簡介:快取穿透是指快取和資料庫中都沒有的資料,而使用者不斷髮起請求,如發起為id為“-1”的資料或id為特別大不存在的資料。這時的使用者很可能是攻擊者,攻擊會導致資料庫壓力過大。
解決辦法:
介面層增加校驗,如使用者鑑權校驗,id做基礎校驗,id<=0的直接攔截;
從快取取不到的資料,在資料庫中也沒有取到,這時也可以將key-value對寫為key-null,快取有效時間可以設定30秒
快取擊穿
簡介: 快取擊穿是指快取中沒有但資料庫中有的資料,這時由於併發使用者特別多,同時讀快取沒讀到資料,又同時去資料庫去取資料,引起資料庫壓力瞬間增大
解決方法:
設定熱點資料永遠不過期。
加互斥鎖
解決 Redis 併發競爭 Key 問題
問題描述:多客戶端同時併發寫一個key,可能本來應該先到的資料後到了,導致資料版本錯了。或者是多客戶端同時獲取一個key,修改值之後再寫回去,只要順序錯了,資料就錯了。一個key的值是1,本來按順序修改為2,3,4,最後是4,但是順序變成了4,3,2,最後變成了2.
我個人認為比較好的方案是分散式鎖+時間戳:
1.整體技術方案
這種情況,主要是準備一個分散式鎖,大家去搶鎖,加鎖的目的實際上就是把並行讀寫改成序列讀寫的方式,從而來避免資源競爭。利用SETNX非常簡單地實現分散式鎖。
2.時間戳
由於key的操作需要順序執行,所以需要儲存一個時間戳判斷順序。假設系統B先搶到鎖,將key1設定為{ValueB 7:05}。接下來系統A搶到鎖,發現自己的key1的時間戳早於快取中的時間戳(7:00<=7:05),那就不做set操作了。
3.什麼是分散式鎖
分散式鎖可以基於很多種方式實現,比如zookeeper、redis等,不管哪種方式實現,基本原理是不變的:用一個狀態值表示鎖,對鎖的佔用和釋放通過狀態值來標識。
保證快取與資料庫雙寫時的資料一致性
可能對大部分來說最先想到的方案就是讀請求和寫請求序列化,串到一個記憶體佇列裡去。但是這個方案有著特別大的缺點:它也會導致系統的吞吐量大幅度降低,用比正常情況下多幾倍的機器去支撐線上的一個請求。
最經典的快取+資料庫讀寫的模式。
讀的時候,先讀快取,快取沒有的話,就讀資料庫,然後取出資料後放入快取,同時返回響應。
更新的時候,先更新資料庫,然後再刪除快取。
如果喜歡我的文章,歡迎關注我的個人公眾號:程式猿周先森。
****