2018年10月31日 星期三

《面試官別再問》PHP-高並發和大流量的解決方案

高並發架構相關概念

1、QPS (每秒查詢率) : 每秒鐘請求或者查詢的數量,在互聯網領域,指每秒響應請求數(指HTTP請求)

2、PV(Page View):綜合瀏覽量,即頁面瀏覽量或者點擊量,一個訪客在24小時內訪問的頁面數量

--注:同一個人瀏覽你的網站的同一頁面,只記做一次pv

3、吞吐量(fetches/sec) :單位時間內處理的請求數量(通常由QPS和並發數決定)

4、響應時間:從請求發出到收到響應花費的時間

5、獨立訪客(UV):一定時間範圍內,相同訪客多次訪問網站,只計算為1個獨立訪客

6、帶寬:計算帶寬需關注兩個指標,峰值流量和頁面的平均大小

7、日網站帶寬: PV/統計時間(換算到秒) * 平均頁面大小(kb)* 8

高並發解決方案

1、前端優化

(1) 減少HTTP請求[將css,js等合併]

(2) 添加異步請求(先不將所有數據都展示給用戶,用戶觸發某個事件,才會異步請求數據)

(3) 啟用瀏覽器緩存和文件壓縮

(4) CDN加速

(5) 建立獨立的圖片服務器(減少I/O)

2、服務端優化

(1) 頁面靜態化

(2) 並發處理

(3) 隊列處理 

3、數據庫優化


(1) 數據庫緩存

(2) 分庫分錶,分區

(3) 讀寫分離

(4) 負載均衡

4、web服務器優化


(1) nginx反向代理實現負載均衡

(2) lvs實現負載均衡



涉及搶購、秒殺、抽獎、搶票等活動時,為了避免超賣,那麼庫存數量是有限的,但是如果同時下單人數超過了庫存數量,就會導致商品超賣問題。

那麼我們怎麼來解決這個問題呢

高並發的情況下,正常邏輯寫的話數據庫的庫存會出現負數,對付這類問題有很多解決方案,我就不一一贅述,我這次用的是redis的隊列機制。


採用ab壓力測試:

-r 指定接收到錯誤信息時不退出程序

-t 等待響應的最大時間

-n 指定壓力測試總共的執行次數

-c 用於指定壓力測試的並發數

進入apache的bin目錄 cmd輸入下列分代碼

E:\phpstudy\Apache\bin>ab -r -t 60 -n 3000 -c 600 http://127.0.0.1/api/kill/index/id/1

結果

2018年10月30日 星期二

《面試官別再問》PHP實作CI3 + JQuery前後端分離

我剛開始接觸前後端分離的時候,正值它開始慢慢擴散的時候,也還沒有意識到它帶來的好處。覺得它甚是麻煩,當我改一個接口的時候,我需要同時修改兩部分的代碼,以及對應的測試。反而,還不如直接修改原有的模板來得簡單。
可是當我去使用這個,由前後端分離做成的單頁面應用時,我開始覺得這些是值得。當頁面加載完後,每打開一個新的鏈接時,不再需要等網絡返回給我結果;我也能快速的回到上一個頁面,像一個 APP 一樣的體現這樣的應用。整個過程裡,我們只是不斷地從後台去獲取數據,不需要重複地請求頁面——因為這些頁面的模板已經存在本地了,我們所缺少的只是實時的數據。
後來,當我從架構去考慮這件事時,我才發現這種花費是值得的。
Web研發模式演變


傳統的 MVC 架構裡,因為某些原因有相當多的商業邏輯,會被放置到View 層,也就是模板層裡。換句話來說,就是這些邏輯都會被放到前端。我們看到的可能就不是各種if 、else 還有簡單的equal 判斷,還會包含一些複雜的商業邏輯,比如說對某些產品進行特殊的處理。
如果這個時候,我們還需要做各種頁面交互,如填寫表單、 Popup 、動態數據等等,就不再是簡單的和模板引擎打交道了。我們需要編寫大量的JavaScript 代碼,因為業務的不斷增加,僅使用jQuery 無法管理如此復雜的代碼。

一、前端取得資料使用令牌進行身份驗證過程

客戶端接收到令牌後對其進行儲存,每次訪問時需攜帶的令牌
服務端在接受到客戶端請求時需驗證令牌,驗證成功則回傳資料。
生成與驗證令牌的方法有很多種,這裡使用的是jwt(Json Web Token)。
Use composer to manage your dependencies and download PHP-JWT:

composer require firebase/php-jwt


二、定義後端API接口

這是有可能的使用 HTTP 動詞(request method)去定義你的路由規則。這是特別有用的當建立 RESTful 應用程式的時後。你可以使用標準的 HTTP 動詞(GET、PUT、POST、DELETE、PATCH)或者客製化的動詞像是(e.g. PURGE)。 HTTP 動詞規則不區分大小寫。所有你需要做的路由,就是將動詞增加到你的陣列索引裡面。

例如:

$route['api']['put'] = 'product/insert';

上述例子,PUT 請求到 URI“products” 稱之為 Product::insert() 控制器方法。

$route['api/(:num)']['DELETE'] = 'product/delete/$1';

DELETE 請求到 URL“products”第一個片段,數字在第二個片段將會重新映射到 Product::delete() 方法,傳入數值到第一個參數上。


使用 HTTP 動詞當然是可選的(非必要)。




三、建立基本Layout前端HTML

JQueryBootstrapAwesomeDataTablesDatepicker

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Page Title</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
</head>
<body>

</body>

</html>




四、非SPA(Single Page Application)也沒有Node.js 內建的Web Server

Web Server用的是XAMPP開發環境
前端使用Purl (A JavaScript URL parser)判斷uri決定js要Include哪一個HTML

$.get(`view/${uri}.html`,function(data){
$("body").html(data);
});

Codeigniter 3負責Layout的Server side render,還有api跟jwt管理

//簽發Token
public function getLssue()
{
$key = $this->CI->config->item('KEY'); //key
$time = time(); //當前時間
$token = [
'iss' => $this->CI->config->item('DOMAIN'), //簽發者 可選
'aud' => $this->CI->config->item('DOMAIN'), //接收該JWT的一方,可選
'iat' => $time, //簽發時間
'nbf' => $time , //(Not Before):某個時間點後才能訪問,比如設置time+30,表示當前時間30秒後才能使用
'exp' => $time+5, //過期時間,這裡設置5秒
];
echo JWT::encode($token, $key); //輸出Token
exit;
}
public function verification($jwt='')
{
$key = $this->CI->config->item('KEY'); //key
try {
JWT::$leeway = 60;//當前時間減去60,把時間留點餘地
$decoded = JWT::decode($jwt, $key, ['HS256']); //HS256方式,這裡要和簽發的時候對應
$arr = (array)$decoded;
return $arr;
} catch(\Firebase\JWT\SignatureInvalidException $e) { //簽名不正確
log_message('debug', $e->getMessage());
}catch(\Firebase\JWT\BeforeValidException $e) { // 簽名在某個時間點之後才能用
log_message('debug', $e->getMessage());
}catch(\Firebase\JWT\ExpiredException $e) { // token過期
log_message('debug', $e->getMessage());
}catch(Exception $e) { //其他錯誤
log_message('debug', $e->getMessage());
}
//Firebase定義了多個 throw new,我們可以捕獲多個catch來定義問題,catch加入自己的業務,比如token過期可以用當前Token刷新一個新Token
}
public function verToken()
{
$headers = $this->CI->input->request_headers();
$mate = preg_match('/Bearer\s(\S+)/', $headers['Authorization'], $matches); //判斷Authorization格式是否正確
if (!empty($mate)){
$auth = $this->verification(str_replace('Bearer ','',$headers['Authorization']));
return ($auth['aud']==$this->CI->config->item('DOMAIN')) ? 1 : 0; //判斷來源與Config設定值是否相同
}
}

is_ajax_request() 判斷是否由ajax發出的請求
Returns: TRUE if it is an Ajax request, FALSE if not
Return type: bool

過去測試都是用Postman發出請求看回傳值是否正確

