我们都知道memcahce是解决大并发量的很好一个工具,但是有一个问题值得注意,当突然有很多用户请求我们的某个页面,请求的参数相同,但对应的值却在memcahce中没有,此时这些请求都将会去查询数据库操作,瞬间对数据库照成了很大的压力。
下面我做了一个小例子来模拟这种情况。
原页面代码:
connect("127.0.0.1",11211) or die("connect failed");
$key = $_GET['key'];
if($result = $memcache->get($key)) {
$s = "get result form memcache: ".$key."/".$result."\n";
file_put_contents("/var/www/html/log1.txt",$s,FILE_APPEND);
echo $s;
}else {
//模拟查询数据库
sleep(5); //休眠5秒
$result = mt_rand(1,100);
$memcache -> set($key,$result,0,3600);
$s = "get result not from memcache: ".$key."/".$result."\n";
file_put_contents("/var/www/html/log2.txt",$s,FILE_APPEND);
echo $s;
}
?>
此代码的意思是,我通过某个参数key来访问这个页面,如果此key在memcahce中存在,则在log1.txt中写入一条数据,如果不存在则休眠5秒(模拟查询数据库),在log2.txt中写入一条数据,同时将此key得到的结果放入memcahce中。
这里值得注意的是我在连接memcahce时候加了一句“or die(“connect failed”);”,即当连接报错时退出,因为有可能当memcahce的连接数超出了它本身限制的次数后,导致连接不上,从而会执行会执行else中的语句(而非memcahce中无值)。
测试并发
1) 使用ab命令测试
# ab -c 200 -n 30000 http://192.168.1.78/complicateTest.php?key=abcde
以上命令表示200个并发请求30000次
可以看到在log2文件中有许多数据,说明查询了很多次数据库:
可以使用对数据库查询加锁来处理,即在查询数据的时候,先在缓存中存一个值K,其他客户端需要查询数据库的时候要先判断K是否存在,若存在则休眠,如下:
connect("127.0.0.1",11211) or die("connect failed");
$key = $_GET['key'];
getResult($memcache,$key);
function getResult($memcache,$key) {
if($result = $memcache->get($key)) {
$s = "get result form memcache: ".$key."/".$result."\n";
file_put_contents("/var/www/html/log1.txt",$s,FILE_APPEND);
echo $s;
}else {
$tmp_key = "_temp_".$key;
if($memcache -> add($tmp_key, "temp key",0,60)) {
sleep(5);
$result = mt_rand(1,100);
$memcache -> set($key,$result,0,3600);
$s = "get result not from memcache: ".$key."/".$result."\n";
file_put_contents("/var/www/html/log2.txt",$s,FILE_APPEND);
echo $s;
$memcache -> delete($tmp_key);
}else {
sleep(3);
file_put_contents("/var/www/html/log3.txt","sleep...\n",FILE_APPEND);
getResult($memcache, $key);
}
}
}
?>
此时我们再可以观察log2.txt中的日志,发现只有一条的,即只有一条是从数据库中查询的数据。
但如果sleep的线程如果太多的话,也可能造成进程阻塞,
另一种方法是,将数据设置在memcache的时间为永久性的,在数据中设置一个过期时间,每次获取数据时判断是否过期,若过期则加锁重新查询数据库。加锁失败则返回旧数据,但是这样做的前提是要确保memcache有这个数据,若没有则还是会返回空。所以事先需要先将用户可以查询到的数据都放到内存中,但遇到内存突然重启就会出现返回空数据的情况。
connect("127.0.0.1",11211) or die("connect failed");
$key = $_GET['key'];
print_r(getResult($memcache,$key));
function getResult($memcache,$key) {
$result = array();
if($result = $memcache->get($key)) {
if($result["expired"] > strtotime(date("Y-m-d H:i:s"))) {
//memcache中有数据,且未过期,返回原数据
return $result;
}else {
return lockData($result,$memcache,$key);
}
}else {
//memcache中无数据
return lockData($result,$memcache,$key);
}
}
//加锁,查询数据库
function lockData($result,$memcache,$key) {
$tmp_key = "_temp_".$key;
if($memcache -> add($tmp_key, "temp key",0,60)) {
sleep(5);
$result["data"] = mt_rand(1,100);
$result["expired"] = strtotime(date("Y-m-d H:i:s")." +1 hour");
$memcache -> set($key,$result,0,0);
$memcache -> delete($tmp_key);
}else {
//若memcache中没有数据则会返回空,若有值则是返回旧数据
}
return $result;
}
?>
比较好的方式是使用gearmand来处理并发,gearmand可以确保相同的请求只执行一次,gearmand的介绍可以参考这里
1) gearmand安装
#yum install libdrizzle libdrizzle-devel
#rpm -ivh http://dl.iuscommunity.org/pub/ius/stable/Redhat/6/x86_64/epel-release-6-5.noarch.rpm
#yum install gearmand
2) 安装php的gearmand扩展
首先要先确定有phpize,如果没有可以使用:
#yum install php-devel
#wget http://pecl.php.net/get/gearman-1.1.0.tgz
#tar zxf gearman-1.1.0.tgz
#yum install libgrearman-devel.x86_64
#yum install re2c
#cd gearman-1.1.0
#/usr/bin/phpize
#./configure
#make && make install
修改php.ini文件,加入
extension=gearman.so
3) 启动gearman
#/etc/init.d/gearman start
#service httpd restart
代码示例:
服务端gearmanWorker.php:
addServer("127.0.0.1", 4730);
$worker->addFunction("querydb","querydb");
while ($worker->work());
function querydb($job) {
$key = $job -> workload();
sleep(5);
$result = mt_rand(1,100);
$s = "get result not form memcache: ".$key."/".$result."\n";
file_put_contents("/var/www/html/log2.txt",$s,FILE_APPEND);
return $result;
}
?>
客户端complicateTest.php:
connect("127.0.0.1",11211) or die("connect failed");
$client= new GearmanClient();
$client->addServer("127.0.0.1", 4730);
$key = $_GET['key'];
echo getResult($memcache,$key,$client);
function getResult($memcache,$key,$client) {
if($result = $memcache->get($key)) {
//从缓存中取
$s = "get result form memcache: ".$key."/".$result."\n";
file_put_contents("/var/www/html/log1.txt",$s,FILE_APPEND);
}else {
//memcache中无数据
$result = $client -> doNormal("querydb",$key,$key);
$memcache -> set($key, $result,0,3600);
}
return $result;
}
?>
首先启动服务端
#nohup /usr/bin/php gearmanWorker.php > log.txt 2>&1 &
使用ab命令测试:
ab -c 200 -n 10000 http://192.168.1.78/complicateTest3.php?key=sc842da
可以发现log2.txt中也只有一条记录,即只有一次从数据库中取,其他的都是从缓存中获取。