缓存穿透,是指查询一个数据库一定不存在的数据。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果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} ->
% 将结果发给所有的请求者,CPU不会空跑
[ gen_server:reply(From,V) || From<-L],
Req2 = lists:keydelete(Key,1,Req),
{noreply,State#state{req = Req2}};
false ->
{noreply,State#state{}};
相对上面而言主要节约了2点
- 自旋锁带来的CPU消耗
- 在获得分布式锁之后还要去缓存查一下,还有释放分布式锁,2次IO消耗
4.总结
合理利用消息传递机制,可以很轻松的解决一些经典问题。或许这就是一种范式吧。
网友评论