這次直接用CI3提供的單元測試類別 驗證後端API是否能夠正確產出所需資料

$test = 1 + 1;
$expected_result = 2;
$test_name = 'Adds one plus one';
$this->unit->run($test, $expected_result, $test_name);


五、前端AJAX向後端發出請求並帶JWT令牌

$.ajax({
    type: "POST", //GET, POST, PUT
    url: '/authenticatedService' //the url to call
    data: yourData, //Data sent to server
    contentType: contentType,
    beforeSend: function (xhr) { //Include the bearer token in header

        xhr.setRequestHeader("Authorization", 'Bearer '+ jwt);

    }
}).done(function (response) {
    //Response ok. process reuslt
}).fail(function (err) {
    //Error during request
});

jquery的async:false,這個屬性
預設是true:非同步,false:同步。


六、表單Submit Form to Ajax Api 

let form = $(e.target);
$.ajax({
    url: form.attr("action"),
    type: form.attr("method"),
    data: form.serialize(),
cache: false,
beforeSend: function (xhr) {
    xhr.setRequestHeader("Authorization", "Bearer " + token);
},
    success: function (data) {
return true;
    },
    error: function(xhr) {
return false;
}
});

七、表單Submit Form to Ajax Api & File Upload

$.ajax({
url: form.attr("action"),
    type:form.attr("method"),
    data: new FormData($('form')[0]),
cache: false,
contentType : false,
processData : false,
beforeSend: function (xhr) {
    xhr.setRequestHeader("Authorization", "Bearer " + token);
},
    success: function (data) {
return true;
    },
    error: function(xhr) {
return false;
}
});

八、jQuery分頁組件 jqPaginator

$('#id').jqPaginator({
    totalPages: 100,
    visiblePages: 10,
    currentPage: 1,
    onPageChange: function (num, type) {
        $('#text').html('當前第' + num + '頁');
    }
});

九、Bootstrap 3 Modal(互動視窗)

Use Bootstrap’s JavaScript modal plugin to add dialogs to your site for lightboxes, user notifications, or completely custom content.

2018年10月21日 星期日

《面試官別再問》PHP運用多執行緒(Multi-thread)實現非阻塞方法


詳細的請參考:多程序還是多執行緒的選擇和區別 ,感覺這位牛人寫的特別清楚。下面我們再來看看php環境下使用多程序和多執行緒要注意的。

PHP是單進程同步模型,一個請求對應一個進程,I/O是同步阻塞的。通過nginx/apache/php-fpm等服務的擴展,才使得PHP提供高並發的服務,原理就是維護一個進程池,每個請求服務時單獨起一個新的進程,每個進程獨立存在。

PHP不支持多線程模式和回調處理,因此PHP內部腳本都是同步阻塞式的,如果你發起一個5s的請求,那麼程序就會I/O阻塞5s,直到請求返回結果,才會繼續執行代碼。因此做爬蟲之類的高並發請求需求很吃力。


常見互聯網分佈式架構如上,分為:

( 1 )客戶端層:典型調用方是瀏覽器 browser 或者手機應用 APP

( 2 )反向代理層:系統入口,反向代理

( 3 )站點應用層:實現核心應用邏輯,返回 html 或者 json

( 4 )服務層:如果實現了服務化,就有這一層

( 5 )數據 - 緩存層:緩存加速訪問存儲

( 6 )數據 - 數據庫層:數據庫固化數據存儲


整個系統各層次的水平擴展,又分別是如何實施的呢?

反向代理層的水平擴展



反向代理層的水平擴展,是通過“ DNS 輪詢”實現的:dns-server 對於一個域名配置了多個解析ip ,每次DNS 解析請求來訪問dns-server ,會輪詢返回這些ip 。

當 nginx 成為瓶頸的時候,只要增加服務器數量,新增 nginx 服務的部署,增加一個外網 ip ,就能擴展反向代理層的性能,做到理論上的無限高並發。

php真的有多進程,多線程嗎?

剛接觸php的時候,就看到過php不支持多進程的,多線程,但是php可以利用其他的東西來實現偽多進程,多線程,例如:fsockopen實際是利用socket的多線程,popen,pcntl_fork ,proc_open利用httpd多進程功能的外衣。下面就一些實踐過程,以及這種多進程的效果,到底如何。

  1. <?php  
  2. function thread($count=1) {  
  3.  for($i=0;$i<$count;$i++){  
  4.  $fp=fsockopen($_SERVER['HTTP_HOST'],80);  
  5.  fputs($fp,"GET http://localhost/aaaa/getset.php\r\n");  
  6.  fclose($fp);  
  7.  soc_log('socket',Date('d h:i:s'mktime()) . (double)microtime());  
  8.  }  
  9. }  
  10.   
  11.  function soc_log($script,$start_time)  
  12.  {  
  13.  $fp = fopen($script.".log"'a+');  
  14.  fputs($fp'start time is ' . $start_time"\n");  
  15.  fclose($fp);  
  16.  }  
  17.   
  18.  thread(5);  
  19.  for($j=0;$j<5;$j++){  
  20.  soc_log('nosocket',Date('d h:i:s'mktime()) . (double)microtime());  
  21.  }  
  22.   
  23. ?>  

根據socket的這種特性,寫了一小段代碼,並且記錄下每次連接socket的時間,以及不通過socket來,記錄執行時間,我的本意是,如果php真的能實現多線程的話,socket. log和nosocket.log裡面記錄的時間是相同的。我用壓力測試工具測試一下,這樣做是為了盡量做到並發,這樣log出現相同的時間可能性更大。

$ /usr/local/bin/webbench -c 10 -t 5 http://localhost/aaaa/socket.php

我查看一下二個log文件裡面根本沒有相同的,感覺好像是錯開的。後來我仔細想了想,訪問socket.php這個頁面時,裡面還是通過php來執行程序,所以根本不可能向幾個線程同時,發送請求,肯定有先,有後。

假設有一個client,程序邏輯是要請求三個不同的server,處理各自的響應。傳統模型當然是順序執行,先發送第一個請求,等待收到響應數據後再發送第二個請求,以此類推。就像是單核CPU,一次只能處理一件事,其他事情被暫時阻塞。而並發模式可以讓三個server同時處理各自請求,這就可以使大量時間復用。

畫個圖更好說明問題:



前者為阻塞模式,忽略請求響應等時間,總耗時為700ms;而後者非阻塞模式,由於三個請求可以同時得到處理,總耗時只有300ms。

同步,異步和阻塞,非阻塞的區別

同步、異步形容的是調用的形式,阻塞、非阻塞形容的是調用的狀態。一般同步和阻塞比較容易令人混淆,為了區分這兩組概念,我們通過對同一段​​​​代碼的不同角度來看的方式進行闡述。

先看上文的第二段代碼,從send()函數的調用來看,調用send()函數的形式我們採用同步調用,而在程式執行過程中,send()函數必須返回結果才能繼續往下執行,在執行send()函數的過程中調用mail()函數,線程是掛起的,我們稱這種進程狀態為阻塞模式,而這種調用也可以成為阻塞調用,因此可以看出,同步強調的是怎麼調用(形式),而阻塞強調調用狀態。

再看上文提到的支付包SDK支付的例子。從功能的實現形式來看,它是異步調用,但是從程式的狀態來看,它則是阻塞模式,因為在支付的時候,必須得到支付寶服務器端的返回數據,我們的服務器才能知道支付是否成功,進而做下一步判斷。再看jquery中的$.ajax(),從調用形式上看,它的異步調用,無需立即得到結果,而是通過回調函數來執行異步得到結果時的動作;從程式的狀態來看,它是非阻塞模式,在程式執行過程中,無論$.ajax是否得到結果,其下方的代碼都會立即執行。

實現多執行緒

