之前线上发生过缓存设计导致系统宕机的事故,让我很奇怪的是,已经缓存了数据,还是导致数据库cpu过高,让我很是奇怪,代码是这样的:
public List<Msg> getMsgList() {
String json = redis.get("cachekey");
if (!StringUtils.isEmpty(json)) {
return new Gson().fromJson(json, new TypeToken<List<Msg>>(){}.getType());
} else {
List<Msg> list=db.getMsgList();
if (!CollectionUtils.isEmpty(list)) {
redis.set(key,new Gson.toJson(list));
redis.expire(key,60);
}
return list;
}
}
上面这段代码是大多数缓存应用场景的设计,优先读取缓存,如果缓存过期,那么从db加载到cache,然后设置过期时间。那么有什么问题呢?上面这个方法的应用场景说下,从数据库中读取最后300条留言数据做列表展示,因为做了一个活动,导致人数过多,每秒大概2w请求,这样导致的问题是,在缓存失效的那一瞬间,大量的请求直接到了db,会直接导致数据库大量的计算,导致cpu负荷过高。在请求并发上来的时候,直接导致数据库挂了。
所以在大量并发请求的情况下和初始化非常占据资源的情况下是不适合用类似的方式做缓存设计的。我之前读过memcache的mutex设计,所以我第一时间想到的是mutex的设计思想,这个思想的思路是这样的,如果缓存中的数据已过期,那么设置一个额外的key来做过度,在这个过度时间,去做db到缓存的初始化。redis的实现代码大致是这样:
public String getMsgList(){
String json=redis.get("cachekey");
if (json==null) {
if(redis.setnx("mutexKey","1")==1){
redis.expire("mutexKey",30);
String value=db.get();
redis.set(key,value);
redis.expire("cachekey",60*1000);
redis.del(mutexKey);
return value;
} else {
Thread.sleep(50);
return redis.get("cachekey");
}
} else {
return json;
}
}
这个思路来自于:tim老师的mutextKey设计 ,因为redis本身是单线程的,所以setnx命令能够保证只有一个线程进入.
第二个思路是在新增数据的入口处,维护一个队列,因为业务中是获取最新的留言信息,所以非常适合用队列来做,每次新增数据,往队列新增数据,如果超出长度,对队列进行修剪:
public void pushList(String msg){
int maxCount=10;
int length=redis.llen("cachekey");
if(length>=maxCount){
redis.ltrim(0,maxCount-1);
}
redis.lpush("cachekey",msg);
}
这样读取的时候,只需要使用lrange(0,-1)命令就可以读取到最新的信息了,这种方法也避免掉了,大量并发的时候,大量请求打到db上。实际应用中,我使用了第二种思路,用队列处理。这两种思路都是可以的,当我第一次在tim老师的博客中看到第一种思路的时候,简直惊叹这种设计,原来有这么优秀的设计,可以避免db初始化耗资源的问题。
tim老师还有很多优秀的文章都值得阅读,众所周知新浪微博的用户数量非常的大,很多问题其实我们自己平时可以去考虑这其中的实现,比如关注列表,粉丝列表怎么实现,有些大v有几千万的粉丝,这种设计如何去做才能最优,消息的推送如何去做,千万级的粉丝的推送问题如何优化,是主动推还是拉取。
微博个人主页关注列表的动态如何设计优化,还有每条动态的评论,内容。社会上热门事件可能导致大量的围观。标签下的内容匹配设计等等,都是非常有挑战的设计。
网友评论