Skip to content

MySQL 事务隔离级别

23-4月-18

mysql 拥有分层的架构。上层是服务器层的服务和查询执行引擎,下层则是存储引擎。

mysql 支持 LOCK TABLESUNLOCK TABLES 语句,这是在服务器层实现的,和存储引擎无关。

mysql 服务器层 不管理事务,事务(行级锁)是由下层的存储引擎实现的。

事务

事务就是一组原子性的 SQL 查询,或者说一个独立的工作单元。事务内的语句,要么全部执行成功,要么全部执行失败。

银行应用是解释事务必要性的一个经典例子。假设一个银行的数据库有两张表:支票(checking)表和储蓄(savings)表。现在要从某个用户的支票账户转移 200 美元到他的储蓄账户,事务 SQL 的样本如下:

1 START TRANSACTION;
2 SELECT balance FROM checking WHERE customer_id = 10233276;
3 UPDATE checking SET balance = balance - 200.00 WHERE customer_id = 10233276;
4 UPDATE savings  SET balance = balance + 200.00 WHERE customer_id = 10233276;
5 COMMIT;

1. 事务的标准 ACID 特性

  1. 原子性(atomicity)
    • 一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性。
  2. 一致性(consistency)
    • 数据库总是从一个一致性的状态转换到另一个一致性的状态。一致性是对数据可见性的约束,保证在一个事务中的多次操作的数据中间状态对其它事务不可见,这些中间状态是一个过渡状态,与事务的开始状态和事务的结束状态是不一致的。在前面的例子中,一致性确保了,即使在执行第三、四条语句之间时系统崩溃,支票账户中也不会损失 200 美元。
  3. 隔离性(isolation)
    • 通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。在前面的例子中,当执行完第三条语句、第四条语句还未开始时,此时有另外一个账户汇总程序开始运行,则其看到的支票账户的余额并没有被减去 200 美元。
  4. 持久性(durability)
    • 一旦事务提交,则其所做的修改就会永久保存到数据库中。

原子性和一致性的侧重点不同:原子性关注状态,要么全部成功,要么全部失败。而一致性关注数据的可见性。

2. 隔离级别

在 SQL 标准中定义了四种隔离级别,每一种级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。较低级别的隔离通常可以执行更高的并发,系统的开销也更低。

下面简单地介绍一下四种隔离级别:

隔离级别 脏读可能性 不可重复读可能性 幻读可能性 加锁读
READ UNCOMMITTED Yes Yes Yes No
READ COMMITTED No Yes Yes No
REPEATABLE READ No No Yes No
SERIALIZABLE No No No Yes
  1. READ UNCOMMITTED(未提交读)
    • 在 READ UNCOMMITTED 级别,事务中的修改,即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,这也被称为 脏读(Dirty Read)
  2. READ COMMITTED(提交读)
    • 大多数数据库系统的默认隔离级别都是 READ COMMITTED(但 MySQL 不是)。READ COMMITTED 满足:一个事务开始时,只能“看见”已经提交的事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时候也叫做 不可重复读(nonrepeatable read),因为两次执行同样的查询,可能会得到不一样的结果。
  3. REPEATABLE READ(可重复读)
    • REPEATABLE READ 解决了脏读的问题。保证了在同一个事务中多次读取同样记录的结果是一致的。但是理论上,可重复读隔离级别无法解决幻读问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生 幻行(Phantom Row)。InnoDB 和 XtraDB 存储引擎通过 多版本并发控制(MVCC) 解决了幻读的问题。
    • 可重复读是 MySQL 的默认事务隔离级别。
  4. SERIALIZABLE(可串行化)
    • SERIALIZABLE 强制事务串行执行,避免了前面说的幻读问题。简单来说,SERIALIZABLE 会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁挣用问题。

3. 死锁

死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。

例如,设想下面两个事务同时处理 StockPrice 表:

事务 1
    START TRANSACTION;
    UPDATE StockPrice SET close = 45.50 WHERE stock_id = 4 and date = '2002-05-01';
    UPDATE StockPrice SET close = 19.80 WHERE stock_id = 3 and date = '2002-05-02';
    COMMIT;     

事务 2
    START TRANSACTION;
    UPDATE StockPrice SET close = 20.12 WHERE stock_id = 3 and date = '2002-05-02';
    UPDATE StockPrice SET close = 47.20 WHERE stock_id = 4 and date = '2002-05-01';
    COMMIT;

