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 相关文章推荐
利用static实现表格的颜色隔行显示的代码
Sep 02 PHP
php date()日期时间函数详解
May 16 PHP
Base64在线编码解码实现代码 演示与下载
Jan 08 PHP
如何解决CI框架的Disallowed Key Characters错误提示
Jul 05 PHP
php的declare控制符和ticks教程(附示例)
Mar 21 PHP
PHP判断是否有Get参数的方法
May 05 PHP
thinkphp模板的包含与渲染实例分析
Nov 26 PHP
php使用标签替换的方式生成静态页面
May 21 PHP
PHP开发Apache服务器配置
Jul 15 PHP
linux下php上传文件注意事项
Jun 11 PHP
PHP 在数组中搜索给定的简单实例 array_search 函数
Jun 13 PHP
PHP中抽象类,接口功能、定义方法示例
Feb 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
php cURL和Rolling cURL并发方式比较
2013/10/30 PHP
PHP中Trait及其应用详解
2017/02/14 PHP
javascrpt绑定事件之匿名函数无法解除绑定问题
2012/12/06 Javascript
html向js方法传递参数具体实现
2013/08/08 Javascript
网页中可关闭的漂浮窗口实现可自行调节
2013/08/20 Javascript
JS连接SQL数据库与ACCESS数据库的方法实例
2013/11/21 Javascript
理解javascript函数式编程中的闭包(closure)
2016/03/08 Javascript
BootStrap的弹出框(Popover)支持鼠标移到弹出层上弹窗层不隐藏的原因及解决办法
2016/04/03 Javascript
使用Bootstrap Tabs选项卡Ajax加载数据实现
2016/12/23 Javascript
ES6新特性一: let和const命令详解
2017/04/20 Javascript
jQuery Tree Multiselect使用详解
2017/05/02 jQuery
NodeJS实现同步的方法
2019/03/02 NodeJs
vue+element实现表格新增、编辑、删除功能
2019/05/28 Javascript
详解vue中v-bind:style效果的自定义指令
2020/01/21 Javascript
python服务器端收发请求的实现代码
2014/09/29 Python
Python求两个文本文件以行为单位的交集、并集与差集的方法
2015/06/17 Python
python操作MySQL 模拟简单银行转账操作
2017/09/27 Python
Python 微信之获取好友昵称并制作wordcloud的实例
2019/02/21 Python
python3使用腾讯企业邮箱发送邮件的实例
2019/06/28 Python
python2.7的flask框架之引用js&amp;css等静态文件的实现方法
2019/08/22 Python
Python使用enumerate获取迭代元素下标
2020/02/03 Python
Keras 中Leaky ReLU等高级激活函数的用法
2020/07/05 Python
python 制作简单的音乐播放器
2020/11/25 Python
python代码实现图书管理系统
2020/11/30 Python
完美解决torch.cuda.is_available()一直返回False的玄学方法
2021/02/06 Python
css3实现平移效果(transfrom:translate)的示例
2020/11/13 HTML / CSS
Html5页面上如何禁止手机虚拟键盘弹出
2020/03/19 HTML / CSS
欧洲最大的笔和书写专家:The Pen Shop
2017/03/19 全球购物
英国最大的纸工艺品商店:CraftStash
2018/12/01 全球购物
护理自荐信
2013/10/22 职场文书
群众路线自查自纠工作情况报告
2014/10/28 职场文书
2015年打非治违工作总结
2015/04/02 职场文书
银行客户经理岗位职责
2015/04/09 职场文书
七年级作文之冬景
2019/11/07 职场文书
解决Go gorm踩过的坑
2021/04/30 Golang
Python机器学习算法之决策树算法的实现与优缺点
2021/05/13 Python