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 项目的方法
Jan 02 PHP
如何使用Linux的Crontab定时执行PHP脚本的方法
Dec 19 PHP
php用户注册页面利用js进行表单验证具体实例
Oct 17 PHP
php中的boolean(布尔)类型详解
Oct 28 PHP
php实现阿拉伯数字和罗马数字相互转换的方法
Apr 17 PHP
Zend Framework教程之Zend_Layout布局助手详解
Mar 04 PHP
Zend Framework教程之响应对象的封装Zend_Controller_Response实例详解
Mar 07 PHP
Symfony2使用Doctrine进行数据库查询方法实例总结
Mar 18 PHP
PHP 的Opcache加速的使用方法
Dec 29 PHP
详解no input file specified 三种解决方法
Nov 29 PHP
php下的原生ajax请求用法实例分析
Feb 28 PHP
PHP终止脚本运行三种实现方法详解
Sep 01 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 操作文件的一些FAQ总结
2009/02/12 PHP
php 生成文字png图片的代码
2011/04/17 PHP
3种php生成唯一id的方法
2015/11/23 PHP
PHP laravel中的多对多关系实例详解
2017/06/07 PHP
js 深拷贝函数
2008/12/04 Javascript
js 分栏效果实现代码
2009/08/29 Javascript
基于JQuery制作的产品广告效果
2010/12/08 Javascript
不同的jQuery API来处理不同的浏览器事件
2012/12/09 Javascript
微信小程序  modal弹框组件详解
2016/10/27 Javascript
深入理解React Native原生模块与JS模块通信的几种方式
2017/07/24 Javascript
angularjs实现分页和搜索功能
2018/01/03 Javascript
angularJS开发注意事项
2018/05/26 Javascript
vue+webpack模拟后台数据的示例代码
2018/07/26 Javascript
angular实现input输入监听的示例
2018/08/31 Javascript
详解ESLint在Vue中的使用小结
2018/10/15 Javascript
js闭包和垃圾回收机制示例详解
2021/03/01 Javascript
python求斐波那契数列示例分享
2014/02/14 Python
python处理PHP数组文本文件实例
2014/09/18 Python
Python使用metaclass实现Singleton模式的方法
2015/05/05 Python
Python判断某个用户对某个文件的权限
2016/10/13 Python
Python处理XML格式数据的方法详解
2017/03/21 Python
python 表达式和语句及for、while循环练习实例
2017/07/07 Python
详细分析Python可变对象和不可变对象
2020/07/09 Python
使用tkinter实现三子棋游戏
2021/02/25 Python
html5 canvas实现给图片添加平铺水印
2019/08/20 HTML / CSS
Intimissimi德国网上商店:意大利知名内衣品牌
2018/04/03 全球购物
英国专业美容产品在线:Mylee(从指甲到脱毛)
2020/07/06 全球购物
MYSQL基础面试题
2012/05/13 面试题
创建精神文明单位实施方案
2014/03/08 职场文书
节水倡议书范文
2014/04/15 职场文书
司法建议书范文
2014/05/13 职场文书
体育教师求职信
2014/06/30 职场文书
社区关爱留守儿童活动方案
2014/08/22 职场文书
埃及王子观后感
2015/06/16 职场文书
实践论读书笔记
2015/06/29 职场文书
mysql查找连续出现n次以上的数字
2022/05/11 MySQL