緩存操作
讀緩存
讀緩存可以分為兩種情況命中(cache hit)和未命中(cache miss):
緩存命中
- 首先從緩存中擷取資料
- 将緩存中的資料傳回
緩存未命中
- 此時緩存未命中,從資料庫擷取資料
- 将資料寫入緩存
- 傳回資料
讀緩存的的處理由緩存中有沒有資料? 決定,如果緩存中有資料那就是緩存命中,如果沒有那就是緩存未命中:
寫緩存
寫緩存可以分為
更新緩存
和
删除緩存
。
更新緩存
更新緩存時需要分兩種情況:
- 更新簡單資料類型(如string)
- 更新複雜資料類型 (如hash)
對于
簡單資料類型
可以直接更新緩存,如果是複雜資料類型會增加額外的更新開銷:
- 從緩存中擷取資料
- 将資料序反列化成對象
- 更新對象資料
- 将更新後的資料序列化存入緩存
對複雜資料緩存的更新最少需要4步,而且每次寫資料時都需要更新緩存,這樣對讀緩存較少的場景,可能更新資料7-8次讀緩存才發生一次想想就劃不來,另外每次更新緩存時都要對緩存資料進行計算,很明顯寫資料時計算緩存資料然後再更新緩存是沒必要的,可以将緩存的更新,推遲到讀緩存(緩存未命中)時。
删除緩存
删除緩存也稱為淘汰緩存,删除緩存的操作非常簡單的,直接将緩存從緩存庫中的删除就可以了。
緩存操作順序
緩存一般都是配合資料庫一起使用,從資料庫中擷取資料然後再更新緩存。為什麼要讨論緩存操作的順序呢?因為在有些情況下不同的操作順序會産生不一樣的結果,常見的操作順序可以分為:
- 先資料庫,再緩存
- 先緩存,再資料庫
不管是哪種順序都要經過資料庫、緩存兩步操作,這兩操作不是一個原子性的操作在一些情況會出現資料不一緻問題。下面來分别說明不同的順序所帶的資料不一緻、并發等問題。
先資料庫後緩存
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5yMxQDMlRmN3EjN2YjY3MDMzQjYjV2YzkDN3YmZ1QGZw8CX5IzLcVDMxIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjL4M3Lc9CX6MHc0RHaiojIsJye.png)
如上圖先将資料寫入資料庫,然後再去更新或删除緩存。兩個步驟1、2都可能失敗,如果是第一步失敗可以通過抛出業務異常,業務調用方捕獲異常資訊進行處理,因為這個時候并沒有操作緩存可以了解為寫資料庫失敗了。
如果是第一步成功(寫資料庫成功),然後再操作緩存的時候失敗,這裡有兩種情況:
- 資料庫復原:如果是業務需要保證緩存與資料庫強一緻性時,可以抛出業務異常給調用方。
- 不作處理:與
相反,業務可以接受在緩存過期時間達到之前,緩存與資料庫允許資料不一緻。資料庫復原
舉個例子,假設有一個字元串資料類型的緩存資料,它的key為
name
并且現在資料庫和緩存中的值都是
arch-digest
String name = "arch-digest";
現在要将
name
的值更新成
juejin
,按照先資料庫後緩存的順序:
//将name的值更新為juejin
public void update(String name){
db.insert(...); //更新資料庫
cache.delete(name); //更新緩存
}
正常情況下
db.insert(...)
cache.delete(name)
都執行成功沒有異議。如果是一些其他原因
cache.delete(name)
執行失敗,那資料庫中的值是更新後的值
juejin
,而緩存中的資料還是
arch-digest
這樣在下次讀取緩存的時候拿到的值就是
arch-digest
public String getNameFromCache(String name){
String value = cache.get(name); //從緩存中擷取資料
...
return value;
}
讀取緩存的時候在
getNameFromCache
方法中,如果
name
緩存沒有過期那會一直拿到
arch-digest
,這樣情況就會導緻使用者看到的資料不一緻。
先緩存後資料庫
先緩存後資料庫和之前說到的先資料庫後緩存差不多除了會可能導緻資料不一緻外,還會有并發問題。
如上面現在是更新資料,如果是在
更新資料庫
的時候失敗會發生什麼呢?這裡要根據緩存的操作分兩種情況:
- 更新緩存:更新緩存資料,緩存中為最新資料,資料庫中是老資料,下次讀取時會拿到緩存中的新資料(資料不一緻)。
- 删除緩存:删除緩存中的資料,下次讀取時從資料庫中擷取(資料一緻)。
更新緩存
删除緩存
操作上面已經介紹過了,不多做解釋了。很明顯關于
更新緩存
删除緩存
在這種情況先
删除緩存
更合适,沒有資料不一緻的問題,但是在使用
删除緩存
時也要注意會引發并發問題:
- 線程A删除緩存成功
- 線程B讀取緩存未命中
- 線程B從資料庫中擷取資料
- 線程B将資料庫中的資料寫入緩存
- 線程A寫入資料庫成功
在高并發場景下,緩存和資料庫資料不一緻的情況還是會出現。那要解決資料庫和緩存的資料一緻性有哪些解決方案呢?
資料一緻性優化方案
這裡說的是
優化方案
不是解決方案哦,因為在分布式環境下事務是個難題,現在也沒有好的解決方案。隻能找到最适合業務的優化方案,使資料不一緻的可能性或延遲降到一個業務可接受的範圍内。
常見的幾種優化方案可以包括:
- 不處理
- 延時雙删
- 訂閱Binglog
3 種方案從簡單到複雜,可以根據業務需要選擇最合适的優化方案。
不處理是最簡單的方式了,即資料庫與緩存中的資料不一緻時在業務允許的情況下不做處理。雖然有點不合适,但是很香!
延時雙删可以用來優化在先緩存後資料庫中的并發問題:
- 線程A休眠1秒然後删除緩存
這種方案增加第6步,寫入資料庫完成後使寫入線程休眠1秒,然後再将緩存資料删除掉,使其他線程再次讀取資料時導緻緩存未命中從資料庫擷取資料并更新緩存。
這個1秒怎麼确定的,具體該休眠多久呢?
針對上面的情形,應該自行評估自己的項目的讀資料業務邏輯的耗時。然後寫資料的休眠時間則在讀資料業務邏輯的耗時基礎上,加幾百ms即可。這麼做的目的,就是確定讀請求結束,寫請求可以删除讀請求造成的緩存髒資料。
采用這種同步淘汰政策,吞吐量降低怎麼辦?
第二次删除作為異步的。自己起一個線程,異步删除。這樣,寫的請求就不用沉睡一段時間後了,再傳回。這麼做,加大吞吐量。
binlog訂閱
使用binlog訂閱,這樣一旦MySQL中産生了新的寫入、更新、删除等操作,就可以把binlog相關的消息推送至Redis,Redis再根據binlog中的記錄,對Redis進行更新。
其實這種機制,很類似MySQL的主從備份機制,因為MySQL的主備也是通過binlog來實作的資料一緻性。
這裡可以結合使用canal(阿裡的一款開源架構),通過該架構可以對MySQL的binlog進行訂閱,而canal正是模仿了mysql的slave資料庫的備份請求,使得Redis的資料更新達到了相同的效果。
當然,這裡的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等來實作推送更新緩存。
推薦閱讀:
- 大廠面試必問的volatile關鍵字,這一篇文章搞定!
- 萬變不離其宗,高并發秒殺系統的設計思考!
- 技術總監的反思錄,我是如何失去團隊掌控的?
- 假如有人今天把支付寶的存儲伺服器炸了,支付寶裡的錢是不是就沒了。。。
- 再也不用擔心被虐啦,高頻率JVM面試題,都在這裡!
- 餓了麼千萬級交易系統的重構設計思路
- 支付系統高可用架構設計實戰,可用性高達99.999!
- 大型網際網路公司分布式ID方案總結