自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

緩存擊穿!竟然不知道怎么寫代碼???

存儲(chǔ) 存儲(chǔ)軟件
在Redis中有三大問題:緩存雪崩、緩存擊穿、緩存穿透,今天我們來聊聊緩存擊穿。關(guān)于緩存擊穿相關(guān)理論文章,相信大家已經(jīng)看過不少,但是具體代碼中是怎么實(shí)現(xiàn)的,怎么解決的等問題,可能就一臉懵逼了。

[[407549]]

在Redis中有三大問題:緩存雪崩、緩存擊穿、緩存穿透,今天我們來聊聊緩存擊穿。

關(guān)于緩存擊穿相關(guān)理論文章,相信大家已經(jīng)看過不少,但是具體代碼中是怎么實(shí)現(xiàn)的,怎么解決的等問題,可能就一臉懵逼了。

今天,老田就帶大家來看看,緩存擊穿解決和代碼實(shí)現(xiàn)。

場景

請(qǐng)看下面這段代碼:

  1. /**  
  2.  * @author 田維常 
  3.  * @公眾號(hào) java后端技術(shù)全棧 
  4.  * @date 2021/6/27 15:59 
  5.  */ 
  6. @Service 
  7. public class UserInfoServiceImpl implements UserInfoService { 
  8.  
  9.     @Resource 
  10.     private UserMapper userMapper; 
  11.     @Resource 
  12.     private RedisTemplate<Long, String> redisTemplate; 
  13.  
  14.     @Override 
  15.     public UserInfo findById(Long id) { 
  16.         //查詢緩存 
  17.         String userInfoStr = redisTemplate.opsForValue().get(id); 
  18.         //如果緩存中不存在,查詢數(shù)據(jù)庫 
  19.         //1 
  20.         if (isEmpty(userInfoStr)) { 
  21.             UserInfo userInfo = userMapper.findById(id); 
  22.             //數(shù)據(jù)庫中不存在 
  23.             if(userInfo == null){ 
  24.                   return null
  25.             } 
  26.             userInfoStr = JSON.toJSONString(userInfo); 
  27.             //2 
  28.             //放入緩存 
  29.             redisTemplate.opsForValue().set(id, userInfoStr); 
  30.         } 
  31.         return JSON.parseObject(userInfoStr, UserInfo.class); 
  32.     } 
  33.  
  34.     private boolean isEmpty(String string) { 
  35.         return !StringUtils.hasText(string); 
  36.     } 

整個(gè)流程:

如果,在//1到//2之間耗時(shí)1.5秒,那就代表著在這1.5秒時(shí)間內(nèi)所有的查詢都會(huì)走查詢數(shù)據(jù)庫。這也就是我們所說的緩存中的“緩存擊穿”。

其實(shí),你們項(xiàng)目如果并發(fā)量不是很高,也不用怕,并且我見過很多項(xiàng)目也就差不多是這么寫的,也沒那么多事,畢竟只是第一次的時(shí)候可能會(huì)發(fā)生緩存擊穿。

但,我們也不要抱著一個(gè)僥幸的心態(tài)去寫代碼,既然是多線程導(dǎo)致的,估計(jì)很多人會(huì)想到鎖,下面我們使用鎖來解決。

改進(jìn)版

既然使用到鎖,那么我們第一時(shí)間應(yīng)該關(guān)心的是鎖的粒度。

如果我們放在方法findById上,那就是所有查詢都會(huì)有鎖的競爭,這里我相信大家都知道我們?yōu)槭裁床环旁诜椒ㄉ稀?/p>

  1. /**  
  2.  * @author 田維常 
  3.  * @公眾號(hào) java后端技術(shù)全棧 
  4.  * @date 2021/6/27 15:59 
  5.  */ 
  6. @Service 
  7. public class UserInfoServiceImpl implements UserInfoService { 
  8.  
  9.     @Resource 
  10.     private UserMapper userMapper; 
  11.     @Resource 
  12.     private RedisTemplate<Long, String> redisTemplate; 
  13.  
  14.     @Override 
  15.     public UserInfo findById(Long id) { 
  16.         //查詢緩存 
  17.         String userInfoStr = redisTemplate.opsForValue().get(id); 
  18.         if (isEmpty(userInfoStr)) { 
  19.             //只有不存的情況存在鎖 
  20.             synchronized (UserInfoServiceImpl.class){ 
  21.                 UserInfo userInfo = userMapper.findById(id); 
  22.                 //數(shù)據(jù)庫中不存在 
  23.                 if(userInfo == null){ 
  24.                      return null
  25.                 } 
  26.                 userInfoStr = JSON.toJSONString(userInfo); 
  27.                 //放入緩存 
  28.                 redisTemplate.opsForValue().set(id, userInfoStr); 
  29.             } 
  30.         } 
  31.         return JSON.parseObject(userInfoStr, UserInfo.class); 
  32.     } 
  33.  
  34.     private boolean isEmpty(String string) { 
  35.         return !StringUtils.hasText(string); 
  36.     } 

看似解決問題了,其實(shí),問題還是沒得到解決,還是會(huì)緩存擊穿,因?yàn)榕抨?duì)獲取到鎖后,還是會(huì)執(zhí)行同步塊代碼,也就是還會(huì)查詢數(shù)據(jù)庫,完全沒有解決緩存擊穿。

