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 相关文章推荐
Zend的MVC机制使用分析(二)
May 02 PHP
php输出1000以内质数(素数)示例
Feb 16 PHP
php根据年月获取季度的方法
Mar 31 PHP
Smarty局部缓存的几种方法简介
Jun 17 PHP
ThinkPHP的MVC开发机制实例解析
Aug 23 PHP
PHP整合PayPal支付
Jun 11 PHP
PHP批量去除BOM头代码分享
Jun 26 PHP
详解在PHP的Yii框架中使用行为Behaviors的方法
Mar 18 PHP
CI分页类首页、尾页不显示的解决方法
Mar 28 PHP
php实现在线通讯录功能(附源码)
May 13 PHP
PHP递归删除多维数组中的某个值
Apr 17 PHP
如何理解PHP核心特性命名空间
May 28 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编程效率的53个要点(经验小结)
2010/09/04 PHP
PHP二分查找算法示例【递归与非递归方法】
2016/09/29 PHP
Yii2框架可逆加密简单实现方法
2017/08/25 PHP
动态为事件添加js代码示例
2009/02/15 Javascript
javascript 框架小结 个人工作经验
2009/06/13 Javascript
JavaScript Event学习第八章 事件的顺序
2010/02/07 Javascript
javascript学习(一)构建自己的JS库
2013/01/02 Javascript
关于JS管理作用域的问题
2013/04/10 Javascript
5个书写JavaScript代码的坏习惯,看看你中枪了没?
2014/11/06 Javascript
Javascript实现通过选择周数显示开始日和结束日的实现代码
2016/05/30 Javascript
jQuery实现左侧导航模块的显示与隐藏效果
2016/07/04 Javascript
关于微信中a链接无法跳转问题
2016/08/02 Javascript
基于JSON格式数据的简单jQuery幻灯片插件(jquery-slider)
2016/08/10 Javascript
jQuery Validate 相关参数及常用的自定义验证规则
2017/03/06 Javascript
详解从零搭建 vue2 vue-router2 webpack3 工程
2017/11/22 Javascript
js自定义trim函数实现删除两端空格功能
2018/02/09 Javascript
ionic grid(栅格)九宫格制作详解
2018/06/30 Javascript
Vue中插入HTML代码的方法
2018/09/21 Javascript
微信小程序实现点击卡片 翻转效果
2019/09/04 Javascript
vue项目中使用eslint+prettier规范与检查代码的方法
2020/01/16 Javascript
windows10系统中安装python3.x+scrapy教程
2016/11/08 Python
Python图形绘制操作之正弦曲线实现方法分析
2017/12/25 Python
Python实现定时备份mysql数据库并把备份数据库邮件发送
2018/03/08 Python
Python3.6笔记之将程序运行结果输出到文件的方法
2018/04/22 Python
Flask web开发处理POST请求实现(登录案例)
2018/07/26 Python
python爬取微信公众号文章
2018/08/31 Python
Python将列表数据写入文件(txt, csv,excel)
2019/04/03 Python
python3对接mysql数据库实例详解
2019/04/30 Python
python爬虫 爬取超清壁纸代码实例
2019/08/16 Python
Python操作Excel工作簿的示例代码(\*.xlsx)
2020/03/23 Python
python 贪心算法的实现
2020/09/18 Python
详解基于python的全局与局部序列比对的实现(DNA)
2020/10/07 Python
详解matplotlib绘图样式(style)初探
2021/02/03 Python
Python基于爬虫实现全网搜索并下载音乐
2021/02/14 Python
优秀党员主要事迹材料
2015/11/04 职场文书
Python如何将list中的string转换为int
2022/07/15 Ruby