redis中提供了丰富的数据类型,如字符串(string)、列表(list)、哈希表(hash)、集合(set)及有序集合(Sorted set)等,可以实现各种有趣的应用,如使用有序集合,实现IP查找。在这里,我们进行一下比较,测试一下使用redis实现ip查找在php中的效率,并与使用二分查找IP方法进行比较。

一、准备

搭建测试环境,安装Redis 2.4.6、php 5.2.17以及phpredis扩展。

通常我们需要根据用户的ip地址,获得归属地信息,而ip库中的ip信息一般是这种格式:“1.12.0.0-1.15.255.255 北京方正宽带”,表示1.12.0.0-1.15.255.255这个段内的ip,是北京的ip,使用方正宽带上网。

针对这种情况,我们构造两个数组,格式分别是

$ipadd[0]->start="17563648";

$ipadd[0]->end="17825791";

以及

$ipinfo["17563648-17825791"]="北京方正宽带";

$ip数组中存放IP段起始、截止ip经过ip2long函数转换后的数值。根据ip,可以获得存放在$ipadd数组中对应的ip段(17563648-1782579),然后就可以在$ipinfo数组中唯一确定该IP所在地的信息“北京方正宽带”。在这里,就是测试根据ip查找对应的ip段的效率。

把$ipadd数组,保存到redis里面,保存成有序集合格式。其中ipaddress.dat中保存了完整的按照$ipadd格式组织的ip库信息,共有11万多条记录。

[root@localhost test]# more addredis.php<?php 
require'ipaddress.dat';$redis=newredis();
$redis->connect('127.0.0.1',6379);foreach ($ipaddas$ip) {
$redis->zadd('ip',$ip->start,'start:'.$ip->start.'-'.$ip->end);$redis->zadd('ip',$ip->start,'end:'.$ip->start.'-'.$ip->end);}
[root@localhost test]# php addredis.php

进入redis,查看集合情况

[root@localhost test]# telnet localhost6379Trying127.0.0.1...
Connectedtolocalhost.localdomain (127.0.0.1).
Escape characteris'^]'.zcard ip
:215706

可以看到,ip集合中有21万多条,包括了10万多条ip信息(实际上是11万多条,因为起始ip有重复,这里简单的认为起始ip就是唯一的)。

准备测试数据,采用某个网站的实际访问日志test.access_log,ip分布在全国各地。格式如下:

[root@localhost test]# tail test.access_log302"-"119.189.159.152test.com [07/Feb/2012:15:58:01+0800]"GET /test.php HTTP/1.0 0"0"84658813-1322969571-03601900"302"-"221.239.111.102test.com [07/Feb/2012:15:58:01+0800]"GET /test.php HTTP/1.0 0"0"48818100-1328600341-28058000"302"-"60.30.33.171test.com [07/Feb/2012:15:58:01+0800]"GET /test.php HTTP/1.0 0"0"69217158-1328601481-34381000"302"-"60.190.0.6test.com [07/Feb/2012:15:58:01+0800]"GET /test.php HTTP/1.0 0"0"58494331-1328593662-19393300"302"-"60.13.46.192test.com [07/Feb/2012:15:58:01+0800]"GET /test.php HTTP/1.0 0"0"74718190-1328442091-23296100"302"-"60.190.210.178test.com [07/Feb/2012:15:58:01+0800]"GET /test.php HTTP/1.0 0"0"93053412-1328583110-29350700"302"-"60.190.40.66test.com [07/Feb/2012:15:58:01+0800]"GET /test.php HTTP/1.0 0"0"90432067-1328596666-46225600"302"-"1.86.220.76test.com [07/Feb/2012:15:58:01+0800]"GET /test.php HTTP/1.0 0"0"74104724-1325918365-59565400"302"-"61.189.53.66test.com [07/Feb/2012:15:58:01+0800]"GET /test.php HTTP/1.0 0"0"33954701-1328587981-80413400"302"-"121.15.174.113test.com [07/Feb/2012:15:58:01+0800]"GET /test.php HTTP/1.0 0"0"52170030-1328594341-01346100"

测试记录条数:200000

[root@localhost test]# cat test.access_log |wc-l200000${PageNumber}

二、程序代码

准备在php中,读取test.access_log中的文件,通过正则表达式,获得其中的ip信息,然后使用ip2long转换为整型,再使用redis获得该ip所在的ip段起止范围。同时,我们实现一个二分查找ip的起止范围函数get_ip,与使用redis查找的方法运行时间进行比较。

