簡單來說 redis 就是一個數據庫,不過與傳統數據庫不同的是 Redis 的數據是存在內存中的,所以存寫速度非常快,因此 Redis 被廣泛應用于緩存方向。
另外,Redis 也經常用來做分布式鎖。Redis 提供了多種數據類型來支持不同的業務場景。
除此之外,Redis 支持事務 、持久化、LUA 腳本、LRU 驅動事件、多種集群方案。
本文將從以下幾個方面全面解讀 Redis:
- 為什么要用 Redis / 為什么要用緩存
- 為什么要用 Redis 而不用 map/guava 做緩存
- Redis 和 Memcached 的區別
- Redis 常見數據結構以及使用場景分析
- Redis 設置過期時間
- Redis 內存淘汰機制
- Redis 持久化機制(怎么保證 Redis 掛掉之后再重啟數據可以進行恢復)
- Redis 事務
- 緩存雪崩和緩存穿透問題解決方案
- 如何解決 Redis 的并發競爭 Key 問題
- 如何保證緩存與數據庫雙寫時的數據一致性
為什么要用 Redis / 為什么要用緩存?
主要從“高性能”和“高并發”這兩點來看待這個問題。
高性能
假如用戶第一次訪問數據庫中的某些數據。這個過程會比較慢,因為是從硬盤上讀取的。
將該用戶訪問的數據存在緩存中,這樣下一次再訪問這些數據的時候就可以直接從緩存中獲取了。
操作緩存就是直接操作內存,所以速度相當快。如果數據庫中的對應數據改變了之后,同步改變緩存中相應的數據即可!
高并發
直接操作緩存能夠承受的請求是遠遠大于直接訪問數據庫的,所以我們可以考慮把數據庫中的部分數據轉移到緩存中去,這樣用戶的一部分請求會直接到緩存這里而不用經過數據庫。
為什么要用 Redis 而不用 map/guava 做緩存
緩存分為本地緩存和分布式緩存。以 JAVA 為例,使用自帶的 map 或者 guava 實現的是本地緩存,最主要的特點是輕量以及快速,生命周期隨著 JVM 的銷毀而結束。
并且在多實例的情況下,每個實例都需要各自保存一份緩存,緩存不具有一致性。
使用 Redis 或 Memcached 之類的稱為分布式緩存,在多實例的情況下,各實例共用一份緩存數據,緩存具有一致性。
缺點是需要保持 Redis 或 Memcached 服務的高可用,整個程序架構上較為復雜。
Redis 和 Memcached 的區別
現在公司一般都是用 Redis 來實現緩存,而且 Redis 自身也越來越強大了!
對于 Redis 和 Memcached 我總結了下面四點:
- Redis 支持更豐富的數據類型(支持更復雜的應用場景):Redis 不僅僅支持簡單的 K/V 類型的數據,同時還提供 list、set、zset、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 是一個 String 類型的 Field 和 Value 的映射表,Hash 特別適合用于存儲對象。
后續操作的時候,你可以直接僅僅修改這個對象中的某個字段的值。比如我們可以 Hash 數據結構來存儲用戶信息,商品信息等等。
比如下面我就用 Hash 類型存放了我本人的一些信息:
key=JavaUser293847
value={
“id”:1,
“name”:“SnailClimb”,
“age”:22,
“location”:“Wuhan,Hubei”
}
List
常用命令:lpush、rpush、lpop、rpop、lrange 等。
List 就是鏈表,Redis List 的應用場景非常多,也是 Redis 最重要的數據結構之一。
比如微博的關注列表,粉絲列表,消息列表等功能都可以用 Redis 的 List 結構來實現。
Redis List 的實現為一個雙向鏈表,即可以支持反向查找和遍歷,更方便操作,不過帶來了部分額外的內存開銷。
另外可以通過 lrange 命令,就是從某個元素開始讀取多少個元素,可以基于 List 實現分頁查詢。
這是很棒的一個功能,基于 Redis 實現簡單的高性能分頁,可以做類似微博那種下拉不斷分頁的東西(一頁一頁的往下走),性能高。
Set
常用命令:sadd、spop、smembers、sunion 等。
Set 對外提供的功能與 List 類似是一個列表的功能,特殊之處在于 Set 是可以自動排重的。
當你需要存儲一個列表數據,又不希望出現重復數據時,Set 是一個很好的選擇。
并且 Set 提供了判斷某個成員是否在一個 Set 集合內的重要接口,這個也是 List 所不能提供的。你可以基于 Set 輕易實現交集、并集、差集的操作。
比如:在微博應用中,可以將一個用戶所有的關注人存在一個集合中,將其所有粉絲存在一個集合。Redis 可以非常方便的實現如共同關注、共同粉絲、共同喜好等功能。
這個過程也就是求交集的過程,具體命令如下:
sinterstorekey1key2key3將交集存在key1內
Sorted Set
常用命令:zadd、zrange、zrem、zcard 等。
和 Set 相比,Sorted Set 增加了一個權重參數 Score,使得集合中的元素能夠按 Score 進行有序排列。
舉例:在直播系統中,實時排行信息包含直播間在線用戶列表,各種禮物排行榜,彈幕消息(可以理解為按消息維度的消息排行榜)等信息,適合使用 Redis 中的 Sorted Set 結構進行存儲。
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 內存淘汰機制
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 不同于 Memcached 的很重要一點就是,Redis 支持持久化,而且支持兩種不同的持久化操作。
Redis 的一種持久化方式叫快照(snapshotting,RDB),另一種方式是只追加文件(Append-only file,AOF)。
這兩種方法各有千秋,下面我會詳細講這兩種持久化方法是什么,怎么用,如何選擇適合自己的持久化方法。
快照(snapshotting)持久化(RDB)
Redis 可以通過創建快照來獲得存儲在內存里面的數據在某個時間點上的副本。
Redis 創建快照之后,可以對快照進行備份,可以將快照復制到其他服務器從而創建具有相同數據的服務器副本(Redis 主從結構,主要用來提高 Redis 性能),還可以將快照留在原地以便重啟服務器的時候使用。
快照持久化是 Redis 默認采用的持久化方式,在 redis.conf 配置文件中默認有此下配置:
save9001#在900秒(15分鐘)之后,如果至少有1個key發生變化,Redis就會自動觸發BGSAVE命令創建快照。
save30010#在300秒(5分鐘)之后,如果至少有10個key發生變化,Redis就會自動觸發BGSAVE命令創建快照。
save6010000#在60秒(1分鐘)之后,如果至少有10000個key發生變化,Redis就會自動觸發BGSAVE命令創建快照。
AOF(append-only file)持久化
與快照持久化相比,AOF 持久化的實時性更好,因此已成為主流的持久化方案。
默認情況下 Redis 沒有開啟 AOF(append only file)方式的持久化,可以通過 appendonly 參數開啟:
appendonlyyes
開啟 AOF 持久化后每執行一條會更改 Redis 中的數據的命令,Redis 就會將該命令寫入硬盤中的 AOF 文件。
AOF 文件的保存位置和 RDB 文件的位置相同,都是通過 dir 參數設置的,默認的文件名是 appendonly.aof。
在 Redis 的配置文件中存在三種不同的 AOF 持久化方式,它們分別是:
appendfsyncalways#每次有數據修改發生時都會寫入AOF文件,這樣會嚴重降低Redis的速度
appendfsynceverysec#每秒鐘同步一次,顯示地將多個寫命令同步到硬盤
appendfsyncno#讓操作系統決定何時進行同步
為了兼顧數據和寫入性能,用戶可以考慮 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。
如何保證緩存與數據庫雙寫時的數據一致性
你只要用緩存,就可能會涉及到緩存與數據庫雙存儲雙寫,你只要是雙寫,就一定會有數據一致性的問題,那么你如何解決一致性問題?
一般來說,就是如果你的系統不是嚴格要求緩存+數據庫必須一致性的話,緩存可以稍微的跟數據庫偶爾有不一致的情況。
最好不要做這個方案,讀請求和寫請求串行化,串到一個內存隊列里去,這樣就可以保證一定不會出現不一致的情況。
串行化之后,就會導致系統的吞吐量會大幅度的降低,用比正常情況下多幾倍的機器去支撐線上的一個請求。
參考文章:
- https://segmentfault.com/q/1010000009106416
- Redis設計與實現(第二版)
- https://www.jianshu.com/p/8bddd381de06
出處:51CTO技術棧