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学习笔记 数组的常用函数
Jun 13 PHP
php中模拟POST传递数据的两种方法分享
Sep 16 PHP
Linux fgetcsv取得的数组元素为空字符串的解决方法
Nov 25 PHP
php 操作数组(合并,拆分,追加,查找,删除等)
Jul 20 PHP
解析php框架codeigniter中如何使用框架的session
Jun 24 PHP
9个实用的PHP代码片段分享
Jan 22 PHP
php实现仿写CodeIgniter的购物车类
Jul 29 PHP
Nginx服务器上安装并配置PHPMyAdmin的教程
Aug 18 PHP
yii添删改查实例
Nov 16 PHP
如何使用PHP给图片加水印
Oct 12 PHP
PHP实现简单ajax Loading加载功能示例
Dec 28 PHP
实例讲解PHP表单处理
Feb 15 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获取YouTube视频信息的方法
2015/02/11 PHP
javascript 无提示关闭窗口脚本
2009/08/17 Javascript
初窥JQuery(二)事件机制(2)
2010/12/06 Javascript
ExtJS 刷新后如何默认选中刷新前最后一次选中的节点
2014/04/03 Javascript
实现音乐播放器的代码(html5+css3+jquery)
2015/08/04 Javascript
Bootstrap基本样式学习笔记之表单(3)
2016/12/07 Javascript
JS实现隔行换色的表格排序
2017/03/27 Javascript
xmlplus组件设计系列之选项卡(Tabbar)(5)
2017/05/03 Javascript
详谈AngularJs 控制器、数据绑定、作用域
2017/07/09 Javascript
vue移动端模态框(可传参)的实现
2019/11/20 Javascript
webpack3.0升级4.0的方法步骤
2020/04/02 Javascript
Vue父子组件传值的一些坑
2020/09/16 Javascript
python代码制作configure文件示例
2014/07/28 Python
python开发之基于thread线程搜索本地文件的方法
2015/11/11 Python
Python selenium 父子、兄弟、相邻节点定位方式详解
2016/09/15 Python
深入理解python中的select模块
2017/04/23 Python
python 除法保留两位小数点的方法
2018/07/16 Python
python3使用QQ邮箱发送邮件
2020/05/20 Python
详解python校验SQL脚本命名规则
2019/03/22 Python
python3 中的字符串(单引号、双引号、三引号)以及字符串与数字的运算
2019/07/18 Python
10个Python面试常问的问题(小结)
2019/11/20 Python
Python标准库:内置函数max(iterable, *[, key, default])说明
2020/04/25 Python
Pytorch - TORCH.NN.INIT 参数初始化的操作
2021/02/27 Python
html5中canvas学习笔记1-画板的尺寸与实际显示尺寸
2013/01/06 HTML / CSS
几个数据库方面的面试题
2016/07/01 面试题
诺思信科技(南京)有限公司.NET笔试题答案
2013/07/06 面试题
HSRP的含义以及如何工作
2014/09/10 面试题
大学军训感言1000字
2014/02/25 职场文书
施工安全汇报材料
2014/08/17 职场文书
预备党员期盼十八届四中全会召开思想汇报
2014/10/17 职场文书
迎新生欢迎词
2015/01/23 职场文书
学生会副主席竞选稿
2015/11/19 职场文书
Nginx进程管理和重载原理详解
2021/04/22 Servers
css position fixed 左右双定位的实现代码
2021/04/29 HTML / CSS
MySQL系列之六 用户与授权
2021/07/02 MySQL
Python简易开发之制作计算器
2022/04/28 Python