面試題:什么是緩存擊穿、緩存穿透和緩存雪崩?它們分別會帶來什么危害?該如何解決和預防?
一、面試官:請說說看什么是緩存擊穿,他會帶來什么危害,以及該如何解決?
1.緩存擊穿
(1) 定義:緩存擊穿是指在高并發(fā)訪問下,某個熱點數(shù)據(jù)的緩存過期失效,而此時恰好有大量并發(fā)請求訪問該數(shù)據(jù),導致這些請求直接繞過緩存,訪問后端數(shù)據(jù)庫或存儲系統(tǒng),使數(shù)據(jù)庫或存儲系統(tǒng)負載急劇增加,甚至可能引發(fā)系統(tǒng)崩潰的現(xiàn)象。
(2) 危害:
- 數(shù)據(jù)庫壓力增大:大量請求直接訪問數(shù)據(jù)庫,可能導致數(shù)據(jù)庫負載過高,響應時間延長,甚至引發(fā)數(shù)據(jù)庫崩潰。
- 系統(tǒng)性能下降:由于數(shù)據(jù)庫處理請求的速度遠低于緩存,因此緩存擊穿會導致系統(tǒng)整體性能下降。
(3) 解決方案:
① 互斥鎖(Mutex)和分布式鎖(在分布式系統(tǒng)中):
在緩存失效時,使用互斥鎖機制確保只有一個請求能夠訪問數(shù)據(jù)庫并更新緩存,其他請求則等待鎖釋放后從緩存中獲取數(shù)據(jù)。
下面是一個使用 Redis 分布式鎖和 Redis 事務來解決緩存擊穿問題的具體代碼示例(Python實現(xiàn))。
import redis
import time
import uuid
# 連接到 Redis 服務器
redis_client = redis.StrictRedis(host='localhost', port=6379, db=0)
# 設置分布式鎖的鍵和過期時間(秒)
LOCK_KEY = 'cache_擊穿_lock'
LOCK_EXPIRE = 10 # 鎖的有效期,可以根據(jù)需要調整
# 緩存的鍵和值(示例)
CACHE_KEY = 'some_hot_data'
def acquire_lock(redis_client, lock_key, lock_value, expire):
"""
嘗試獲取分布式鎖
:param redis_client: Redis 客戶端
:param lock_key: 鎖的鍵
:param lock_value: 鎖的值(通常是唯一標識符)
:param expire: 鎖的過期時間(秒)
:return: 是否成功獲取鎖
"""
while True:
# 嘗試設置鎖,NX 表示只有鍵不存在時才設置,PX 表示過期時間(毫秒)
result = redis_client.set(lock_key, lock_value, nx=True, px=expire * 1000)
if result:
return True
# 休眠一小段時間后重試,避免忙等待
time.sleep(0.01)
def release_lock(redis_client, lock_key, lock_value):
"""
釋放分布式鎖
:param redis_client: Redis 客戶端
:param lock_key: 鎖的鍵
:param lock_value: 鎖的值(必須是獲取鎖時使用的相同值)
:return: 是否成功釋放鎖
"""
# 使用 Lua 腳本確保原子性釋放鎖
lua_script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
redis_client.eval(lua_script, 1, lock_key, lock_value)
return True
def get_data_with_cache_and_lock(redis_client, cache_key):
"""
使用緩存、分布式鎖和 Redis 事務獲取數(shù)據(jù)
:param redis_client: Redis 客戶端
:param cache_key: 緩存的鍵
:return: 數(shù)據(jù)值或 None(如果數(shù)據(jù)不存在)
"""
# 嘗試從緩存中獲取數(shù)據(jù)
cache_value = redis_client.get(cache_key)
if cache_value is not None:
return cache_value.decode('utf-8') # 假設數(shù)據(jù)是字符串類型
# 嘗試獲取分布式鎖
lock_value = str(uuid.uuid4())
if acquire_lock(redis_client, LOCK_KEY, lock_value, LOCK_EXPIRE):
try:
# 使用 Redis 事務確保原子性
pipe = redis_client.pipeline(True)
try:
# 嘗試再次從緩存中獲取數(shù)據(jù)(防止其他客戶端在獲取鎖后更新了緩存)
pipe.watch(cache_key)
cache_value = pipe.get(cache_key)
if cache_value is not None:
pipe.unwatch()
pipe.reset()
return cache_value.decode('utf-8')
# 從數(shù)據(jù)庫中獲取數(shù)據(jù)(模擬)
# 在實際應用中,這里應該是訪問數(shù)據(jù)庫的邏輯
data_from_db = "data_from_db" # 假設從數(shù)據(jù)庫中獲取的數(shù)據(jù)
# 更新緩存
pipe.multi()
pipe.set(cache_key, data_from_db)
pipe.execute()
# 返回從數(shù)據(jù)庫中獲取的數(shù)據(jù)
return data_from_db
except redis.WatchError:
# 如果在事務執(zhí)行過程中,緩存被其他客戶端更新,則重新嘗試獲取數(shù)據(jù)
pass
finally:
# 釋放鎖
release_lock(redis_client, LOCK_KEY, lock_value)
# 如果無法獲取鎖或緩存仍然為空,則返回 None(或根據(jù)業(yè)務邏輯返回默認值)
return None
# 示例調用
data = get_data_with_cache_and_lock(redis_client, CACHE_KEY)
print(f"獲取的數(shù)據(jù): {data}")
② 熱點數(shù)據(jù)永不過期:
對于重要的熱點數(shù)據(jù),可以設置其永不過期,以避免緩存過期引發(fā)的擊穿問題。
但需要注意數(shù)據(jù)更新時的及時性和準確性,以及可能帶來的內存占用問題。
③ 提前異步刷新緩存:
在緩存即將過期之前,通過定時任務或后臺線程提前異步加載緩存數(shù)據(jù),確保在緩存失效之前已經(jīng)有新的數(shù)據(jù)加載到緩存中。
二、面試官:再說說看什么是緩存穿透,如何檢測是否存在緩存穿透,以及該如何解決?
緩存穿透是指查詢一個一定不存在的數(shù)據(jù),由于緩存是不命中時需要從數(shù)據(jù)庫查詢,查不到數(shù)據(jù)則不寫入緩存,這將導致這個不存在的數(shù)據(jù)每次請求都要到數(shù)據(jù)庫去查詢,造成緩存穿透。
如果數(shù)據(jù)庫查詢不到這條數(shù)據(jù),則不會寫入緩存,這將導致這個不存在的數(shù)據(jù)每次請求都會去查詢數(shù)據(jù)庫,對數(shù)據(jù)庫造成很大的壓力。
緩存穿透通常是由惡意用戶或攻擊者請求不存在于緩存和后端存儲中的數(shù)據(jù)來發(fā)起的攻擊。
一般來說,緩存穿透一開始會由開發(fā)者發(fā)現(xiàn)系統(tǒng)接口變慢或監(jiān)控告警發(fā)覺,再檢測系統(tǒng)日志證實。檢查數(shù)據(jù)庫訪問日志和緩存訪問日志,查看是否存在大量對不存在的鍵的查詢。這些查詢如果頻繁發(fā)生,那么很可能是緩存穿透。
另外,監(jiān)控緩存的命中率。如果命中率突然下降,且伴隨著數(shù)據(jù)庫訪問量的增加,這可能是緩存穿透的征兆。
再者,可以分析系統(tǒng)接收到的請求參數(shù),特別是那些明顯不符合業(yè)務邏輯的非法參數(shù)。
以下是兩種防止緩存穿透的策略:
1.布隆過濾器(Bloom Filter)
布隆過濾器是一種空間效率很高的數(shù)據(jù)結構,它利用多個哈希函數(shù)來將一個元素映射到一個位數(shù)組的多個位中。當查詢一個元素時,它會檢查對應的位是否都為1,如果是,則認為元素可能存在(注意是可能存在,因為存在哈希沖突的情況),否則認為元素一定不存在。
在緩存穿透的場景中,可以在查詢緩存之前先使用布隆過濾器檢查元素是否存在。如果布隆過濾器認為元素不存在,則直接返回一個錯誤信息或默認值,而不去查詢數(shù)據(jù)庫。這樣可以有效減少對數(shù)據(jù)庫的無效查詢。
布隆過濾器的缺點:
- 誤判率:布隆過濾器通過多個哈希函數(shù)將元素映射到位數(shù)組中,因此存在哈希沖突的可能性。這意味著,當查詢一個元素時,布隆過濾器可能會誤判該元素存在(即位數(shù)組中對應的位都為1),而實際上該元素在數(shù)據(jù)庫中并不存在。雖然誤判率可以通過增加哈希函數(shù)數(shù)量和位數(shù)組長度來降低,但這也會增加計算復雜度和空間開銷。
- 刪除困難:布隆過濾器不支持直接刪除元素。如果要從布隆過濾器中刪除一個元素,需要將其對應的所有位都重置為0。然而,這可能會影響其他元素的判斷,因為多個元素可能共享同一個位。因此,在實際應用中,布隆過濾器通常用于只讀場景或需要頻繁查詢但很少更新的場景。
2.空值緩存(并不推薦):
對于那些查詢結果為空的數(shù)據(jù),也將其緩存起來,但設置一個較短的過期時間。這樣,當下次再次查詢這個不存在的數(shù)據(jù)時,可以直接從緩存中獲取空值,而不是去查詢數(shù)據(jù)庫。
控制緩存的缺點:
- 額外的內存消耗:當數(shù)據(jù)庫中不存在某個數(shù)據(jù)時,系統(tǒng)仍然需要將其作為一個空值(或特殊標記)緩存起來。這會導致緩存中存儲大量的空值或特殊標記,從而占用額外的內存空間。如果這類空值數(shù)據(jù)過多,會顯著影響緩存的存儲效率和性能。
- 數(shù)據(jù)不一致性:空值緩存的過期時間需要合理設置。如果過期時間設置得過長,當數(shù)據(jù)庫中實際數(shù)據(jù)發(fā)生變化(例如,原本不存在的數(shù)據(jù)被插入)時,緩存中的空值數(shù)據(jù)仍然有效,這會導致數(shù)據(jù)不一致的問題。相反,如果過期時間設置得過短,可能會頻繁觸發(fā)緩存失效和數(shù)據(jù)庫查詢,增加系統(tǒng)負擔。
- 難以維護:空值緩存需要額外的邏輯來處理過期時間和數(shù)據(jù)更新等問題。這增加了系統(tǒng)的復雜性和維護成本。同時,由于空值數(shù)據(jù)在緩存中的存在,也可能導致緩存污染和命中率下降等問題。
三、面試官:什么是緩存雪崩,緩存雪崩產(chǎn)生的常見原因有哪些?
緩存雪崩是指在分布式系統(tǒng)中,緩存中的大量數(shù)據(jù)同時失效或過期,導致大量請求直接訪問數(shù)據(jù)庫或后端服務,造成系統(tǒng)性能急劇下降甚至癱瘓的現(xiàn)象。
這種現(xiàn)象通常會對系統(tǒng)造成災難性的影響,因為它會導致后端數(shù)據(jù)庫或服務承受巨大的壓力,可能引發(fā)服務不可用或數(shù)據(jù)丟失等問題。
雪崩和擊穿、熱key的問題不太?樣的是,他是指?規(guī)模的緩存都過期失效了。
緩存雪崩產(chǎn)生的常見原因主要包括以下幾點:
- 緩存中大量key同時過期:如果系統(tǒng)中存在大量緩存數(shù)據(jù)的過期時間被設置為相同或相近,那么當這些緩存數(shù)據(jù)同時過期時,系統(tǒng)將無法從緩存中獲取數(shù)據(jù),轉而直接訪問數(shù)據(jù)庫。這將導致數(shù)據(jù)庫承受巨大的訪問壓力,可能引發(fā)性能下降或崩潰。
- 緩存服務器故障:緩存服務器作為系統(tǒng)中的關鍵組件,如果發(fā)生故障或宕機,將導致緩存數(shù)據(jù)無法被訪問。此時,系統(tǒng)同樣會轉向直接訪問數(shù)據(jù)庫,從而引發(fā)緩存雪崩。
- 系統(tǒng)壓力增大:在高并發(fā)或大規(guī)模用戶訪問的情況下,系統(tǒng)壓力會急劇增大。如果此時緩存無法有效承載這些請求,或者緩存的命中率顯著下降,那么大量請求將直接落到數(shù)據(jù)庫上,從而引發(fā)緩存雪崩。
為了預防緩存雪崩的發(fā)生,可以采取以下措施:
- 避免大量key同時過期:可以通過微調key的過期時間,使其有一定的相差間隔,從而避免大量key同時過期的場景。
- 使用緩存降級策略:在緩存失效或訪問壓力過大的情況下,可以啟動降級策略,如返回默認值或錯誤信息,以減少對后端系統(tǒng)的壓力。
- 緩存預熱:在系統(tǒng)啟動時,提前將部分或全部熱點數(shù)據(jù)加載到緩存中。
- 后備緩存:使用二級緩存(如本地緩存)作為后備,當主緩存失效時,可以從后備緩存中獲取數(shù)據(jù)。