get_ip函数与计时函数:

[root@localhost test]# more func.php<?phpfunctionget_ip ($ipadd,$ip) {
$start=0;
$end=count($ipadd)-1;while($start<=$end) {
$index=intval(($start+$end)/2);if($ipstart) {
$end=$index-1;
}elseif($ip>$ipadd [$index]->end) {
$start=$index+1;
}else{//$ipadd [$index]->end>=$ip&&$ipadd [$index]->start<=$ip
return ("i". $ipadd [$index]->start ."p". $ipadd [$index]->end);
}
}
return"Unknown";
}functionmicrotime_float()
{
list($usec, $sec)=explode("", microtime());
return ((float)$usec+(float)$sec);
}

逐条读取日志文件,对其中每一条根据正则表达式获得ip信息,然后再在redis中获得对应的ip段。同时记录每一步操作的时间。

testredis.php脚本

[root@localhost test]# more testredis.php<?php 
require'ipaddress.dat';//ip地址数组require_once('func.php');$redis=newredis();
$redis->connect('127.0.0.1',6379);$filename='test.access_log';echo"二分法开始\n";
$time1=0;
$handle=@fopen($filename,'r');while(!feof($handle)){
$log=fgets($handle,4096);
$t1_start=microtime_float();if(preg_match('/^\d+ \".*\" (.*) test\.com \[.*\] \"GET \/test.php .* \".*\"/i',$log,$arr_log)){$time_start=microtime_float();
$pieces=get_ip($ipadd,ip2long($arr_log[2]));
$time_mid=microtime_float();
$time1+=$time_mid-$time_start;
}
$t1_stop=microtime_float();
$t1+=$t1_stop-$t1_start;
}
fclose($handle);
echo"get_ip使用时间: $time1 seconds\n";
echo"二分法总使用时间: $t1 seconds\n";
echo"二分法结束\n";
echo"Redis 开始\n";
$time2=0;
$handle=@fopen($filename,'r');while(!feof($handle)){
$log=rawurldecode(fgets($handle,4096));
$t2_start=microtime_float();if(preg_match('/^\d+ \".*\" (.*) test\.com \[.*\] \"GET \/test.php .* \".*\"/i',$log,$arr_log)){$i++;
$time_start=microtime_float();//实际需要再判断一下是否是截止ip,是截止ip时,才得到该ip段信息,否则ip是不属于该ip段的,这里省略了这一步
$pieces=$redis->zrangebyscore('ip',ip2long($arr_log[2]),4294967295,array('withscores' =>true,'limit'=>array(0, 1)));$time_mid=microtime_float();
$time2+=$time_mid-$time_start;
}
$t2_stop=microtime_float();
$t2+=$t2_stop-$t2_start;
}
fclose($handle);
echo"Redis使用时间: $time2 seconds\n";
echo"Redis总使用时间: $t2 seconds\n";
echo"Redis结束\n";
运行结果如下:
[root@localhost test]# php testredis.php
二分法开始
get_ip使用时间:3.89317107201seconds
二分法总使用时间:26.6202020645seconds
二分法结束
Redis 开始
Redis使用时间:12.8361856937seconds
Redis总使用时间:36.9929895401seconds
Redis结束

根据redis的文档,ZRANGEBYSCORE函数的时间复杂度是O(log(N)+M),其中N是有序集合中元素个数,M是返回的结果集中元素个数。在这里M是常量1,可以认为是O(log(N)),与二分查找的时间复杂度是相同的,但是这里运行时间相差比较大,接近是二分查找的三倍,是什么造成的呢?

通过监控cpu可以看到,开始运行二分查找的时候,cpu使用率在50%左右,系统使用在2%左右;而运行redis中zrangebyscore时,cpu使用率下降,但是系统使用上升到了5%以上,怀疑是因为上下文切换导致的。

