2016年4月

PHP/MySQL是主流的应用开发搭配方式,印象中PHP支持使用很多种数据库,而且MySQL就有mysql、mysqli、pdo三种API可用。本文旨在从PHP数据库访问设计的思路为轴,简单的捋清这些关系。

PHP手册中《数据库扩展》一章有两部分:数据库抽象层、针对各数据库系统对应的扩展,已经是足够的说明了,要更深刻的理解可去研读。

首先,直接的想法是,PHP应该为不同的数据库实现不同的数据库抽象层,调用客户端库API来使用DBMS。如下图:
1.png

然后,考虑这个场景:当PHP应用写好后,环境数据库不同时,就要用对应的数据库抽象层重写项目所有数据库相关代码。所以能否把访问抽象出来,用不同数据库时,只要切换不同实现就ok了。如下图:
2.png

最后,主流选用MySQL当然是既可以使用mysql、mysqli两种数据库抽象层,又可以使用PDO数据访问抽象层(须安装PDO_MYSQL扩展实现PDO接口)。如下图:
3.png

当然任何一个数据库抽象层,都要在PHP编译时都要指定数据库的客户端库以使用数据库服务器。以MySQL为例,推荐用mysqlnd库,配置项:--with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-mysql=mysqlnd

现在是不是清晰些了呢?

程序就是数据结构+算法,还有个重要的问题,就是编码!编码是信息从一种形式或格式转换为另一种形式的过程,有诸如字符/文字/语义/PCM等多种编码。
这篇文章只限于字符编码(Character encoding),作为程序员或是多数计算机工作者,你可能研究或看过很多编码ASCII/GB(K)/Unicode(utf8/utf16/utf32)/BIG5/ISO等,但还未搞清楚它到底是什么东东,它们有什么区别和联系,if so,可以继续看下文啦。

简约的前置总结:ASCII是美标,用一个字节表示字母/符号;GBK是国标,用两个字节表示汉字;UTF-8是UNICODE的改进版,兼容几乎所有语言符号,最为流行通用。

ASCII,美国信息交换标准编码,简称“美标”。大家知道,美国抢占了计算机与互联网的先机,美标自然地成为了国际上大部分电脑的通用编码,它规定用从0到127的128个数字来代表信息的规范编码,其中包括33个控制码、1个空格码、94个形象码(英文字母,阿拉伯数字,标点符号等)。我们平时阅读的英文电脑文本,就是以形象码的方式传递和存储的。

GB2312,老美的美标虽通用,但不适合博大精深的中文。1981年我国开始实施的一套国家标准GB2312,把六千余汉字、标点符号、外文字母等,整个字符集分成94个区,每区有94个位。例如“中”字在方阵中处于第54区第48位,它的区位码就是5448。然而GB编码文字较少,致使GBK的出现。

GBK,国G标B扩展K,向下兼容GB-2312编码,向上支持ISO 10646.1国际标准,共收录汉字21003个、符号883个、提供1894个造字码位,简、繁体字融于一库。GBK 采用双字节表示,总体编码范围为 8140-FEFE,首字节在 81-FE 之间,尾字节在 40-FE 之间,剔除 xx7F 一条线。总计23940个码位,共收入 21886 个汉字和图形符号。(另外,BIG5码是针对繁体汉字的汉字编码)
ISO1993年,国际标准ISO10646 定义了通用字符集(Universal Character Set, UCS)。 UCS 是所有其他字符集标准的一个超集。

UNICODE的渊源:中日韩等国家使用的语言中字符多达几千个,当计算机普及到此时,原来字符采用的单字节编码(最多只可容纳2^8=256个字符),既然一个字节不够,人们就采用两个字节,但其中的ASCII码等仍用单字节表示。问题出现了:因为每当涉及到双字节字符串的处理时,总是要判断当中的一个字节到底表示的是一个字符还是半个字符,如果是半个字符,那是前一半还是后一半?
最后两个标准组织合作使Unicode诞生,Unicode对每个字符都固定使用两个字节即16位表示,于是当处理字符时,不必担心只处理半个字符。Unicode在网络、Windows系统和很多大型软件中得到应用。

UTF-8:为了提高等宽字节的Unicode的编码效率,于是出现了UTF-8编码,也是迄今为止最为通用流行的编码,UTF-8可以根据不同的符号自动选择编码的长短。比如英文字母可以只用1个字节就够了。UTF-8的编码是这样得出来的,以”汉”这个字为例:
“汉”字的Unicode编码是\u6c49,然后把\u6c49通过UTF-8编码器进行编码,最后输出的UTF-8编码是汉

未完待续。。。

1. Do not repeat yourself(重构、抽象...)

2. 工具能做的麻烦事,不要自己做(格式化、语法检查...)

IDE能做的事儿,不要浪费自己时间去做。
把时间用在代码逻辑、性能考虑上。

3. 接口方法,只做应该做的事,不多不乱,保持原数据结构/顺序。