如果凑巧,两个事务都执行了第一条 UPDATE 语句,更新了一行数据,同时也锁定了改行数据,接着每个事务都尝试去执行第二条 UPDATE 语句,却发现改行已经被对方锁定,然后两个事务都等待对方释放锁,同时又持有对方需要的锁,则陷入死循环。除非有外部因素介入才可能解除死锁。

MyISAM 表锁不会产生死锁。InnoDB 目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚(这是相对简单的死锁回滚算法)。

参考资料

数据库事务原子性、一致性是怎样实现的?

PHP 笔试猴子选大王

22-4月-18

猴子选大王是一个典型的编程问题,一般可用链表(可以用很大的数)或者 while 循环(使用此办法不能用太大的数)解决。

问题描述

n 只猴子围坐成一个圈,按顺时针方向从 1 到 n 编号。然后从 1 号猴子开始沿顺时针方向从 1 开始报数,报到 m 的猴子出局,再从刚出局猴子的下一个位置重新开始报数,如此重复,直至剩下一个猴子,它就是大王。要求编程模拟此过程,输入 m、n,输出最后那个大王的编号。

解决方法

<?php
/**
 * 猴子选大王
 * @param int $m 数到第 m 只踢出
 * @param int $n 猴子总数
 */
function king($n, $m)
{
    // 生成数组
    $arr = range(1, $n);    
    $i = 1; // 从 1 开始数
    while (count($arr) > 1) {
        if ($i % $m  == 0) {
            unset($arr[$i-1]);
        } else {
            // 本轮非出局的猴子放在数组尾部
            array_push($arr, $arr[$i-1]);
            unset($arr[$i-1]);
        }
        $i++;
    }
    return $arr;
}

扩展资料

  1. PHP 高级研发工程师面试题总结

PHP 四种基础算法

21-4月-18

一. 冒泡排序

冒泡排序步骤如下:

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

实现示例

function bubbleSort($arr)
{
    $len = count($arr);
    // 该层循环控制需要冒泡的轮数
    for ($i=0; $i<$len; $i++) {
        // 该层循环控制每轮冒出一个数需要比较的次数
        for ($j=0; $j<$len-$i-1; $j++) {
            if ($arr[$j] > $arr[$j+1]) {
                $temp = $arr[$j];
                $arr[$j] = $arr[$j+1];
                $arr[$j+1] = $temp; 
            }
        }
    }   
    return $arr;
}

二. 快速排序

快速排序步骤如下:

  1. 从数列中挑出一个元素,称为”基准”(pivot)。
  2. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  3. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。

示例动画

占位图

实现示例

function quickSort($arr)
{
    $len = count($arr);
    if ($len <= 1) {
        return $arr;
    }

    $k = $arr[0]; // 基准数    
    $leftArr = array();
    $rightArr = array();
    for ($i=1; $i<$len; $i++) {
        if ($arr[$i] <= $k) {       // 比基准数小的元素放在基准左边   
            $leftArr[] = $arr[$i]; 
        } elseif ($arr[$i] > $k) {  // 比基准数大的元素放在基准右边
            $rightArr[] = $arr[$i]; 
        }
    }
    $leftArr = quickSort($leftArr);
    $rightArr = quickSort($rightArr);
    return array_merge($leftArr, array($k), $rightArr);
}

三. 选择排序

选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理是:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对 n 个元素的表进行排序总共进行至多 n-1 次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。

示例动画

占位图

实现示例

function selectSort($arr) 
{
    $len = count($arr);
    for ($i=0; $i<$len-1; $i++) {
        $min = $i;
        for ($j=$i+1; $j<$len; $j++) {
            if ($arr[$min] > $arr[$j]) {
                $min = $j;
            }
        }
        // 如果最小值的位置和当前假设的位置 $i 不同,则位置互换
        if ($min != $i) {
            $temp = $arr[$min];
            $arr[$min] = $arr[$i];
            $arr[$i] = $temp;
        }
    }   
    return $arr;
}

四. 插入排序

插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工作原理是:通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用 in-place 排序,因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

插入排序步骤如下:

  1. 从第一个元素开始,该元素可以认为已经被排序
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
  5. 将新元素插入到该位置后
  6. 重复步骤2~5

示例动画

占位图

实现示例

function insertSort($arr)
{
    for ($i=1; $i<count($arr); $i++) {
        if ($arr[$i-1] > $arr[$i]) {
            $temp = $arr[$i];
            for ($j=$i-1; $j>=0 && $arr[$j]>$temp; $j--) {
                $arr[$j+1] = $arr[$j];              
            }
            $arr[$j+1] = $temp;
        }
    }
    return $arr;
}

