[文章作者:孙立 链接:http://www.cnblogs.com/sunli/ 更新时间:2010-07-27]
上周六去参加了csdn举办的TUP活动,最后一场的Tim Yang讲的《微博cache设计谈》,个人觉得讲得非常好和非常到位,其中有两点非常感同身受,就是内网流量问题和cache的key mutex问题导致大量请求穿透到db。后他又写了一篇博客《Memcache mutex设计模式》阐述这个问题。关于cache的key mutex问题在我的开发中叶碰到过很多,这里也就谈下我的解决办法。问题的产生如下图:
图一:key mutex问题
我这里指的cache不局限于memcached,事实上,key mutex在很多场合都是需要的,比如mysql中的innodb中说的机遇记录集的锁也可以当成key mutex。
http反向代理服务器cache的key mutex问题
我在09年为一个当时性能不是很高,但是短时间又难于优化的一个系统开发了一个反向代理服务器ICProxy(类似于squid,varnish等,只是增加了一些自己的业务逻辑)来提高系统的性能。但是很快问题就来了,
在访问高峰期,比如
- 一个热门的新闻页面,设置缓存5分钟
- 5分钟后,会有大量的请求在同一时间得到缓存失效的标识
- 大量请求并同时都穿透到后端web服务器请求加载数据到缓存,造成后端频繁宕机
$cache = IFengMem :: getInstance ( ' bbsmemcache ' );
// 获取缓存中的数据
$result = $cache -> get ( $cacheKey );
if ( $result === false ) {
require_once ' class/class.phplock.php ' ;
// 初始化锁,$cacheKey必须传入,用于内部的hash
$lock = new PHPLock ( ' locks/ ' , $cacheKey );
$lock -> startLock ();
// 加锁
$lock -> Lock ();
// 进入锁后,查询一次缓存看是否已经有数据了
$result = $cache -> get ( $cacheKey );
// 如果还是没有数据,表示是自己最先进去到锁的,查询数据库并加载到缓存
if ( $result === false ) {
$query = $db -> query ( $sql );
$result = $db -> fetchArray ( $query );
$cache -> set ( $cacheKey , $result , $cacheTime );
}
// 释放锁
$lock -> unlock ();
$lock -> endLock ();
}
?>
phplock你可以在 http://code.google.com/p/phplock/获取到。
在php应用中,特别是基于nginx等fast-cgi的应用,你应该尽量优化你的查询,让其尽量的快,缩短锁等待,不然不管是穿透到后端db还是在锁等待,都会导致系统负载的急剧上升。你因该根据你系统的业务情况,看是否可以后台线程来更新缓存。
注意
在以上的场景中,锁的利用实际上是使用了分离锁,也即是把大量需要进行锁的资源hash到指定的数目锁上,避免过多的锁,也即不是直接进行全局的lock(),这样会降低系统的吞吐量,在phplock中,有个$hashNum可以设置可以hash桶数目,也即可以同时进行访问的key的数量,这样也避免了大量的不同的key的穿透到数据库,phplock是单机版本的,对于多台机器使用的话,可能会有最大机器数的请求穿透的服务器,如果你的web服务器不是非常多的话,也不会有什么问题。
key mutex的延伸应用
在很多系统中,我们要对某一个资源进行一个比较耗时的操作时,比如图片下载,网络请求,数据查询,搜索等,由于在大并发下,有大量的请求到同一个资源和不同的资源,那么key mutex的场景都是适用的。当你要开发一个nosql数据库的时候,你必须要考虑这个问题。
比如你要开发一个类似于memcached的increment的数据累加器,
你可能不希望
int num = storage.get(key);
storage.set(key,num + 1 );
lock.unlock();
而是如下的方式来提高吞吐量
int num = storage.get(key);
storage.set(key,num + 1 );
lock.unlock(key);
这个比较类似于mysql中的mysiam和innodb的表锁和行锁的概念,显然行锁的并发能力比表锁高很多。