緩存穿透,是指查詢一個數據庫一定不存在的數據。正常的使用緩存流程大致是邻眷,數據查詢先進行緩存查詢,如果key不存在或者key已經過期音同,再對數據庫進行查詢,并把查詢到的對象秃嗜,放進緩存瘟斜。如果數據庫查詢對象為空,則不放進緩存痪寻。
1.緩存擊穿的偽代碼
Value find(Key){
RedisKey = RedisKeyFunc(Key);
V = redisGet(RedisKey);
if (V != null){
// 命中緩存
return V;
}
V = dbLookUp(Key);
if (v != null){
redisPut(RedisKey,V);
}
// 如果沒有數據庫中更沒有數據,那么每次查找這條記錄就會去數據庫查詢一次虽惭,數據庫就像被攻擊一樣
return V;
}
2.傳統的應對緩存擊穿的處理方案
采用redis
全局鎖 + 自旋鎖
這樣子雖然是可以解決這個問題橡类,但是帶來的負面影響是空跑CPU
偽代碼:
Value find(Key){
RedisKey = RedisKeyFunc(Key);
V = redisGet(RedisKey);
if (V != null){
// 命中緩存
return V;
}
RedisLockKey = RedisLockKeyFunc(Key)
while(true){
if(redisLock(RedisLockKey)){
V = redisGet(RedisKey);
if( V == null){
// 從數據庫查詢放入緩存
V = dbLookUp(Key);
redisSet(RedisKey,V);
}
redisUnLock(RedisLockKey);
return V
}
// 這邊空跑消耗CPU
}
}
3.erlang actor
的模型
利用actor
的消息傳遞機制,很輕松的避免自旋鎖(空跑)
偽代碼:
handle_call({find,Key},From,#{req=Req}=State) ->
case lists:keyfind(Key,1, Req) of
{_,L} ->
Req2 = lists:keyreplace(Key,1,Req,{Key,[From|L]}),
{noreply,State#state{req = Req2};
false ->
Actor = self(),
% 新開一個proc去處理查詢芽唇,將結果放進緩存
erlang:spawn(fun()-> V = lookup(Key), Actor ! {Key,V}),
Req2 = [{Key,[From]}|Req],
{noreply,State#state{req = Req2};
handle_info({Key,V},#state{req = Req})->
case lists:keyfind(Key,1, Req) of
{_,L} ->
% 將結果發(fā)給所有的請求者顾画,CPU不會空跑
[ gen_server:reply(From,V) || From<-L],
Req2 = lists:keydelete(Key,1,Req),
{noreply,State#state{req = Req2}};
false ->
{noreply,State#state{}};
相對上面而言主要節(jié)約了2點
- 自旋鎖帶來的CPU消耗
- 在獲得分布式鎖之后還要去緩存查一下,還有釋放分布式鎖匆笤,2次IO消耗
4.總結
合理利用消息傳遞機制研侣,可以很輕松的解決一些經典問題∨谂酰或許這就是一種范式吧庶诡。