程序执行过程

15-3月-18

以下是 hello.c 的 C 语言源程序代码。

#include <stdio.h>

int main()
{
    printf("hello, world\n");
}

Unix 系统上,从源文件到目标文件的转化是由编译器驱动程序完成的:

linux> gcc -o hello hello.c

在这里,GCC 编译器驱动程序读取源程序文件 hello.c,并把它翻译成一个可执行目标文件 hello。这个翻译过程可分为四个阶段完成,如图所示。执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)一起构成了编译系统(compilation system)。

类图

  • 预处理阶段。预处理器(cpp)根据以字符 # 开头的命令,修改原始的 C 程序。比如 hello.c 中第 1 行的 #include <stdio.h> 命令告诉预处理器读取系统头文件 stdio.h 的内容,并把它直接插入程序文本中。结果就得到了另一个 C 程序,通常是以 .i 作为文件扩展名。

  • 编译阶段。编译器(ccl)将文本文件 hello.i 翻译成文本文件 hello.s,它包含一个汇编语言程序。对同一台机器来说,不管什么高级语言,编译转换后的输出结果使用的都是同一种机器级代码。

  • 汇编阶段。接下来,汇编器(as)将 hello.s 翻译成机器语言指令,把这些指令打包成一种叫做 可重定位目标程序(relocatable object program) 的格式,并将结果保存在目标文件 hello.o 中。hello.c 文件是一个二进制文件,它包含的 17 个字节是函数 main 的指令编码。

  • 链接阶段。请注意,hello 程序调用了 printf 函数,它是每个 C 编译器都提供的标准 C 库中的一个函数。printf 函数存在一个名为 printf.o 的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的 hello.o 程序中。链接器(ld)就负责处理这种合并。结果就得到 hello 文件,它是一个可执行目标文件(或者简称为可执行文件),可以被加载到内存中,由系统执行。

TCP 四次握手断开连接(图解)

26-2月-18

建立连接非常重要,它是数据正确传输的前提;断开连接同样重要,它让计算机释放不再使用的资源。如果连接不能正常断开,不仅会造成数据传输错误,还会导致套接字不能关闭,持续占用资源,如果并发量高,服务器压力堪忧。

建立连接需要三次握手,断开连接需要四次握手,可以形象的比喻为下面的对话:

  • [Shake 1] 套接字 A:“任务处理完毕,我希望断开连接。”
  • [Shake 2] 套接字 B:“哦,是吗?请稍等,我准备一下。”
  • 等待片刻后……
  • [Shake 3] 套接字 B:“我准备好了,可以断开连接了。”
  • [Shake 4] 套接字 A:“好的,谢谢合作。”

下图演示了客户端主动断开连接的场景:

TCP 断开连接图解

建立连接后,客户端和服务器都处于 ESTABLISED 状态。这时,客户端发起断开连接的请求:

1)客户端调用 close() 函数后,向服务器发送 FIN 数据包,进入 FIN_WAIT_1 状态。FIN 是 Finish 的缩写,表示完成任务需要断开连接。

2)服务器收到数据包后,检测到设置了 FIN 标志位,知道要断开连接,于是向客户端发送“确认包”,进入 CLOSE_WAIT 状态。

注意:服务器收到请求后并不是立即断开连接,而是先向客户端发送“确认包”,告诉它我知道了,我需要准备一下才能断开连接。

3)客户端收到“确认包”后进入 FIN_WAIT_2 状态,等待服务器准备完毕后再次发送数据包。

4)等待片刻后,服务器准备完毕,可以断开连接,于是再主动向客户端发送 FIN 包,告诉它我准备好了,断开连接吧。然后进入 LAST_ACK 状态。

5)客户端收到服务器的 FIN 包后,再向服务器发送 ACK 包,告诉它你断开连接吧。然后进入 TIME_WAIT 状态。

6)服务器收到客户端的 ACK 包后,就断开连接,关闭套接字,进入 CLOSED 状态。

关于 TIME_WAIT 状态的说明

客户端最后一次发送 ACK 包后进入 TIME_WAIT 状态,而不是直接进入 CLOSED 状态关闭连接,这是为什么呢?

TCP 是面向连接的传输方式,必须保证数据能够正确到达目标机器,不能丢失或出错,而网络是不稳定的,随时可能会毁坏数据,所以机器 A 每次向机器 B 发送数据包后,都要求机器 B ”确认“,回传 ACK 包,告诉机器 A 我收到了,这样机器 A 才能知道数据传送成功了。如果机器 B 没有回传 ACK 包,机器 A 会重新发送,直到机器 B 回传 ACK 包。

