Skip to content

PHP 实现 Hash 表

09-12月-17

Hash 表(HashTable)又称散列表,通过把关键字 Key 映射到数组中的一个位置来访问记录,以加快查找的速度。这个映射函数称为 Hash 函数,存放记录的数组称为 Hash 表。

Hash 表结构

Hash 表结构

Hash 表的实现步骤如下:

  1. 创建一个固定大小的数组用于存放数据。
  2. 设计 Hash 函数。
  3. 通过 Hash 函数把关键字 Key 映射到数组的某个位置,并在此位置上进行数据存取。

PHP 具体实现

首先创建一个 HashTable 类。

<?php
class HashTable 
{
    private $buckets;   // 用于存储数据的数组
    private $size = 10; // $buckets 数组大小

    public function __construct()
    {
        $this->buckets = new SplFixedArray($this->size);    
    }

    /**
     * Hash 函数
     */
    private function hashfunc($key)
    {
        $strlen = strlen($key);
        $hashval = 0;
        for ($i = 0; $i < $strlen; $i++) {
            $hashval += ord($key[$i]);
        }
        return $hashval % $this->size;
    }

    /**
     * 插入数据
     */
    public function insert($key, $value)
    {
        $index = $this->hashfunc($key);
        // 新建一个节点
        if (isset($this->buckets[$index])) {
            $newNode = new HashNode($key, $value, $this->buckets[$index]);
        } else {
            $newNode = new HashNode($key, $value, null);
        }
        $this->buckets[$index] = $newNode;
    }

    /**
     * 查找数据
     */
    public function find($key)
    {
        $index = $this->hashfunc($key);
        $current = $this->buckets[$index];
        while (isset($current)) {           // 遍历当前链表
            if ($current->key == $key) {    // 比较当前节点的关键字
                return $current->value;     // 查找成功
            }
            $current = $current->nextNode;  // 比较下一个节点
        }
        return null; // 查找失败
    }
}

节点需要保存关键字 Key 和 Value,同时还要记录具有相同 Hash 值的节点。所以创建一个 HashNode 类存储这些信息。

<?php
class HashNode
{
    public $key;        // 节点关键字
    public $value;      // 节点值
    public $nextNode;   // 指向具有相同 Hash 值节点的指针

    public function __construct($key, $value, $nextNode = null)
    {
        $this->key = $key;
        $this->value = $value;
        $this->nextNode = $nextNode;
    }
}

测试代码如下:

<?php
$ht = new HashTable();
$ht->insert('key1', 'value1');
$ht->insert('key12', 'value12');
echo $ht->find('key1');
echo $ht->find('key12');

/** 
*   相同 Hash 值的节点会被连接在同一个链表。
*   
*   HashNode Object
*   (
*       [key] => key12
*       [value] => value12
*       [nextNode] => HashNode Object
*           (
*               [key] => key1
*               [value] => value1
*               [nextNode] => 
*           )
*   
*   )
*/

Hash 表冲突

冲突的原因是:不同的关键字通过 Hash 函数计算出来的 Hash 值相同。通过打印 “key1” 和 “key12” 的 Hash 值,可以发现它们都是 8。也就是说 “value1” 和 “value12” 同时被存储在 Hash 表的第 9 个位置上(索引从 0 开始)。

修改后的插入算法流程如下:

  1. 使用 Hash 函数计算关键字的 Hash 值,通过 Hash 值定位到 Hash 表的指定位置。
  2. 如果此位置已经被其他节点占用,把新节点的 $nextNode 指向此节点,否则把新节点的 $nextNode 设置为 NULL。
  3. 把新节点保存到 Hash 表的当前位置。

修改后的查找算法流程如下:

  1. 使用 Hash 函数计算关键字的 Hash 值,通过 Hash 值定位到 Hash 表的指定位置。
  2. 遍历当前链接表,比较链表中每个节点的关键字与查找关键字是否相等。如果相等,查找成功。
  3. 如果整个链表都没有要查找的关键字,查找失败。

Redis 使用与实践

25-11月-17

RedisKey-Value 类型的内存数据库,支持 String、List、Set、Sorted Set、Hash 等数据类型,支持 Snapshottiong(快照)和 Append-Only file(追加)两种数据持久化方式,支持主从复制。

1. Redis 安装步骤

// 下载 Redis 的稳定版本
$ wget http://download.redis.io/releases/redis-4.0.2.tar.gz 
// 解压缩
$ tar zvxf redis-4.0.2.tar.gz
// 编译 Redis
$ make
// 安装 Redis,Redis 没有提供 make install 命令,需要手动安装
$ sudo cp redis.conf /etc/
$ sudo cp redis-benchmark redis-cli redis-server /usr/bin/