當有人想要實現並發功能時,他們通常會想到用fork或者spawn threads,但是當他們發現php不支持多線程的時候,大概會轉換思路去用一些不夠好的語言,比如perl。
其實的是大多數情況下,你大可不必使用fork或者線程,並且你會得到比用fork或thread更好的性能。
假設你要建立一個服務來檢查正在運行的n台服務器,以確定他們還在正常運轉。你可能會寫下面這樣的代碼:
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
/**
task.php 內容為
$arr   = unserialize(stream_get_contents(STDIN)); 
$arr['time'] = date('Y-m-d H:i:s', $arr['time']); 
echo serialize($arr);
*/

function foo() {
 $descriptorspec = array(
    0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
    1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
    2 => array("pipe", "w") // stderr is a file to write to
 );

 $cwd = 'tmp';
 $env = array('some_option' => 'aeiou');

 $proc = proc_open('C:\xampp\php\php.exe C:\xampp\htdocs\task.php', $descriptorspec, $pipes, $cwd, $env);
 //var_dump($pipes); //resource of type (stream)
 if(is_resource($proc)) {
  //stdin
  $stdin = serialize(array('time' => time()));
  fwrite($pipes[0], $stdin); //把參數傳給程式檔task.php
  fclose($pipes[0]); //fclose關閉管道後proc_close才能退出子進程,否則會發生死鎖
  register_shutdown_function(function() use($pipes, $proc) { //事件驅動(程式檔結束事件),非同步回調
   //stdout
   $stdout = stream_get_contents($pipes[1]);
   fclose($pipes[1]);
   //stderr
   $stderr = stream_get_contents($pipes[2]);
   fclose($pipes[2]);
   //exit code (返回進程的終止狀態碼,如果發生錯則返回-1)
   $status = proc_close($proc);
   $data = array(
    'stdout' => $stdout,
    'stderr' => $stderr,
    'status' => $status,
   );
   var_export($data); //echo json_encode($data);
  });
 }
}
foo();
//輸出:
array ( 'stdout' => 'a:1:{s:4:"time";s:19:"2018-10-22 02:05:00";}', 
'stderr' => '', 
'status' => 0, )

2018年10月20日 星期六

《面試官別再問》跨站請求偽造(Cross-site request forgery)&& JWT (JSON Web Token)

利用網站可信認用戶的權限去執行未授權的命令的一種惡意攻擊。通過偽裝可信用戶的請求來利用信任該用戶的網站,這種攻擊方式雖然不是很流行,但是卻難以防範,其危害也不比其他安全漏洞小。

簡單點說,CSRF攻擊就是攻擊者利用受害者的身份,以受害者的名義發送惡意請求。與XSS(跨站點腳本,跨站腳本攻擊)不同的是,XSS的目的是獲取用戶的身份信息,攻擊者竊取到的是用戶的身份(會話/ cookie中),而CSRF則是利用用戶當前的身份去做一些未經過授權的操作。

CSRF攻擊最早在2001年被發現,由於它的請求是從用戶的IP地址發起的,因此在服務器上的網絡日誌中可能無法檢測到是否受到了CSRF攻擊,正是由於它的這種隱蔽性,很長時間以來都沒有被公開的報告出來,直到2007年才真正的被人們所重視。

CSRF有哪些危害
CSRF可以盜用受害者的身份,完成受害者在網頁瀏覽器有權限進行的任何操作,想想吧,能做的事情太多了。

以你的名義發送詐騙郵件,消息
用你的賬號購買商品
用你的名義完成虛擬貨幣轉賬

洩露個人隱私



第一,登錄受害者網站。如果受害者網站是基於cookie的用戶驗證機制,那麼當用戶登錄成功後,瀏覽器就會保存一份服務端的SESSION ID。

第二,這時候在同一個瀏覽器打開攻擊者網站,雖然說它無法獲取SESSION ID是什麼(因為設置了http only的cookie是無法被JavaScript獲取的),但是從瀏覽器向受害者網站發出的任何請求中,都會攜帶它的cookie,無論是從哪個網站發出。

第三,利用這個原理,在攻擊者網站發出一個請求,命令受害者網站進行一些敏感操作。由於此時發出的請求是處於會話中的,所以只要該用戶有權限,那麼任何請求都會被執行。

跨站腳本攻擊(Cross-site scripting)
跨站腳本攻擊Cross-site scripting (XSS)是一種安全漏洞,攻擊者可以利用這種漏洞在網站上註入惡意的客戶端代碼。當被攻擊者登陸網站時就會自動運行這些惡意代碼,從而,攻擊者可以突破網站的訪問權限,冒充受害者。
如果web應用程序沒有部署足夠的安全驗證,那麼,這些攻擊很容易成功。瀏覽器無法探測到這些惡意腳本是不可信的,所以,這些腳本可以任意讀取cookie,session tokens,或者其它敏感的網站信息,或者讓惡意腳本重寫html內容。
XSS利用的是用戶對指定網站的信任,CSRF利用的是網站對用戶網頁瀏覽器的信任。

防禦措施


檢查引薦字段
添加校驗令牌

JSON Web Token(JWT)是一個非常輕巧的規範。這個規範允許我們使用JWT在用戶和服務器之間傳遞安全可靠的信

首先,某client 使用自己的賬號密碼發送post 請求login,由於這是首次接觸,服務器會校驗賬號與密碼是否合法,如果一致,則根據密鑰生成一個token 並返回,client 收到這個token 並保存在本地。在這之後,需要訪問一個受保護的路由或資源時,只要附加上token(通常使用Header 的Authorization 屬性)發送到服務器,服務器就會檢查這個token 是否有效,並做出響應。

一個JWT實際上就是一個字符串,它由三部分組成,頭部,載荷與簽名。
// $Signature
HS256(Base64(Header) + "." + Base64(Payload), secretKey)

// JWT
JWT = Base64(Header) + "." + Base64(Payload) + "." + $Signature
頭部(頭)

頭部用於描述關於該JWT的最基本的信息,例如其類型以及簽名所用的算法等。這也可以被表示成一個JSON對象。

載荷(Payload)
我們先將上面的添加好友的操作描述成一個JSON對象。其中添加了一些其他的信息,幫助今後收到這個JWT的服務器理解這個JWT。

iss: 該JWT的簽發者
sub: 該JWT所面向的用戶
aud: 接收該JWT的一方
exp(expires): 什麼時候過期,這裡是一個Unix時間戳
iat(issued at): 在什麼時候簽發的

JWT的幾個特點

(1)JWT默認是不加密,但也是可以加密的。生成原始Token以後,可以用密鑰再加密一次。

(2)JWT不加密的情況下,不能將秘密數據寫入JWT。

(3)JWT不僅可以用於認證,也可以用於交換信息。有效使用JWT,可以降低服務器查詢數據庫的次數。

(4)JWT的最大缺點是,由於服務器不保存會話狀態,因此無法在使用過程中廢止某個令牌,或者更改令牌的權限。也就是說,一旦JWT簽發了,在到期之前就會始終有效,除非服務器部署額外的邏輯。

(5)JWT本身包含了認證信息,一旦洩露,任何人都可以獲得該令牌的所有權限。為了減少盜用,JWT的有效期應該設置得比較短。對於一些比較重要的權限,使用時應該再次對用戶進行認證。

(6)為了減少盜用,JWT不應該使用HTTP協議明碼傳輸,要使用HTTPS協議傳輸。


使用 JWT 可以預防 CSRF 攻擊?

CSRF 的防禦是透過 CSRF Token 來防範,在此與 Session 機制無關。由於使用方式是透過 Cookie 跟 LocalStorage,只要 JavaScript 可以 Work 就有風險被竊取 Token 造成攻擊

2018年10月17日 星期三

《面試官別再問》Web 前後端分離有意義嗎?

前後端邏輯混合開發模式:

優點:
1. 用戶體驗好,在相同的網絡條件和業務複雜度以及硬件環境下,他可以快速進行首頁展示,避免ajax請求所帶來的渲染延時。
2. 有利於seo搜索引擎優化。
3. 方便靜態化,在訪問高峰期可以將某些訪問量大並且業務數據大部分不變的頁面生成靜態頁面進行緩存,有利於快速渲染。


