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 相关文章推荐
C#使用PHP服务端的Web Service通信实例
Apr 08 PHP
PHP实现ftp上传文件示例
Aug 21 PHP
php_imagick实现图片剪切、旋转、锐化、减色或增加特效的方法
Dec 15 PHP
ajax+php控制所有后台函数调用
Jul 15 PHP
百万级别知乎用户数据抓取与分析之PHP开发
Sep 28 PHP
php如何获取文件的扩展名
Oct 28 PHP
laravel 5.3中自定义加密服务的方案详解
May 09 PHP
使用php自动备份数据库表的实现方法
Jul 28 PHP
PHP使用函数用法详解
Sep 30 PHP
php实现的生成排列算法示例
Jul 25 PHP
PHP反射原理与用法深入分析
Sep 28 PHP
laravel http 自定义公共验证和响应的方法
Sep 29 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函数(ignore_user_abort)
2012/08/01 PHP
Docker 如何布置PHP开发环境
2016/06/21 PHP
php+redis实现注册、删除、编辑、分页、登录、关注等功能示例
2017/02/15 PHP
PHP 序列化和反序列化函数实例详解
2020/07/18 PHP
javascript一点特殊用法
2008/05/28 Javascript
Javascript学习笔记5 类和对象
2010/01/11 Javascript
异步加载script的代码
2011/01/12 Javascript
jquery 无限级联菜单案例分享
2013/03/26 Javascript
jQuery制作仿腾讯web qq用户体验桌面
2013/08/20 Javascript
jQuery+HTML5实现手机摇一摇换衣特效
2015/06/05 Javascript
vue,angular,avalon这三种MVVM框架优缺点
2016/04/27 Javascript
设计模式中的组合模式在JavaScript程序构建中的使用
2016/05/18 Javascript
BootStrap selectpicker
2016/06/20 Javascript
Node.js Streams文件读写操作详解
2016/07/04 Javascript
Vue-cli proxyTable 解决开发环境的跨域问题详解
2017/05/18 Javascript
vue.js实现价格格式化的方法
2017/05/23 Javascript
vue addRoutes实现动态权限路由菜单的示例
2018/05/15 Javascript
详解关于JSON.parse()和JSON.stringify()的性能小测试
2019/03/14 Javascript
ES6中的class是如何实现的(附Babel编译的ES5代码详解)
2019/05/17 Javascript
elementUI vue this.$confirm 和el-dialog 弹出框 移动 示例demo
2019/07/03 Javascript
redux处理异步action解决方案
2020/03/22 Javascript
Vue3不支持Filters过滤器的问题
2020/09/24 Javascript
django传值给模板, 再用JS接收并进行操作的实例
2018/05/28 Python
python使用requests.session模拟登录
2019/08/09 Python
python网络编程之多线程同时接受和发送
2019/09/03 Python
Python+OpenCV实现将图像转换为二进制格式
2020/01/09 Python
Django-imagekit的使用详解
2020/07/06 Python
自我鉴定200字
2013/10/28 职场文书
个人自荐信
2013/12/05 职场文书
家长对孩子评语
2014/01/30 职场文书
善意的谎言事例
2014/02/15 职场文书
正科级干部考察材料
2014/05/29 职场文书
党的群众路线教育实践活动查摆问题自查报告
2014/10/10 职场文书
大学生考试作弊检讨书1000字
2014/10/14 职场文书
退休劳动合同怎么写?
2019/10/25 职场文书
Appium中scroll和drag_and_drop根据元素位置滑动
2022/02/15 Python