2. 运行 Redis 服务器

// 启动 Redis 服务器,并指定配置文件 
$ /usr/bin/redis-server /etc/redis.conf
// 启动一个客户端
$ /usr/bin/redis-cli -a 'password'

3. Redis 支持的数据类型

3.1 String 类型

占位图

String 类型支持 incr 操作,可以用做统计计算,如统计网站访问次数、博客访问次数等。

3.2 List 类型

占位图

List 数据类型 key 对应的 value 是一个双向链表结构。

3.3 Set 类型

占位图

Set 数据类型是一种无序集合,在 Redis 内部通过 HashTable 实现。优点是快速查找元素是否存在,用于记录一些不能重复的数据。

Set 类型通常用于记录做过某些事情。例如在某些投票系统中,每个用户一天只能投票一次,那么可以使用 Set 来记录某个用户的投票情况,只需要以日期作为 Set 的 key,则将用户 ID 作为 member 即可。要查看某个用户今天是否投过票,只需要以今天的日期作为 key 去查询此用户 ID 是否存在。

3.4 Sorted Set 类型

// 添加元素 member 到集合,元素在集合中存在则更新对应 score
zadd key score member
// 增加对应 member 的 score 值,并且重新排序,返回更新后的 score 值
zincrby key incr member

占位图

Sorted Set 类型是排序的 Set 类型,属于有序集合。

Sorted Set 类型在 Web 应用中非常有用。例如排行榜应用中的按 “顶贴” 次数排序,方法是:将排序的值设置成 Sorted Set 的 score 值,将具体数据设置成相应的 value,用户每次按 “顶贴” 按钮时,只需执行 zadd 命令修改 score 值。

3.5 Hash 类型

占位图

Hash 类型是每个 key 对应一个 HashTable。Hash 类型适合应用于存储对象,例如用户信息对象。把用户 ID 作为 key,可把用户信息保存到 Hash 类型中。

4. 持久化

Redis 是基于内存的数据库,内存数据库有一个严重的弊端:突然宕机或者断电时,内存的数据不会保存。为了解决这个问题,Redis 提供两种持久化方式:内存快照(Snapshotting)和 日志追加(Append-only file)。

4.1 内存快照

内存快照方式是将内存中的数据以快照方式写入二进制文件中,默认文件名为 dump.rdb。

Redis 每隔一段时间进行一次内存快照操作,客户端使用 save 或者 bgsave 命令告诉 Redis 需要做一次内存快照操作。save 命令在主线程中保存内存快照,Redis 由单线程处理所有请求,执行 save 命令可能阻塞其他客户端请求,从而导致不能快速响应请求,所以建议不要使用 save 命令。另外要注意,内存快照每次都把内存数据完整地写入硬盘,而不是只写入增量数据。所以如果数据量大,写入操作比较频繁,从而严重影响性能。

与内存快照相关的配置选项如下:

// 经过 900 秒或数据更改 1 次就进行一次内存快照操作
save 900 1
// 设置多个这样的条件实现不同内存快照方案,这样其中一个条件成立,Redis 都进行一次内存快照操作
save 900 1
save 300 10
save 60 10000

4.2 日志追加

日志追加(aof)方式是把增加、修改数据的命令通过 write 函数追加到文件尾部(默认是 appendonly.aof)。Redis 重启时读取 appendonly.aof 文件中的所有命令并且执行,从而把数据写入内存中。

另外,操作系统内核的 I/O 接口可能存在缓存,所以日志追加方式不可能立即写入文件中,这样就有可能丢失部分数据。幸运的是 Redis 提供了解决方法,通过修改配置文件告诉 Redis 应该在什么时候用 fsync 函数强制操作系统把缓存写入磁盘。有以下三种写法:

appendonly yes           # 启动日志追加持久化方式
# appendfsync always     # 每次收到增加或者修改命令就立刻强制写入磁盘
appendfsync everysec     # 每秒强制写入磁盘一次
# appendfsync no         # 是否写入磁盘完全依赖操作系统

日志追加方式有效降低数据丢失的风险,同时也带来另一个问题,即持久化文件(appendonly.aof)不断膨胀。例如调用 incr nums 命令 100 次,文件就会保存 100 条 incr nums 命令,其实 99 条都是多余的,因为要恢复数据只需要 set nums 100

为了压缩日志文件,Redis 提供 bgrewriteaof 命令。当 Redis 收到此命令,就使用类似于内存快照方式将内存的数据以命令的方式保存到临时文件中,最好替换原来的日志文件。

5. 扩展库 phpredis 安装及使用

