redis小結
1.redis的簡單介紹
redis是一種高性能的Key-Value類型的內存數據庫,之所以說性能非常好,是因為redis是存放在內存中的,存取都是不用進行io磁盤操作,所以效率十分高。
2.為什麽會使用redis這種數據結構
其一:就是性能好,可以節約大量的時間。其二:在高並發的情況下,如果所有的請求(一般查詢偏多)都直接請求數據庫,會導致數據庫連接異常。如果在這種情況下,先請求redis緩存,即可有效緩解這種問題。
3.redis可支持的數據類型及使用場景
redis除了性能和高並發還能支持多種數據類型,包括:list,set,hash,sorted set, hash共五中數據接口。
4.redis單線程效率為何那麽高
- 純內存操作,沒有IO磁盤操作,因此效率很高。
- 單線程操作,避免了頻繁的上下文切換,從一定角度來看也是提高了效率。
- 采用了非阻塞I/O多路復用機制
5.redis數據持久化
Redis 提供了多種不同級別的持久化方式:
- RDB 持久化可以在指定的時間間隔內生成數據集的時間點快照(point-in-time snapshot)。
- AOF 持久化記錄服務器執行的所有寫操作命令,並在服務器啟動時,通過重新執行這些命令來還原數據集。 AOF 文件中的命令全部以 Redis 協議的格式來保存,新命令會被追加到文件的末尾。 Redis 還可以在後臺對 AOF 文件進行重寫(rewrite),使得 AOF 文件的體積不會超出保存數據集狀態所需的實際大小。
- Redis 還可以同時使用 AOF 持久化和 RDB 持久化。 在這種情況下, 當 Redis 重啟時, 它會優先使用 AOF 文件來還原數據集, 因為 AOF 文件保存的數據集通常比 RDB 文件所保存的數據集更完整。
- 你甚至可以關閉持久化功能,讓數據只在服務器運行時存在。
了解 RDB 持久化和 AOF 持久化之間的異同是非常重要的, 以下幾個小節將詳細地介紹這這兩種持久化功能, 並對它們的相同和不同之處進行說明。
RDB:RDB 是一個非常緊湊(compact)的文件,它保存了 Redis 在某個時間點上的數據集。 這種文件非常適合用於進行備份。雖然 Redis 允許你設置不同的保存點(save point)來控制保存 RDB 文件的頻率, 但是, 因為RDB 文件需要保存整個數據集的狀態, 所以它並不是一個輕松的操作。 因此你可能會至少 5 分鐘才保存一次 RDB 文件。 在這種情況下, 一旦發生故障停機, 你就可能會丟失好幾分鐘的數據。所以如果不允許丟失數據的RDB不合適。
AOF:使用 AOF 持久化會讓 Redis 變得非常耐久。AOF 文件有序地保存了對數據庫執行的所有寫入操作。對於相同的數據集來說,AOF 文件的體積通常要大於 RDB 文件的體積。並且效率比RDB低。
究竟選擇哪一種方法:
如果你非常關心你的數據, 但仍然可以承受數分鐘以內的數據丟失, 那麽你可以只使用 RDB 持久化。
有很多用戶都只使用 AOF 持久化, 但我們並不推薦這種方式: 因為定時生成 RDB 快照(snapshot)非常便於進行數據庫備份, 並且 RDB 恢復數據集的速度也要比 AOF 恢復的速度要快, 除此之外, 使用 RDB 還可以避免之前提到的 AOF 程序的 bug 。
6.redis分布式鎖的實現
可靠性
首先,為了確保分布式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件:
- 互斥性。在任意時刻,只有一個客戶端能持有鎖。
- 不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他客戶端能加鎖。
- 具有容錯性。只要大部分的Redis節點正常運行,客戶端就可以加鎖和解鎖。
- 解鈴還須系鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了。
加鎖的簡單實現方法:每個客戶端或者說每一個對象對於同一個key都只能執行一次set,保證了加鎖的機制。等操作完成及時解鎖。
public class RedisTool { private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; /** * 嘗試獲取分布式鎖 * @param jedis Redis客戶端 * @param lockKey 鎖 * @param requestId 請求標識 * @param expireTime 超期時間 * @return 是否獲取成功 */ public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } }
解鎖的簡單實現 :根據key對應的value,進行刪除鎖。
public class RedisTool { private static final Long RELEASE_SUCCESS = 1L; /** * 釋放分布式鎖 * @param jedis Redis客戶端 * @param lockKey 鎖 * @param requestId 請求標識 * @return 是否釋放成功 */ public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { String script = "if redis.call(‘get‘, KEYS[1]) == ARGV[1] then return redis.call(‘del‘, KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; } }
應用場景:使用redis實現分布式鎖,達到分布式部署場景下用戶借款串行處理(防止產生多筆重復借款)。
7.redis的過期策略以及內存淘汰機制
redis采用的是定期刪除和惰性刪除,而還有一種叫定時刪除,
為什麽不用定時刪除策略?
定時刪除,用一個定時器來負責監視key,過期則自動刪除。雖然內存及時釋放,但是十分消耗CPU資源。在大並發請求下,CPU要將時間應用在處理請求,而不是刪除key,因此沒有采用這一策略.
定期刪除,redis默認每個100ms檢查,是否有過期的key,有過期key則刪除。需要說明的是,redis不是每個100ms將所有的key檢查一次,而是隨機抽取進行檢查(如果每隔100ms,全部key進行檢查,redis豈不是卡死)。因此,如果只采用定期刪除策略,會導致很多key到時間沒有刪除。
於是,惰性刪除派上用場。也就是說在你獲取某個key的時候,redis會檢查一下,這個key如果設置了過期時間那麽是否過期了?如果過期了此時就會刪除。
如果定期刪除沒刪除key。然後你也沒即時去請求key,也就是說惰性刪除也沒生效。這樣,redis的內存會越來越高。那麽就應該采用內存淘汰機制。
內存淘汰機制:
在redis.conf中有一行配置
# maxmemory-policy volatile-lru (最大內存策略)
一共6種方式:
1)noeviction:當內存不足以容納新寫入數據時,新寫入操作會報錯。應該沒人用吧。
2)allkeys-lru:當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的key。推薦使用,目前項目在用這種。
3)allkeys-random:當內存不足以容納新寫入數據時,在鍵空間中,隨機移除某個key。應該也沒人用吧,你不刪最少使用Key,去隨機刪。
4)volatile-lru:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,移除最近最少使用的key。這種情況一般是把redis既當緩存,又做持久化存儲的時候才用。不推薦
5)volatile-random:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,隨機移除某個key。依然不推薦
6)volatile-ttl:當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,有更早過期時間的key優先移除。不推薦
8.redis所帶來的問題以及解決方案
(一)緩存和數據庫雙寫一致性問題
具體問題是指在更新數據庫時,因為有緩存存在,會導致緩存和數據庫中數據不一致的問題。
一般解決方案有以下幾種:
(1)先更新數據庫,在更新緩存。
多線程情況下:
1)線程A更新了數據庫
2)線程B更新了數據庫
3)線程B更新了緩存
4)線程A更新了緩存
明顯應該A先更新緩存,可B卻先更新了緩存,導致了臟數據。並且如果寫操作比較多,緩存還沒有被讀就一直在更新,就會浪費這次操作,影響性能。
因此:一般都采用這種方式解決一致性問題。
(2)先刪除緩存,在更新數據庫。
多線程的案例:
1)請求A進行寫操作,刪除緩存
2)請求B查詢發現緩存不存在
3)請求B去數據庫查詢得到舊值
4)請求B將舊值寫入緩存
5)請求A將新值寫入數據庫
導致了緩存和數據庫不一致。如果沒有key過期作廢的機制,這個臟數據就會一直存在。
(3)先更新數據庫,在刪除緩存。
發生這種情況的案例:
1)緩存剛好失效
2)請求A查詢數據庫,得一個舊值
3)請求B將新值寫入數據庫
4)請求B刪除緩存
5)請求A將查到的舊值寫入緩存
如果發生上述情況,確實是會發生臟數據。但是概率很小。因為步驟5)一般都會在步驟3)前執行。因為讀操作遠快於寫操作,所以這種策略比較常用。
(二)緩存雪崩問題
可能是並發大量請求導致緩存失效(查不到值),即緩存同一時間大面積的失效,這個時候又來了一波請求,結果請求都懟到數據庫上,從而導致數據庫連接異常。
解決方案:
(1)給緩存的失效時間,加上一個隨機值,避免集體失效。
(2)使用互斥鎖,但是該方案吞吐量明顯下降了。
(3)雙緩存。我們有兩個緩存,緩存A和緩存B。緩存A的失效時間為20分鐘,緩存B不設失效時間。自己做緩存預熱操作。然後細分以下幾個小點
-
I 從緩存A讀數據庫,有則直接返回
-
II A沒有數據,直接從B讀數據,直接返回,並且異步啟動一個更新線程。
-
III 更新線程同時更新緩存A和緩存B。
(三)緩存擊穿問題
緩存穿透,即黑客故意去請求緩存中不存在的數據,導致所有的請求都懟到數據庫上,從而數據庫連接異常。
解決方案:
(1)利用互斥鎖,緩存中不存在數據的時候,先去獲得鎖,得到鎖了,再去請求數據庫。沒得到鎖,則休眠一段時間重試
(2)采用異步更新策略,無論key是否取到值,都直接返回。value值中維護一個緩存失效時間,緩存如果過期,異步起一個線程去讀數據庫,更新緩存。需要做緩存預熱(項目啟動前,先加載緩存)操作。
(3)提供一個能迅速判斷請求是否有效的攔截機制,比如,利用布隆過濾器,內部維護一系列合法有效的key。迅速判斷出,請求所攜帶的Key是否合法有效。如果不合法,則直接返回。
(四)緩存的並發競爭key問題
多個子系統同時set同一個值,導致競爭key的問題。
解決方案:
(1)如果對這個key操作,不要求順序
這種情況下,準備一個分布式鎖,大家去搶鎖,搶到鎖就做set操作即可,比較簡單。
(2)如果對這個key操作,要求順序
系統A key 1 {valueA 3:00}
系統B key 1 {valueB 3:05}
系統C key 1 {valueC 3:10}
當系統搶到分布式鎖的時候,還要判斷時間戳,如果小與緩存中的時間戳,則不去set操作。
redis小結