客户端最后一次向服务器回传 ACK 包时,有可能会因为网络问题导致服务器收不到,服务器会再次发送 FIN 包,如果这时客户端完全关闭了连接,那么服务器无论如何也收不到 ACK 包了,所以客户端需要等待片刻、确认对方收到 ACK 包后才能进入 CLOSED 状态。那么,要等待多久呢?

数据包在网络中是有生存时间的,超过这个时间还未到达目标主机就会被丢弃,并通知源主机。这称为 报文最大生存时间(MSL,Maximum Segment Lifetime)TIME_WAIT 要等待 2MSL 才会进入 CLOSED 状态。ACK 包到达服务器需要 MSL 时间,服务器重传 FIN 包也需要 MSL 时间,2MSL 是数据包往返的最大时间,如果 2MSL 后还未收到服务器重传的 FIN 包,就说明服务器已经收到了 ACK 包。

TCP 三次握手(图解)

07-2月-18

TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的通信协议,数据在传输前要建立连接,传输完毕后还要断开连接。

客户端在收发数据前要使用 connect() 函数和服务器建立连接。建立连接的目的是保证IP地址、端口、物理链路等正确无误,为数据的传输开辟通道。

TCP 建立连接时要传输三个数据包,俗称 三次握手(Three-way Handshaking)。可以形象的比喻为下面的对话:

  • [Shake 1] 套接字 A:“你好,套接字 B,我这里有数据要传送给你,建立连接吧。”
  • [Shake 2] 套接字 B:“好的,我这边已准备就绪。”
  • [Shake 3] 套接字 A:“谢谢你受理我的请求。”

TCP 数据包结构

我们先来看一下 TCP 数据包的结构:

TCP 数据包结构图

带阴影的几个字段需要重点说明一下:

1)序号:Seq(Sequence Number)序号占 32 位,用来标识从计算机 A 发送到计算机 B 的数据包的序号,计算机发送数据时对此进行标记。

2)确认号:Ack(Acknowledge Number)确认号占 32 位,客户端和服务器端都可以发送,Ack = Seq + 1。

3)标志位:每个标志位占用 1Bit,共有 6 个,分别为 URG、ACK、PSH、RST、SYN、FIN,具体含义如下:

  • URG:紧急指针(urgent pointer)有效。
  • ACK:确认序号有效。
  • PSH:接收方应该尽快将这个报文交给应用层。
  • RST:重置连接。
  • SYN:建立一个新连接。
  • FIN:断开一个连接。

对英文字母缩写的总结:Seq 是 Sequence 的缩写,表示序列;Ack(ACK) 是 Acknowledge 的缩写,表示确认;SYN 是 Synchronous 的缩写,原意是“同步的”,这里表示建立同步连接;FIN 是 Finish 的缩写,表示完成。

连接的建立(三次握手)

使用 connect() 建立连接时,客户端和服务器端会相互发送三个数据包,请看下图:

TCP 数据报结构图

客户端调用 socket() 函数创建套接字后,因为没有建立连接,所以套接字处于 CLOSED 状态;服务器端调用 listen() 函数后,套接字进入 LISTEN 状态,开始监听客户端请求。

这个时候,客户端开始发起请求:

1)当客户端调用 connect() 函数后,TCP 协议会组建一个数据包,并设置 SYN 标志位,表示该数据包是用来建立同步连接的。同时生成一个随机数字 1000,填充“序号(Seq)”字段,表示该数据包的序号。完成这些工作,开始向服务器端发送数据包,客户端就进入了 SYN-SEND 状态。

2)服务器端收到数据包,检测到已经设置了 SYN 标志位,就知道这是客户端发来的建立连接的“请求包”。服务器端也会组建一个数据包,并设置 SYN 和 ACK 标志位,SYN 表示该数据包用来建立连接,ACK 用来确认收到了刚才客户端发送的数据包。

服务器生成一个随机数 2000,填充“序号(Seq)”字段。2000 和客户端数据包没有关系。

服务器将客户端数据包序号(1000)加 1,得到 1001,并用这个数字填充“确认号(Ack)”字段。

服务器将数据包发出,进入 SYN-RECV 状态。

3)客户端收到数据包,检测到已经设置了 SYN 和 ACK 标志位,就知道这是服务器发来的“确认包”。客户端会检测“确认号(Ack)”字段,看它的值是否为 1000+1,如果是就说明连接建立成功。