// 下载 phpredis 源码
$ wget https://github.com/phpredis/phpredis/archive/3.1.4.tar.gz
// 解压
$ tar -xzvf 3.1.4.tar.gz
// 编译安装
$ cd phpredis-3.1.4
$ /usr/local/php/bin/phpize
$ ./configure -with -php -config = /usr/local/php/bin/php -config
$ make
$ make install
// 修改 php.ini
添加 extension = redis.so,重启 Web 服务器

扩展资料

  1. Redis 未授权访问漏洞

MySQL 主从复制

20-11月-17

主从复制功能通过在主服务器和从服务器之间切分处理客户查询的负荷,可以得到更好的客户响应时间, SELECT 查询可以发送到从服务器,以降低主服务器的查询处理负荷。修改数据的语句仍然发送到主服务器,以使主、从服务器保持同步。

1. 主从复制原理

主从复制通过 3 个过程实现,其中一个过程发生在主服务器上,另外两个过程发生在从服务器上。具体如下:

  • 主服务器将用户对数据库更新的操作以二进制格式保存到 Binary Log 日志文件中,然后由 Binlog Dump 线程将 Binary Log 日志文件传输给从服务器。

  • 从服务器通过一个 I/O 线程将主服务器的 Binary Log 日志文件中的更新操作复制到一个叫 Relay Log 的中继日志文件中。

  • 从服务器通过另一个 SQL 线程将 Relay Log 中继日志文件中的操作依次在本地执行,从而实现主从之间数据的同步。

主从复制详细过程如图:

主从复制

BinLog Dump 线程:

BinLog Dump 线程运行在主服务器上,主要工作是把 Binary Log 二进制日志文件的数据发送给从服务器。

I/O 线程:

从服务器执行 START SLAVE 语句后,创建一个 I/O 线程。此线程运行在从服务器上,与主服务器建立连接,然后向主服务器发出更新请求。之后,I/O 线程将主服务器发送的更新操作复制到本地 Relay Log 日志文件中。

SQL 线程:

SQL 线程运行在从服务器上,主要工作是读取 Relay Log 日志文件中的更新操作,并将操作依次执行,从而使主从服务器数据得到同步。

2. 主从复制配置

  1. 确认主从服务器的 MySQL 版本。

    MySQL 不同版本的 BinLog 格式可能不一样,最好采用相同版本。

  2. 在主服务器上为从服务器设置一个连接账户,授予 REPLICATION SLAVE 权限。

    mysql> create user 'backup'@'%' identified by '123456';
    mysql> grant replication slave on *.* to 'backup'@'%' identified by '123456';       
    
  3. 配置主服务器。

    打开二进制日志,指定唯一 Server ID。例如,在 my.cnf 配置文件中加入如下值:

    [mysqld]        
    log-bin = mysql-bin
    server-id = 1
    
  4. 重启主服务器。

    运行 show master status 语句,输出如图所示。

    占位图

    File 表示主服务器正在使用的 binlog 文件; Position 的值与 binlog 文件的大小相同,表示下一个被记录事件的位置; Binlog_Do_DBBinlog_lgnore_DB 是主服务器控制写入 binlog 文件内容的过滤选项,默认为空,表示不做任何过滤。

    FilePosition 两个字段指明从服务器将从哪个 binlog 文件中复制,以及复制的开始位置,它们也是 CHANGE MASTER TO 语句的参数。

  5. 配置从服务器。

    从服务器的配置与主服务器类似,必须提供一个唯一 Server ID (不能跟主服务器 ID 相同),配置完成后重启。

    [mysqld]
    log-bin = mysql-bin
    server-id = 2
    
  6. 指定主服务器信息。

    mysql> change master to 
    -> master_host='主服务器ip',
    -> master_user='backup',
    -> master_password='123456',
    -> master_port=3306,
    -> master_log_file='mysql-bin.000001',
    -> master_log_pos=0;
    

    此处指定 master_log_pos 的值为 0,因为要从日志的开始位置开始读。

3. 连接主从服务器。

  1. 执行 start slave 语句开始复制。

    mysql> start slave;
    
  2. 使用 show slave status \G 查看输出结果:

    占位图

    Slave_IO_RunningSlave_SQL_Running 值为 Yes 表明复制过程正常。

  3. 使用 show processlist 语句查看主、从服务器的线程状态。

    在主服务器上

    占位图

    第 1 行就是处理从服务器的 I/O 线程的连接。

    在从服务器上

    占位图

    第 1 行为 I/O 线程状态,图中没有 SQL 线程状态,因为 SQL 线程未开始运行。



扩展

// 刷新权限
mysql> flush privileges;
// 查看用户权限
mysql> show grants for backup;
// 可赋予的权限
mysql> grant select,insert,update,delete,alter,create,drop,lock tables on dbname.* to 'backup'@'%';
// 取消所有权限
mysql> revoke all on dbname.* from 'backup'@'%';
// 获取全局读锁
mysql> flush tables with read lock;
// 释放锁
mysql> unlock tables;