缺點:
1. 耦合度太高,在協作開發的時候前端的開發人員要與後端的開發人員互相等待來完成整體的功能,而且後端開發人員需要了解前端   的頁面結構來填充邏輯代碼,大大降低開發效率並且一旦出問題無法快速定位問題。
2. 不易維護,由於對於一個頁面的維護需要牽扯到兩端的開發人員來共同進行維護,在需求變更後容易出現bug。
3. 對後端開發語言進行了強依賴,一旦這兩種語言參雜在一起,對於後端來講前端是無法復用的。


WEB 前後端分離三個最大的優點在於:

1:最大的好處就是前端JS可以做很大部分的數據處理工作,對服務器的壓力減小到最小
2:後台錯誤不會直接反映到前台,錯誤減少較為友好
3:由於後台是很難去探知前台頁面的分佈情況,而這又是JS的強項,而JS又是無法獨立和服務器進行通訊的。所以單單用後台去控制整體頁面,又或者只靠JS完成效果,都會難度加大,前後台各盡其職可以最大程度的減少開發難度。

為了解決傳統Web開發模式帶來的各種問題,我們進行了許多嘗試,但由於前/後端的物理鴻溝,嘗試的方案都大同小異。痛定思痛,今天我們重新思考了“前後端”的定義,引入前端同學都熟悉的NodeJS,試圖探索一條全新的前後端分離模式。

大家一致認同的前後端分離的例子就是SPA(Single-page application),所有用到的展現數據都是後端通過異步接口(AJAX/JSONP)的方式提供的,前端只管展現。
從某種意義上來說,SPA確實做到了前後端分離,但這種方式存在兩個問題:
  • WEB服務中,SPA類佔的比例很少。很多場景下還有同步/同步+異步混合的模式,SPA不能作為一種通用的解決方案。
  • 現階段的SPA開發模式,接口通常是按照展現邏輯來提供的,有時候為了提高效率,後端會幫我們處理一些展現邏輯,這就意味著後端還是涉足了View層的工作,不是真正的前後端分離。
SPA式的前後端分離,是從物理層做區分(認為只要是客戶端的就是前端,服務器端的就是後端),這種分法已經無法滿足我們前後端分離的需求,我們認為從職責上劃分才能滿足目前我們的使用場景:
  • 前端:負責View和Controller層。
  • 後端:只負責Model層,業務處理/數據等。

1、對於後端java工程師:

把精力放在java基礎,設計模式,jvm原理,spring+springmvc原理及源碼,linux,mysql事務隔離與鎖機制,mongodb,http/tcp,多線程,分佈式架構,彈性計算架構,微服務架構, java性能優化,以及相關的項目管理等等。

後端追求的是:三高(高並發,高可用,高性能),安全,存儲,業務等等。

2、對於前端工程師:

把精力放在html5,css3,jquery,angularjs,bootstrap,reactjs,vuejs,webpack,less/sass,gulp,nodejs,Google V8引擎,javascript多線程,模塊化,面向切面編程,設計模式,瀏覽器兼容性,性能優化等等。

前端追求的是:頁面表現,速度流暢,兼容性,用戶體驗等等。

術業有專攻,這樣你的核心競爭力才會越來越高,正所謂你往生活中投入什麼,生活就會反饋給你什麼。並且兩端的發展都越來越高深,你想什麼都會,那你畢竟什麼都不精。

2018年10月16日 星期二

《面試官別再問》行為驅動BDD / 測試驅動TDD 軟體開發模式

行為驅動開發(BDD)Behavior-Driven Development

定義


這種開發模式也可以看作是對TDD 的一種補充,它鼓勵軟件項目中的開發人員,測試人員和非技術人員或者客戶之間的協作,從用戶的需求出發,強調系統行為。在TDD 中,我們並不能完全保證根據設計所編寫的測試就是用戶所期望的功能,用戶並一定能看懂測試用例。 BDD 將這一部分用更接近自然語言的形式來描述,讓測試用例更自然化和簡單,使開發人員,測試人員和客戶能在這個基礎上達成一致。

TDD 與BDD 的自動化測試比較
圖1 顯示了在TDD 技術下測試人員的測試過程,TDD 的測試要求的是單元測試,一般開發人員用什麼語言,單元測試就用什麼,基本上沒有選擇,在此其中,測試人員參與的機會不多,通常有了測試任務後,測試人員會將它分成測試計劃,然後再細分成測試點列表,每一份測試列表可能又對應著一份自動化測試用例,所以這個階段就需要保持三份文檔: 需求文檔+ 測試點文檔(計劃和測試點)+ 自動化測試用例,整個TDD 的過程中,自動化測試用例的編寫是在測試點列表出來之後。

圖1. TDD 技術下的測試過程



圖1. TDD 技術下的測試過程

與TDD 側重於針對單元測試不同,BDD 以用戶的目標以及他們為了實現這些目標而採取的步驟為側重點,BDD 將三種文檔進行了整合,用戶行為描述了用戶與系統交互的場景,而係統行為描述系統提供的功能場景,模塊行為描述模塊間交互的場景,整個過程中只需要一份文檔,用戶行為也是用戶需求,也是測試點文檔和自動化測試用例,隨著系統行為或模塊的行為的實現,一系列的測試活動都已經自動化了。


測試驅動開發(Test-Driven Development)

定義

TDD是測試驅動開發(Test-Driven Development)的英文簡稱,是敏捷開發中的一項核心實踐和技術,也是一種設計方法論。 TDD的原理是在開發功能代碼之前,先編寫單元測試用例代碼,測試代碼確定需要編寫什麼產品代碼。 TDD雖是敏捷方法的核心實踐,但不只適用於XP(Extreme Programming),同樣可以適用於其他開發方法和過程。

為什麼選擇

TDD執行更高質量的編程。通過TDD,可以編寫足夠的代碼來滿足測試。這會產生更模塊化,更精簡的代碼,不僅經過測試,而且更具可擴展性和可維護性。 TDD還可以降低總體擁有成本(TCO)。你有更少的缺陷。當修復成本較低時,您還可以在周期的早期捕獲缺陷。

它如何令人失望

TDD的困難之處
下面是幾個我認為TDD不容易掌控的地方,甚至就有些不可能(如果有某某TDD的Fans或是ThoughtWorks的諮詢師和你鼓吹TDD,你可以問問他們下面這些問題)

測試範圍的確定。
TDD開發流程,一般是先寫Test Case。
Test Case有很多種,有Functional的,有Unit的,有Integration的……,最難的是Test Case要寫成什麼樣的程度呢。
如果寫的太過High Level,那麼,當你的Test Case 失敗的時候,你不知道哪裡出問題了,你得要花很多精力去debug代碼。而我們希望的是其能夠告訴我是哪個模塊出的問題。只有High Level的Test Case,豈不就是Waterfall中的Test環節?

如果寫的太過Low Level,那麼,帶來的問題是,你需要花兩倍的時間來維護你的代碼,一份給test case,一份給實現的功能代碼。
另外,如果寫得太Low Level,根據Agile的迭代開發來說,你的需求是易變的,很多時候,我們的需求都是開發人員自己做的Assumption。所以,你把Test Case 寫得越細,將來,一旦需求或Assumption發生變化,你的維護成本也是成級數增加的。

當然,如果我把一個功能或模塊實現好了,我當然知道Test的Scope在哪裡,我也知道我的Test Case需要寫成什麼樣的程度。但是,TDD的悖論就在於,你在實現之前先把Test Case就寫出來,所以,你怎麼能保證你一開始的Test Case是適合於你後面的代碼的?不要忘了,程序員也是在開發的過程中逐漸了解需求和系統的。如果邊實現邊調整Test Case,為什麼不在實現完後再寫Test Case呢?如果是這樣的話,那就不是TDD了。

關注測試而不是設計。這可能是TDD的一個弊端,就像《十條不錯的編程觀點》中所說的一樣——“Unit Test won't help you write the good code”,在實際的操作過程中,我看到很多程序員為了趕工或是應付工作,導致其寫的代碼是為了滿足測試的,而忽略了代碼質量和實際需求。有時候,當我們重構代碼或是fix bug的時候,甚至導致程序員認為只要所有的Test Case都通過了,代碼就是正確的。當然,TDD的粉絲們一定會有下面的辯解:
可以通過結對編程來保證代碼質量。
代碼一開始就是需要滿足功能正確,後面才是重構和調優,而TDD正好讓你的重構和優化不會以犧牲功能為代價。