接下来,客户端会继续组建数据包,并设置 ACK 标志位,表示客户端正确接收了服务器发来的“确认包”。同时,将刚才服务器发来的数据包序号(2000)加 1,得到 2001,并用这个数字来填充“确认号(Ack)”字段。

客户端将数据包发出,进入 ESTABLISED 状态,表示连接已经成功建立。

4)服务器端收到数据包,检测到已经设置了 ACK 标志位,就知道这是客户端发来的“确认包”。服务器会检测“确认号(Ack)”字段,看它的值是否为 2000+1,如果是就说明连接建立成功,服务器进入 ESTABLISED 状态。

至此,客户端和服务器都进入了 ESTABLISED 状态,连接建立成功,接下来就可以收发数据了。

最后的说明

三次握手的关键是要确认对方收到了自己的数据包,这个目标就是通过“确认号(Ack)”字段实现的。计算机会记录下自己发送的数据包序号 Seq,待收到对方的数据包后,检测“确认号(Ack)”字段,看 Ack = Seq + 1是否成立,如果成立说明对方正确收到了自己的数据包。

通用 Linux 服务器安全配置指南

25-1月-18

本文是可参考的实际操作,不涉及如 IP 欺骗这样的原理,而且安全问题也不是几行命令就能预防的,这里只是 Linux 系统上基本的安全加固方法,后续有新的内容再添加进来。

注:所有文件在修改之前都要进行备份如

cp /etc/passwd{,.bak}

1. 禁用不使用的用户

可以使用命令 usermod -L usernamepasswd -l username 锁定。

2. 关闭不使用的服务

启动 Linux 系统时,可以进入不同的模式,这模式我们称为执行等级(run level)。不同的执行等级有不同的功能与服务,目前你先知道正常的执行等级有两个,一个是具有 X 窗口界面的 run level 5,另一个则是纯文本界面的 run level 3。

chkconfig --list | grep '3:on'      // 显示出目前在 run level 为 3 启动的服务
chkconfig --level 2345 postfix on   // 让 postfix 这个服务在 run level 为 2,3,4,5 时启动

// 关闭邮件服务
service postfix stop
chkconfig --level 2345 postfix off

3. 禁用 IPV6

IPV6 是为了解决 IPV4 地址耗尽的问题,但我们的服务器一般用不到它,反而禁用 IPV6 不仅仅会加快网络,还会有助于减少管理开销和提高安全级别。

4. iptables 规则

启用 linux 防火墙来禁止非法程序访问。使用 iptable 的规则来过滤入站、出站和转发的包。我们可以针对来源和目的地址进行特定 udp/tcp 端口的准许和拒绝访问。

// 只允许指定 ip 访问 6379 端口服务
# vim /etc/sysconfig/iptables
-I INPUT -p tcp --dport 6379 -j DROP
-I INPUT -s 127.0.0.1 -p tcp --dport 6379 -j ACCEPT
-I INPUT -s 183.14.11.* -p tcp --dport 6379 -j ACCEPT

5. SSH 安全

如果有可能,第一件事就是修改 ssh 的默认端口 22,改成如 20002 这样的较大端口会大幅提高安全系数,降低 ssh 破解登录的可能性。

// 修改 ssh 端口
# vim /etc/ssh/sshd_config
Port 22333

// 登录超时。用户在线 5 分钟无操作则超时断开连接,在 `/etc/profile`中添加
export TMOUT=300
readonly TMOUT

// 禁止 root 直接远程登录
# vim /etc/ssh/sshd_config
PermitRootLogin no

// 限制登录失败次数并锁定。在 `/etc/pam.d/login` 后添加
auth required pam_tally2.so deny=6 unlock_time=180 even_deny_root root_unlock_time=180 // 登录失败 5 次锁定 180 秒

// 登录 IP 限制。在 sshd_config 中定死允许 ssh 的用户和来源 ip
## allowed ssh users sysmgr
AllowUsers sysmgr@172.29.73.*

6. 配置只能使用密钥文件登录

使用密钥文件代替普通的简单密码认证也会极大的提高安全性。

占位图

将公钥重名为 authorized_key

# mv ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys
# chmod 600 ~/.ssh/authorized_keys

下载私钥文件 id_rsa 到本地(为了更加容易识别,可重命名为 hostname_username_id_rsa),保存到安全的地方。以后 username 用户登录这台主机就必须使用这个私钥,配合密码短语来登录(不再使用 username 用户自身的密码)