PHP cURL 扩展库

19-11月-17

cURL 是一个通用的库,并非 PHP 独有。PHP cURL 扩展库 是一个封装的函数库。可以用来模拟浏览器和服务器进行交互,功能比较强大。

1. 建立 cURL 请求

<?php
// 1. 初始化
$ch = curl_init();
// 2. 设置选项,包括 URL
curl_setopt($ch, CURLOPT_URL, "http://www.163100.com");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // 将 curl_exec() 获取的信息以文件流的形式返回,而不是直接输出
curl_setopt($ch, CURLOPT_HEADER, 1);         // 启动时会将头文件的信息作为数据流输出
curl_setopt($ch, CURLOPT_TIMEOUT, 60);       // 设置 cURL 允许执行的最长秒数
// 3. 执行并获取 HTML 文档内容
$output = curl_exec($ch);
if ($output === false) {
    echo 'cURL Error:' . curl_error($ch);
}
// 4. 释放 cURL 句柄
curl_close($ch);
echo $output; 

很多时候并不需要 header 头,把 CURLOPT_HEADER 设为 0 或者不设置(默认为 0)。

2. 获取请求相关的信息

通过 curl_getinfo() 函数可以返回 cURL 执行后这一请求相关的信息,如下所示:

<?php 
// ...
curl_exec($ch);
$info = curl_getinfo($ch);
echo '获取' . $info['url'] . '耗时' . $info['total_time'] . '秒';

// 返回 $info 数组如下
Array
(
    [url] => http://www.163100.com
    [content_type] => text/html; charset=UTF-8  // 内容编码
    [http_code] => 200
    [header_size] => 252                        // header 的大小
    [request_size] => 53                        // 请求的大小
    [filetime] => -1                            // 文件创建时间
    [ssl_verify_result] => 0                    // SSL 验证结果
    [redirect_count] => 0                       // 跳转次数
    [total_time] => 0.109                       // 耗时
    [namelookup_time] => 0                      // DNS 查询时间
    [connect_time] => 0.016                     // 连接时间
    [pretransfer_time] => 0.016                 // 准备传输耗时
    [size_upload] => 0                          // 上传数据大小
    [size_download] => 15903                    // 下载数据大小
    [speed_download] => 145899                  // 下载速度
    [speed_upload] => 0                         // 上传速度
    [download_content_length] => -1
    [upload_content_length] => 0
    [starttransfer_time] => 0.094               // 开始传输耗时
    [redirect_time] => 0                        // 重定向耗时
)

3. 在 cURL 中用 POST 方法发送数据

<?php
$url = "http://localhost/post.php";
$post_data = array(
    'foo'    =>  'bar',
    'query'  =>  'php',
    'action' => 'submit'
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
// 设置为 POST
curl_setopt($ch, CURLOPT_POST, 1);
// 把 POST 的变量加上
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
$output = curl_exec($ch);
if ($output === false) {
    echo 'cURL Error:' . curl_error($ch);
}
curl_close($ch);
echo $output;

4. 使用 cURL 上传文件

上传文件和 POST 类似,只需要把文件路径当作一个 POST 变量传过去,不过记得在前面加上 @ 符号。

<?php
// ...
$post_data = array(
    'foo'    => 'bar',
    'upload' => '@test.zip'
);
// ....


参考资料

  1. Techniques for Mastering cURL

MySQL 数据库规约

11-11月-17

此规范摘抄自【Java 编码规范】《阿里巴巴 Java 开发手册》,仅供参考,作者本人很是喜欢,所以整理于此。

一. 建表规约

  1. 【强制】表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint ( 1 表示是,0 表示否)。
    • 说明:任何字段如果为非负数,必须是 unsigned。
    • 正例:表达逻辑删除的字段名 is_deleted,1 表示删除,0 表示未删除。
  2. 【强制】表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只 出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。
    • 说明:MySQL 在 Windows 下不区分大小写,但在 Linux 下默认是区分大小写。因此,数据库 名、表名、字段名,都不允许出现任何大写字母,避免节外生枝。
    • 正例:aliyun_admin,rdc_config,level3_name
    • 反例:AliyunAdmin,rdcConfig,level_3_name
  3. 【强制】表名不使用复数名词。
    • 说明:表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数 形式,符合表达习惯。
  4. 【强制】禁用保留字,如 desc、range、match、delayed 等,请参考 MySQL 官方保留字。
  5. 【强制】主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx_字段名。 说明:pk_ 即 primary key;uk_ 即 unique key;idx_ 即 index 的简称。
  6. 【强制】小数类型为 decimal,禁止使用 float 和 double。
    • 说明:float 和 double 在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不 正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数分开存储。
  7. 【强制】如果存储的字符串长度几乎相等,使用 char 定长字符串类型。
  8. 【强制】varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长 度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索 引效率。
  9. 【强制】表必备三字段:id, gmt_create, gmt_modified。
    • 说明:其中 id 必为主键,类型为 unsigned bigint、单表时自增、步长为 1。gmt_create, gmt_modified 的类型均为 date_time 类型,前者现在时表示主动创建,后者过去分词表示被动更新。
  10. 【推荐】表的命名最好是加上“业务名称_表的作用”。
    • 正例:alipay_task / force_project / trade_config
  11. 【推荐】库名与应用名称尽量一致。
  12. 【推荐】如果修改字段含义或对字段表示的状态追加时,需要及时更新字段注释。
  13. 【推荐】字段允许适当冗余,以提高查询性能,但必须考虑数据一致。冗余字段应遵循:1)不是频繁修改的字段;2)不是 varchar 超长字段,更不能是 text 字段。

    • 正例:商品类目名称使用频率高,字段长度短,名称基本一成不变,可在相关联的表中冗余存 储类目名称,避免关联查询。
  14. 【参考】合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。

    对象 年龄区间 类型 字节 表示范围
    150岁之内 unsigned tinyint 1 无符号值:0 到 255
    数百岁 unsigned smalllint 2 无符号值:0 到 65535
    恐龙化石 数千万年 unsigned int 4 无符号值:0 到约 42.9 亿
    太阳 约 50 亿年 unsigned bigint 8 无符号值:0 到约 10 的 19 次方