雙重檢查鎖

由此,我們引入雙重檢查鎖,我們?cè)谏系陌姹局羞M(jìn)行稍微改變,在同步模塊中再次校驗(yàn)緩存中是否存在。

  1. /**  
  2.  * @author 田維常 
  3.  * @公眾號(hào) java后端技術(shù)全棧 
  4.  * @date 2021/6/27 15:59 
  5.  */ 
  6. @Service 
  7. public class UserInfoServiceImpl implements UserInfoService { 
  8.  
  9.     @Resource 
  10.     private UserMapper userMapper; 
  11.     @Resource 
  12.     private RedisTemplate<Long, String> redisTemplate; 
  13.  
  14.     @Override 
  15.     public UserInfo findById(Long id) { 
  16.         //查緩存 
  17.         String userInfoStr = redisTemplate.opsForValue().get(id); 
  18.         //第一次校驗(yàn)緩存是否存在 
  19.         if (isEmpty(userInfoStr)) { 
  20.             //上鎖 
  21.             synchronized (UserInfoServiceImpl.class){  
  22.                 //再次查詢緩存,目的是判斷是否前面的線程已經(jīng)set過了 
  23.                 userInfoStr = redisTemplate.opsForValue().get(id); 
  24.                 //第二次校驗(yàn)緩存是否存在 
  25.                 if (isEmpty(userInfoStr)) { 
  26.                     UserInfo userInfo = userMapper.findById(id); 
  27.                     //數(shù)據(jù)庫中不存在 
  28.                     if(userInfo == null){ 
  29.                         return null
  30.                     } 
  31.                     userInfoStr = JSON.toJSONString(userInfo); 
  32.                     //放入緩存 
  33.                     redisTemplate.opsForValue().set(id, userInfoStr); 
  34.                 } 
  35.             } 
  36.         } 
  37.         return JSON.parseObject(userInfoStr, UserInfo.class); 
  38.     } 
  39.  
  40.     private boolean isEmpty(String string) { 
  41.         return !StringUtils.hasText(string); 
  42.     } 

這樣,看起來我們就解決了緩存擊穿問題,大家覺得解決了嗎?

惡意攻擊

回顧上面的案例,在正常的情況下是沒問題,但是一旦有人惡意攻擊呢?

比如說:入?yún)d=10000000,在數(shù)據(jù)庫里并沒有這個(gè)id,怎么辦呢?

第一步、緩存中不存在

第二步、查詢數(shù)據(jù)庫

第三步、由于數(shù)據(jù)庫中不存在,直接返回了,并沒有操作緩存

第四步、再次執(zhí)行第一步.....死循環(huán)了吧

方案1:設(shè)置空對(duì)象

就是當(dāng)緩存中和數(shù)據(jù)庫中都不存在的情況下,以id為key,空對(duì)象為value。

  1. set(id,空對(duì)象); 

回到上面的四步,就變成了。

比如說:入?yún)d=10000000,在數(shù)據(jù)庫里并沒有這個(gè)id,怎么辦呢?

第一步、緩存中不存在

第二步、查詢數(shù)據(jù)庫

第三步、由于數(shù)據(jù)庫中不存在,以id為key,空對(duì)象為value放入緩存中

第四步、執(zhí)行第一步,此時(shí),緩存就存在了,只是這時(shí)候只是一個(gè)空對(duì)象。

