Redis緩存的主要異常及解決方案實(shí)例
1 導(dǎo)讀
Redis 是當(dāng)前最流行的 NoSQL數(shù)據(jù)庫。Redis主要用來做緩存使用,在提高數(shù)據(jù)查詢效率、保護(hù)數(shù)據(jù)庫等方面起到了關(guān)鍵性的作用,很大程度上提高系統(tǒng)的性能。當(dāng)然在使用過程中,也會(huì)出現(xiàn)一些異常情景,導(dǎo)致Redis失去緩存作用。
2 異常類型
異常主要有 緩存雪崩 緩存穿透 緩存擊穿。
2.1 緩存雪崩
2.1.1 現(xiàn)象
緩存雪崩是指大量請求在緩存中沒有查到數(shù)據(jù),直接訪問數(shù)據(jù)庫,導(dǎo)致數(shù)據(jù)庫壓力增大,最終導(dǎo)致數(shù)據(jù)庫崩潰,從而波及整個(gè)系統(tǒng)不可用,好像雪崩一樣。
2.1.2 異常原因
- 緩存服務(wù)不可用。
- 緩存服務(wù)可用,但是大量KEY同時(shí)失效。
2.1.3 解決方案
1.緩存服務(wù)不可用
redis的部署方式主要有單機(jī)、主從、哨兵和 cluster模式。
- 單機(jī)
只有一臺(tái)機(jī)器,所有數(shù)據(jù)都存在這臺(tái)機(jī)器上,當(dāng)機(jī)器出現(xiàn)異常時(shí),redis將失效,可能會(huì)導(dǎo)致redis緩存雪崩。
- 主從
主從其實(shí)就是一臺(tái)機(jī)器做主,一個(gè)或多個(gè)機(jī)器做從,從節(jié)點(diǎn)從主節(jié)點(diǎn)復(fù)制數(shù)據(jù),可以實(shí)現(xiàn)讀寫分離,主節(jié)點(diǎn)做寫,從節(jié)點(diǎn)做讀。
優(yōu)點(diǎn):當(dāng)某個(gè)從節(jié)點(diǎn)異常時(shí),不影響使用。
缺點(diǎn):當(dāng)主節(jié)點(diǎn)異常時(shí),服務(wù)將不可用。
- 哨兵
哨兵模式也是一種主從,只不過增加了哨兵的功能,用于監(jiān)控主節(jié)點(diǎn)的狀態(tài),當(dāng)主節(jié)點(diǎn)宕機(jī)之后會(huì)進(jìn)行投票在從節(jié)點(diǎn)中重新選出主節(jié)點(diǎn)。
優(yōu)點(diǎn):高可用,當(dāng)主節(jié)點(diǎn)異常時(shí),自動(dòng)在從節(jié)點(diǎn)當(dāng)中選擇一個(gè)主節(jié)點(diǎn)。
缺點(diǎn):只有一個(gè)主節(jié)點(diǎn),當(dāng)數(shù)據(jù)比較多時(shí),主節(jié)點(diǎn)壓力會(huì)很大。
- cluster模式
集群采用了多主多從,按照一定的規(guī)則進(jìn)行分片,將數(shù)據(jù)分別存儲(chǔ),一定程度上解決了哨兵模式下單機(jī)存儲(chǔ)有限的問題。
優(yōu)點(diǎn):高可用,配置了多主多從,可以使數(shù)據(jù)分區(qū),去中心化,減小了單臺(tái)機(jī)子的負(fù)擔(dān).
缺點(diǎn):機(jī)器資源使用比較多,配置復(fù)雜。
小結(jié)
從高可用得角度考慮,使用哨兵模式和cluster模式可以防止因?yàn)閞edis不可用導(dǎo)致的緩存雪崩問題。
2.大量KEY同時(shí)失效
可以通過設(shè)置永不失效、設(shè)置不同失效時(shí)間、使用二級緩存和定時(shí)更新緩存失效時(shí)間
- 設(shè)置永不失效
如果所有的key都設(shè)置不失效,不就不會(huì)出現(xiàn)因?yàn)镵EY失效導(dǎo)致的緩存雪崩問題了。
redis設(shè)置key永遠(yuǎn)有效的命令如下:
PERSIST key
缺點(diǎn):會(huì)導(dǎo)致redis的空間資源需求變大。
- 設(shè)置隨機(jī)失效時(shí)間
如果key的失效時(shí)間不相同,就不會(huì)在同一時(shí)刻失效,這樣就不會(huì)出現(xiàn)大量訪問數(shù)據(jù)庫的情況。
redis設(shè)置key有效時(shí)間命令如下:
Expire key
示例代碼如下,通過RedisClient實(shí)現(xiàn)
/** * 隨機(jī)設(shè)置小于30分鐘的失效時(shí)間 * @param redisKey * @param value */ private void setRandomTimeForReidsKey(String redisKey,String value){ //隨機(jī)函數(shù) Random rand = new Random(); //隨機(jī)獲取30分鐘內(nèi)(30*60)的隨機(jī)數(shù) int times = rand.nextInt(1800); //設(shè)置緩存時(shí)間(緩存的key,緩存的值,失效時(shí)間:單位秒) redisClient.setNxEx(redisKey,value,times); }
- 使用二級緩存
二級緩存是使用兩組緩存,1級緩存和2級緩存,同一個(gè)Key在兩組緩存里都保存,但是他們的失效時(shí)間不同,這樣1級緩存沒有查到數(shù)據(jù)時(shí),可以在二級緩存里查詢,不會(huì)直接訪問數(shù)據(jù)庫。
示例代碼如下:
public static void main(String[] args) { CacheTest test = new CacheTest(); //從1級緩存中獲取數(shù)據(jù) String value = test.queryByOneCacheKey("key"); //如果1級緩存中沒有數(shù)據(jù),再二級緩存中查找 if(StringUtils.isBlank(value)){ value = test.queryBySecondCacheKey("key"); //如果二級緩存中沒有,從數(shù)據(jù)庫中查找 if(StringUtils.isBlank(value)){ value =test.getFromDb(); //如果數(shù)據(jù)庫中也沒有,就返回空 if(StringUtils.isBlank(value)){ System.out.println("數(shù)據(jù)不存在!"); }else{ //二級緩存中保存數(shù)據(jù) test.secondCacheSave("key",value); //一級緩存中保存數(shù)據(jù) test.oneCacheSave("key",value); System.out.println("數(shù)據(jù)庫中返回?cái)?shù)據(jù)!"); } }else{ //一級緩存中保存數(shù)據(jù) test.oneCacheSave("key",value); System.out.println("二級緩存中返回?cái)?shù)據(jù)!"); } }else { System.out.println("一級緩存中返回?cái)?shù)據(jù)!"); } }
- 異步更新緩存時(shí)間
每次訪問緩存時(shí),啟動(dòng)一個(gè)線程或者建立一個(gè)異步任務(wù)來,更新緩存時(shí)間。
示例代碼如下:
public class CacheRunnable implements Runnable { private ClusterRedisClientAdapter redisClient; /** * 要更新的key */ public String key; public CacheRunnable(String key){ this.key =key; } @Override public void run() { //更細(xì)緩存時(shí)間 redisClient.expire(this.getKey(),1800); } public String getKey() { return key; } public void setKey(String key) { this.key = key; } } public static void main(String[] args) { CacheTest test = new CacheTest(); //從緩存中獲取數(shù)據(jù) String value = test.getFromCache("key"); if(StringUtils.isBlank(value)){ //從數(shù)據(jù)庫中獲取數(shù)據(jù) value = test.getFromDb("key"); //將數(shù)據(jù)放在緩存中 test.oneCacheSave("key",value); //返回?cái)?shù)據(jù) System.out.println("返回?cái)?shù)據(jù)"); }else{ //異步任務(wù)更新緩存 CacheRunnable runnable = new CacheRunnable("key"); runnable.run(); //返回?cái)?shù)據(jù) System.out.println("返回?cái)?shù)據(jù)"); } }
3.小結(jié)
上面從服務(wù)不可用和key大面積失效兩個(gè)方面,列舉了幾種解決方案,上面的代碼只是提供一些思路,具體實(shí)施還要考慮到現(xiàn)實(shí)情況。當(dāng)然也有其他的解決方案,我這里舉例是比較常用的。畢竟現(xiàn)實(shí)情況,千變?nèi)f化,沒有最好的方案,只有最適用的方案。
2.2 緩存穿透
2.2.1 現(xiàn)象
緩存穿透是指當(dāng)用戶在查詢一條數(shù)據(jù)的時(shí)候,而此時(shí)數(shù)據(jù)庫和緩存卻沒有關(guān)于這條數(shù)據(jù)的任何記錄,而這條數(shù)據(jù)在緩存中沒找到就會(huì)向數(shù)據(jù)庫請求獲取數(shù)據(jù)。用戶拿不到數(shù)據(jù)時(shí),就會(huì)一直發(fā)請求,查詢數(shù)據(jù)庫,這樣會(huì)對數(shù)據(jù)庫的訪問造成很大的壓力。
2.2.2 異常原因
- 非法調(diào)用
2.2.3 解決方案
1.非法調(diào)用
可以通過緩存空值或過濾器來解決非法調(diào)用引起的緩存穿透問題。
- 緩存空值
當(dāng)緩存和數(shù)據(jù)庫中都沒有值時(shí),可以在緩存中存放一個(gè)空值,這樣就可以減少重復(fù)查詢空值引起的系統(tǒng)壓力增大,從而優(yōu)化了緩存穿透問題。
示例代碼如下:
private String queryMessager(String key){ //從緩存中獲取數(shù)據(jù) String message = getFromCache(key); //如果緩存中沒有 從數(shù)據(jù)庫中查找 if(StringUtils.isBlank(message)){ message = getFromDb(key); //如果數(shù)據(jù)庫中也沒有數(shù)據(jù) 就設(shè)置短時(shí)間的緩存 if(StringUtils.isBlank(message)){ //設(shè)置緩存時(shí)間(緩存的key,緩存的值,失效時(shí)間:單位秒) redisClient.setNxEx(key,null,60); }else{ redisClient.setNxEx(key,message,1800); } } return message; }
缺點(diǎn):大量的空緩存導(dǎo)致資源的浪費(fèi),也有可能導(dǎo)致緩存和數(shù)據(jù)庫中的數(shù)據(jù)不一致。
- 布隆過濾器
布隆過濾器由布隆在 1970 年提出。它實(shí)際上是一個(gè)很長的二進(jìn)制向量和一系列隨機(jī)映射函數(shù)。布隆過濾器可以用于檢索一個(gè)元素是否在一個(gè)集合中。是以空間換時(shí)間的算法。
布隆過濾器的實(shí)現(xiàn)原理是一個(gè)超大的位數(shù)組和幾個(gè)哈希函數(shù)。
假設(shè)哈希函數(shù)的個(gè)數(shù)為 3。首先將位數(shù)組進(jìn)行初始化,初始化狀態(tài)的維數(shù)組的每個(gè)位都設(shè)置位 0。如果一次數(shù)據(jù)請求的結(jié)果為空,就將key依次通過 3 個(gè)哈希函數(shù)進(jìn)行映射,每次映射都會(huì)產(chǎn)生一個(gè)哈希值,這個(gè)值對應(yīng)位數(shù)組上面的一個(gè)點(diǎn),然后將位數(shù)組對應(yīng)的位置標(biāo)記為 1。當(dāng)數(shù)據(jù)請求再次發(fā)過來時(shí),用同樣的方法將 key 通過哈希映射到位數(shù)組上的 3 個(gè)點(diǎn)。如果 3 個(gè)點(diǎn)中任意一個(gè)點(diǎn)不為 1,則可以判斷key不為空。反之,如果 3 個(gè)點(diǎn)都為 1,則該KEY一定為空。
缺點(diǎn):
可能出現(xiàn)誤判,例如 A 經(jīng)過哈希函數(shù) 存到 1、3和5位置。B經(jīng)過哈希函數(shù)存到 3、5和7位置。C經(jīng)過哈希函數(shù)得到位置 3、5和7位置。由于3、5和7都有值,導(dǎo)致判斷A也在數(shù)組中。這種情況隨著數(shù)據(jù)的增多,幾率也變大。
布隆過濾器沒法刪除數(shù)據(jù)。
- 布隆過濾器增強(qiáng)版
增強(qiáng)版是將布隆過濾器的bitmap更換成數(shù)組,當(dāng)數(shù)組某位置被映射一次時(shí)就+1,當(dāng)刪除時(shí)就-1,這樣就避免了普通布隆過濾器刪除數(shù)據(jù)后需要重新計(jì)算其余數(shù)據(jù)包Hash的問題,但是依舊沒法避免誤判。
- 布谷鳥過濾器
但是如果這兩個(gè)位置都滿了,它就不得不「鳩占鵲巢」,隨機(jī)踢走一個(gè),然后自己霸占了這個(gè)位置。不同于布谷鳥的是,布谷鳥哈希算法會(huì)幫這些受害者(被擠走的蛋)尋找其它的窩。因?yàn)槊恳粋€(gè)元素都可以放在兩個(gè)位置,只要任意一個(gè)有空位置,就可以塞進(jìn)去。所以這個(gè)傷心的被擠走的蛋會(huì)看看自己的另一個(gè)位置有沒有空,如果空了,自己挪過去也就皆大歡喜了。但是如果這個(gè)位置也被別人占了呢?好,那么它會(huì)再來一次「鳩占鵲巢」,將受害者的角色轉(zhuǎn)嫁給別人。然后這個(gè)新的受害者還會(huì)重復(fù)這個(gè)過程直到所有的蛋都找到了自己的巢為止。
缺點(diǎn):
如果數(shù)組太擁擠了,連續(xù)踢來踢去幾百次還沒有停下來,這時(shí)候會(huì)嚴(yán)重影響插入效率。這時(shí)候布谷鳥哈希會(huì)設(shè)置一個(gè)閾值,當(dāng)連續(xù)占巢行為超出了某個(gè)閾值,就認(rèn)為這個(gè)數(shù)組已經(jīng)幾乎滿了。這時(shí)候就需要對它進(jìn)行擴(kuò)容,重新放置所有元素。
2.小結(jié)
以上方法雖然都有缺點(diǎn),但是可以有效的防止因?yàn)榇罅靠諗?shù)據(jù)查詢導(dǎo)致的緩存穿透問題,除了系統(tǒng)上的優(yōu)化,還要加強(qiáng)對系統(tǒng)的監(jiān)控,發(fā)下異常調(diào)用時(shí),及時(shí)加入黑名單。降低異常調(diào)用對系統(tǒng)的影響。
2.3 緩存擊穿
2.3.1 現(xiàn)象
key中對應(yīng)數(shù)據(jù)存在,當(dāng)key中對應(yīng)的數(shù)據(jù)在緩存中過期,而此時(shí)又有大量請求訪問該數(shù)據(jù),緩存中過期了,請求會(huì)直接訪問數(shù)據(jù)庫并回設(shè)到緩存中,高并發(fā)訪問數(shù)據(jù)庫會(huì)導(dǎo)致數(shù)據(jù)庫崩潰。redis的高QPS特性,可以很好的解決查數(shù)據(jù)庫很慢的問題。但是如果我們系統(tǒng)的并發(fā)很高,在某個(gè)時(shí)間節(jié)點(diǎn),突然緩存失效,這時(shí)候有大量的請求打過來,那么由于redis沒有緩存數(shù)據(jù),這時(shí)候我們的請求會(huì)全部去查一遍數(shù)據(jù)庫,這時(shí)候我們的數(shù)據(jù)庫服務(wù)會(huì)面臨非常大的風(fēng)險(xiǎn),要么連接被占滿,要么其他業(yè)務(wù)不可用,這種情況就是redis的緩存擊穿。
2.3.2 異常原因
熱點(diǎn)KEY失效的同時(shí),大量相同KEY請求同時(shí)訪問。
2.3.3 解決方案
1.熱點(diǎn)key失效
- 設(shè)置永不失效
如果所有的key都設(shè)置不失效,不就不會(huì)出現(xiàn)因?yàn)镵EY失效導(dǎo)致的緩存雪崩問題了。redis設(shè)置key永遠(yuǎn)有效的命令如下:
PERSIST key
缺點(diǎn):會(huì)導(dǎo)致redis的空間資源需求變大。
- 設(shè)置隨機(jī)失效時(shí)間
如果key的失效時(shí)間不相同,就不會(huì)在同一時(shí)刻失效,這樣就不會(huì)出現(xiàn)大量訪問數(shù)據(jù)庫的情況。
redis設(shè)置key有效時(shí)間命令如下:
Expire key
示例代碼如下,通過RedisClient實(shí)現(xiàn)
/** * 隨機(jī)設(shè)置小于30分鐘的失效時(shí)間 * @param redisKey * @param value */ private void setRandomTimeForReidsKey(String redisKey,String value){ //隨機(jī)函數(shù) Random rand = new Random(); //隨機(jī)獲取30分鐘內(nèi)(30*60)的隨機(jī)數(shù) int times = rand.nextInt(1800); //設(shè)置緩存時(shí)間(緩存的key,緩存的值,失效時(shí)間:單位秒) redisClient.setNxEx(redisKey,value,times); }
- 使用二級緩存
二級緩存是使用兩組緩存,1級緩存和2級緩存,同一個(gè)Key在兩組緩存里都保存,但是他們的失效時(shí)間不同,這樣1級緩存沒有查到數(shù)據(jù)時(shí),可以在二級緩存里查詢,不會(huì)直接訪問數(shù)據(jù)庫。
示例代碼如下:
public static void main(String[] args) { CacheTest test = new CacheTest(); //從1級緩存中獲取數(shù)據(jù) String value = test.queryByOneCacheKey("key"); //如果1級緩存中沒有數(shù)據(jù),再二級緩存中查找 if(StringUtils.isBlank(value)){ value = test.queryBySecondCacheKey("key"); //如果二級緩存中沒有,從數(shù)據(jù)庫中查找 if(StringUtils.isBlank(value)){ value =test.getFromDb(); //如果數(shù)據(jù)庫中也沒有,就返回空 if(StringUtils.isBlank(value)){ System.out.println("數(shù)據(jù)不存在!"); }else{ //二級緩存中保存數(shù)據(jù) test.secondCacheSave("key",value); //一級緩存中保存數(shù)據(jù) test.oneCacheSave("key",value); System.out.println("數(shù)據(jù)庫中返回?cái)?shù)據(jù)!"); } }else{ //一級緩存中保存數(shù)據(jù) test.oneCacheSave("key",value); System.out.println("二級緩存中返回?cái)?shù)據(jù)!"); } }else { System.out.println("一級緩存中返回?cái)?shù)據(jù)!"); } }
- 異步更新緩存時(shí)間
每次訪問緩存時(shí),啟動(dòng)一個(gè)線程或者建立一個(gè)異步任務(wù)來,更新緩存時(shí)間。
示例代碼如下:
public class CacheRunnable implements Runnable { private ClusterRedisClientAdapter redisClient; /** * 要更新的key */ public String key; public CacheRunnable(String key){ this.key =key; } @Override public void run() { //更細(xì)緩存時(shí)間 redisClient.expire(this.getKey(),1800); } public String getKey() { return key; } public void setKey(String key) { this.key = key; } } public static void main(String[] args) { CacheTest test = new CacheTest(); //從緩存中獲取數(shù)據(jù) String value = test.getFromCache("key"); if(StringUtils.isBlank(value)){ //從數(shù)據(jù)庫中獲取數(shù)據(jù) value = test.getFromDb("key"); //將數(shù)據(jù)放在緩存中 test.oneCacheSave("key",value); //返回?cái)?shù)據(jù) System.out.println("返回?cái)?shù)據(jù)"); }else{ //異步任務(wù)更新緩存 CacheRunnable runnable = new CacheRunnable("key"); runnable.run(); //返回?cái)?shù)據(jù) System.out.println("返回?cái)?shù)據(jù)"); } }
- 分布式鎖
使用分布式鎖,同一時(shí)間只有1個(gè)請求可以訪問到數(shù)據(jù)庫,其他請求等待一段時(shí)間后,重復(fù)調(diào)用。
示例代碼如下:
/** * 根據(jù)key獲取數(shù)據(jù) * @param key * @return * @throws InterruptedException */ public String queryForMessage(String key) throws InterruptedException { //初始化返回結(jié)果 String result = StringUtils.EMPTY; //從緩存中獲取數(shù)據(jù) result = queryByOneCacheKey(key); //如果緩存中有數(shù)據(jù),直接返回 if(StringUtils.isNotBlank(result)){ return result; }else{ //獲取分布式鎖 if(lockByBusiness(key)){ //從數(shù)據(jù)庫中獲取數(shù)據(jù) result = getFromDb(key); //如果數(shù)據(jù)庫中有數(shù)據(jù),就加在緩存中 if(StringUtils.isNotBlank(result)){ oneCacheSave(key,result); } }else { //如果沒有獲取到分布式鎖,睡眠一下,再接著查詢數(shù)據(jù) Thread.sleep(500); return queryForMessage(key); } } return result; }
2.小結(jié)
除了以上解決方法,還可以預(yù)先設(shè)置熱門數(shù)據(jù),通過一些監(jiān)控方法,及時(shí)收集熱點(diǎn)數(shù)據(jù),將數(shù)據(jù)預(yù)先保存在緩存中。
3 總結(jié)
Redis緩存在互聯(lián)網(wǎng)中至關(guān)重要,可以很大的提升系統(tǒng)效率。 本文介紹的緩存異常以及解決思路有可能不夠全面,但也提供相應(yīng)的解決思路和代碼大體實(shí)現(xiàn),希望可以為大家提供一些遇到緩存問題時(shí)的解決思路。如果有不足的地方,也請幫忙指出,大家共同進(jìn)步,更多關(guān)于Redis緩存異常解決的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解redis在服務(wù)器linux下啟動(dòng)的相關(guān)命令(安裝和配置)
這篇文章主要介紹了redis在服務(wù)器linux下的啟動(dòng)的相關(guān)命令(安裝和配置),本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-08-08Redis核心原理與實(shí)踐之字符串實(shí)現(xiàn)原理
這本書深入地分析了Redis常用特性的內(nèi)部機(jī)制與實(shí)現(xiàn)方式,內(nèi)容源自對Redis源碼的分析,并從中總結(jié)出設(shè)計(jì)思路、實(shí)現(xiàn)原理。對Redis字符串實(shí)現(xiàn)原理相關(guān)知識(shí)感興趣的朋友一起看看吧2021-09-09詳解redis是如何實(shí)現(xiàn)隊(duì)列消息的ack
這篇文章主要介紹了關(guān)于redis是如何實(shí)現(xiàn)隊(duì)列消息的ack的相關(guān)資料,文中介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來一起看看吧。2017-04-04Redis數(shù)據(jù)庫的數(shù)據(jù)傾斜詳解
Redis,英文全稱是Remote Dictionary Server(遠(yuǎn)程字典服務(wù)),是一個(gè)開源的使用ANSI C語言編寫、支持網(wǎng)絡(luò)、可基于內(nèi)存亦可持久化的日志型、Key-Value數(shù)據(jù)庫,需要的朋友可以參考下2023-07-07Redis序列化反序列化不一致導(dǎo)致String類型值多了雙引號問題
這篇文章主要介紹了Redis序列化反序列化不一致導(dǎo)致String類型值多了雙引號問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08