當前位置:網站首頁>你的數據庫真的穿“防彈衣”了嗎
你的數據庫真的穿“防彈衣”了嗎
2022-05-14 21:24:48【蘇州程序大白】
博主介紹
作者主頁:蘇州程序大白
作者簡介:CSDN人工智能域優質創作者,蘇州市凱捷智能科技有限公司創始之一,目前合作公司富士康、歌爾等幾家新能源公司
如果文章對你有幫助,歡迎關注、點贊、收藏
有任何問題歡迎私信,看到會及時回複
關注蘇州程序大白,分享粉絲福利
前言
華强來到一家程序員商店買緩存,問程序員:“你這緩存保熟嗎?” …
緩存經常被用來减少數據庫訪問量,以此來提高系統性能,承受更多的並發請求,就像“防彈衣”一樣保護著數據庫,防止被一顆顆“請求子彈”擊中。
但引入緩存,也帶來了一些新的問題,比如緩存擊穿、緩存穿透、緩存雪崩、緩存數據一致性等問題。今天來聊聊緩存擊穿,百度一搜有很多相關的文章,但按照網上的一些教程去解决緩存擊穿,真的可以保證這一“防彈衣”不被擊穿嗎?
看一段示例代碼
public ResponseDTO getRoleById(Long id) throws Exception {
String key = "User:Role:" + id;
List<Long> data = (List<Long>) redisUtil.get(key);
if (data == null) {
data = userMapper.selectRoleIdByUserId(id);
Long buffTime = (long) new Random().nextInt(30) * 60;
redisUtil.set(key, data, buffTime);
}
return new ResponseDTO(Status.SUCCESS.code, "", data);
}
這段代碼使用redis
做了緩存,只要緩存中有數據就不會去查數據庫,但如果緩存中沒數據,這時又恰好又大量請求來襲,那這些請求就會去訪問數據庫,如果並發請求量很大,數據庫就有可能被打死,這就是緩存擊穿。
解决思路也很簡單,只讓一個請求去查數據庫然後更新緩存,其他請求先等著,等查數據庫的兄弟更新完緩存我再去查緩存。具體實現也很容易,加個鎖不就好了。那一起來看看接下來這幾段代碼。
解决緩存擊穿的方法
public ResponseDTO getRoleById(Long id) throws Exception {
String key = "User:Role:" + id;
List<Long> data = (List<Long>) redisUtil.get(key);
boolean isLock = false;
ReentrantLock lock = new ReentrantLock();
try {
if (data == null) {
if (lock.tryLock()) {
isLock = true;
data = userMapper.selectRoleIdByUserId(id);
Long buffTime = (long) new Random().nextInt(30) * 60;
redisUtil.set(key, data, buffTime);
} else {
Thread.sleep(100); // 此處僅為例子,具體由實際查詢情况定,也可以循環查詢幾次
data = (List<Long>) redisUtil.get(key);
}
}
}
finally {
if (isLock)
lock.unlock();
}
return new ResponseDTO(Status.SUCCESS.code, "", data);
}
這個解决方案完全是一種錯誤的方案,因為這裏的鎖,鎖了個寂寞。
鎖是為了解决多線程問題的,即多個線程爭用一把鎖,誰搶到了誰用,但在SpringMVC中,一個請求就會建立一個線程,把鎖定義在方法中(ReentrantLock lock = … 那行代碼),那不就是一個線程一把鎖,我和自己搶,然後我鎖我自己嗎,鎖了個寂寞。所以這種方法無法防止緩存擊穿。
方法二
把鎖拿到外面定義並實例化,這樣就能做到所有線程用一把鎖。
static ReentrantLock lock = new ReentrantLock();
@Override
public ResponseDTO getRoleById(Long id) throws Exception {
String key = "User:Role:" + id;
List<Long> data = (List<Long>) redisUtil.get(key);
boolean isLock = false;
try {
if (data == null) {
if (lock.tryLock()) {
isLock = true;
data = userMapper.selectRoleIdByUserId(id);
Long buffTime = (long) new Random().nextInt(30) * 60;
// Thread.sleep(90); // 模擬用
redisUtil.set(key, data, buffTime);
} else {
Thread.sleep(100); // 此處僅為例子,具體由實際查詢情况定,也可以循環查詢幾次
data = (List<Long>) redisUtil.get(key);
}
}
}
finally {
if (isLock)
lock.unlock();
}
return new ResponseDTO(Status.SUCCESS.code, "", data);
}
這樣看著可行,但又有了新問題,如果有兩個並發請求,請求A和請求B,A和B請求參數不同,即想要得到的數據也不同,並且此時緩存中也沒有A、B想要的數據,於是去查數據庫,假設此時A拿到了鎖,正在查數據庫,剛好B這時到達,因為A拿到了鎖還沒有釋放,導致B加鎖失敗,於是B睡眠然後等著查緩存。這時候A查完了數據,也更完了緩存,返回了正確的數據,過了一會,B睡醒了,但緩存中依然沒有B想要的數據,於是返回了null。
下面來複現一下這種情况,為了模擬這種並發情况,我們在查數據庫時也Thread.sleep()一下,模擬鎖還沒釋放,又有其他非同參的請求到達。
那使用ReentrantLock是不是無法解决緩存擊穿呢,倒也不是,可以維護一個ConcurrentHashMap,以方法名和請求參數為key,如果key存在數據且無法用已經存在的鎖成功加鎖,說明已經有其他相同請求線程在讀數據庫,然後就可以睡眠,稍後去讀緩存,如果沒有數據,則新建一把鎖,加鎖後讀數據庫、刷緩存。
方法三
用synchronized關鍵字加鎖。
public ResponseDTO getRoleById(Long id) throws Exception {
String key = "User:Role:" + id;
List<Long> data = (List<Long>) redisUtil.get(key);
if(data == null) {
synchronized (this) {
data = (List<Long>) redisUtil.get(key);
if(data != null) {
return new ResponseDTO(Status.SUCCESS.code, "", data);
}
data = userMapper.selectRoleIdByUserId(id);
Long buffTime = (long) new Random().nextInt(30) * 60;
redisUtil.set(key, data, buffTime);
}
}
return new ResponseDTO(Status.SUCCESS.code, "", data);
}
這種方法可以解决緩存擊穿問題,但會影響系統性能。比如此時緩存中無數據,然後有1000個請求同時到來,然後這1000個線程開始爭奪鎖,一個加鎖成功,去查了數據庫,更新緩存,剩下999個在等待鎖釋放再搶鎖,然後此時即使緩存有數據,這999個也是挨個加鎖、讀緩存,變成了一種串行執行,而不是並行讀緩存。只有接下來到達的其他請求,才是並行去讀取緩存。
方法四
使用分布式鎖。(個人認為這才是最佳解决方案)
public ResponseDTO getRoleById(Long id) throws Exception {
String key = "User:Role:" + id;
List<Long> data = (List<Long>) redisUtil.get(key);
if(data == null) {
// 加鎖,並設置過期時間為 30s,即超過30s自動解鎖
if(redisUtil.setNx("getRoleById:" + id, 30L)) {
data = userMapper.selectRoleIdByUserId(id);
Long buffTime = (long) new Random().nextInt(30) * 60;
redisUtil.set(key, data, buffTime);
redisUtil.remove("getRoleById:"+id); // 解鎖
} else {
// 輪詢五次,每次間隔 100 ms 此處為例子,具體策略有具體情况定
int count = 0;
while(data == null && count < 5) {
Thread.sleep(100);
data = (List<Long>) redisUtil.get(key);
count++;
}
}
}
return new ResponseDTO(Status.SUCCESS.code, "", data);
}
這種類似於ReentrantLock + ConcurrentHashMap解决方案,不同類請求加不同鎖,同類請求加鎖失敗就等待讀緩存。既保證了緩存無數據時到達的請求可以並發訪問更新後的緩存,又保證了不同參數的請求能讀到正確數據。
如果是單機部署,可以使用synchronized或者ReentrantLock + ConcurrentHashMap解决,但用synchronized會導致部分請求串行,性能較低。
如果要分布式部署,使用單機鎖也可以,畢竟部署幾十臺幾百臺,這點並發量數據庫還是扛得住的,但顯然使用分布式鎖更合適。
如果目前是單機部署,但考慮到未來可能會分布式部署,用redis做了緩存,那就用分布式鎖吧,畢竟欠下的技術債,總歸是要還的。
點擊直接資料領取
版權聲明
本文為[蘇州程序大白]所創,轉載請帶上原文鏈接,感謝
https://cht.chowdera.com/2022/134/202205141816338400.html
邊欄推薦
猜你喜歡
隨機推薦
- VMware虛擬機 之 NAT模式詳解
- 【Devops】kubernetes網絡
- 新式茶飲“拿捏”年輕人,“八馬茶業”們的出路在哪?
- 機器學習之金融風控
- 1.67版本vscode括號著色(Bracket Pair Colorizer)取消
- MySQL日期查詢使用的方法函數
- HugeGraph客戶端APP開發(一)
- [.Net]使用Soa庫+Abp搭建微服務項目框架(五):服務發現和健康監測
- 添加虛擬內存,不添加硬盤的方式
- Redis源碼學習(25),雙端鏈錶學習,adlist.h
- 虛幻5新特性之EnhancedInput
- 緩存命中錶示什麼?
- sencha touch 在線實戰培訓 第一期 第四節
- “我們從 Google 離職了”
- yolov5訓練測試與源碼解讀
- 原生JS 實現輪播圖效果
- 邏輯回歸 解决報錯:ValueError: Solver lbfgs supports only ‘l2‘ or ‘none‘ penalties, got l1 penalty.
- Oracle OCI 計算、存儲、網絡工具旨在降低雲複雜性
- Go項目實戰之日志必備篇[開源十年項目第11次更新]
- Shell脚本變量和運算符
- 聊聊找工作
- 是能力更是文化,談談IT系統的安全發布
- tensorflow學習筆記(五)
- vitest支持cjs的workaround(TypeScript產物commonjs場景)
- 並發編程系列之Lock鎖可重入性與公平性
- 淺談 Fiori Fundamentals 和 SAP UI5 Web Components 的關系
- RAM/FIFO學習回顧
- 最新版2022年任我行管家婆工貿版ERP M7 V22.0進銷存財務生產管理軟件網絡版——雲上的集團化制造管理系統
- 【機器學習05】LASSO回歸與ElasticNet(彈性網)
- Idea快捷鍵
- 關於創建模態窗口和非模態窗口的研究
- An End-to-End Steel Surface Defect Detection Approach via Fusing Multiple Hierarchical Features-閱讀筆記
- 【性能測試】第五篇 | Jmeter環境安裝
- Matplotlib使用指南,100個案例從入門到進階!(附源代碼)
- Dots + interval stats and geoms
- SIGIR2022 | 基於用戶價格偏好及興趣偏好的會話推薦
- Cloudreve自建雲盤實站:容量和速度自己來决定
- 利用騰訊雲函數搭建免費代理池
- Redis的安裝及基本數據類型
- js輪播圖效果,透明度漸變實現