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 相关文章推荐
如何提高MYSQL数据库的查询统计速度 select 索引应用
Apr 11 PHP
PHP中for与foreach的区别分析
Mar 09 PHP
php Rename 更改文件、文件夹名称
May 24 PHP
解析zend Framework如何自动加载类
Jun 28 PHP
php使用codebase生成随机数
Mar 25 PHP
php简单获取文件扩展名的方法
Mar 24 PHP
Zend Framework实现Zend_View集成Smarty模板系统的方法
Mar 05 PHP
浅谈PHP中的
Apr 23 PHP
Laravel框架中VerifyCsrfToken报错问题的解决
Aug 30 PHP
ThinkPHP框架获取最后一次执行SQL语句及变量调试简单操作示例
Jun 13 PHP
ThinkPHP5.0框架验证码功能实现方法【基于第三方扩展包】
Mar 11 PHP
在 Laravel 6 中缓存数据库查询结果的方法
Dec 11 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
PHP 开发环境配置(Zend Server安装)
2010/04/28 PHP
解析PayPal支付接口的PHP开发方式
2010/11/28 PHP
通过php修改xml文档内容的方法
2015/01/23 PHP
简单解决新浪SAE无法上传文件的问题
2015/05/13 PHP
Jquery 快速构建可拖曳的购物车DragDrop
2009/11/30 Javascript
25个优雅的jQuery Tooltip插件推荐
2011/05/25 Javascript
js实现翻页后保持checkbox选中状态的实现方法
2012/11/03 Javascript
文件编码导致jquery失效的解决方法
2013/06/26 Javascript
JavaScript对象学习经验整理
2013/10/12 Javascript
javascript获取form里的表单元素的示例代码
2014/02/14 Javascript
JQuery仿小米手机抢购页面倒计时效果
2014/12/16 Javascript
javascript实现微信分享
2014/12/23 Javascript
jquery限定文本框只能输入数字(整数和小数)
2016/01/08 Javascript
利用D3.js实现最简单的柱状图示例代码
2016/12/09 Javascript
完美解决手机网页中输入框被输入法遮挡的问题
2017/12/19 Javascript
bootstrap treeview 树形菜单带复选框及级联选择功能
2018/06/08 Javascript
vue组件中iview的modal组件爬坑问题之modal的显示与否应该是使用v-show
2019/04/12 Javascript
在HTML中使用JavaScript的两种方法
2020/12/24 Javascript
[01:14]3.19DOTA2发布会 三代刀塔人第二代
2014/03/25 DOTA
Python实现Tab自动补全和历史命令管理的方法
2015/03/12 Python
Python计算三角函数之asin()方法的使用
2015/05/15 Python
动态规划之矩阵连乘问题Python实现方法
2017/11/27 Python
Python SMTP发送邮件遇到的一些问题及解决办法
2018/10/24 Python
windows7 32、64位下python爬虫框架scrapy环境的搭建方法
2018/11/29 Python
解决Django加载静态资源失败的问题
2019/07/28 Python
使用Jupyter notebooks上传文件夹或大量数据到服务器
2020/04/14 Python
Application Cache未缓存文件无法访问无法加载问题
2014/05/31 HTML / CSS
NFL欧洲商店(德国):NFL Europe Shop DE
2018/11/03 全球购物
表彰先进的通报
2014/01/31 职场文书
日语专业个人求职信范文
2014/02/02 职场文书
节能环保家庭事迹材料
2014/08/27 职场文书
四风剖析查摆对照检查材料思想汇报
2014/09/24 职场文书
学校食品安全责任书
2015/01/29 职场文书
质量整改通知单
2015/04/21 职场文书
幼儿园国培研修日志
2015/11/13 职场文书
关于CSS浮动与取消浮动的问题
2021/06/28 HTML / CSS