另外还要修改 /etc/ssh/sshd_conf 文件,打开注释

RSAAuthentication yes
PubkeyAuthentication yes
AuthorizedKeysFile      .ssh/authorized_keys

PasswordAuthentication no

使用命令 service sshd restart 重启 sshd 服务。

7. 减少 history 命令记录

执行过的历史命令记录越多,从一定程度上讲会给维护带来简便,但同样会伴随安全问题。

# vim /etc/profile
HISTSIZE=50

或每次退出时,使用命令 history -c 清理 history。

8. 增强特殊文件权限

给下面的文件加上不可更改属性,从而防止非授权用户获取权限

chattr +i /etc/passwd
chattr +i /etc/shadow
chattr +i /etc/group
chattr +i /etc/gshadow
chattr +i /etc/services         // 给系统服务端口列表文件加锁,防止未经许可的删除或添加服务
chattr +i /etc/pam.d/su
chattr +i /etc/ssh/sshd_config
chattr +i /var/spool/cron/root  // root 用户定时任务

9. 防止一般网络攻击

网络攻击不是几行设置就能避免的,以下都只是些简单的将可能性降到最低,增大攻击的难度但并不能完全阻止。

// 禁 ping
# echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all

// 或使用 iptable 禁 ping
# iptables -A INPUT -p icmp --icmp-type 0 -s 0/0 -j DROP
# iptables -A OUTPUT -p icmp --icmp-type 8 -j DROP          // 不允许ping其他主机

// 防止 IP 欺骗。编辑 `/etc/host.conf` 文件并增加如下几行
order hosts,bind
multi on
nospoof on

10. 定期做日志安全检查

将日志移动到专用的日志服务器里,这可避免入侵者轻易的改动本地日志。

大型网站架构演化

19-1月-18

很久以前读过一本书,书名 《大型网站技术架构》。因为架构是个很大的命题,此书篇幅有限,写的内容比较泛,比较适合刚接触网站架构的童鞋,读完此书对网站架构体系能有一个整体的认知。现将书中主要内容整理如下:

1. 初始阶段的网站架构

应用程序、数据库、文件等所有的资源都放在一台服务器上。使用 LNAMP,汇集各种免费开源软件及一台廉价服务器就可以开始网站的发展之路了。

占位图

2. 应用服务和数据服务分离

随着网站业务的发展,一台服务器逐渐不能满足需求,这时就需要将应用和数据分离。应用和数据分离后整个网站使用三台服务器:应用服务器、文件服务器、和数据库服务器。

这三台服务器对硬件资源的要求各不相同:

  1. 应用服务器需要处理大量的业务逻辑,因此需要更快更强大的 CPU;
  2. 数据库服务器需要快速磁盘检索和数据缓存,因此需要更快的硬盘和更大的内存;
  3. 文件服务器需要存储大量用户上传的文件,因此需要更大的硬盘。

占位图

3. 使用缓存改善网站性能

随着用户逐渐增多,网站又一次面临挑战:数据库压力太大导致访问延迟,进而影响整网站的性能。

网站访问特点和现实世界的财富分配一样遵循二八定律:80% 的业务访问集中在 20% 的数据上。

既然大部分的业务访问集中在一小部分数据上,把这一小部分数据缓存在内存中,减少数据库的访问压力,提高整个网站的数据访问速度。

网站使用的缓存可以分为两种:缓存在应用服务器上的本地缓存和缓存在专门的分布式缓存服务器上的远程缓存。本地缓存的访问速度更快一些,但是受应用服务器内存限制,其缓存数据量有限,而且会出现和应用程序争用内存的情况。远程分布式缓存可以使用集群的方式,部署大内存的服务器作为专门的缓存服务器。

占位图

4. 使用应用服务器集群改善网站的并发处理能力

单一应用服务器能够处理的请求连接有限,在网站访问高峰期,应用服务器成为整个网站的瓶颈。

对网站架构而言,只要能通过增加一台服务器的方式改善负载压力,就可以以同样的方式持续增加服务器不断改善系统性能,从而实现系统的可伸缩性

通过负载均衡调度服务器,可将来自用户浏览器的访问请求分发到应用服务器集群中的任何一台服务器上。

占位图

5. 数据库读写分离

使用缓存后,仍有一部分缓存访问不命中、缓存过期和全部的写操作需要访问数据库。

主从复制,实现数据库读写分离,从而改善数据库负载压力。

占位图

6. 使用反向代理和 CDN 加速网站响应