說的沒錯,但僅在理論上。操作起來可能會並不會得到期望的結果。 1)“共同開發”其並不能保證開發的兩個人都不會以滿足測試為目的,因為重構或是優化的過程中,一旦程序員看到N多的test cases 都failed了,人是會緊張的,你會不自然地去fix你的代碼以讓所有的test case都通過。 2)另外,我不知道大家怎麼編程,我一般的做法是從大局思考一下各種可行的實現方案,對於一些難點需要實際地去編程試試,最後權衡比較,挑選一個最好的方案去實現。而往往著急著去實現某一功能,通常在會導致的是返工,而後面的重構基本上因為前期考慮不足和成為了重寫。所以,在實際操作過程中,你會發現,很多時候的重構通常意味著重寫,因為那些”非功能性”的需求,你不得不re-design。而re-design往往意味著,你要重寫很多Low-Level的Test Cases,搞得你只敢寫High Level的Test Case。

2018年10月15日 星期一

《面試官別再問》What is RESTful API? GET 與POST 有什麼差別?

REST描述的是在網絡中client和server的一種交互形式;REST本身不實用,實用的是如何設計RESTful API(REST風格的網絡接口)


RESTful的4種層次

Representational status transfer

個人理解為:表現形式的狀態傳遞

1、只有一個接口交換xml來實現整個服務

 目前我們的移動站點的服務就是類似的結構,我們有兩個URI接口/mapp/lead和/msdk/safepay

2、每一個資源對應一個具體的URI,比1好維護,但是問題依然很明顯,資源版本更新會引入時間戳維護,資源的獲取和更新修改必須對應不同的URI

 目前PC主站和移動站點的靜態內容(包括html文件)都是這種形式

3、在2的基礎上使用了http verb,每個URI可以有不同的動作,充分利用了http協議,所以自然居然http協議的完整優勢,比如緩存和健壯性

 HTML4.0只支持POST和GET,所以無論DELETE還是PUT操作,都用POST去模擬了

 在WEB開發者看來,就是如果有數據變動,就用POST,如果沒有,就用GET

 所以目前中國用戶來看,PC端實現RESTful很困難,只有移動端支持Html5的瀏覽器,才能讓前端做出嘗試

4、現在似乎更加無法實際應用,Hypemedia control,也就是RESTful的本意,合理的架構原理和以網絡為基礎的設計相結合,帶來一個更加方便、功能強大的通信架構

HTTP動詞
對於資源的具體操作類型,由HTTP動詞表示。

常用的HTTP動詞有下面五個(括號裡是對應的SQL命令)。

GET(SELECT):從服務器取出資源(一項或多項)。
POST(CREATE):在服務器新建一個資源。
PUT(UPDATE):在服務器更新資源(客戶端提供改變後的完整資源)。
PATCH(UPDATE):在服務器更新資源(客戶端提供改變的屬性)。
DELETE(DELETE):從服務器刪除資源。
還有兩個不常用的HTTP動詞。

HEAD:獲取資源的元數據。
OPTIONS:獲取信息,關於資源的哪些屬性是客戶端可以改變的。
下面是一些例子。

GET /zoos:列出所有動物園
POST /zoos:新建一個動物園
GET /zoos/ID:獲取某個指定動物園的信息
PUT /zoos/ID:更新某個指定動物園的信息(提供該動物園的全部信息)
PATCH /zoos/ID:更新某個指定動物園的信息(提供該動物園的部分信息)
DELETE /zoos/ID:刪除某個動物園
GET /zoos/ID/animals:列出某個指定動物園的所有動物
DELETE /zoos/ID/animals/ID:刪除某個指定動物園的指定動物

狀態碼(Status Codes)
服務器向用戶返回的狀態碼和提示信息,常見的有以下一些(方括號中是該狀態碼對應的HTTP動詞)。

200 OK - [GET]:服務器成功返回用戶請求的數據,該操作是冪等的(Idempotent)。
201 CREATED - [POST/PUT/PATCH]:​​​​用戶新建或修改數據成功。
202 Accepted - [*]:表示一個請求已經進入後台排隊(異步任務)
204 NO CONTENT - [DELETE]:用戶刪除數據成功。
400 INVALID REQUEST - [POST/PUT/PATCH]:​​​​用戶發出的請求有錯誤,服務器沒有進行新建或修改數據的操作,該操作是冪等的。
401 Unauthorized - [*]:表示用戶沒有權限(令牌、用戶名、密碼錯誤)。
403 Forbidden - [*] 表示用戶得到授權(與401錯誤相對),但是訪問是被禁止的。
404 NOT FOUND - [*]:用戶發出的請求針對的是不存在的記錄,服務器沒有進行操作,該操作是冪等的。
406 Not Acceptable - [GET]:用戶請求的格式不可得(比如用戶請求JSON格式,但是只有XML格式)。
410 Gone -[GET]:用戶請求的資源被永久刪除,且不會再得到的。
422 Unprocesable entity - [POST/PUT/PATCH] 當創建一個對象時,發生一個驗證錯誤。
500 INTERNAL SERVER ERROR - [*]:服務器發生錯誤,用戶將無法判斷發出的請求是否成功。

當我們被問及HTTP的GET與POST兩種請求方式的區別的時候,很多答案是說GET的數據須通過URL以查詢參數來傳送,而POST可以通過請求體來發送數據,所以因URL的受限,往往GET無法發送太多的字符。這個回答好比在啟用了HTTPS時,GET請求URL中的參數仍然是明文傳輸的一樣。

GET果真不能通過Request Body來傳送數據嗎?非也。如此想法多半是因循著網頁中形式的方法屬性只有get與post兩種而來。因為把形式的方法設置為post,表單數據會放在body中,而方法為get(默認值)時,提交時瀏覽器會把表單中的字符拼接到動作的URL後作為查詢參數傳送。於是偶乎就有了這麼一種假像:HTTP GET必須通過URL的查詢參數來發送數據。

HTML Form 表單有兩種資料傳遞方式,分別為 GET 與 PSOT 這兩種,當網有填好表單資料並按下送出表單的按鈕之後,必須透過這兩種方式將資料送出到伺服器(Web Server),以下為兩種方式的 HTML Code 寫法。

GETPOST
網址差異網址會帶有 HTML Form 表單的參數與資料。資料傳遞時,網址並不會改變。
資料傳遞量由於是透過 URL 帶資料,所以有長度限制。由於不透過 URL 帶參數,所以不受限於 URL 長度限制。
安全性表單參數與填寫內容可在 URL 看到。透過 HTTP Request 方式,故參數與填寫內容不會顯示於 URL。

2018年10月14日 星期日

《面試官別再問》什麼是持續整合、持續發佈(Continuous Integration & Continous Delivery) ?

為什麼我們需要 CI / CD ?
在比較小且快速的循環中,持續驗證系統開發結果 ,小部分小部分地儘早確認,期望開發產出能符合原始需求,或依據產出進行快速修正。 簡單來說就是儘量減少手動人力,將一些日常工作交給自動化工具。例如:環境建置、單元測試、日誌紀錄、產品部署。

CI的目的
降低風險。
減少人工手動的繁複程序。
可隨時產生一版可部署的版本。
增加系統透明度。
建立團隊信心。
什麼是持續性整合(CI)呢?持續性整合的目的為:針對軟體系統每個變動,能持續且自動地進行驗證。此驗證可能包含了:

建置 (build)
測試 (test)
程式碼分析 (source code analysis)
其他相關工作 (自動部署)
驗證完成後,進一步可以整合自動化發佈或部署 (Continuous Delivery / Continuous Deployment) 。透過此流程可以確保軟體品質,不會因為一個錯誤變動而產生錯誤結果或崩潰(Crash)。此流程中的各類工具,也會產生一些回饋給開發者或其他角色,包含網頁/報表等等,用來追蹤並改善軟體潛藏的問題。

