php二分法在IP地址查询中的应用


Posted in PHP onAugust 12, 2008

数据库大概存储几十万条IP记录,记录集如下:

+----------+----------+------------+---------+---------+--------+--------+ 
| ip_begin | ip_end   | country_id | prov_id | city_id | isp_id | netbar | 
+----------+----------+------------+---------+---------+--------+--------+ 
|        0 | 16777215 |          2 |       0 |       0 |      0 |      0 | 
| 16777216 | 33554431 |          2 |       0 |       0 |      0 |      0 | 
| 33554432 | 50331647 |          2 |       0 |       0 |      0 |      0 | 
| 50331648 | 67108863 |          3 |       0 |       0 |      0 |      0 | 
| 67108864 | 67829759 |          3 |       0 |       0 |      0 |      0 | 
+----------+----------+------------+---------+---------+--------+--------+ 

这样做查询需要用到如下SQL:
<?php
$sql = 'SELECT * FROM i_m_ip WHERE ip_begin <= $client_ip AND ip_end >= $client_ip';
?>

这样的检索显然用不到索引,即使用到,MySQL查询效率也不大可能达到每秒500次以上,我做了很多并发优化,最终平均查询效率也只有每秒200次左右,实在是头痛。一开始我也有想到借鉴纯真IP库的检索方法,但是我一直对算法有抵触,也以为二分法很难,所以就没有尝试使用,直到最后没有办法了,才最终实现了二分法的IP地址检索。

从上表可以看到IP库是从0到4294967295的一个连续数值,这个数值要是拆开存储,会有几百G的数据,所以没办法使用索引也没办法哈希。最终我使用PHP将这些东东转为二进制存储,抛弃了数据库的检索。可以看到IP起止长度为一个4字节的长整型,后面的国家ID、省份ID等,可以使用2个字节的短整型来存储,总共一行数据就有18个字节,总共31万条数据,算起来也就5M的样子。具体IP库生成代码如下:
<?php
/*
IP文件格式:
3741319168    3758096383    182    0    0    0    0
3758096384    3774873599    3    0    0    0    0
3774873600    4026531839    182    0    0    0    0
4026531840    4278190079    182    0    0    0    0
4294967040    4294967295    312    0    0    0    0
*/
set_time_limit(0);
$handle = fopen('./ip.txt', 'rb');
$fp = fopen("./ip.dat", 'ab');
if ($handle) {
    while (!feof($handle)) {
        $buffer = fgets($handle);
        $buffer = trim($buffer);
        $buffer = explode("\t", $buffer);
        foreach ($buffer as $key => $value) {
            $buffer[$key] = (float) trim($value);
        }
        $str = pack('L', $buffer[0]);
        $str .= pack('L', $buffer[1]);
        $str .= pack('S', $buffer[2]);
        $str .= pack('S', $buffer[3]);
        $str .= pack('S', $buffer[4]);
        $str .= pack('S', $buffer[5]);
        $str .= pack('S', $buffer[6]);
        fwrite($fp, $str);
    }
}
?>

这样IP就按照顺序每18字节一个单位排列了,所以很容易就使用二分法来检索出IP信息:
function getip($ip, $fp) {
    fseek($fp, 0);
    $begin = 0;
    $end   = filesize('./ip.dat');
    $begin_ip = implode('', unpack('L', fread($fp, 4)));
    fseek($fp, $end - 14);
    $end_ip   = implode('', unpack('L', fread($fp, 4)));
    $begin_ip = sprintf('%u', $begin_ip);
    $end_ip   = sprintf('%u', $end_ip);

    do {
        if ($end - $begin <= 18) {
            fseek($fp, $begin + 8);
            $info = array();
            $info[0] = implode('', unpack('S', fread($fp, 2)));
            $info[1] = implode('', unpack('S', fread($fp, 2)));
            $info[2] = implode('', unpack('S', fread($fp, 2)));
            $info[3] = implode('', unpack('S', fread($fp, 2)));
            $info[4] = implode('', unpack('S', fread($fp, 2)));
            return $info;
        }

        $middle_seek = ceil((($end - $begin) / 18) / 2) * 18 + $begin;

        fseek($fp, $middle_seek);
        $middle_ip = implode('', unpack('L', fread($fp, 4)));
        $middle_ip = sprintf('%u', $middle_ip);

        if ($ip >= $middle_ip) {
            $begin = $middle_seek;
        } else {
            $end = $middle_seek;
        }
    } while (true);
}

以上$fp为打开ip.dat的文件句柄,由于是循环检索,所以写在函数外面,免得每次检索都要打开一次文件,30W行数据二分法最多也只需要循环7次(2^7)左右即可找到准确的IP信息。之后本来还想将ip.dat放在内存中加快检索速度,后来发现,字符串定位函数的效率,根本和文件指针的偏移定位不是在一个数量级的,所以还是放弃使用内存来存放IP库。