二. 索引规约

  1. 【强制】业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引。
    • 说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。
  2. 【强制】超过三个表禁止 join。需要 join 的字段,数据类型必须绝对一致;多表关联查询时,保证被关联的字段需要有索引。
    • 说明:即使双表 join 也要注意表索引、SQL 性能。
  3. 【强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度即可。
    • 说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90%以上,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度 来确定。
  4. 【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
    • 说明:索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
  5. 【推荐】如果有 order by 的场景,请注意利用索引的有序性。order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 file_sort 的情况,影响查询性能。
    • 正例:where a=? and b=? order by c; 索引:a_b_c
    • 反例:索引中有范围查找,那么索引有序性无法利用,如:WHERE a>10 ORDER BY b; 索引 a_b 无法排序。
  6. 【推荐】利用覆盖索引来进行查询操作,避免回表。
    • 说明:如果一本书需要知道第 11 章是什么标题,会翻开第 11 章对应的那一页吗?目录浏览 一下就好,这个目录就是起到覆盖索引的作用。
    • 正例:能够建立索引的种类:主键索引、唯一索引、普通索引,而覆盖索引是一种查询的一种 效果,用 explain 的结果,extra 列会出现:using index。
  7. 【推荐】利用延迟关联或者子查询优化超多分页场景。
    • 说明:MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。
    • 正例:先快速定位需要获取的 id 段,然后再关联:
      • SELECT a.* FROM 表 1 a, (select id from 表 1 where 条件 LIMIT 100000,20 ) b where a.id=b.id
  8. 【推荐】 SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,如果可以是 consts 最好。
    • 说明:
      • consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。
      • ref 指的是使用普通的索引(normal index)。
      • range 对索引进行范围检索。
    • 反例:explain 表的结果,type=index,索引物理文件全扫描,速度非常慢,这个 index 级别比 range 还低,与全表扫描是小巫见大巫。
  9. 【推荐】建组合索引的时候,区分度最高的在最左边。
    • 正例:如果 where a=? and b=? ,a 列的几乎接近于唯一值,那么只需要单建 idx_a 索引即可。
    • 说明:存在非等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如:where a>? and b=? 那么即使 a 的区分度更高,也必须把 b 放在索引的最前列。
  10. 【推荐】防止因字段类型不同造成的隐式转换,导致索引失效。
  11. 【参考】创建索引时避免有如下极端误解:
    • 宁滥勿缺。认为一个查询就需要建一个索引。
    • 宁缺勿滥。认为索引会消耗空间、严重拖慢更新和新增速度。
    • 抵制惟一索引。认为业务的惟一性一律需要在应用层通过“先查后插”方式解决。