工具的選擇
Jenkins
談到 CI,最廣為人知且老牌的工具是 Jenkins。Jenkins的功能完整,也提供了上千個外掛 (Plugins) 來對應各種開發語言與工具。Jenkins目前已發展到了 2.x 版,新版本中對於 Pipeline 概念及容器 (Container) 整合也趨於完整,是一套可以自訂運用的系統。但也因為其功能強大、客製程度高,上手需要一些時間。然而一旦流程被定義,並整合好相關環境,它可以發揮持續性整合威力,大幅增加開發生產力

Git — 版本管理
GitHub — 程式碼託管、審查
CircleCI — 自動化建置、測試、部署
Docker — 可攜式、輕量級的執行環境(使用 Docker,統一開發者、測試人員、以及產品的執行環境。)
AWS Elastic Beanstalk — 雲端平台
Slack — 團隊溝通、日誌、通知
建立 CI / CD 流程雛形

我們結合了 Git Flow + Protected Branch Flow 的開發流程,流程如下:

開發者 (Developer) 先開立 (create) 一個功能分支 (feature branch)。
開發者提交一個 Pull Request, SCM 系統會自動觸發 Jenkins 進行建置以及測試。這個觸發通常是經由 Webhook 來實現。
在軟體建置完成後,在 Jenkins 增加一個步驟來送出原碼掃瞄 (Code Scan) 的請求給 Sonarqube 系統。Sonarqube 則在完成程式碼掃瞄後將結果寫回 SCM 系統。
由於我們在 SCM 上連結了 Slack,每個步驟完成(成功或失敗)的通知便可以送到 Slack 群組。
原碼審核者 (Reviewer) 可以到 SCM 上查看這個Pull Request的相關訊息,搭配 Code Scan的結果決定是否將這個分支合併 (merge) 回主線(develop/master branch)。
分支合併可以觸發另一個 CI 工作,使 Jenkins 將主線建置後部署到測試環境提供給其他人員進行測試。

《面試官別再問》什麼叫Scrum 敏捷式開發?

Scrum 團隊 

Scrum 團隊由產品負責人、開發團隊和一位 Scrum Master 組成。Scrum 團隊是一個自我組 織和跨職能的團隊。自我組織的團隊會自行選擇最好的方式來完成工作,而不是被團隊外 的人指示如何做。跨職能的團隊不需依靠非團隊成員而擁有所有完成工作所必備的能力。 Scrum 中的團隊模式是設計用來將彈性、創意、和生產力最大化。Scrum 團隊必須證明自 己在前述的情況和錯綜複雜的工作中越來越有效。 Scrum 團隊用迭代和逐步增量的方式交付產品,將回饋的機會最大化。用逐步增量的方式 交付「完成」的產品,可以確保一直提供一個潛在可用的產品版本。

傳統專案管理的方法,是明確的分階段往後推展。 比方說先提出概念、再做設計、然後施工、萬一有問題再來修改。 這在需要投入實體資源的專案中,有其不得不為的苦衷,也有它的好處。 因為材料投入往往成本耗費甚巨,所以最好能在前期透過紙上作業的方法確定彼此的認知。 不然一旦投入材料又要修改,那可是巨額的開支。 而且也可以盡量透過小成本的逐步投入,讓變動風險盡量壓低。 (換言之,等候期大量成本投入時,需求已經很清楚了。)

可是這在面臨軟體開發專案,尤其是遊戲相關案子的開發時,這方法將會碰到兩個問題。

一個問題在於,軟體開發的成本大多都是人力。 人力配置在哪一階段,成本其實大都是類似的。 可是軟體的問題在於很多東西其實使用者並不完全清楚自己的需求為何,也往往不具備開發所需的專業知識。 往往得等實體出現後,認知才會更清楚。 對於一般使用者而言,他無法閱讀ER Diagram;但東西做出來後,他才會突然跟你說,那個同樣公司的客戶,有辦法透過選單來選,而不要每次都由使用者手動輸入嗎?

遊戲開發時這狀況更明顯。 舉例而言,馬力歐的跳躍感是無法靠規格書來描述的。 必須等整個馬力歐世界都構築出來,包含主角、包含磚塊、包含敵人的移動方式,真正的呈現在螢幕上,並跟角色互動了,我們才能知道這樣的跳躍模式到底是否有充分的遊戲性以及挑戰性。 所以很難讓大家分頭工作,最後整合起來就自動1+1=2的變成一個好玩的遊戲。 反而是得有一個摸索期,做出簡單的可動版本來找出遊戲的關鍵好玩之處。 等關鍵確定後,再考量剩餘時間,加入其餘的配料。

所以Scrum或是Agile等方法的核心概念就是 - 既然使用者很可能無法一下子定義出自己的真實需求,很多東西也很難紙上靠流程圖或規格書來解釋清楚,那為何我們不盡快的做個Prototype?

這Prototype不用很完美,也不用一次就完整,只要能每次精進一些。 每次都讓使用者能多看到些東西、能按些按鈕、能有些反饋,這才會知道產品到底跟他想像的有沒有任何差異。 然後隨著逐次使用者的回饋,每次重新計畫,讓下一階段的Prototype能越來越朝向正確的方向。

這就有點像雕刻一個雕像一般。 我們一開始是個方形的大理石,我們並不是先精細雕刻出鼻子或眼睛。 而是先把方形大理石的周圍大範圍的敲出一個人型。 看看比例是否OK,型態是否如我們想要的。 沒問題,繼續敲敲打打,這時候看出姿勢與動作。 還是沒問題,那再把五官跟手指的形狀修出來。 臉會不會太寬? 肌肉線條還需不需要修? 然後把肌肉的張力以及表情在生動的修上。 換句話說,逐漸的精細化。

聽到這裡,可能有讀者會直覺認為。 如果是逐步修正,表示就避開了一開始做大量計畫的時段了嘛。 因為傳統專案管理方法,很重視前期的訪談、紀錄、與紙上規劃。 以及透過紙上規劃與各利害關係人的交互驗證。 如果我可以直接做個版本去跟利害關係人討論,那不就等於規畫整個被跳過了?

這就是一般人最大的誤解了,所以請仔細看一下我幫大家歸納出的五大重點:

這方法的幾個重點

1. 透過短的階段,盡快作需求上的驗證。

這類方法的目的,都是覺得與其前面做很多流程圖與規格書讓User畫押,還不如先根據User模糊的需求,在很短的時間之內(如2周到4周間),開發出一個可執行的版本。 (根據書中的定義,兩三個循環後,每次完成的版本甚至應該是要能立刻上市的。 這句話的意思是說,如果一些需求沒完成,但若User看完這版本決定那些需求放棄不做,那現在這版本就要立刻能上線。)

而根據每次的版本,使用者(在Scrum中稱為Product Owner的人)將檢視目前版本,調整他手上的需求清單(稱為Product Backlog)。 看哪些需求要再加入,哪些需求的重要性是否要調整。 並據此在跟開發團隊討論,定出下階段要能實現或修改的功能。

換言之,每個循環都要先滿足「首要功能」。 若首要功能及早滿足,這產品甚至有可能可以提早上線。 至於次要功能,則是看時間來決定加入多少。 若開發時間到了,首要功能有達到、但次要功能有些沒完成的,也必須強制犧牲。

2. 時間主導 無法做完的就放棄

這方法另一個考量的重點,在於堅守時間。 每個周期中,必須根據Product Owner定義的需求優先順序開發,沒完成的需求就往後推移。 萬一到的最終的上線日期(或上市日期),還有一些次要需求未能完成,那這些需求將必須被割捨。 要不是下個案子再放入,要不就是得當成下個版本的需求了。

所以這方法其實跟所有專案管理的手法考量點都相同,就是時間主導。 因為就現實面而言,幾乎9成的專案而言時間其實都是最重要的。 所以這方法的重點在於盡一切力量在時限前,提供一個符合「主要價值」的產出。