代碼實(shí)現(xiàn)部分:

  1. /**  
  2.  * @author 田維常 
  3.  * @公眾號(hào) java后端技術(shù)全棧 
  4.  * @date 2021/6/27 15:59 
  5.  */ 
  6. @Service 
  7. public class UserInfoServiceImpl implements UserInfoService { 
  8.  
  9.     @Resource 
  10.     private UserMapper userMapper; 
  11.     @Resource 
  12.     private RedisTemplate<Long, String> redisTemplate;  
  13.  
  14.     @Override 
  15.     public UserInfo findById(Long id) { 
  16.         String userInfoStr = redisTemplate.opsForValue().get(id); 
  17.         //判斷緩存是否存在,是否為空對(duì)象 
  18.         if (isEmpty(userInfoStr)) { 
  19.             synchronized (UserInfoServiceImpl.class){ 
  20.                 userInfoStr = redisTemplate.opsForValue().get(id); 
  21.                 if (isEmpty(userInfoStr)) { 
  22.                     UserInfo userInfo = userMapper.findById(id); 
  23.                     if(userInfo == null){ 
  24.                         //構(gòu)建一個(gè)空對(duì)象 
  25.                         userInfo= new UserInfo(); 
  26.                     } 
  27.                     userInfoStr = JSON.toJSONString(userInfo); 
  28.                     redisTemplate.opsForValue().set(id, userInfoStr); 
  29.                 } 
  30.             } 
  31.         } 
  32.         UserInfo userInfo = JSON.parseObject(userInfoStr, UserInfo.class); 
  33.         //空對(duì)象處理 
  34.         if(userInfo.getId() == null){ 
  35.             return null
  36.         } 
  37.         return JSON.parseObject(userInfoStr, UserInfo.class); 
  38.     } 
  39.  
  40.     private boolean isEmpty(String string) { 
  41.         return !StringUtils.hasText(string); 
  42.     } 

方案2 布隆過濾器

布隆過濾器(Bloom Filter):是一種空間效率極高的概率型算法和數(shù)據(jù)結(jié)構(gòu),用于判斷一個(gè)元素是否在集合中(類似Hashset)。它的核心一個(gè)很長的二進(jìn)制向量和一系列hash函數(shù),數(shù)組長度以及hash函數(shù)的個(gè)數(shù)都是動(dòng)態(tài)確定的。

Hash函數(shù):SHA1,SHA256,MD5..

布隆過濾器的用處就是,能夠迅速判斷一個(gè)元素是否在一個(gè)集合中。因此他有如下三個(gè)使用場景:

  • 網(wǎng)頁爬蟲對(duì)URL的去重,避免爬取相同的URL地址
  • 反垃圾郵件,從數(shù)十億個(gè)垃圾郵件列表中判斷某郵箱是否垃圾郵箱(垃圾短信)
  • 緩存擊穿,將已存在的緩存放到布隆過濾器中,當(dāng)黑客訪問不存在的緩存時(shí)迅速返回避免緩存及DB掛掉。

其內(nèi)部維護(hù)一個(gè)全為0的bit數(shù)組,需要說明的是,布隆過濾器有一個(gè)誤判率的概念,誤判率越低,則數(shù)組越長,所占空間越大。誤判率越高則數(shù)組越小,所占的空間越小。布隆過濾器的相關(guān)理論和算法這里就不聊了,感興趣的可以自行研究。

優(yōu)勢(shì)和劣勢(shì)

優(yōu)勢(shì)

  • 全量存儲(chǔ)但是不存儲(chǔ)元素本身,在某些對(duì)保密要求非常嚴(yán)格的場合有優(yōu)勢(shì);
  • 空間高效率
  • 插入/查詢時(shí)間都是常數(shù)O(k),遠(yuǎn)遠(yuǎn)超過一般的算法

劣勢(shì)

  • 存在誤算率(False Positive),默認(rèn)0.03,隨著存入的元素?cái)?shù)量增加,誤算率隨之增加;
  • 一般情況下不能從布隆過濾器中刪除元素;
  • 數(shù)組長度以及hash函數(shù)個(gè)數(shù)確定過程復(fù)雜;