例子:
任务页面有召集人(多个)的列表,是后台运营人员填写的,有时第一次访问时顺序有混乱,之后访问右正常了。觉得很奇怪,看代码知道了 原来是缓存搞的鬼!

背景:
微博的内容产品几乎都有好友/粉丝 与 物 的关系,所以通过array(uid,uid,uid,...)取用户详细信息的方法,是早已抽象好的,逻辑是:对传进来的array进行缓存查找,得到已缓存的条目未命中的uid列表,再以此查库将返回结果追加到已缓存的条目,返回结果。

策略不错,但有个小问题,参数传进来可能是有顺序的,所以期望的结果集的顺序应该是与参数一致的。
有两个办法,1改接口、2重新排序,因为作为调用方不便改接口,所以重新排序!

怎么排呢,经同事指正,几行代码:

$users = Dr_User::get_user_infos($uids, false);//结果集已经乱序了

$flip_users = array_flip($uids);//反转uid列表,array(uid=>0,uid=>1,...),再遍历users存到对应键上
foreach ($users as $user) {
    $flip_users[$user['id']] = $user;
}
$users = $flip_users;
unset($flip_users);

结论:如果很多调用方都有保持顺序的需求,接口的设计就该添加一个参数以控制结果是否保持顺序。

4. 务必打开所有错误显示,包括NOTICE级

5. 变量的检查,统一在view controller里做

6. 明确开发/生产环境...

...

Tips

  1. 新增/修改时,sql语句慎用replace,当字段中有主键和唯一键时,若已有记录,修改的实际操作是:将原纪录删除,新插入纪录,这显然不是期望的处理方式。
  2. 在做客户端API时,有时需要一个默认的图片card,不希望它跳转或作为普通图片可查看大图,只是让它作为默认显示(something like 背景图)
'pic_items' => array(
    '0' => array(
        'pic' => 'http://example.com/pic.jpg',
        'scheme'=>'sinaweibo://javascript:void(0);',
    ),
),

类似浏览器js的写法,scheme为sinaweibo://javascript:void(0);就好了。

以前面试时被问一个问题:有10万个乱序的数,要前5个最大(或最小)的数?
作为一个没好好学算法的人,还没有算法时间、空间复杂度的概念,只提出了冒泡、快速排序等,然后取前5。这显然不是合理的做法。

读了几本书,有一点点心得,下面介绍两个做法:
假设:输入为[31,5,12,24,41,63,7,61,42,21,9,123,24...] ,总数为N=100000,要求前M=5个最大的数

  1. 对10万个建立二叉堆,然后应用堆排序5次,即取出前5个最大(或最小)的数。
    只是一个可行的方法,在此不敖述,具体可参见《数据结构与算法分析:C语言描述》、《数据结构(C语言版)》严蔚敏等书中的堆排序。
  2. 考虑:能否维护一个数据结构用来存储排好序的5个数,要求如果输入数大于5个中最小的数,就将其插入至正确位置,并删除最小的数。这样对输入进行一次遍历,即可找出最大的5个数。
    此处想到的是用单链表,首先对输入中前5个数字升序排序,插入空的链表中。
//简单冒泡排序,输入少,对整体性能影响可忽略不计
for(int j=1; j<M; j++){
    for(int k=0; k<M-j; k++){
        if(input[k]>input[k+1]){
            tmp = input[k];
            input[k] = input[k+1];
            input[k+1]=tmp;
        }
    }
}
for(int i=0; i<M; i++){
    Insert(input[i],L,P);//依次插入链表
    P = P->Next;
}

图片 1.png

Position Tmp,TmpCell;
for( ; i<N; i++){ //对其余输入进行一次遍历
    P = Header(L); //表头
    do{
        Tmp = P;//暂存前驱元,保存位置
        P = P->Next;//第一个元素
        if( input[i] <= P->Value ){ //小于第一个元素或者后面的某一个元素
            if(P != L->Next){ //input[i]大小介于第一个元素与此位置的元素
                Insert(in[i],L1,Tmp); //插入
                TmpCell = L1->Next;
                L1->Next = TmpCell->Next;
                free( TmpCell ); //挤出第一个元素,也就是5+1=6个中最小的元素
            }
            break;
        }else if(input[i] > P->Value && IsLast( P, L )){ //如果大于最后一个(也就是最大的)元素
            Insert(in[i],L,P); //插入到最后
            TmpCell = L->Next; L->Next = TmpCell->Next; free(TmpCell); //删除第一个元素(6个中最小的)
            break;
        }
    } while( !IsLast(P, L) );
}

插入可能是这样的:
图片 2.png

删除首元可能是这样的:
图片 3.png

小结:当输入大数据量,而只需前m个最大(最小)值时,应用链表不失为一个好办法,它只对输入进行一次遍历,时间复杂度O(N),空间也只不过额外是一个含6个元素的链表大小而已。
欢迎指教。