三. SQL 语句

  1. 【强制】不要使用 count(列名)或 count(常量)来替代 count(*),count(*)是 SQL92 定义的 标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。
    • 说明:count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为 NULL 值的行。
  2. 【强制】count(distinct col) 计算该列除 NULL 之外的不重复行数,注意 count(distinct col1, col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0。
  3. 【强制】当某一列的值全是 NULL 时,count(col)的返回结果为 0,但 sum(col)的返回结果为 NULL,因此使用 sum()时需注意 NPE 问题。
    • 正例:可以使用如下方式来避免 sum 的 NPE 问题:SELECT IF(ISNULL(SUM(g)),0,SUM(g)) FROM table;
  4. 【强制】使用 ISNULL()来判断是否为 NULL 值。
    • 说明:NULL 与任何值的直接比较都为 NULL。
      • NULL<>NULL 的返回结果是 NULL,而不是 false。
      • NULL=NULL 的返回结果是 NULL,而不是 true。
      • NULL<>1 的返回结果是 NULL,而不是 true。
  5. 【强制】 在代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句。
  6. 【强制】不得使用外键与级联,一切外键概念必须在应用层解决。
    • 说明: 以学生和成绩的关系为例,学生表中的student_id是主键,那么成绩表中的student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。
  7. 【强制】禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。
  8. 【强制】数据订正时,删除和修改记录时,要先 select,避免出现误删除,确认无误才能执行更新语句。
  9. 【推荐】in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在 1000 个之内。
  10. 【参考】如果有全球化需要,所有的字符存储与表示,均以 utf-8 编码,注意字符统计函数的区别。
    • 说明:
      • SELECT LENGTH(“轻松工作”); 返回为 12
      • SELECT CHARACTER_LENGTH(“轻松工作”); 返回为 4
      • 如果需要存储表情,那么选择 utfmb4 来进行存储,注意它与 utf-8 编码的区别。
  11. 【参考】 TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少,但 TRUNCATE 无事务且不触发 trigger,有可能造成事故,故不建议在开发代码中使用此语句。
    • 说明:TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同。

UML 类图中类之间的关系详解

05-11月-17

类与类之间的关系主要有六种:泛化实现组合聚合关联依赖

从一个示例开始

请看下面这张类图,类之间的关系是我们需要关注的:

类图

  • 车的类图结构为<<abstract>>,表示车是一个抽象类。
  • 它有两个继承类:小汽车和自行车;它们之间的关系为 实现关系,使用带空心箭头的虚线表示。
  • 小汽车与SUV之间也是继承关系,它们之间的关系为 泛化关系,使用带空心箭头的实线表示。
  • 小汽车与发动机之间是 组合关系,使用带实心箭头的实线表示。
  • 学生与班级之间是 聚合关系,使用带空心箭头的实线表示。
  • 学生与身份证之间为 关联关系,使用一根实线表示。
  • 学生上学需要用到自行车,与自行车是一种 依赖关系,使用带箭头的虚线表示。

示例解释说明

类的继承关系表现在 UML 中为:泛化实现

泛化关系在最终代码中表现为 继承非抽象类

小汽车在现实中有实现,可用小汽车定义具体的对象;小汽车与 SUV 之间为泛化关系。

实现关系在最终代码中表现为 继承抽象类

“车”是一个抽象概念,在现实中并无法直接用来定义对象,只有指明具体的子类(汽车还是自行车),才可以用来定义对象(“车”这个类在 C++ 中用抽象类表示,在 JAVA 中有接口这个概念,更容易理解)。

注:接口是抽象类的变体,接口中所有方法都是抽象的,没有一个有方法体。

聚合关系表示整体与部分的关系,整体和部分不是强依赖的,即使整体不存在了,部分仍然存在。

例如,一个部门由多个员工组成;部门撤销了,人员不会消失,他们依然存在。

组合关系表示整体与部分的关系,是一种强依赖的特殊聚合关系,如果整体不存在了,则部分也将不存在。

例如,公司由多个部门组成;公司不存在了,部门也将不存在。

关联关系描述不同类的对象之间的联系,是一种 最常用强关联 的关系,在最终代码中 关联对象通常是以成员变量的形式实现的

例如,乘车人和车票之间就是一种关联关系,学生和学校就是一种关联关系。

注:在 UML 图中,单向关联或自关联有一个箭头,双向关联可以有两个箭头或者没有箭头。关联关系默认为双向的,不强调方向。

依赖关系描述一个对象在运行期间会用到另一个对象的关系。在最终代码中 依赖关系体现在某个类的方法使用了另一个类的对象作为参数

与关联关系不同的是,它是一种临时性的关系,通常在运行期间产生,并且随着运行时的变化,依赖关系也可能发生变化。依赖也有方向,双向依赖是一种非常糟糕的结构,我们总是应该保持单向依赖,杜绝双向依赖的产生。

设计模式原则

03-11月-17

设计模式概念

设计模式用于解决反复出现的问题,是解决特定问题的指导方针。设计模式不是在应用中引用的类、package 或者库,而是在某些特定场景下解决特定问题的指导方针。

维基百科中这样描述设计模式

在软件工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。设计模式并不直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案。

并非所有的软件模式都是设计模式,设计模式特指软件“设计”层次上的问题。还有其他非设计模式的模式,如架构模式。同时,算法不能算是一种设计模式,因为算法主要是用来解决计算上的问题,而非设计上的问题。

经典的设计模式有 23 种,每种都是对代码复用和设计的总结。

设计模式分类

  • 创建型模式:专注于如何初始化对象。
  • 结构型模式:主要关注对象组合,换句话说,关注实体之间如何互相使用。或者还有另外一个解释,结构型模式有助于回答“如何构建软件组件?”
  • 行为型模式:关心对象之间的责任分配。与结构型模式不同的是,行为型模式不仅仅指定结构,而且还概述了它们之间的消息传递/通信的模式。或者换句话说,行为型模式帮助回答了“软件组件是如何运行的?”

设计模式原则

1. 单一职责原则(Single Responsibility Principle,SRP)

在 《敏捷软件开发》 中,把 职责 定义为 变化的原因,也就是说,就一个类而言,应该只有一个引起它变化的原因。这是一个最简单,最容易理解却最不容易做到的一个设计原则。

单一职责有两个含义:一个是避免相同的职责分散到不同的类中,另一个是避免一个类承担太多职责

设计模式举例:工厂模式、命令模式、代理模式

2. 接口隔离原则(Interface Segregation Principle,ISP)

使用多个专门的接口比使用单个接口要好得多。

ISP 强调的是接口对客户端(接口的使用方)的承诺越少越好,并且要做到专一。当某个客户程序的要求发生变化,而迫使接口发生改变时,影响到其他客户程序的可能性小。这实际上就是接口污染的问题。

过于臃肿的接口设计是对接口的污染。所谓接口污染就是为接口添加不必要的职责。为了能够重用被污染的接口,接口的实现类就被迫要实现并维护不必要的功能方法。

3. 开放-封闭原则(Open-Close Principle,OCP)

  • Open(Open for extension) 模块的行为必须是开放的、支持扩展的,而不是僵化的。
  • Closed(Closed for modification) 在对模块的功能进行扩展时,不应该影响或大规模地影响已有的程序模块。

用一句话概括就是:一个模块在扩展性方面应该是开放的而在更改性方面是封闭的

设计模式举例:装饰模式

4. 里氏替换原则(Liskov Substitution Principle,LSP)

LSP 主要是针对继承的设计原则,它指导我们如何正确地进行继承与派生。 子类必须能够替换掉他们的父类、并出现在父类能够出现的任何地方

如何遵守该设计原则呢?

  • 父类的方法都要在子类中实现或者重写,并且派生类只实现其抽象类中声明的方法,而不应当给出多余的方法定义或实现。
  • 在客户端程序中只应该使用父类对象而不应当直接使用子类对象,这样可以实现运行期内绑定(动态多态性)。

5. 依赖倒置原则(Dependence Inversion Principle,DIP)

什么是依赖倒置呢?简单的讲就是将依赖关系倒置为依赖接口。

为什么要依赖接口?因为接口体现对问题的抽象,同时由于抽象一般是相对稳定的或者是相对变化不频繁的,而具体是易变的。因此,依赖抽象是实现代码扩展和运行期内绑定(多态)的基础。

PHP 编码规范

02-11月-17

本规范包含 PHP 开发时程序编码中命名规范、代码缩进规则、控制结构、函数调用、函数定义、注释、包含代码、PHP标记、常量命名等方面的规则。

依据”约定大于规范”原则,本规范不强制指定和推荐某种格式,并就实际开发中个人习惯做了一些调整。

1. 文件格式

1.1 文件标记

所有 PHP 文件,其代码标记均使用 <?php ?> 完整 PHP 标签,不建议使用 <?= ?> 短标签。

对于只含有 PHP 代码的文件,必须 省略最后的 ?> 结束标签。这是为了防止多余空格或者其他字符影响到代码。

1.2 文件和目录命名

程序文件名和目录名均采用有意义的英文命名,不使用拼音或无意义的字母,多个词间使用驼峰法命名。

// 类统一采用
DemoTest.class.php
// 接口统一采用
DemoTest.interface.php

// 其他按照各自的方式

2. 命名规范

2.1 命名空间和类命名

根据规范,每个类都独立为一个文件,且命名空间至少有一个层次:顶级的组织名称(vendor name).

类的命名 必须 遵循 class MyClass 大写开头的驼峰命名规范。

<?php
// PHP 5.3及以后版本的写法
namespace Vendor\Model;

class Foo
{

}

2.2 类的常量、属性和方法命名

类的常量名所有字母都 必须 大写,多个词间使用下划线分隔。

<?php
namespace Vendor\Model;

class Foo
{
    const VERSION = '1.0';
    const DATE_APPROVED = '2017-11-01';
}

类的属性命名 可以 遵循小写开头的驼峰式 $userAvatar 或下划线分割式 $user_avatar。此条规范不做强制要求,但无论遵循那种命名方式,都 应该 在一定范围内保持一致。这个范围可以是整个团队、整个包、整个类或整个方法。

方法名称 必须 符合 showMsg() 式的小写开头的驼峰命名规范,建议采用动词或动词加名词的命名方式。不建议下面的函数名:

getPublishedAdvertisementByCategoryAndCategoryIdAndPosition()
// 上面的函数名可以提炼为
getAd($category, $categoryId, $position, $published)

2.3 关键字以及 True/False/Null

PHP 所有的 关键字 必须 全部小写。常量 truefalsenull必须 全部小写。

2.4 数据库命名

在数据库相关的命名中,一律不出现大写。详细规则如下所示。

  1. 数据表命名遵循以下规范:
    • 表名均使用小写字母;
    • 表名字使用统一的前缀,且前缀不能为空(模块化,且可有效规避 MYSQL 保留字);
    • 对于多个单词组成的表名,使用 “_” 间隔。
  2. 表字段命名遵循如下规则:
    • 全部使用小写字母命名;
    • 多个单词不用下划线进行分割,如 “opentime”、“addtime”;
    • 如果有必要,给常用字段加上表名首字母作为前缀;
    • 避免使用关键字和保留字,但约定俗成的除外。
  3. 存储过程、触发器、event 以及视图的命名在表的命名规则基础上,遵循以下规则:
    • 存储过程以 proc_ 开头,如 “proc_syn_nick_name_friend”;
    • 触发器以 tri_ 开头,如 “tri_blog_user_u”;
    • Event 调度以 event_ 开头,如 “event_rank”;
    • 视图以 view_ 开头,如 “view_blog_user”。

3. 代码风格

3.1 缩进和空格

使用 4 个空格作为缩进,而不使用 tab 缩进。

变量赋值时,等号左右留出空格。

3.2 namespace 以及 use 声明

namespace 声明后 必须 插入一个空白行。所有 use 必须namespace 后声明。use 声明语句后 必须 要有一个空白行。

<?php
namespace Vendor\Package;

use FooClass;
use BarClass as Bar;
use OtherVender\OtherPackage\BazClass;

// ... 更多的 PHP 代码在这里 ...

3.3 类、属性和方法

  • 类的开始花括号 必须 独占一行,结束花括号也 必须 在类主体后独占一行。
  • 方法的开始花括号 必须 独占一行,结束花括号也 必须 在方法主体后独占一行。参数左括号和右括号钱 一定不可 有空格。
  • 方法参数列表中,每个逗号后面 必须 要有一个空格,而逗号前面 一定不可 有空格。有默认值的参数 必须 放到参数列表的末尾。
  • 所有属性和方法都 必须 添加访问修饰符。
  • 需要添加 abstractfinal 声明时,必须 写在访问修饰符前,而 static必须 写在其后。

以下例子程序简单地展示了以上大部分规范:

<?php
namespace Vendor\Package;

use FooInterface;
use BarClass as Bar;
use OtherVendor\OtherPackage\BazClass;

class Foo extends Bar implements FooInterface
{
    public function sampleFunction($a, $b = null)
    {
        if ($a === $b) {
            bar();
        } elseif ($a > $b) {
            $foo->bar($arg1);
        } else {
            BazClass::bar($arg2, $arg3);
        }
    }   

    final public static function bar()
    {
        // 方法的内容
    }
}    

3.4 控制结构

  • 控制结构关键词后 必须 有一个空格。
  • 左括号 (一定不可 有空格,右括号 ) 前也 一定不可 有空格。
  • 右括号 ) 与开始花括号 {必须 有一个空格。
  • 结束花括号 } 必须 在结构体主体后单独成行。

3.5 switchcase

如果存在非空的 case 直穿语句,主体里 必须 有类似 // no break 的注释。

<?php
switch ($expr) {
    case 0:
        echo 'First case, with a break';
        break;  
    case 1:
        echo 'Second case, which falls through';
        // no break;    
    case 2:
    case 3:
    case 4:
        echo 'Third case, return instead of break';
        break;
    default:
        echo 'Default case';
        break;
}

4.注释规范

代码注释应该描述为什么,而不是做什么,给代码阅读者提供最主要的信息,不能为了注释而注释。