随着网站业务不断发展,用户规模越来越大,由于中国复杂的网络环境,不同地区的用户访问网站时,速度差别也极大。使用 CDN 和反向代理的目的都是尽早返回数据给用户。

CDN 和反向代理的基本原理都是缓存,区别在于 CDN 部署在网络提供商的机房,使用户在请求网站服务时,可以从距离自己最近的网络提供商机房获取数据;而反向代理则部署在网站的中心机房。

占位图

7. 使用分布式文件系统和分布式数据库系统

任何强大的单一服务器都满足不了大型网站持续增长的业务需求。数据库经过读写分离后,从一台服务器拆分成两台服务器,但是随着网站业务的发展依然不能满足需求,这时需要使用分布式数据库。文件系统也一样,需要使用分布式文件系统。

分布式数据库是网站数据库拆分的最后手段,只有在单表数据规模非常庞大的时候才使用。不到不得已时,网站更常用的数据库拆分手段是业务分库,将不同业务的数据部署在不同的物理服务器上。

占位图

8. 使用 NoSQL 和搜索引擎

随着网站业务越来越复杂,对数据存储和检索的需求也越来越复杂,网站需要采用一些非关系型数据库技术如 NoSQL 和非数据库查询技术如搜索引擎。

NoSQL 和搜索引擎对可伸缩的分布式特性具有更好的支持。应用服务器则通过一个统一数据访问模块访问各种数据,减轻应用程序管理诸多数据源的麻烦。

占位图

9. 业务拆分

大型网站为了应对日益复杂的业务场景,通过使用分而治之的手段将整个网站业务分成不同的产品线,如大型购物交易网站就会将首页、商铺、订单、买家、卖家等拆分成不同的产品线,分归不同的业务团队负责。

具体到技术上,也会根据产品线划分,将一个网站拆分成许多不同的应用,每个应用独立部署维护。

占位图

10. 分布式服务

随着业务拆分越来越小,存储系统越来越庞大,应用系统的整体复杂度呈指数级增加,部署维护越来越困难。由于所有应用要和所有数据库系统连接,在数万台服务器规模的网站中,这些连接的数目是服务器规模的平方,导致数据库连接资源不足,拒绝服务。

既然每一个应用系统都需要执行许多相同的业务操作,比如用户管理、商品管理等,那么可以将这些共用的业务提取出来,独立部署。由这些可复用的业务连接数据库,提供共用业务服务,而应用系统只需要管理用户界面,通过分布式服务调用共用业务服务完成具体业务操作。

占位图

常用命令笔记(一)

26-12月-17

工作中常用命令汇总。持续更新 ing ……

// 去除字段内容中的换行和回车
UPDATE `table` SET `position`= REPLACE(REPLACE(`position`, CHAR(10), ''), CHAR(13), '') WHERE 条件;

// svn 更新冲突选项说明
tc 以svn服务器上为准
mc 以本地代码为准

// git 提交
git add .
git commit -m 'My first file'
git push origin master

// find 命令删除文件  
find . -type f -size 768c -delete // c 代表 bytes
find . -name 'COCKIE*' -delete

// 统计文件数量
ls -l|grep 'error_log'|wc -l

// 动态修改 Redis 配置参数
CONFIG SET requirepass password

// 打包并以 gzip 压缩
tar -zcvf abc.tar.gz test 123.txt // 目录 test 文件 123.txt

// scp 命令
scp -P22 -r ./backupdatabase.sh root@120.24.**.**:/root/

// 设置临时 IP 地址
ifconfig eth0 ip 地址 

// 创建软链接
ln -s ./release_v4.2.1.20171102/api.xxx.com ./api.xxx.com

// chattr 命令
chattr +i api.xxx.com               // 设定文件不能被删除、改名、设定链接关系,同时不能写入或新增内容
chattr -R +a uploadFiles            // 只能向文件中附加数据

// 查看磁盘使用情况
df -h

// 查看当前目录下所有一级子目录文件夹大小
du -h --max-depth=1 ./

// 批量杀死进程
kill -9 $(lsof -i tcp:6379 -t)
ps -ef|grep root.sh|grep -v grep|awk '{print "kill -9 " $2}'|sh

// 查看某一端口的占用情况
lsof -i:端口号
netstat -tunlp|grep 端口号

// 查看所有端口
netstat -tunlp  // t 代表 tcp,u 代表 udp

// 查看不同状态的连接数量
netstat -an | awk '/^tcp/ {++y[$NF]} END {for(w in y) print w, y[w]}'