3. 提供更多的成員自主性,但也仰賴團隊的成熟度

Scrum的書裏頭,常常會強調Empowerment這個字。 意思是說,在這類方法中,必須放更多的管理責任在團隊身上。 PM將從一個完全主導的腳色,退居成一個Facilitator。 除了主導每日進度會議,做流程改善、解決問題、整合團隊外,倒是不做細節規畫。 而要團隊來根據每個周期的需求,自己來做工作規劃。 (也就是Scrum Backlog)

書裡頭講的好聽,用Empowerment(授權)這個字,但實際上其實意味著工作團隊除了本業工作外,還得了解其他的專業,並得做大量的溝通與合作。

這其實是很多公司要導Scrum第一個會碰到的問題。 因為若你手上有一大批沉默寡言、對於自己以外工作豪不感興趣的一群人時,要一下子導入這樣的方案,常常就會有所衝突了。

4. 團隊整合必須很緊密

Scrum是一個抓大放小的方法。 換言之,我們先解決最核心的需求,行有餘力慢慢把其次的需要加入。

所以從這角度來看,團隊強,有可能在時限內可以完成很多內容。 但團隊若很弱,缺乏跨專業的整合能力時,要就是只完成了少量的關鍵功能,並放棄其他需求。 或是不斷調整做法,最後只完成唯一的首要功能都有可能。

所以並不是說用了這方法,產值會暴增。 產值跟用甚麼方法是毫不相干的。 這方法的目的還是在於確認需求、抓大放小、以及防止Matrix組織中常見資源被抽調的問題(因為Scrum比較像一個小型的Projectized組織,人員是必須Dedicate在專案中)。

5. 需要更多的規畫,而非更少的規畫

最多人有誤解的,在於以為使用這類更機動的管理方法,計畫就可以更機動。 甚至希望可以甚麼計畫都不做,大家機動對抗問題就好。

但這是完全的誤解,也是一個非常嚴重的誤解!

Scrum的方法中,計畫的要求其實恐怕比傳統Waterfall還嚴格。 甚至如果是完全遵照這方法執行的團隊,執行計畫的總人時,會遠比Waterfall還來的多。

這是因為在一般專案管理的方法中,大部分的計畫責任是放在PM身上,極高比例的團隊成員都是「執行者」。 PM把計劃做好後,大家則照著執行。 所以專案的花在計畫的「總人時」並不多。

加上另一個更現實的問題在於,就是呢.. 就算PM想花很長時間規劃,但現今大部分公司的經理人對於專案管理概念都不足,很少老闆會能理解為何PM花個兩三個禮拜,甚麼都不做,只是自己埋頭分析與計畫。

所以一般而言,除非是大型工程的專案、且有業主強制要求做計劃審核。 否則一般專案能有個一周做計劃、寫文件、開會,在很多公司裏頭已經是極限了。

可是對於Scrum來說,他起始的計劃時間雖然看似不多,但每個周期的開始與結束,可是強制要團隊必須根據Product Owner新的需求清單檢視,檢討、重新開會,並做出下一周期的計畫。 而每次計畫都需要安排一到兩個完整工作天。 除了這以外,每天也必須有段時間做Daily Scrum的日回報與日計畫。 所以整個累積起來,其實是很多的。

尤其因為每次計畫都需要所有專案成員加入。 所以若一個團隊有十個人,每投入一天的Sprint Planning Meeting(也就是階段前的計畫會議)就等於10人日的計畫時間。 而如果是個一年的案子,並以每四周做為一個周期的話,表示會跑約十二個周期。 每個周期開始前若都需要一整天來計畫,並全員(10人)都加入。 這表示整個專案生命週期會最少花上120個人日作計畫。

相較傳統以一個PM投入規劃的狀態而言,120個人日等於4個日曆月、等於5.4個工作月。 所以計畫時數其實更多,而非更少。

但這本來就是他的目的。 因為計畫這件事情,本來在任何專案就都很重要。 Scrum把計畫的時數拆散,讓大家感覺好像變少了,但實際其實是變多了。

《面試官別再問》MySQL 大量寫入的加速技巧

在MySQL數據庫中,如果要插入上百萬級的記錄,用普通的來操作非常不現實,速度慢人力成本高,推薦使用或存儲過程來導入數據,我總結了一些方法分享如下,主要基於MyISAM和InnoDB引擎。insert intoLoad Data

1 InnoDB存儲引擎
首先建立資料庫(可選):

> CREATE DATABASE ecommerce;
> USE  ecommerce ;
> CREATE TABLE employees (
  id INT NOT NULL ,
  fname VARCHAR( 30 ),
  lname VARCHAR( 30 ),
  birth TIMESTAMP,
  hired DATE NOT NULL  DEFAULT  '1970-01-01' ,
  separated DATE NOT NULL  DEFAULT  '9999-12-31' ,
  job_code INT NOT NULL ,
  store_id INT NOT NULL
  )
  partition BY RANGE (store_id) (
  partition p0 VALUES LESS THAN ( 10000 ),
  partition p1 VALUES LESS THAN ( 50000 ),
  partition p2 VALUES LESS THAN ( 100000 ),
  partition p3 VALUES LESS THAN ( 150000 ),
  Partition p4 VALUES LESS THAN MAXVALUE
  );
然後建立存儲過程,其中,delimiter命令用來把語句定界符從;變為//,不然到遇上第一個分號MySQL就錯誤停止:declare var int;

> use  ecommerce ;
> DROP PROCEDURE BatchInser IF EXISTS;
> delimiter // --把界定符改成雙斜杠
> CREATE PROCEDURE BatchInsert(IN init INT, IN loop_time INT) -- 第一個參數為初始ID號(可自定義),第二個位生成MySQL記錄個數
  BEGIN
      DECLARE  Var INT;
       DECLARE ID INT;
      SET Var = 0 ;
      SET ID = init;
      WHILE  Var < loop_time DO 
          insert into employees(id, fname, lname, birth, hired, separated, job_code, store_id) values (ID, CONCAT( 'chen' , ID), CONCAT( 'haixiang' , ID), Now() , Now(), Now(), 1 , ID);
          SET ID = ID + 1 ;
          SET Var = Var + 1 ;
      END WHILE ;
  END;
  //
> delimiter ; -- 界定符改回分號
> CALL BatchInsert( 30036 , 200000 ); --調用存儲過程插入函數
也可以把上面的內容(除了語句之前的>號)複製到MySQL查詢框中執行。



2 MyISAM存儲引擎
首先建立資料庫(可選):

> use ecommerce;
> CREATE TABLE ecommerce.customer (
 id INT NOT NULL ,
 email VARCHAR( 64 ) NOT NULL ,
 name VARCHAR( 32 ) NOT NULL ,
 password VARCHAR( 32 ) NOT NULL ,
 phone VARCHAR( 13 ),
 birth DATE,
 sex INT( 1 ),
 avatar BLOB,
 address VARCHAR( 64 ),
 regtime DATETIME,
 lastip VARCHAR( 15 ),
 modifytime TIMESTAMP NOT NULL ,
 PRIMARY KEY ( id )
 ) ENGINE = MyISAM ROW_FORMAT = DEFAULT
 partition BY RANGE ( id ) (
 partition p0 VALUES LESS THAN ( 100000 ),
 partition p1 VALUES LESS THAN ( 500000 ),
 partition p2 VALUES LESS THAN ( 1000000 ),
 partition p3 VALUES LESS THAN ( 1500000 ),
 partition p4 VALUES LESS THAN ( 2000000 ),
 Partition p5 VALUES LESS THAN MAXVALUE
 );
再建立存儲過程:

> use  ecommerce ;
> DROP PROCEDURE ecommerce.BatchInsertCustomer IF EXISTS;
> delimiter //
> CREATE PROCEDURE BatchInsertCustomer(IN start INT,IN loop_time INT)
  BEGIN
      DECLARE  Var INT;
       DECLARE ID INT;
      SET Var = 0 ;
      SET ID= start;
      WHILE  Var < loop_time DO
          insert into customer(ID, email, name, password, phone, birth, sex, avatar, address, regtime, lastip, modifytime) 
          values (ID, CONCAT(ID, '@sina.com' ), CONCAT( 'name_' , rand(ID)* 10000 mod 200 ), 123456 , 13800000000 , adddate( '1995-01-01' , (rand(ID )* 36520 ) mod 3652 ), Var % 2 , 'http:///it/u=2267714161, 58787848&fm=52&gp=0.jpg' , '北京市海淀區' , adddate( '1995-01-01' , (rand(ID)* 36520 ) mod 3652 ), '8.8.8.8' , adddate( '1995-01-01' ,(rand(ID)* 36520 ) mod 3652));
          SET Var = Var + 1 ;
          SET ID= ID + 1 ;
      END WHILE ;
  END;
  // 
> delimiter ;
調用存儲過程插入數據

> ALTER TABLE customer DISABLE KEYS; 
> CALL BatchInsertCustomer(1, 2000000); 
> ALTER TABLE customer ENABLE KEYS;
通過以上對比發現對於插入大量數據時可以使用MyISAM存儲引擎,如果再需要修改MySQL存儲引擎可以使用命令:

 ALTER  TABLE ecommerce ENGINE = MYISAM;
3 關於批次寫入
很久很久以前,為了寫某個程式,必須在MySQL數據庫中插入大量的數據,一共有85766121條。近一億條的數據,怎麼才能快速插入到MySQL裡呢?

當時的做法是用一條一條地插入,Navicat估算需要十幾個小時的時間才能完成,就放棄了。最近幾天學習了一下MySQL,提高數據插入效率的基本原則如下:INSERT INTO

  1. 批量插入數據的效率比單數據行插入的效率高
  2. 插入無索引的數據表比插入有索引的數據表快一些
  3. 較短的SQL語句的數據插入比較長的語句快
這些因素有些看上去是微不足道的,但是如果插入大量的資料,即使很小的影響效率的因素也會形成不同的結果。根據上面討論的規則,我們可以就如何快速地加載資料得出幾個實用的結論。

  1. 使用語句要比語句效率高,因為它批量插入數據行。服務器只需要對一個語句(而不是多個語句)進行語法分析和解釋。索引只有在所有數據行處理完之後才需要刷新,而不是每處理一行都刷新。LOAD DATAINSERT
  2. 如果你只能使用INSERT語句,那就要使用將多個數據行在一個語句中給出的格式:,這將會減少你需要的語句總數,最大程度地減少了索引刷新的次數。INSERT INTO table_name VALUES(...),(...),...
根據上面的結論,今天又對相同的數據和數據表進行了測試,發現用速度快了不只是一點點,竟然只用了十多分鐘!所以在MySQL需要快速插入大量數據時,是你不二的選擇。

順便說一下,在默認情況下,語句將假設各數據列的值以製表符(t)分隔,各數據行以換行符(n)分隔,數據值的排列順序與各數據列在數據表裡的先後順序一致。但你完全可以用它來讀取其他格式的數據文件或者按其他順序來讀取各數據列的值,有關細節請參照MySQL文件。LOAD DATA

4 總結
1. 對於Myisam類型的表,可以通過以下方式快速的導入大量的數據。

ALTER  TABLE tblname DISABLE  KEYS ;
loading the data
ALTER  TABLE tblname ENABLE  KEYS ;
這兩個命令用來打開或者關閉MyISAM表非唯一索引的更新。在導入大量的數據到一個非空的MyISAM表時,通過設置這兩個命令,可以提高導入的效率。對於導入大量數據到一個空的MyISAM表,默認就是先導入數據然後才創建索引的,所以不用進行設置。

2. 而對於Innodb類型的表,這種方式並不能提高導入數據的效率。對於Innodb類型的表,我們有以下幾種方式可以提高導入的效率:

因為Innodb類型的表是按照主鍵的順序保存的,所以將導入的數據按照主鍵的順序排列,可以有效的提高導入數據的效率。如果Innodb表沒有主鍵,那麼系統會默認創建一個內部列作為主鍵,所以如果可以給表創建一個主鍵,將可以利用這個優勢提高導入數據的效率。
在導入數據前執行,關閉唯一性校驗,在導入結束後執行,恢復唯一性校驗,可以提高導入的效率。SET  UNIQUE_CHECKS=0SET  UNIQUE_CHECKS=1
如果應用使用自動提交的方式,建議在導入前執行,關閉自動提交,導入結束後再執行SET  AUTOCOMMIT=0


參考資料:http://www.111cn.net/database/mysql/53274.htm

LOAD DATA基本語法:

load data [low_priority] [local] infile 'file_name txt' [replace | ignore]
into table tbl_name
[fields
[terminated by't']
[OPTIONALLY] enclosed by '']
[escaped by'' ]]
[lines terminated by'n']
[ignore number lines]
[(co​​l_name, )]

load data infile語句從一個文本文件中以很高的速度讀入一個表中。使用這個命令之前,mysqld進程(服務)必須已經在運行。為了安全原因,當讀取位於服務器上的文本文件時,文件必須處於數據庫目錄或可被所有人讀取。另外,為了對服務器上文件使用load data infile,在服務器主機上你必須有file的權限。
1 如果你指定關鍵詞low_priority,那麼MySQL將會等到沒有其他人讀這個表的時候,才把插入數據。可以使用如下的命令:
load data low_priority infile "/home/mark/data sql" into table Orders;

2 如果指定local關鍵詞,則表明從客戶主機讀文件。如果local沒指定,文件必須位於服務器上。

3 replace和ignore關鍵詞控制對現有的唯一鍵記錄的重複的處理。如果你指定replace,新行將代替有相同的唯一鍵值的現有行。如果你指定ignore,跳過有唯一鍵的現有行的重複行的輸入。如果你不指定任何一個選項,當找到重複鍵時,出現一個錯誤,並且文本文件的餘下部分被忽略。例如:
load data low_priority infile "/home/mark/data sql" replace into table Orders;

4 分隔符
(1) fields關鍵字指定了文件記段的分割格式,如果用到這個關鍵字,MySQL剖析器希望看到至少有下面的一個選項:
terminated by分隔符:意思是以什麼字符作為分隔符
enclosed by字段括起字符
escaped by轉義字符
terminated by描述字段的分隔符,默認情況下是tab字符(t)
enclosed by描述的是字段的括起字符。
escaped by描述的轉義字符。默認的是反斜杠(backslash: ) 
例如:load data infile "/home/mark/Orders txt" replace into table Orders fields terminated by',' enclosed by '"';
(2)lines 關鍵字指定了每條記錄的分隔符默認為'n'即為換行符
如果兩個字段都指定了那fields必須在lines之前。如果不指定fields關鍵字缺省值與如果你這樣寫的相同: fields terminated by't' enclosed by ' '' ' escaped by'\'
如果你不指定一個lines子句,缺省值與如果你這樣寫的相同: lines terminated by'n'
例如:load data infile "/jiaoben/load.txt" replace into table test fields terminated by ',' lines terminated by '/n';
5 load data infile 可以按指定的列把文件導入到數據庫中。當我們要把數據的一部分內容導入的時候,,需要加入一些欄目(列/字段/field)到MySQL數據庫中,以適應一些額外的需要。比方說,我們要從Access數據庫升級到MySQL數據庫的時候
下面的例子顯示瞭如何向指定的欄目(field)中導入數據:
load data infile "/home/Order txt" into table Orders(Order_Number, Order_Date, Customer_ID);
6 當在服務器主機上尋找文件時,服務器使用下列規則:
(1)如果給出一個絕對路徑名,服務器使用該路徑名。
(2)如果給出一個有一個或多個前置部件的相對路徑名,服務器相對服務器的數據目錄搜索文件。
(3)如果給出一個沒有前置部件的一個文件名,服務器在當前數據庫的數據庫目錄尋找文件。
例如: /myfile txt”給出的文件是從服務器的數據目錄讀取,而作為“myfile txt”給出的一個文件是從當前數據庫的數據庫目錄下讀取。

網誌存檔