这个实现,使IP检索效率提高了近百倍,只是一个简单的二分法的应用,从此算法在WEB应用中不重要的观念彻底打消了。其实要实现这个,我还请教了金狐,我一开始是请他帮我生成一个纯真格式的IP库,然后用Discuz的IP查询函数来检索,不过他不肯帮我,最后造就了我的这个实践和学习。有时候,求人不如求己。

PHP 相关文章推荐
PHP 操作文件的一些FAQ总结
Feb 12 PHP
PHP生成自适应大小的缩略图类及使用方法分享
May 06 PHP
php中最简单的字符串匹配算法
Dec 16 PHP
大家在抢红包,程序员在研究红包算法
Aug 31 PHP
thinkPHP使用post方式查询时分页失效的解决方法
Dec 09 PHP
全面解读PHP的Yii框架中的日志功能
Mar 17 PHP
Yii调试查看执行SQL语句的方法
Jul 15 PHP
PHP+Ajax异步带进度条上传文件实例
Nov 01 PHP
php实现微信模板消息推送
Mar 30 PHP
PHP实现登录验证码校验功能
May 17 PHP
php中关于换行的实例写法
Sep 26 PHP
PHP替换Word中变量并导出PDF图片的实现方法
Nov 26 PHP
PHP调用MySQL的存储过程的实现代码
Aug 12 #PHP
PHP+MYSQL 出现乱码的解决方法
Aug 08 #PHP
php自动适应范围的分页代码
Aug 05 #PHP
用PHP读取RSS feed的代码
Aug 01 #PHP
IStream与TStream之间的相互转换
Aug 01 #PHP
特详细的PHPMYADMIN简明安装教程
Aug 01 #PHP
php-accelerator网站加速PHP缓冲的方法
Jul 30 #PHP
You might like
编写Smarty插件在模板中直接加载数据的详细介绍
2013/06/26 PHP
PHP的Laravel框架中使用消息队列queue及异步队列的方法
2016/03/21 PHP
ThinkPHP模板循环输出Volist标签用法实例详解
2016/03/23 PHP
thinkPHP3.1验证码的简单实现方法
2016/04/22 PHP
php英文单词统计器
2016/06/23 PHP
php 微信开发获取用户信息如何实现
2016/12/13 PHP
ThinkPhP+Apache+PHPstorm整合框架流程图解
2020/11/23 PHP
JavaScript实现Sleep函数的代码
2007/03/04 Javascript
javascript与CSS复习(三)
2010/06/29 Javascript
浅谈Javascript面向对象编程
2011/11/15 Javascript
10个基于浏览器的JavaScript调试工具分享
2013/02/07 Javascript
AngularJS基础 ng-value 指令简单示例
2016/08/03 Javascript
浅谈js中StringBuffer类的实现方法及使用
2016/09/02 Javascript
AngularJS实现标签页的两种方式
2016/09/05 Javascript
JavaScript 数组的进化与性能分析
2017/09/18 Javascript
JavaScript学习笔记之DOM基础操作实例小结
2019/01/09 Javascript
Vue多选列表组件深入详解
2021/03/02 Vue.js
Python高级应用实例对比:高效计算大文件中的最长行的长度
2014/06/08 Python
Python cookbook(字符串与文本)在字符串的开头或结尾处进行文本匹配操作
2018/04/20 Python
python绘制热力图heatmap
2020/03/23 Python
在Pycharm terminal中字体大小设置的方法
2019/01/16 Python
Python识别快递条形码及Tesseract-OCR使用详解
2019/07/15 Python
如何在mac环境中用python处理protobuf
2019/12/25 Python
基于Python3.6中的OpenCV实现图片色彩空间的转换
2020/02/03 Python
Python中import导入不同目录的模块方法详解
2020/02/18 Python
django序列化时使用外键的真实值操作
2020/07/15 Python
Python爬虫回测股票的实例讲解
2021/01/22 Python
canvas实现圆形进度条动画的示例代码
2017/12/26 HTML / CSS
英国的一家创新礼品和小工具零售商:Menkind
2019/08/24 全球购物
美团网旗下网上订餐平台:美团外卖
2020/03/05 全球购物
自动化专业职业生涯规划书范文
2014/01/16 职场文书
经理管理专业毕业自荐书范文
2014/02/12 职场文书
2014年学校德育工作总结
2014/12/05 职场文书
2014年幼儿园园长工作总结
2014/12/17 职场文书
工作服管理制度范本
2015/08/06 职场文书
Python爬虫数据的分类及json数据使用小结
2021/03/29 Python