Iptables 配置
// 屏蔽网址
-A OUTPUT -m string --string "lnk0.com" --algo bm -j DROP 
// 只允许指定 ip 访问 redis 服务
-I INPUT -p tcp --dport 6379 -j DROP
-I INPUT -s 127.0.0.1 -p tcp --dport 6379 -j ACCEPT
-I INPUT -s 183.14.28.173 -p tcp --dport 6379 -j ACCEPT

PHP 加解密函数

26-12月-17

超好用的一对 PHP 加解密函数。

/**
 * 加密函数
 * @author zhuhz
 * @param string $string 需要加密的字符串
 * @param string $key 密钥
 * @return string 返回加密结果
 */
function encrypt($string, $key = '') {
    if (empty($string)) return $string;
    if (empty($key)) $key = config_item('encryption_key');
    $chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.";
    $ikey ="vxGx2BkOm0qIr9s36-Rgp6Z.r6ogZm_W";
    $nh1 = rand(0,64);
    $nh2 = rand(0,64);
    $nh3 = rand(0,64);
    $ch1 = $chars{$nh1};
    $ch2 = $chars{$nh2};
    $ch3 = $chars{$nh3};
    $nhnum = $nh1 + $nh2 + $nh3;
    $knum = 0;$i = 0;
    while(isset($key{$i})) {
        $knum +=ord($key{$i++});
    }
    $mdKey = substr(md5(md5(md5($key.$ch1).$ch2.$ikey).$ch3),$nhnum%8,$knum%8 + 16);
    $string = base64_encode(time().'_'.$string);
    $string = str_replace(array('+','/','='),array('-','_','.'),$string);
    $tmp = '';
    $j=0;$k = 0;
    $tlen = strlen($string);
    $klen = strlen($mdKey);
    for ($i=0; $i<$tlen; $i++) {
        $k = ($k == $klen) ? 0 : $k;
        $j = ($nhnum+strpos($chars,$string{$i})+ord($mdKey{$k++}))%64;
        $tmp .= $chars{$j};
    }
    $tmplen = strlen($tmp);
    $tmp = substr_replace($tmp,$ch3,$nh2 % ++$tmplen,0);
    $tmp = substr_replace($tmp,$ch2,$nh1 % ++$tmplen,0);
    $tmp = substr_replace($tmp,$ch1,$knum % ++$tmplen,0);

    return $tmp;
}

/**
 * 解密函数
 * @author zhuhz
 * @param string $string 需要解密的字符串
 * @param string $key 密匙
 * @return string 字符串类型的返回结果
 */
function decrypt($string, $key = '', $ttl = 0) {
    if (empty($string)) return $string;
    if (empty($key)) $key = config_item('encryption_key');
    $chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.";
    $ikey ="vxGx2BkOm0qIr9s36-Rgp6Z.r6ogZm_W";
    $knum = 0;$i = 0;
    $tlen = strlen($string);
    while(isset($key{$i})) {
        $knum +=ord($key{$i++});
    }
    $ch1 = $string{$knum % $tlen};
    $nh1 = strpos($chars,$ch1);
    $string = substr_replace($string,'',$knum % $tlen--,1);
    $ch2 = $string{$nh1 % $tlen};
    $nh2 = strpos($chars,$ch2);
    $string = substr_replace($string,'',$nh1 % $tlen--,1);
    $ch3 = $string{$nh2 % $tlen};
    $nh3 = strpos($chars,$ch3);
    $string = substr_replace($string,'',$nh2 % $tlen--,1);
    $nhnum = $nh1 + $nh2 + $nh3;
    $mdKey = substr(md5(md5(md5($key.$ch1).$ch2.$ikey).$ch3),$nhnum % 8,$knum % 8 + 16);
    $tmp = '';
    $j=0; $k = 0;
    $tlen = strlen($string);
    $klen = strlen($mdKey);
    for ($i=0; $i<$tlen; $i++) {
        $k = ($k == $klen) ? 0 : $k;
        $j = strpos($chars,$string{$i})-$nhnum - ord($mdKey{$k++});
        while ($j<0) $j+=64;
        $tmp .= $chars{$j};
    }
    $tmp = str_replace(array('-','_','.'),array('+','/','='),$tmp);
    $tmp = trim(base64_decode($tmp));

    if (preg_match("/\d{10}_/s",substr($tmp,0,11))){
        if ($ttl > 0 && (time() - substr($tmp,0,11) > $ttl)){
            $tmp = null;
        }else{
            $tmp = substr($tmp,11);
        }
    }

    return $tmp;
}