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 相关文章推荐
mysql+php分页类(已测)
Mar 31 PHP
php守护进程 加linux命令nohup实现任务每秒执行一次
Jul 04 PHP
php-fpm配置详解
Feb 12 PHP
php之curl设置超时实例
Nov 03 PHP
php实现字符串首字母大写和单词首字母大写的方法
Mar 14 PHP
详细解读PHP中接口的应用
Aug 12 PHP
如何使用PHP Embed SAPI实现Opcodes查看器
Nov 10 PHP
Yii2中关联查询简单用法示例
Aug 10 PHP
PHP并发查询MySQL的实例代码
Aug 09 PHP
PHP实现的猴王算法(猴子选大王)示例
Apr 30 PHP
PHP一致性hash分布式算法封装类定义与用法示例
Aug 04 PHP
php常用经典函数集锦【数组、字符串、栈、队列、排序等】
Aug 23 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
javascript 密码强度验证规则、打分、验证(给出前端代码,后端代码可根据强度规则翻译)
2010/05/18 Javascript
在IE 浏览器中使用 jquery的fadeIn() 效果 英文字符字体加粗
2011/06/02 Javascript
jQuery学习笔记 操作jQuery对象 属性处理
2012/09/19 Javascript
jquery html动态生成select标签出问题的解决方法
2013/11/20 Javascript
angular中使用路由和$location切换视图
2015/01/23 Javascript
11种ASP连接数据库的方法
2015/09/18 Javascript
jQuery实现指定区域外单击关闭指定层的方法【经典】
2016/06/22 Javascript
bootstrap制作jsp页面(根据值让table显示选中)
2017/01/05 Javascript
jquery实现弹窗功能(窗口居中显示)
2017/02/27 Javascript
JavaScript中的普通函数和箭头函数的区别和用法详解
2017/03/21 Javascript
html5+canvas实现支持触屏的签名插件教程
2017/05/08 Javascript
详解vue 配合vue-resource调用接口获取数据
2017/06/22 Javascript
vue使用iframe嵌入网页的示例代码
2020/06/09 Javascript
解决vue2 在mounted函数无法获取prop中的变量问题
2018/11/15 Javascript
JavaScript常见继承模式实例小结
2019/01/11 Javascript
JS中的算法与数据结构之队列(Queue)实例详解
2019/08/20 Javascript
layui 上传图片 返回图片地址的方法
2019/09/26 Javascript
vue项目中企业微信使用js-sdk时config和agentConfig配置方式详解
2020/12/15 Vue.js
Python表示矩阵的方法分析
2017/05/26 Python
Python数据结构之栈、队列的实现代码分享
2017/12/04 Python
python利用requests库模拟post请求时json的使用教程
2018/12/07 Python
python TCP包注入方式
2020/05/05 Python
Python调用JavaScript代码的方法
2020/10/27 Python
浅谈matplotlib默认字体设置探索
2021/02/03 Python
CSS3 毛玻璃效果
2019/08/14 HTML / CSS
新百伦折扣店:Joe’s New Balance Outlet
2016/08/20 全球购物
定制别致的瑜伽垫:Sugarmat
2019/06/21 全球购物
.NET remoting中对象激活的两种方式
2015/06/08 面试题
物理教师自荐信范文
2013/12/28 职场文书
校园报刊亭创业计划书
2014/01/02 职场文书
《哪吒闹海》教学反思
2014/02/28 职场文书
关于教师节的演讲稿
2014/09/04 职场文书
篮球友谊赛通讯稿
2014/10/10 职场文书
审美与表现自我评价
2015/03/09 职场文书
我的收音机情缘
2022/04/05 无线电
JS函数式编程实现XDM一
2022/06/16 Javascript