[root@localhost ~]# vmstat-n3procs-----------memory-------------swap-------io------system-------cpu------r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st009655952166932985812009658611980000965595216693298586000001105274001000000965607616693298586000015111331300100000096560761669329858600003721159283001000000965646416695298586400024411173461198000096564641669529858640000110327300100000196485321669689915480018331112631900964010964221616657288811600482422311594484644820109643544166572886184003460011323244825000109643008166580886912003459111132322482500010966072816658486908800343224411514114834800109650228166592879532003501121133330491500010964284816658488667600346041151325482500010964350816652088616000345901140330482500010965639216643287092400341940411695424934800109646132166460881272003427275117135248248301096439721664488833240034601711323254925000109646196166416882168002225231113556664474800109645948166416882260000138111101384341104900109645948166416882260000191103137474110490010964664016644488223200021511131599636174700109646640166444882260000161158126884385000109646640166444882260000191105125584375000109646640166444882260000011031268943750001096467801664768822640002391115126544484900109646780166480882260000111103126444375000109646780166480882264000151711331253943750001096471681664888822560002211114126344394900109647168166488882268000011041264143750000096216728166492826272000201105309683900000962176121665048262600002131132356119800${PageNumber}

我们知道,使用正则匹配的时候很耗费cpu资源,使用redis进行查找也耗费cpu资源,将两者分开看看是否能提高效率。先通过正则匹配,把ip数据取出来,然后再分别判断ip情况,修改写法如下:

[root@localhost test]# more test.php<?php 
require'ipaddress.dat';//ip地址数组require_once('func.php');$redis=newredis();
$redis->connect('127.0.0.1',6379);$filename='test.access_log';$arr_tmp=array();
$handle=@fopen($filename,'r');while(!feof($handle)){
$log=rawurldecode(fgets($handle,4096));if(preg_match('/^\d+ \".*\" (.*) test\.com \[.*\] \"GET \/test.php .* \".*\"/i',$log,$arr_log)){$arr_tmp[]=ip2long($arr_log[2]);
}
}
fclose($handle);
$time_start=microtime_float();
foreach ($arr_tmpas$v) {
$pieces=get_ip($ipadd,$v);
}
$time_mid=microtime_float();
$time=$time_mid-$time_start;
echo"二分法: $time seconds\n";
$time_start=microtime_float();
foreach ($arr_tmpas$v) {
$pieces=$redis->zrangebyscore('ip',$v,4294967295,array('withscores' =>true,'limit'=>array(0, 1)));}
$time_mid=microtime_float();
$time=$time_mid-$time_start;
echo"Redis: $time seconds\n";

运行一下看看效果:

[root@localhost test]# php test.php
二分法:2.70506095886seconds
Redis:6.70395517349seconds

这里可以看到,速度提高了很多,但是仍然比二分法慢,怀疑是因为与redis交互导致的时间长。

下面减少redis中ip集合里面的数据量

[root@localhost test]# telnet localhost6379Trying127.0.0.1...
Connectedtolocalhost.localdomain (127.0.0.1).
Escape characteris'^]'.zcard ip
:95650
运行结果
[root@localhost test]# php test.php
二分法:2.72314596176seconds
Redis:6.59329199791seconds
继续减少,
root@localhost test]# telnet localhost6379Trying127.0.0.1...
Connectedtolocalhost.localdomain (127.0.0.1).
Escape characteris'^]'.zcard ip
:19662
运行结果
[root@localhost test]# php test.php
二分法:2.72199296951seconds
Redis:6.13260388374seconds
继续减少
[root@localhost test]# telnet localhost6379Trying127.0.0.1...
Connectedtolocalhost.localdomain (127.0.0.1).
Escape characteris'^]'.zcard ip
:1970
运行结果
[root@localhost test]# php test.php
二分法:2.75167584419seconds
Redis:5.84946107864seconds

测试ip库为空的极端情况。清空redis中ip集合中的数据,再运行程序。

运行结果如下:
[root@localhost test]# php test.php
二分法:0.29176902771seconds
Redis:5.43927407265seconds[root@localhost test]# telnet localhost6379Trying127.0.0.1...
Connectedtolocalhost.localdomain (127.0.0.1).
Escape characteris'^]'.zcard ip
:215706del ip
:1zcard ip
:0

三、测试结果

通过上面的测试可以看到,在php中使用redis进行ip查找,查找速度会随集合数据量的减少而减少,但是与集合的数据量多少关系不大,时间应当主要消耗在与redis内部处理zrangebyscore函数上面。整体速度比使用二分查找慢。因此在使用phpredis扩展访问redis的情况下,不建议使用redis进行ip查找。另外,在php进行密集cpu运算(如运行preg_match)时,应避免与redis交互。