代碼實(shí)現(xiàn):

  1. /**  
  2.  * @author 田維常 
  3.  * @公眾號(hào) java后端技術(shù)全棧 
  4.  * @date 2021/6/27 15:59 
  5.  */ 
  6. @Service 
  7. public class UserInfoServiceImpl implements UserInfoService { 
  8.  
  9.     @Resource 
  10.     private UserMapper userMapper; 
  11.     @Resource 
  12.     private RedisTemplate<Long, String> redisTemplate; 
  13.     private static Long size = 1000000000L; 
  14.  
  15.     private static BloomFilter<Long> bloomFilter = BloomFilter.create(Funnels.longFunnel(), size); 
  16.  
  17.     @Override 
  18.     public UserInfo findById(Long id) { 
  19.         String userInfoStr = redisTemplate.opsForValue().get(id); 
  20.         if (isEmpty(userInfoStr)) { 
  21.             //校驗(yàn)是否在布隆過濾器中 
  22.             if(bloomFilter.mightContain(id)){ 
  23.                 return null
  24.             } 
  25.             synchronized (UserInfoServiceImpl.class){ 
  26.                 userInfoStr = redisTemplate.opsForValue().get(id); 
  27.                 if (isEmpty(userInfoStr) ) { 
  28.                     if(bloomFilter.mightContain(id)){ 
  29.                         return null
  30.                     } 
  31.                     UserInfo userInfo = userMapper.findById(id); 
  32.                     if(userInfo == null){ 
  33.                         //放入布隆過濾器中 
  34.                         bloomFilter.put(id); 
  35.                         return null
  36.                     } 
  37.                     userInfoStr = JSON.toJSONString(userInfo); 
  38.                     redisTemplate.opsForValue().set(id, userInfoStr); 
  39.                 } 
  40.             } 
  41.         } 
  42.         return JSON.parseObject(userInfoStr, UserInfo.class); 
  43.     }  
  44.     private boolean isEmpty(String string) { 
  45.         return !StringUtils.hasText(string); 
  46.     } 

方案3 互斥鎖

使用Redis實(shí)現(xiàn)分布式的時(shí)候,有用到setnx,這里大家可以想象,我們是否可以使用這個(gè)分布式鎖來解決緩存擊穿的問題?

這個(gè)方案留給大家去實(shí)現(xiàn),只要掌握了Redis的分布式鎖,那這個(gè)實(shí)現(xiàn)起來就非常簡單了。

總結(jié)

搞定緩存擊穿、使用雙重檢查鎖的方式來解決,看到雙重檢查鎖,大家肯定第一印象就會(huì)想到單例模式,這里也算是給大家復(fù)習(xí)一把雙重檢查鎖的使用。

由于惡意攻擊導(dǎo)致的緩存擊穿,解決方案我們也實(shí)現(xiàn)了兩種,至少在工作和面試中,肯定是能應(yīng)對(duì)了。

另外,使用鎖的時(shí)候注意鎖的力度,這里建議換成分布式鎖(Redis或者Zookeeper實(shí)現(xiàn)),因?yàn)槲覀兗热灰刖彺?,大部分情況下都會(huì)是部署多個(gè)節(jié)點(diǎn)的,同時(shí),引入分布式鎖了,我們就可以使用方法入?yún)d用起來,這樣是不是更爽!

希望大家能領(lǐng)悟到的是文中的一些思路,并不是死記硬背技術(shù)。

責(zé)任編輯:武曉燕 來源: Java后端技術(shù)全棧
相關(guān)推薦

2020-12-21 09:00:04

MySQL緩存SQL

2019-10-28 08:44:29

Code Review代碼團(tuán)隊(duì)

2020-12-21 09:44:53

MySQL查詢緩存數(shù)據(jù)庫

2020-07-21 18:37:14

代碼條件變量

2017-01-19 15:11:37

AndroidRetrofitRxCache

2021-02-03 08:24:32

JavaScript技巧經(jīng)驗(yàn)

2022-07-04 07:09:55

架構(gòu)

2021-07-12 10:37:42

Spring面試事務(wù)

2021-06-03 08:05:46

VSCode 代碼高亮原理前端

2020-08-26 13:30:18

代碼設(shè)計(jì)模式前端

2019-07-12 15:28:41

緩存數(shù)據(jù)庫瀏覽器

2018-09-02 15:43:56

Python代碼編程語言

2022-03-03 07:00:43

Mybatiswhere標(biāo)簽

2022-04-24 16:00:15

LinuxLinux命令ls命令

2020-06-12 09:20:33

前端Blob字符串

2020-07-28 08:26:34

WebSocket瀏覽器

2011-09-15 17:10:41

2022-10-13 11:48:37

Web共享機(jī)制操作系統(tǒng)

2009-12-10 09:37:43

2021-02-01 23:23:39

FiddlerCharlesWeb
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)