redis 簡介
簡單來說 redis 就是一個數據庫,不過與傳統數據庫不同的是 redis 的數據是存在內存中的,所以讀寫速度非常快,因此 redis 被廣泛應用於緩存方向。另外,redis 也經常用來做分布式鎖。redis 提供了多種數據類型來支持不同的業務場景。除此之外,redis 支持事務 、持久化、LUA腳本、LRU驅動事件、多種集群方案。
為什么要用 redis?/為什么要用緩存?
主要從“高性能”和“高並發”這兩點來看待這個問題。
高性能:
假如用戶第一次訪問數據庫中的某些數據。這個過程會比較慢,因為是從硬盤上讀取的。將該用戶訪問的數據存在緩存中,這樣下一次再訪問這些數據的時候就可以直接從緩存中獲取了。操作緩存就是直接操作內存,所以速度相當快。如果數據庫中的對應數據改變的之后,同步改變緩存中相應的數據即可!
高並發:
直接操作緩存能夠承受的請求是遠遠大於直接訪問數據庫的,所以我們可以考慮把數據庫中的部分數據轉移到緩存中去,這樣用戶的一部分請求會直接到緩存這里而不用經過數據庫。
為什么要用 redis 而不用 map/guava 做緩存?
緩存分為本地緩存和分布式緩存。以 Java 為例,使用自帶的 map 或者 guava 實現的是本地緩存,最主要的特點是輕量以及快速,生命周期隨着 jvm 的銷毀而結束,並且在多實例的情況下,每個實例都需要各自保存一份緩存,緩存不具有一致性。
使用 redis 或 memcached 之類的稱為分布式緩存,在多實例的情況下,各實例共用一份緩存數據,緩存具有一致性。缺點是需要保持 redis 或 memcached服務的高可用,整個程序架構上較為復雜。
redis 的線程模型
redis 內部使用文件事件處理器 file event handler,這個文件事件處理器是單線程的,所以 redis 才叫做單線程的模型。它采用 IO 多路復用機制同時監聽多個 socket,根據 socket 上的事件來選擇對應的事件處理器進行處理。
文件事件處理器的結構包含 4 個部分:
- 多個 socket
- IO 多路復用程序
- 文件事件分派器
- 事件處理器(連接應答處理器、命令請求處理器、命令回復處理器)
多個 socket 可能會並發產生不同的操作,每個操作對應不同的文件事件,但是 IO 多路復用程序會監聽多個 socket,會將 socket 產生的事件放入隊列中排隊,事件分派器每次從隊列中取出一個事件,把該事件交給對應的事件處理器進行處理。
redis 和 memcached 的區別
對於 redis 和 memcached 我總結了下面四點。現在公司一般都是用 redis 來實現緩存,而且 redis 自身也越來越強大了!
1、redis支持更豐富的數據類型(支持更復雜的應用場景):Redis不僅僅支持簡單的k/v類型的數據,同時還提供list,set,zset,hash等數據結構的存儲。memcache支持簡單的數據類型,String。
2、Redis支持數據的持久化,可以將內存中的數據保持在磁盤中,重啟的時候可以再次加載進行使用,而Memecache把數據全部存在內存之中。
3、集群模式:memcached沒有原生的集群模式,需要依靠客戶端來實現往集群中分片寫入數據;但是 redis 目前是原生支持 cluster 模式的.
4、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 中的 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 內存淘汰機制。
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-eviction:禁止驅逐數據,也就是說當內存不足以容納新寫入數據時,新寫入操作會報錯。這個應該沒人使用吧!
4.0版本后增加以下兩種:
7. volatile-lfu:從已設置過期時間的數據集(server.db[i].expires)中挑選最不經常使用的數據淘汰
8. allkeys-lfu:當內存不足以容納新寫入數據時,在鍵空間中,移除最不經常使用的key
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同一個事務中如果有一條命令執行失敗,其后的命令仍然會被執行,沒有回滾。
緩存雪崩和緩存穿透問題解決方案
緩存雪崩
簡介:緩存同一時間大面積的失效,所以,后面的請求都會落到數據庫上,造成數據庫短時間內承受大量請求而崩掉。
解決辦法:
- 事前:盡量保證整個 redis 集群的高可用性,發現機器宕機盡快補上。選擇合適的內存淘汰策略。
- 事中:本地ehcache緩存 + hystrix限流&降級,避免MySQL崩掉
- 事后:利用 redis 持久化機制保存的數據盡快恢復緩存
緩存穿透
簡介:一般是黑客故意去請求緩存中不存在的數據,導致所有的請求都落到數據庫上,造成數據庫短時間內承受大量請求而崩掉。
解決辦法:
有很多種方法可以有效地解決緩存穿透問題,最常見的則是采用布隆過濾器,將所有可能存在的數據哈希到一個足夠大的bitmap中,一個一定不存在的數據會被 這個bitmap攔截掉,從而避免了對底層存儲系統的查詢壓力。另外也有一個更為簡單粗暴的方法(我們采用的就是這種),如果一個查詢返回的數據為空(不管是數 據不存在,還是系統故障),我們仍然把這個空結果進行緩存,但它的過期時間會很短,最長不超過五分鍾。
如何解決 Redis 的並發競爭 Key 問題
所謂 Redis 的並發競爭 Key 的問題也就是多個系統同時對一個 key 進行操作,但是最后執行的順序和我們期望的順序不同,這樣也就導致了結果的不同!
推薦一種方案:分布式鎖(zookeeper 和 redis 都可以實現分布式鎖)。(如果不存在 Redis 的並發競爭 Key 問題,不要使用分布式鎖,這樣會影響性能)
基於zookeeper臨時有序節點可以實現的分布式鎖。大致思想為:每個客戶端對某個方法加鎖時,在zookeeper上的與該方法對應的指定節點的目錄下,生成一個唯一的瞬時有序節點。 判斷是否獲取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節點刪除即可。同時,其可以避免服務宕機導致的鎖無法釋放,而產生的死鎖問題。完成業務流程后,刪除對應的子節點釋放鎖。
在實踐中,當然是從以可靠性為主。所以首推Zookeeper。
如何保證緩存與數據庫雙寫時的數據一致性?
你只要用緩存,就可能會涉及到緩存與數據庫雙存儲雙寫,你只要是雙寫,就一定會有數據一致性的問題,那么你如何解決一致性問題?
一般來說,就是如果你的系統不是嚴格要求緩存+數據庫必須一致性的話,緩存可以稍微的跟數據庫偶爾有不一致的情況,最好不要做這個方案,讀請求和寫請求串行化,串到一個內存隊列里去,這樣就可以保證一定不會出現不一致的情況
串行化之后,就會導致系統的吞吐量會大幅度的降低,用比正常情況下多幾倍的機器去支撐線上的一個請求。