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下操作Linux消息队列完成进程间通信的方法
Jul 24 PHP
获取php页面执行时间,数据库读写次数,函数调用次数等(THINKphp)
Jun 03 PHP
php include和require的区别深入解析
Jun 17 PHP
PHP的构造方法,析构方法和this关键字详细介绍
Oct 22 PHP
php实现12306火车票余票查询和价格查询(12306火车票查询)
Jan 14 PHP
PHP屏蔽过滤指定关键字的方法
Nov 03 PHP
php可应用于面包屑导航的递归寻找家谱树实现方法
Feb 02 PHP
php内嵌函数用法实例
Mar 20 PHP
PHP实现的mysql读写分离操作示例
May 22 PHP
微信公众号之主动给用户发送消息功能
Jun 22 PHP
详解Laravel设置多态关系模型别名的方式
Oct 17 PHP
Laravel5.5 动态切换多语言的操作方式
Oct 25 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模拟登录的方法
2015/07/08 PHP
Yii2框架dropDownList下拉菜单用法实例分析
2016/07/18 PHP
PHP实现RSA签名生成订单功能【支付宝示例】
2017/06/06 PHP
PHP unlink与rmdir删除目录及目录下所有文件实例代码
2018/02/07 PHP
PHP PDOStatement::bindValue讲解
2019/01/30 PHP
JQuery.validate在ie8下不支持的快速解决方法
2016/05/18 Javascript
echarts3 使用总结(绘制各种图表,地图)
2017/01/05 Javascript
使用jQuery ajaxupload插件实现无刷新上传文件
2017/04/23 jQuery
jquery+ajax实现省市区三级联动 (封装和不封装两种方式)
2017/05/15 jQuery
JS中使用textPath实现线条上的文字
2017/12/25 Javascript
Vue框架之goods组件开发详解
2018/01/25 Javascript
vue模仿网易云音乐的单页面应用
2019/04/24 Javascript
Vue表单绑定的实例代码(单选按钮,选择框(单选时,多选时,用 v-for 渲染的动态选项)
2019/05/13 Javascript
Python中的集合类型知识讲解
2015/08/19 Python
浅析使用Python操作文件
2017/07/31 Python
Python编程pygal绘图实例之XY线
2017/12/09 Python
Python验证文件是否可读写代码分享
2017/12/11 Python
Python冲顶大会 快来答题!
2018/01/17 Python
Python实现求一个集合所有子集的示例
2018/05/04 Python
Python实现对文件进行单词划分并去重排序操作示例
2018/07/10 Python
python感知机实现代码
2019/01/18 Python
python实现代码统计程序
2019/09/19 Python
Python 3.8正式发布重要新功能一览
2019/10/17 Python
PyQt5事件处理之定时在控件上显示信息的代码
2020/03/25 Python
Python实现ElGamal加密算法的示例代码
2020/06/19 Python
详解如何在css中引入自定义字体(font-face)
2018/05/17 HTML / CSS
使用canvas绘制贝塞尔曲线
2014/12/17 HTML / CSS
《桂林山水》教学反思
2014/02/08 职场文书
青年志愿者先进事迹
2014/05/06 职场文书
应届大学生自荐书
2014/06/17 职场文书
委托书格式
2014/08/01 职场文书
运动员代表致辞
2015/07/29 职场文书
新娘婚礼答谢词
2015/09/29 职场文书
运动会主持人开幕词
2016/03/04 职场文书
sentinel支持的redis高可用集群配置详解
2022/04/01 Redis
Vue router配置与使用分析讲解
2022/12/24 Vue.js