web跨域及cookie相關知識總結

  之前對於跨域相關的知識一致都很零碎,正好現在的代碼中用到了跨域相關的,現在來對這些知識做一個匯總整理,方便自己查看,說不定也可能對你有所幫助。

本篇主要內容如下:

  • 瀏覽器同源策略
  • http 請求跨域
  • http 請求跨域解決辦法
  • cookie 機制
  • 如何共享 cookie

瀏覽器同源策略

  相信很多人在 web 入門時,都被跨域問題折磨的死去活來。要想完全掌握跨域就得知道為什麼會有跨域這個問題出現。

  簡單來說跨域問題是因為瀏覽器的同源策略導致的。那瀏覽器為什麼要有同源策略呢?

  當然是為了安全。沒有同源策略限制的瀏覽器環境是非常危險的(即使有了同源策略也不是百分百安全),有興趣的可以去了解了解CSRFXSS攻擊。

  所謂的“同源”指的是“三個相同”:

  • 協議相同。不能一個是 http 協議,一個是 https
  • 域名相同
  • 端口相同

如果非同源頁面有以下限制:

  • LocalStore 和 IndexDB 無法讀取。這兩個顯然是不能讀取的,但是 cookie 有點不一樣,放在後面單獨說明
  • DOM 無法獲取,比如如法在頁面 A 中通過 iframe 獲取異源頁面 B 的 DOM
  • AJAX 請求無法讀取(可以發送請求,但是無法讀取到請求結果。比如在頁面 A 中請求異源接口 B,請求會正常發出處理,但是在頁面 A 中無法獲取請求結果,除非響應頭 Access-Control-Allow-Headers 中允許了頁面 A 的源,這樣就能讀取到結果)

  但是這裡有個例外,所有帶“src”屬性的標籤都可以跨域加載資源,不受同源策略的限制,這樣你應該可以想到一個比較古老的跨域解決方案(JSONP),同時這個特性也會被用作 CSRF 攻擊。

http 請求跨域

  在前端開發中經常會遇到跨域的問題,比如前後端分離中前後端部署在不同的端口上,或者在前端頁面中需要向另外一個服務請求數據,這些都會被跨域所阻擋。

目前主要有以下幾種辦法解決跨域問題:

  1. 關閉瀏覽器同源檢查

  這個太暴力,也太不安全了,不用考慮。

  1. jsonp 實現跨域請求

  前面說過了瀏覽器對於帶 src 屬性的標籤都可以跨域的。因此 jsonp 的實現流失利用了這個特性,在頁面中動態插入一個<script>標籤,然後他的 src 屬性就是接口調用地址,這樣就能訪問過去了,然後再講返回內容特殊處理成立即執行的函數,這樣就看起像進行了一次跨域請求。之所以不推薦這種方式,主要有以下兩個原因:

  • 實現複雜,且需要前後台同時修改才能實現
  • 只能進行 get 請求
  1. 服務器設置運行跨域

  這種方法只需要後台做處理便能實現跨域,前面說的 http 跨域請求是能夠發出去的,只是不能接收,那我們只要在響應頭Access-Control-Allow-Headers中加入允許請求的地址即可,以,分隔,同時*代表所有地址都允許。比如:

Access-Control-Allow-Headers:http://localhost:8081,http://localhost:8082

本方法是較為常用的一中跨域辦法,只需簡單修改服務端代碼即可。

  1. 請求代理

  這也是非常常用的一種跨域方法。跨域限制只是瀏覽器限制,服務端並沒有這個概念,因此我們在前端還是請求同域地址,然後在服務端做一個代理,將請求轉發到真正的 ip 和端口上。通常使用 nginx 實現端口轉發,比如下面一段 nginx 配置:

server {
    # /test1/abc 轉發到 http://a.com:8011/abc
    location /test1/ {
        proxy_pass http://a.com:8011/;
    }

    # /test2/abc 轉發到 http://b.com:8011/main/abc
    location /test2/ {
        proxy_pass http://b.com:8011/main/;
    }

    # /test3/abc 轉發到 http://c.com:8011/test3/abc
    location /test3/ {
        proxy_pass http://c.com:8081;
    }
}

cookie 同源策略

  cookie 的同源策略是通過

Domainpath兩個部分來共同確認一個 cookie 在哪些頁面上可用。

  Domain確定這個 cookie 所屬的域名,不能帶端口或協議。因此 cookie 便可在不同端口/不同協議下共享,只要域名相同。有一個例外是父子域名間也能共享 cookie,只需將 Domain 設置為.父域名

  path就簡單多了,通過 Domain 確定哪些域名可以共享 cookie,然後在通過path來確定 cookie 在哪些路徑下可用。使用/表示所有路徑都可共享。

具體如下:

  • Domain : example,path : /a可獲取 cookie:http://example:8081/a,https://example:8081/a
  • Domain : example,path : /可獲取 cookie:http://example:8081/a,https://example:8081/a , http://example:12/abcd
  • Domain : .example,path : /a可獲取 cookie:http://example:8081/a , https://localhost:8081/a , http://test.example:889/a

注意:在跨域請求中,即時目標地址有 cookie 且發起請求的頁面也能讀取到該 cookie,瀏覽器也不會將 cookie 自動設置到該跨域請求中。比如在http://localhost:8082/a頁面中請求http://localhost:8081/abc,這兩個地址下擁有共享cookie,http請求也不會攜帶cookie。

本篇原創發佈於:FleyX 的個人博客

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

編程哲理小故事:Tina的運動會方陣

自從接到任務后,Tina一直 煩惱着如何讓這群繁忙又缺乏才藝的程序員在運動會開幕式上做出一個有趣的方陣表演。

 

接到了運動會的方陣表演的任務

時間回到1個月前。

Tina正在工位上繁忙地進行着下一期準備上線系統的測試,這時候老大跑了過來

“Tina,我們公司要舉行運動會,開幕式時有方陣的表演,你幫忙組織下?”

Tina心裏咯噔一下,來了一個苦差。

“方陣?就是像奧運會開幕的時候,運動員在體育場跑道上走,展示風採的那種?”

“對,到時要一個6*6共36人的方陣,到時會在市裡最好的體育館進行,公司的大領導會在主席台看我們表演,為了我們部門,你要加油呀”

“嗯,我會加油的,但36個人不少呀,大家最近都挺忙的,感覺我拉不動他們參加呀”,Tina苦笑道。

“怎麼會呢,我看你在男生中還挺有人氣的,不過你不好意思拉人的話,人的問題我解決,你策劃組織好表演就行”

 

 

 

進展緩慢的方陣表演

方陣的排練一周一次,一周一周的過去了,但整體的效果並沒有得到質的提升,排練時總是湊不齊人,舞蹈也相對複雜,交互很多,大家也沒能很好的磨合,方陣表演這件事幾乎成了Tina的心病

“萬一到時我們成了最差的方陣怎麼辦?我會讓老大很失望的”

今天還沒到排練的時間,但她提前到了場地,擔心着,糾結着。

“方陣排練成怎樣啦?”今天老大居然親自過來了。

“我們今晚繼續排練,但效果還不太理想”

“有遇到什麼困難么?”

“唉,一言難盡,待會你看下把,也提一下意見,這個是我們排練模仿的視頻,你也可以看下”

老大把脈

老大在旁邊看着大夥排練,沒有做任何評價與指導,只是在久不久時用言語激勵一下大家。

今天的排練結束了,大夥都散去,Tina和老大留了下來。

“今天的排練我看了,我們的整齊度還略有欠缺,我們的隊伍就像你描述的一樣,經常會有人臨時有事,沒辦法很好的達到一個整體的效果,磨合度有待提升”

“嗯,對呀,但我已經儘力選擇大家都有空的時間了,但要每個人都有空是在是太難了”

“對,這是一個問題,人越多越難協調與同步,我們通過初中數學就可以知道,協調的難度是隨着人數的增加指數級上漲的”

“嗯嗯,但也沒辦法呀,表演方陣要求就是要這麼多人”,Tina應答着,心裏倒翻了個白眼,好好的幹嘛突然扯上數學

“對,總人數是沒辦法減少,但如果分成多個小組,小組人數比較小,是不是集齊一個小組的人更為簡單點?我們可以讓各個小組各自排練,然後每周一次像現在這樣合起來排練。對於類似規模導致的問題,我們大多都可以用分而治之的策略解決,就像公司里把我們的軟件分成了很多子系統一樣”

“啊?那這樣子要怎麼分組,這是一隻完整的舞蹈呀”

“一個完整的舞蹈必然是每個人的交互的有機結合,但舞蹈里必然會有一群人之間的交互更為密切,我們可以把這群人合成一組,那隻涉及他們內部的舞蹈叫可以他們內部自行排練了。”

“但這個界限不是很清晰把?”

“對,更為密切這個概念確實不是很清晰,所以這也是組織設計的藝術之一,甚至於這是一個需要試錯的過程。我們的軟件編碼一樣,強調高內聚低耦合,讓通訊交互成本降到最低的理念是一致的。但我們不能因為無法達到最優的設計而不去分組,分組可能不是最優,但進行了分組就已經在進步了,我們可以在繼續排練的過程中繼續優化”

“嗯嗯,想不到編碼與方陣排練還有這樣類似的東西,哈哈”

“嗯,世界有很多東西是相似的,像公司的組織架構、各種天體系統、人類的各種器官組成等等都有類似高內聚低耦合的特性存在,所以在我看來一個優秀的有悟性的碼農能將其知識遷移到很多領域”

“好,那我認真研究下視頻,然後對團隊進行分組,一個分組的大小多少合適呢?”

“太陽系有8個行星,地球只有1個月球衛星,這都是由於它們質量決定的。一個分組最大能有多少人這實質是由組長的個人能力決定的,組長能協調的人數就是這個組人數的上限,但當然啦,具體人數要和舞蹈自身的需要相結合。組的大小可以取‘舞蹈分組所需人數’和‘組長最大可管理人數’的較小值。”

“這是讓一個組盡可能大的意思么?”

“對的,一個組應該在可控範圍內盡可能的大,因為分組小意味着分組多,而分組多,協調多個分組就會產生更大的成本。當分組很多,甚至於把一個人看成一個組的時候,就跟你當前面臨的情況一樣。對應於我們軟件領域也是一樣的,對於微服務/組件應該拆成多大這個問題,個人理解組件的大小應該在一個普通程序員能理解、控制的複雜度範圍內,程序員就是這個微服務的類、模塊的組長”

“嗯,明白了。我盡量將其分組大小控制在合適範圍內”

“還有一件事就是,每個小組指定了組長之後,之後組內的排練可以適當程度的放權,這樣的話,你才有更多的精力去考慮我們組與組之間的交互應該怎麼進行。但當然啦,如果你得精力足夠的話,去了解和支持某些個特別舞蹈特別複雜的小組的進展也是挺好的。對於人的能力來說,能做高層設計同時也能做底層執行當然是最理想的,但在程序代碼里,一個模塊既處理高層邏輯又處理底層邏輯的話,是一個不好的表現,這會讓我們的代碼更難以理解”

“哈哈,我本來只想看下怎麼排練好方陣,想不到還順便學習了這麼多編碼知識,感謝老大呀!”

“嗯,剛剛的說的都是些簡單的理論,至於落地到實踐還會複雜的多,細節的處理還是得靠你呀,加油~”

最後的展示

有了一些基礎理論的指導,Tina的方陣隊伍的組織架構在排練中演進,各個小組有了組長能自發的組織組內的訓練,各組的表演水平得到了很大的提升。到集體匯總排練的時候,實際上就是以組的維度進行交互,而非之前的以人維度的磨合,因此集體匯總排練的效率也得到了提升。

“起步走!”,方陣前進的音樂和口令在體育館正式響起,方陣邁着整齊的步伐往主席台走去。Tina看着隊伍,再回頭看來下老大,嘴角泛起了自信的微笑。

 

(最近看了本如何寫小說的書,實踐一下小說風格,哈哈,若本文有錯誤缺陷,請批評負責幫助我進步。若本文對你有所啟發和幫助請不吝點贊轉發。這對我真的很重要,拜託了~)

 

作者簡介

多年金融行業經驗,現為某Top2互聯網銀行高級搬磚工,曾在兩家TOP3股份制商業銀行及一家互金創業公司工作(架構、核心業務主程),EasyTransaction作者,歡迎關注個人公眾號,在這裏我會分享日常工作、生活中對於架構、編碼和業務的思考

 

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

Swoole 啟動一個服務,開啟了哪些進程和線程?

目錄

  • 概述
  • 代碼
  • 小結

概述

Swoole 啟動一個服務,開啟了哪些進程和線程?

為了解決這個問題,咱們啟動一個最簡單的服務,一起看看究竟啟動了哪些進程和線程?

然後結合官網運行流程圖,對每個進程和線程進行歸類。

服務啟動后打印出當前 Swoole 版本 和 當前 CPU 核數。

打印 Swoole 版本,是讓大家可以下載這個版本 去運行代碼。

打印 CPU 核數,是因為這個參數下面會用到。

廢話不多說,直接看代碼吧。

代碼

serv.php

<?php

class Server
{
    private $serv;

    public function __construct() {
        $this->serv = new swoole_server("0.0.0.0", 9502);
        $this->serv->set([
            'worker_num'      => 3,
            'task_worker_num' => 3,
        ]);
        $this->serv->on('Start', function ($serv) {
            echo "SWOOLE:".SWOOLE_VERSION . " 服務已啟動".PHP_EOL;
            echo "SWOOLE_CPU_NUM:".swoole_cpu_num().PHP_EOL;
        });
        $this->serv->on('Receive', function ($serv, $fd, $from_id, $data) { });
        $this->serv->on('Task', function ($serv, $task) { });
        $this->serv->on('Finish', function ($serv, $task_id, $data) {});
        $this->serv->start();
    }
}
$server = new Server();

上面的代碼簡單說下,創建了一個 TCP 服務器,啟動了 3 個 worker 進程, 3 個 task 進程,因為啟用了 task 功能,所以必須註冊 onTask、onFinish 2 個事件的回調函數。

咱們運行一下:

使用 ps 查看下:

16390 的父進程是 16389。

16393、16394、16395、16396、16397、16398 的父進程是 16390。

有沒有發現,16391、16392 去哪啦?是不是很奇怪。

再用 pstree 查看下:

出來了吧,16391、16392 是線程 與 16390 進程一個層級。

現在我們了解了,啟動的這個服務使用了 8 個進程、2 個線程。

我們一起看下官方 Swoole Server 的文檔:

https://wiki.swoole.com/wiki/page/p-server.html

看下這張圖:

通過上面的圖,我們可以得到結論:

16389 是 Master 進程。

16390 是 Manager 進程。

16391、16392 是 Reactor 線程。

16393、16394、16395、16396、16397、16398 包括 3 個 Worker 進程,3 個 Task 進程。

小結

一、為什麼是 3 個 Worker 進程、3 個 Task 進程?

因為,在創建服務的時候我們進行了設置 worker_num = 3, task_worker_num = 3。

worker_num 如果不進行設置,默認為 SWOOLE_CPU_NUM,在上面咱們打印出來了,默認為 2,最大不超過,SWOOLE_CPU_NUM * 1000,具體詳情,看官方文檔。

worker_num 文檔:

https://wiki.swoole.com/wiki/page/275.html

task_worker_num 文檔:

https://wiki.swoole.com/wiki/page/276.html

二、為什麼是 2 個 Reactor 線程?它是干什麼的?

因為,Reactor 線程數,默認為 SWOOLE_CPU_NUM,也可以通過 reactor_num 參數進行設置。

reactor_num 文檔:

https://wiki.swoole.com/wiki/page/281.html

它是真正處理 TCP 連接,收發數據的線程。

Reactor線程 文檔:

https://wiki.swoole.com/wiki/page/347.html

三、Reactor、Worker、TaskWorker 的關係是什麼樣的?

一個通俗的比喻,假設Server就是一個工廠,那Reactor就是銷售,接受客戶訂單。而Worker就是工人,當銷售接到訂單后,Worker去工作生產出客戶要的東西。而TaskWorker可以理解為行政人員,可以幫助Worker幹些雜事,讓Worker專心工作。

官方已經解釋的很詳細了,看官方文檔吧:

https://wiki.swoole.com/wiki/page/163.html

如果你想學習 Swoole 可以看下這個 《Swoole 文章匯總(10 篇)》。

本文歡迎轉發,轉發請註明作者和出處,謝謝!

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

一次給女朋友轉賬引發我對分佈式事務的思考

本文在個人技術博客不同步發布,詳情可用力戳
亦可掃描屏幕右側二維碼關注個人公眾號,公眾號內有個人聯繫方式,等你來撩…

   前两天發了工資,第一反應是想着要給遠方的女朋友一點驚喜!於是打開了平安銀行的APP給女朋友轉點錢!填寫上對方招商銀行卡的卡號、開戶名,一鍵轉賬!搞定!在我點擊的那瞬間,就收到了app的賬戶變動的提醒,並且出現了圖一所示的提示界面:“處理中,正在等待對方銀行返回結果…”。嗯!畢竟是跨行轉賬嘛,等個幾秒也正常!腦海開始浮現出女朋友收到轉賬后驚喜與感動的畫面!

  

   然而,一切並沒有那麼順利,剛過一會兒,app卻如圖二所示的提示我“由於收款人戶名不符”導致轉賬失敗!!!

  

   剛剛都已經從我卡里扣過錢了,現在卻提示我轉賬失敗,銀行會不會把我的錢給吞了?轉賬失敗的錢還能退換給我嗎?正在我緊張、焦慮、坐立不安之時又收到一條app沖正的消息,剛剛轉賬失敗的錢已經退還給我了,看來我多慮了……這也證明咱平安銀行的app還是比較安全靠譜的!

   為啥從我卡里扣錢那麼迅速,而對方卻要幾秒才能到賬?並且轉賬失敗后,扣除的錢還能及時的返還到我的卡里?萬一錢返還失敗怎麼辦?又或者我轉一次錢,對方卻收到了兩次轉賬的申請又該如何?帶着這些問題,我腦海中浮現出“事務”二字!

   在我們還在“牙牙學語”的時候,老師經常會通過轉賬的栗子來跟我們講解事務,但跟這裏場景不一樣的是,老師講的是本地事務,而這裏面對的是分佈式事務!我們先來簡單回顧一下本地事務!

本地事務

   談到本地事務,大家可能都很熟悉,因為這個數據庫引擎層面能支持的!所以也稱數據庫事務,數據庫事務四大特徵:原子性(A),一致性(C),隔離性(I)和持久性(D),而在這四大特性中,我認為一致性是最基本的特性,其它的三個特性都為了保證一致性而存在的!

   回到學生時代老師給我們舉的經典栗子,A賬戶給B賬戶轉賬100元(A、B處於同一個庫中),如果A的賬戶發生扣款,B的賬戶卻沒有到賬,這就出現了數據的不一致!為了保證數據的一致性,數據庫的事務機制會讓A賬戶扣款和B在賬戶到賬的兩個操作要麼同時成功,如果有一個操作失敗,則多個操作同時回滾,這就是事務的原子性,為了保證事務操作的原子性,就必須實現基於日誌的REDO/UNDO機制!但是,僅有原子性還不夠,因為我們的系統是運行在多線程環境下,如果多個事務并行,即使保證了每一個事務的原子性,仍然會出現數據不一致的情況。例如A賬戶原來有200元的餘額, A賬戶給B賬戶轉賬100元,先讀取A賬戶的餘額,然後在這個值上減去100元,但是在這兩個操作之間,A賬戶又給C賬戶轉賬100元,那麼最後的結果應該是A減去了200元。但事實上,A賬戶給B賬戶最終完成轉賬后,A賬戶只減掉了100元,因為A賬戶向C賬戶轉賬減掉的100元被覆蓋了!所以為了保證併發情況下的一致性,又引入的隔離性,即多個事務併發執行后的狀態,和它們串行執行后的狀態是等價的!隔離性又有多種隔離級別,為了實現隔離性(最終都是為了保證一致性)數據庫又引入了悲觀鎖、樂觀鎖等等……本文的主題是分佈式事務,所以本地事務就只是簡單回顧一下,需要記住的一點是,事務是為了保證數據的一致性

分佈式理論

  還記得剛畢業那年,帶着滿腔的熱血就去到了一家互聯網公司,領導給我的第一個任務就是在列表上增加一個修改數據的功能。這能難倒我?我分分鐘給你搞出來!不就是在列表上增加了一個“修改”按鈕,點擊按鈕彈出框修改后保存就好了么。然而一切不像我想象的那麼順利,點擊保存並刷新列表后,頁面上的數據還是显示的修改之前的內容,像沒有修改成功一樣!過一會兒再刷新列表,數據就能正常显示了!測試多次之後都是這樣!沒見過什麼大場面的我開始有點慌了,是我哪裡寫得不對么?最終,我不得不求助組內經驗比較豐富的前輩!他深吸了一口氣告訴我說:“畢竟是剛畢業的小伙子啊!我來跟你講講原因吧!我們的數據庫是做了讀寫分離的,部分讀庫與寫庫在不同的網絡分區。你的數據更新到了寫庫,而讀數據的時候是從讀庫讀取的。更新到寫庫的數據同步到讀庫是有一定的延遲的,也就是說讀庫與寫庫會有短暫的數據不一致”! “這樣不會體驗不好么?為什麼不能做到寫入的數據立馬能讀出來?那我這個功能該怎麼實現呢?” 面對我的一堆問題,同事有些不耐煩的說:“聽說過CAP理論嗎?你先自己去了解一下吧”!是我開始查閱各種資料去了解這個陌生的詞背後的秘密!

  CAP理論是由加州大學Eric Brewer教授提出來的,這個理論告訴我們,一個分佈式系統不可能同時滿足一致性(Consistency)、可用性(Availability)、分區容錯性(Partition tolerance)這三個基本需求,最多只能同時滿足其中兩項。
  一致性:這裏的一致性是指數據的強一致,也稱為線性一致性。是指在分佈式環境中,數據在多個副本之間是否能夠保持一致的特性。也就是說對某個數據進行寫操作后立馬執行讀操作,必須能讀取到剛剛寫入的值。(any read operation that begins after a write operation completes must return that value, or the result of a later write operation)
  可用性:任意被無故障節點接收到的請求,必須能夠在有限的時間內響應結果。(every request received by a non-failing node in the system must result in a response)
  分區容錯性:如果集群中的機器被分成了兩部分,這兩部分不能互相通信,系統是否能繼續正常工作。(the network will be allowed to lose arbitrarily many messages sent from one node to another)

  在分佈式系統中,分區容錯性是基本要保證的。也就是說只能在一致性和可用性之間進行取捨。一致性和可用性,為什麼不可能同時成立?回到之前修改列表的例子,由於數據會分佈在不同的網絡分區,必然會存在數據同步的問題,而同步會存在網絡延遲、異常等問題,所以會出現數據的不一致!如果要保證數據的一致性,那麼就必須在對寫庫進行操作時,鎖定其他讀庫的操作。只有寫入成功且完成數據同步后,才能重新放開讀寫,而這樣在鎖定期間,系統喪失了可用性。更詳細關於CAP理論可以參考這篇文章,該文章講得比較通俗易懂!

分佈式事務

   分佈式事務就是在分佈式的場景下,需要滿足事務的需求!上篇文章我們聊過了消息中間件,那這篇文章我們要聊的是分佈式事務,把兩者一結合,便有了基於消息中間件的分佈式事務解決方案!不管是本地事務,還是分佈式事務,都是為了解決數據的一致性問題!一致性這個詞咱們前面多次提及!與本地事務不同的是,分佈式事務需要保證的是分佈式環境下,不同數據庫表中的數據的一致性問題。分佈式事務的解決方案有多種,如XA協議、TCC三階段提交、基於消息隊列等等,本文只會涉及基於消息隊列的解決方案!

   本地事務講到了一致性,分佈式事務不可避免的面臨着一致性的問題!回到最開始跨行轉賬的例子,如果A銀行用戶向B銀行用戶轉賬,正常流程應該是:

1、A銀行對轉出賬戶執行檢查校驗,進行金額扣減。
2、A銀行同步調用B銀行轉賬接口。
3、B銀行對轉入賬戶進行檢查校驗,進行金額增加。
4、B銀行返回處理結果給A銀行。

  

   在正常情況對一致性要求不高的場景,這樣的設計是可以滿足需求的。但是像銀行這樣的系統,如果這樣實現大概早就破產了吧。我們先看看這樣的設計最主要的問題:

1、同步調用遠程接口,如果接口比較耗時,會導致主線程阻塞時間較長。
2、流量不能很好控制,A銀行系統的流量高峰可能壓垮B銀行系統(當然B銀行肯定會有自己的限流機制)。
3、如果“第1步”剛執行完,系統由於某種原因宕機了,那會導致A銀行賬戶扣款了,但是B銀行沒有收到接口的調用,這就出現了兩個系統數據的不一致。
4、如果在執行“第3步”后,B銀行由於某種原因宕機了而無法正確回應請求(實際上轉賬操作在B銀行系統已經執行且入庫),這時候A銀行等待接口響應會異常,誤以為轉賬失敗而回滾“第1步”操作,這也會出現了兩個系統數據的不一致。

   對於問題的1、2都很好解決,如果對消息隊列熟悉的朋友應該很快能想到可以引入消息中間件進行異步和削峰處理,於是又重新設計了一個方案,流程如下:

1、A銀行對賬戶進行檢查校驗,進行金額扣減。
2、將對B銀行的請求異步寫入隊列,主線程返回。
3、啟動後台程序從隊列獲取待處理數據。
4、後台程序對B銀行接口進行遠程調用。
5、B銀行對轉入賬戶進行檢查校驗,進行金額增加。
6、B銀行處理完成回調A銀行接口通知處理結果。

  

   通過上面的圖我們能看到,引入消息隊列后,系統的複雜性瞬間提升了,雖然彌補了我們第一種方案的幾個不足點,但也帶來了更多的問題,比如消息隊列系統本身的可用性、消息隊列的延遲等等!並且,這樣的設計依然沒有解決我們面臨的核心問題-數據的一致性

1、如果“第1步”剛執行完,系統由於某種原因宕機了,那會導致A銀行賬戶扣款了,但是寫入消息隊列失敗,無法進行B銀行接口調用,從而導致數據不一致。
2、如果B銀行在執行“第5步”時由於校驗失敗而未能成功轉賬,在回調A銀行接口通知回滾時網絡異常或者宕機,會導致A銀行轉賬無法完成回滾,從而導致數據不一致。

   面對上述問題,我們不得不對系統再次進行升級改造。為了解決“A銀行賬戶扣款了,但是寫入消息隊列失敗”的問題,我們需要藉助一個轉賬日誌表,或者叫轉賬流水表,該表簡單的設計如下:

字段名稱 字段描述
tId 交易流水id
accountNo 轉出賬戶卡號
targetBankNo 目標銀行編碼
targetAccountNo 目標銀行卡號
amount 交易金額
status 交易狀態(待處理、處理成功、處理失敗)
lastUpdateTime 最後更新時間

   這個流水表需要怎麼用呢?我們在“第1步”進行扣款時,同時往流水表寫入一條操作流水,狀態為“待處理”,並且這兩個操作必須是原子的,也就是說必須通過本地事務保證這兩個操作要麼同時成功,要麼同時失敗!這就保證了只要轉賬扣款成功,必定會記錄一條狀態為“待處理”的轉賬流水。如果在這一步失敗了,那自然就是轉賬失敗,沒有後續操作了。如果這步操作后系統宕機了導致沒有將消息成功寫入消息隊列(也就是“第2步”)也沒關係,因為我們的流水數據已經持久化了!這時候我們只需要加入一個後台線程進行補償,定期的從轉賬流水表中讀取狀態為“待處理”且最後更新的時間距當前時間大於某個閾值的數據,重新放入消息隊列進行補償。這樣,就保證了消息即使丟失,也會有補償機制!B銀行在處理完轉賬請求後會回調A銀行的接口通知轉賬的狀態,從而更新A銀行流水表中的狀態字段!這樣就完美解決了上一個方案中的兩個不足點。系統設計圖如下:
  

   到目前為止,我們很好的解決了消息丟失的問題,保證了只要A銀行轉賬操作成功,轉賬的請求就一定能發送到B銀行!但是該方案又引入了一個問題,通過後台線程輪詢將消息放入消息隊列處理,同一次轉賬請求可能會出現多次放入消息隊列而多次消費的情況,這樣B銀行會對同一轉賬多次處理導致數據出現不一致!那怎麼保證B銀行轉賬接口的冪等性呢?

   同樣的,我們可以在B銀行系統中需要增加一個轉賬日誌表,或者叫轉賬流水表,B銀行每次接收到轉賬請求,在對賬戶進行操作的時候同時往轉賬日誌表中插入一條轉賬日誌記錄,同樣這兩個操作也必須是原子的!在接收到轉賬請求后,首先根據唯一轉賬流水Id在日誌表中查找判斷該轉賬是否已經處理過,如果未處理過則進行處理,否則直接回調返回! 最終的架構圖如下:
  

   所以,我們這裏最核心的就是A銀行通過本地事務保證日誌記錄+後台線程輪詢保證消息不丟失。B銀行通過本地事務保證日誌記錄從而保證消息不重複消費!B銀行在回調A銀行的接口時會通知處理結果,如果轉賬失敗,A銀行會根據處理結果進行回滾。

   當然,分佈式事務最好的解決方案是盡量避免出現分佈式事務!

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

代碼榮辱觀-以運用風格為榮,以隨意編碼為恥

編寫代碼的八榮八恥

1. 產品命名:以簡單有趣為榮,以平庸難記為恥。

2. 單個函數:以短小精悍為榮,以冗長費神為恥。

3. 代碼維護:以持續重構為榮,以停滯不前為恥。

4. 編程風格:以運用風格為榮,以隨意編碼為恥。

5. 程序設計:以開關上線為榮,以自信編碼為恥。

6. 接口定義:以用戶易用為榮,以複雜歧義為恥。

7. 斷言分支:以實時報警為榮,以忽略分支為恥。

8. 監控報警:以定時調整為榮,以放棄維護為恥。

5Why分析

(一)

Q: 誰需要學習編寫代碼的八榮八恥?

A: 項目中的開發人員、項目經理、架構師

(二)

Q: 為什麼學習編寫代碼的八榮八恥?

A: 可以作為實際代碼編寫和review(複查)的指導規範

(三)

Q: 什麼人什麼時候需要review代碼?

A: 

對開發人員來說,需要在時間允許的條件下定期的review自己和別人的代碼,加深對項目的整體理解。對自己的成長做總結。如果過了一段時間,還看到自己之前的代碼,覺得寫的很好的話,就需要質疑自己的成長,更努力的學習了。

對於項目經理和架構師來說,鼓勵所有上線的功能都每周抽出時間來做個組內review。或者定期抽取一些模塊做review。鼓勵大家重構代碼。在review過程中,作為領導者需要對大家有輸出,對代碼怎麼寫是更好的有一些理論基礎。這時候就需要使用編寫代碼的八榮八恥作為review的指導規範。

(四)

Q: 怎麼用作review的指導規範?

A: 八榮八恥中不但介紹了每個條目的意義,而且有通俗易懂的代碼實例便於和實際中的代碼在頭腦中做對比。文中明確的指出了哪些寫法是鼓勵的、哪些是不鼓勵的,是基於什麼理由不鼓勵這樣做。

(五)

Q: 編寫代碼的八榮八恥對於高可用有什麼意義?

A: 我利用美團的內部運維平台對自己參與過的項目可用性做過統計。將影響可用性的case(具體事件)分成:開發因素和設計因素。開發因素包括系統bug、開發不規範、上線不規範、監控報警不及時(影響可用性的恢復時長)等由於具體開發者在設計階段覆蓋不到的階段發生的。設計因素包括機器故障、網絡中斷、異常流量、中間件故障等可以通過設計做容災的。結果95%以上的可用性問題都是開發因素造成的。編寫代碼的八榮八恥是對避免開發因素產生可用性問題的指導規範。

 

編程風格:以運用風格為榮,以隨意編碼為恥

引子

在工作中,經常發現有些程序員用面向對象的語言寫出了面向過程的代碼而自己並沒有感覺到:

前面提到有個java軟件工程師,叫Margaret。她對工作有三個要求:錢多、有趣、離家近。HR想針對這些要求和她具體溝通,問她最低標準是什麼。每一項最低要求回復一個星級。

 

星級

錢多

有趣

離家近

1

年薪10萬

出差+旅遊占工時1%

40公里

☆☆

2

年薪20萬

出差+旅遊占工時10%

20公里

☆☆☆

3

年薪50萬

出差+旅遊占工時20%

10公里

☆☆☆☆

4

年薪100萬

出差+旅遊占工時50%

2公里

☆☆☆☆☆

5

年薪500萬

出差+旅遊占工時80%

1公里

Margaret在外地,所以用了一個常用的數據交換格式json給HR回復如下:

{"moreMoney”:4,"moreFun”:2,"closerToHome”:3}

拿到這個回復時面向過程的解析方式是這樣寫的:

Map json = (HashMap) JSONUtils.parse("{\"moreMoney\":4,\"moreFun\":2,\"closerToHome\":3}");
int moreMoney = (int)json.get("moreMoney");int moreFun = (int)json.get("moreFun");int closerToHome = (int)json.get("closerToHome");

接收方將接收到的數據轉成了json,代碼里一堆get完成了功能。為什麼說這是面向過程的呢?map是一種數據結構,沒有直接的業務意義。功能實現了,表達的意義卻不清晰。

這段代碼更好的一個實現方式是將接收的數據結構定義成一個對象,在java里可以使用jackson等工具直接將json轉成有業務含義的對象。

ObjectMapper objectMapper = new ObjectMapper();
Requirement requirement = objectMapper.readValue("{\"moreMoney\":4,\"moreFun\":2,\"closerToHome\":3}",JavaSoftwareEngineerMargaretRequirement.class);

這樣做,HR拿到的requirement不是一列列数字,需要自己對核對每一項都是什麼意思。而是一個有完整語義的對象,利於理解。而以這種思路來進行編寫的代碼我經常稱他們叫面向對象風格的代碼。

WHY

來看一段寫赤壁山旅行的文章:

今天有幸登上赤壁山,看到這山上的景物,不禁想起了當前戰場上廝殺的場面。想起當年要不是周瑜運氣好,大冬天颳起了東風,恐怕吳國就被曹操滅了。

再來看唐代詩人杜牧經過赤壁山這個著名的古戰場,有感於三國時代的英雄成敗而寫下的《赤壁》:

折戟沉沙鐵未銷,自將磨洗認前朝。

東風不與周郎便,銅雀春深鎖二喬。

前兩句意思是在沙子底下找到一隻斷戟,磨洗之後發現“made in 赤壁”。讀者讀了這兩句不禁會聯想起當前戰場上廝殺的場面吧。

后兩句意思是要不是周瑜運氣好,大冬天颳起了東風。那孫權的老婆大喬和周瑜的老婆小喬這兩位絕世美女都要被曹操這個色老頭關進銅雀台了。因為曹操久仰大小喬的美貌,提前為二人修築銅雀台,作為打敗吳國的戰利品。

杜牧隻字未提戰場和如果沒有東風的運氣,將會亡國的下場。但是讀者卻能心領神會,印象深刻。這是因為杜牧採用了以小見大的風格手法。

代碼與代碼的區別如同文章與文章的區別。能否讓讀者以更短的時間、更輕鬆的讀懂?代碼是給人整體感還是噁心感?這些都決定了代碼的可維護性。而它和系統可用性、穩定性的最直接關係在工作中非常常見:“爺爺的!這是誰寫的代碼這麼爛?忍不了了,老子不幹了。”而這個代碼的作者之所以離職也是因為忍受不了自己的爛代碼。頻繁的人員更替,新接手人員要有學習的成本。成本就包括要踩坑來加深對系統的理解。

HOW

除了開頭提到的面向對象的風格,編寫java代碼時下面三種風格也很常見。

1.fluent風格

fluent風格的代碼常以Builder結尾。比如StringBuilder就是典型的fluent風格。定義一個人的對象,這個對象使用fluent風格代碼這麼寫:

public class Person {    private String name;    private int armCount=2;//胳膊數默認為2 private int legCount=2;//腿數默認為2
    public static Person builder() {        return new Person.Builder(); }
    public Person setName(String name) {        this.name = name;        return this; }
    public Person armCount(int armCount) {        this.armCount = armCount;        return this; }
    public void legCount(int legCount) {        this.legCount = legCount;    }}

如上,就是每次給對象賦屬性的時候同時返回對象本身。這樣調用的時候:

Person.builder().name("Jane").armCount(2).legCount(2);

這樣寫的好處是比每個屬性都用一句set簡潔。在屬性多的時候,用構造函數。調用時容易表達不清楚屬性的含義。方法名起到了解釋的作用。現在流行的做法是代碼即註釋,註釋不用在每個方法都寫。這時候能表達自身意義的代碼就更加重要。注意:我們也可以保留setXXX、getXXX的命名規範,因為jackson等序列化反序列化的組件會根據set、get方法對參數賦值,上面的明明風格在序列化時會有問題。

當然,這個類也可以直接用lombok註解得到。

@Data@Buildrpublic class Person {    private String name;    private int armCount=2;//胳膊數默認為2    private int legCount=2;//腿數默認為2}

2.lambda函數式編程風格

lambda函數式編程比傳統的命令式編程更加簡潔。比如:現在有一群人。

List<Person> personList = Lists.newArrayList();
personList.add(Person.builder().name("Jane"));personList.add(Person.builder().name("Joe").armCount(1));personList.add(Person.builder().name("Stark").legCount(1));

要找出所有的殘疾人:

List<Person> disabledPersonList = Lists.newArrayList();for(Person person : personList) {    if(person.legCount()!=2 || person.armCount()!=2) {        disabledPersonList.add(person);    }}

或者使用lambda函數式編程:

List<Person> disabledPersonList = personList.stream().filter(person -> person.legCount()!=2 || person.armCount()!=2).collect(Collectors.toList());

3.設計模式風格

1995年,GoF(Gang of Four,四人幫)合作出版了《設計模式:可復用面向對象軟件的基礎》一書,共收錄了23中設計模式,人稱“GoF設計模式”。這23種設計模式的本質是面向對象設計原則的實際運用,是一種最佳實踐。

在各種java源碼中,經常看到以設計模式命名的類名和方法名。在我們日常編碼中,設計模式也非常實用。設計模式風格的例子請參考:平時代碼中用不到設計模式?Are you kidding me?

總結

寫有技術追求的代碼

 

相關閱讀

編寫代碼的「八榮八恥」- 以用戶易用為榮,以複雜歧義為恥

編寫代碼的「八榮八恥」- 以開關上線為榮,以自信編碼為恥

編寫代碼的「八榮八恥」(上篇)

 

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

Python 爬蟲從入門到進階之路(四)

之前的文章我們做了一個簡單的例子爬取了百度首頁的 html,我們用到的是 urlopen 來打開請求,它是一個特殊的opener(也就是模塊幫我們構建好的)。但是基本的 urlopen() 方法不支持代理、cookie等其他的HTTP/HTTPS高級功能,所以我們需要用到 Python 的 opener 來自定義我們的請求內容。

具體步驟:

  1. 使用相關的 Handler處理器 來創建特定功能的處理器對象;
  2. 然後通過 build_opener()方法使用這些處理器對象,創建自定義opener對象;
  3. 使用自定義的opener對象,調用open()方法發送請求。

我們先來回顧一下使用 urlopen 獲取百度首頁的 html 代碼實例:

 1 # 導入urllib 庫
 2 import urllib.request
 3 
 4 # url 作為Request()方法的參數,構造並返回一個Request對象
 5 request = urllib.request.Request("http://www.baidu.com")
 6 # Request對象作為urlopen()方法的參數,發送給服務器並接收響應
 7 response = urllib.request.urlopen(request)
 8 # 類文件對象支持 文件對象的操作方法,如read()方法讀取文件全部內容,返回字符串
 9 html = response.read().decode("utf-8")
10 # 打印字符串
11 print(html)

接下來我們看一下使用 opener 的處理方式:

 1 from urllib import request
 2 
 3 # 構建一個HTTPHandler 處理器對象,支持處理HTTP請求
 4 http_handler = request.HTTPHandler()
 5 
 6 # 構建一個HTTPSHandler 處理器對象,支持處理HTTPS請求
 7 # http_handler = request.HTTPSHandler()
 8 
 9 # 調用 request.build_opener()方法,創建支持處理HTTP請求的opener對象
10 opener = request.build_opener(http_handler)
11 
12 # 構建 Request請求
13 request = request.Request("http://www.baidu.com/")
14 
15 # 調用自定義opener對象的open()方法,發送request請求
16 response = opener.open(request)
17 
18 # 獲取服務器響應內容
19 html = response.read().decode("utf-8")
20 
21 # 打印字符串
22 print(html)

 

在上面的第一段代碼中,我們是通過直接  import urllib.request   來導入我們需要的包,這樣當我們要使用時需要   urllib.request   來使用,第二段代碼我們是通過  from urllib import request  來導入我們需要的包,這樣當我們使用時直接  request 來使用就可以了。

第一段代碼在前面的文章中我們已經說過了,這裏就不多做解釋了。

第二段代碼中,我們使用了 opener 的方法來處理我們的請求,這樣我們就可以對代理,cookie 等做進一步的操作,後續文章會講到。最終結果如下:

在  http_handler = request.HTTPHandler() 中,我們還可以添加一個  debuglevel=1 參數,會將 Debug Log 打開,這樣程序在執行的時候,會把收包和發包的報頭在屏幕上自動打印出來,方便調試,有時可以省去抓包的工作。

代碼如下:

 1 from urllib import request
 2 
 3 # 構建一個HTTPHandler 處理器對象,支持處理HTTP請求
 4 http_handler = request.HTTPHandler(debuglevel=1)
 5 
 6 # 構建一個HTTPHandler 處理器對象,支持處理HTTPS請求
 7 # http_handler = request.HTTPSHandler(debuglevel=1)
 8 
 9 # 調用 request.build_opener()方法,創建支持處理HTTP請求的opener對象
10 opener = request.build_opener(http_handler)
11 
12 # 構建 Request請求
13 request = request.Request("http://www.baidu.com/")
14 
15 # 調用自定義opener對象的open()方法,發送request請求
16 response = opener.open(request)
17 
18 # 獲取服務器響應內容
19 html = response.read().decode("utf-8")
20 
21 # 打印字符串
22 print(html)

輸出結果如下:

可以看出在響應結果的時候會為我們打印輸出一些請求信息。

 

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

C#規範整理·異常與自定義異常

這裡會列舉在C#中處理CLR異常方面的規範,幫助大家構建和開發一個運行良好和可靠的應用系統。

前言

  迄今為止,CLR異常機制讓人關注最多的一點就是“效率”問題。其實,這裏存在認識上的誤區,因為正常控制流程下的代碼運行並不會出現問題,只有引發異常時才會帶來效率問題。基於這一點,很多開發者已經達成共識:不應將異常機制用於正常控制流中。達成的另一個共識是:CLR異常機制帶來的“效率”問題不足以“抵消”它帶來的巨大收益。
CLR異常機制至少有以下幾個優點:

  • 正常控制流會被立即中止,無效值或狀態不會在系統中繼續傳播。
  • 提供了統一處理錯誤的方法。
  • 提供了在構造函數、操作符重載及屬性中報告異常的便利機制。
  • 提供了異常堆棧,便於開發者定位異常發生的位置。

  另外,“異常”其名稱本身就說明了它的發生是一個小概率事件。所以,因異常帶來的效率問題會被限制在一個很小的範圍內。實際上,try catch所帶來的效率問題幾乎是可以忽略的。在某些特定的場合,如Int32的Parse方法中,確實存在着因為濫用而導致的效率問題。在這種情況下,我們就應該考慮提供一個TryParse方法,從設計的角度讓用戶選擇讓程序運行得更快。另一種規避因為異常而影響效率的方法是:Tester-doer模式

正文

1.用拋出異常代替返回錯誤代碼

在異常機制出現之前,應用程序普遍採用返回錯誤代碼的方式來通知調用者發生了異常。本建議首先闡述為什麼要用拋出異常的方式來代替返回錯誤代碼的方式。對於一個成員方法而言,它要麼執行成功,要麼執行失敗。成員方法執行成功的情況很容易理解,但是如果執行失敗了卻沒有那麼簡單,因為我們需要將導致執行失敗的原因通知調用者。拋出異常和返回錯誤代碼都是用來通知調用者的手段。

但是當我們想要告訴調用者更多細節的時候,就需要與調用者約定更多的錯誤代碼。於是我們很快就會發現,錯誤代碼飛速膨脹,直到看起來似乎無法維護,因為我們總在查找並確認錯誤代碼。
在沒有異常處理機制之前,我們只能返回錯誤代碼。但是,現在有了另一種選擇,即使用異常機制。如果使用異常機制,那麼最終的代碼看起來應該是下面這樣的:

static void Main(string[]args)
{    
try  
    {   
     SaveUser(user); 
    }    
catch(IOException)   
    {       
    //IO異常,通知當前用戶 
    }    
catch(UnauthorizedAccessException)
    {       
    //權限失敗,通知客戶端管理員  
    }    
catch(CommunicationException) 
    {        
   //網絡異常,通知發送E-mail給網絡管理員  
    }
}

private static void SaveUser(User user)
{   
  SaveToFile(user); 
  SaveToDataBase(user);
}

使用CLR異常機制后,我們會發現代碼變得更清晰、更易於理解了。至於效率問題,還可以重新審視“效率”的立足點:throw exception產生的那點效率損耗與等待網絡連接異常相比,簡直微不足道,而CLR異常機制帶來的好處卻是顯而易見的。

這裏需要稍加強調的是,在catch(CommunicationExcep-tion)這個代碼塊中,代碼所完成的功能是“通知發送”而不是“發送”本身,因為我們要確保在catch和finally中所執行的代碼是可以被執行的。換句話說,盡量不要在catch和finally中再讓代碼“出錯”,那會讓異常堆棧信息變得複雜和難以理解。

在本例的catch代碼塊中,不要真的編寫發送郵件的代碼,因為發送郵件這個行為可能會產生更多的異常,而“通知發送”這個行為穩定性更高(即不“出錯”)。

以上通過實際的案例闡述了拋出異常相比於返回錯誤代碼的優越性,以及在某些情況下錯誤代碼將無用武之地,如構造函數、操作符重載及屬性。語法特性決定了其不能具備任何返回值,於是異常機制被當做取代錯誤代碼的首要選擇。

2.不要在不恰當的場合下引發異常

程序員,尤其是類庫開發人員,要掌握的兩條首要原則是:
正常的業務流程不應使用異常來處理。
不要總是嘗試去捕獲異常或引發異常,而應該允許異常向調用堆棧往上傳播。
那麼,到底應該在怎樣的情況下引發異常呢?

第一類情況 如果運行代碼後會造成內存泄漏、資源不可用,或者應用程序狀態不可恢復,則應該引發異常。
在微軟提供的Console類中有很多類似這樣的代碼:

if((value<1)||(value>100))
{    
    throw new ArgumentOutOfRangeException("value",value, Environment.GetResourceString("ArgumentOutOfRange_CursorSize"));
}

或者:

if(value==null)
{    
  throw new ArgumentNullException("value");
}

在開頭首先提到的就是:對在可控範圍內的輸入和輸出不引發異常。沒錯,區別就在於“可控”這兩個字。所謂“可控”,可定義為:發生異常后,系統資源仍可用,或資源狀態可恢復。

第二類情況 在捕獲異常的時候,如果需要包裝一些更有用的信息,則引發異常。
這類異常的引發在UI層特別有用。系統引發的異常所帶的信息往往更傾向於技術性的描述;而在UI層,面對異常的很可能是最終用戶。如果需要將異常的信息呈現給最終用戶,更好的做法是先包裝異常,然後引發一個包含友好信息的新異常。

第三類情況 如果底層異常在高層操作的上下文中沒有意義,則可以考慮捕獲這些底層異常,並引發新的有意義的異常。
例如在下面的代碼中,如果拋出InvalidCastException,則沒有任何意義,甚至會造成誤解,所以更好的方式是拋出一個ArgumentException:

private void CaseSample(object o)
{  
  if(o==null)    
  {        
   throw new ArgumentNullException("o");   
  }
}   
 
User user=null;   
try  
{   
   user=(User)o; 
}    
catch(InvalidCastException)
{    
   throw new ArgumentException("輸入參數不是一個User","o"); 
}  

//do something}

需要重點介紹的正確引發異常的典型例子就是捕獲底層API錯誤代碼,並拋出。查看Console這個類,還會發現很多地方有類似的代碼:

int errorCode=Marshal.GetLastWin32Error();
if(errorCode==6)
{   
  throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_ConsoleKeyAvailableOnFile"));
}

Console為我們封裝了調用Windows API返回的錯誤代碼,而讓代碼引發了一個新的異常。

很顯然,當需要調用Windows API或第三方API提供的接口時,如果對方的異常報告機制使用的是錯誤代碼,最好重新引發該接口提供的錯誤,因為你需要讓自己的團隊更好地理解這些錯誤。

3.重新引發異常時使用Inner Exception

當捕獲了某個異常,將其包裝或重新引發異常的時候,如果其中包含了Inner Exception,則有助於程序員分析內部信息,方便代碼調試。
以一個分佈式系統為例,在進行遠程通信的時候,可能會發生的情況有:
1)網卡被禁用或網線斷開,此時會拋出SocketException,消息為:“由於目標計算機積極拒絕,無法連接。”
2)網絡正常,但是要連接的目標機沒有端口沒有處在偵聽狀態,此時,會拋出SocketException,消息為:“由於連接方在一段時間后沒有正確答覆或連接的主機沒有反應,連接嘗試失敗。”
3)連接超時,此時需要通過代碼實現關閉連接,並拋出一個SocketException,消息為:“連接超過約定的時長。”
發生以上三種情況中的任何一種情況,在返回給最終用戶的時候,我們都需要將異常信息包裝成為“網絡連接失敗,請稍候再試”。

所以,一個分佈式系統的業務處理方法,看起來應該是這樣的:

try
{    
SaveUser5(user);
}
catch(SocketException err)
{  
  throw new CommucationFailureException("網絡連接失敗,請稍後再試",err);
}

但是,在提示這條消息的時候,我們可能需要將原始異常信息記錄到日誌里,以供開發者分析具體的原因(因為如果這種情況頻繁出現,這有可能是一個Bug)。那麼,在記錄日誌的時候,就非常有必要記錄導致此異常出現的內部異常或是堆棧信息。
上文代碼中的:就是將異常重新包裝成為一個CommucationFailureException,並將SocketException作為Inner Exception(即err)向上傳遞。

此外還有一個可以採用的技巧,如果不打算使用Inner Exception,但是仍然想要返回一些額外信息的話,可以使用Exception的Data屬性。如下所示:

try
{   
 SaveUser5(user);
}
catch(SocketException err)
{    
 err.Data.Add("SocketInfo","網絡連接失敗,請稍後再試");   
 throw err;
}

在上層進行捕獲的時候,可以通過鍵值來得到異常信息:

catch(SocketException err)
{   
 Console.WriteLine(err.Data["SocketInfo"].ToString());
}

4.避免在finally內撰寫無效代碼

你應該始終認為finally內的代碼會在方法return之前執行,哪怕return是在try塊中。
C#編譯器會清理那些它認為完全沒有意義的C#代碼。

private static int TestIntReturnInTry()
{   
  int i;    
  try    
  {        
    return i=1;  
  } 
  
finally   
 {        
    i=2;      
   Console.WriteLine("\t將int結果改為2,finally執行完畢");   
 }
}

5.避免嵌套異常

應該允許異常在調用堆棧中往上傳播,不要過多使用catch,然後再throw。過多使用catch會帶來兩個問題:

  • 代碼更多了。這看上去好像你根本不知道該怎麼處理異常,所以你總在不停地catch。
  • 隱藏了堆棧信息,使你不知道真正發生異常的地方。

嵌套異常會導致 調用堆棧被重置了。最糟糕的情況是:如果方法捕獲的是Exception。所以也就是說,如果這個方法中還存在另外的異常,在UI層將永遠不知道真正發生錯誤的地方。
除了第3點提到的需要包裝異常的情況外,無故地嵌套異常是我們要極力避免的。當然,如果真的需要捕獲這個異常來恢復一些狀態,然後重新拋出,代碼看起來應該是這樣的:

try{ 
  MethodTry();
}
catch(Exception)
{ 
   //工作代碼   
 throw;
}

或者:

try
{    
 MethodTry();
}
catch
{    
  //工作代碼 
   throw;
}

盡量避免像下面這樣引發異常:

catch(Exception err)
{   
 //工作代碼  
  throw err;
}

直接throw err而不是throw將會重置堆棧信息。

6.避免“吃掉”異常

嵌套異常是很危險的行為,一不小心就會將異常堆棧信息,也就是真正的Bug出處隱藏起來。但這還不是最嚴重的行為,最嚴重的就是“吃掉”異常,即捕獲,然後不向上層throw拋出。如果你不知道如何處理某個異常,那麼千萬不要“吃掉”異常,如果你一不小心“吃掉”了一個本該往上傳遞的異常,那麼,這裏可能誕生一個Bug,而且,解決它會很費周折。

避免“吃掉”異常,並不是說不應該“吃掉”異常,而是這裏面有個重要原則:該異常可被預見,並且通常情況它不能算是一個Bug。 比如有些場景存在你可以預見的但不重要的Exception,這個就不算一個bug。

7.為循環增加Tester-Doer模式而不是將try-catch置於循環內

如果需要在循環中引發異常,你需要特別注意,因為拋出異常是一個相當影響性能的過程。應該盡量在循環當中對異常發生的一些條件進行判斷,然後根據條件進行處理。

8.總是處理未捕獲的異常

處理未捕獲的異常是每個應用程序應具備的基本功能,C#在AppDomain提供了UnhandledException事件來接收未捕獲到的異常的通知。常見的應用如下:

static void Main(string[]args)
{    
  AppDomain.CurrentDomain.UnhandledException+=new  UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
}

static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{    
  Exception error=(Exception)e.ExceptionObject;   
 Console.WriteLine("MyHandler caught:"+error.Message);
}

未捕獲的異常通常就是運行時期的Bug,我們可以在App-Domain.CurrentDomain.UnhandledException的註冊事件方法CurrentDomain_UnhandledException中,將未捕獲異常的信息記錄在日誌中。值得注意的是,UnhandledException提供的機制並不能阻止應用程序終止,也就是說,執行CurrentDomain_UnhandledException方法后,應用程序就會被終止。

9.正確捕獲多線程中的異常

多線程的異常處理需要採用特殊的方法。以下的處理方式會存在問題:

try{  
  Thread t=new Thread((ThreadStart)delegate    
{        
  throw new Exception("多線程異常");    
});   

 t.Start();
}

catch(Exception error)
{ 
   MessageBox.Show(error.Message+Environment.NewLine+error.StackTrace);
}

應用程序並不會在這裏捕獲線程t中的異常,而是會直接退出。從.NET 2.0開始,任何線程上未處理的異常,都會導致應用程序的退出(先會觸發AppDomain的UnhandledException)。上面代碼中的try-catch實際上捕獲的還是當前線程的異常,而t屬於新起的異常,所以,正確的做法應該是把 try-catch放在線程裏面

Thread t=new Thread((ThreadStart)delegate
{    
try   
 {      
  throw new Exception("多線程異常");   
 }    
catch(Exception error)   {  ....   });

t.Start();

10.慎用自定義異常

除非有充分的理由,否則一般不要創建自定義異常。如果要對某類程序出錯信息做特殊處理,那就自定義異常。需要自定義異常的理由如下:
1)方便調試。通過拋出一個自定義的異常類型實例,我們可以使捕獲代碼精確地知道所發生的事情,並以合適的方式進行恢復。
2)邏輯包裝。自定義異常可包裝多個其他異常,然後拋出一個業務異常。
3)方便調用者編碼。在編寫自己的類庫或者業務層代碼的時候,自定義異常可以讓調用方更方便處理業務異常邏輯。例如,保存數據失敗可以分成兩個異常“數據庫連接失敗”和“網絡異常”。
4)引入新異常類。這使程序員能夠根據異常類在代碼中採取不同的操作。

11.從System.Exception或其他常見的基本異常中派生異常

這個不說了,自定義異常一般是從System.Exception派生。。事實上,現在如果你在Visual Studio中輸入Exception,然後使用快捷鍵Tab,VS會自動創建一個自定義異常類。

12.應使用finally避免資源泄漏

前面已經提到過,除非發生讓應用程序中斷的異常,否則finally總是會先於return執行。finally的這個語言特性決定了資源釋放的最佳位置就是在finally塊中;另外,資源釋放會隨着調用堆棧由下往上執行(即由內到外釋放)。

13.避免在調用棧較低的位置記錄異常

即避免在內部深處處理記錄異常。最適合記錄異常和報告的是應用程序的最上層,這通常是UI層。
並不是所有的異常都要被記錄到日誌,一類情況是異常發生的場景需要被記錄,還有一類就是未被捕獲的異常。未被捕獲的異常通常被視為一個Bug,所以,對於它的記錄,應該被視為系統的一個重要組成部分。

如果異常在調用棧較低的位置被記錄或報告,並且又被包裝后拋出;然後在調用棧較高位置也捕獲記錄異常。這就會讓記錄重複出現。在調用棧較低的情況下,往往異常被捕獲了也不能被完整的處理。所以,綜合考慮,應用程序在設計初期,就應該為開發成員約定在何處記錄和報告異常。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

一次線上Redis類轉換異常排查引發的思考

之前同事反饋說線上遇到Redis反序列化異常問題,異常如下:

XxxClass1 cannot be cast to XxxClass2

已知信息如下:

  • 該異常不是必現的,偶爾才會出現;
  • 出現該異常后重啟應用或者過一會就好了;
  • 序列化協議使用了hessian。

因為偶爾出現,首先看了報異常那塊業務邏輯是不是有問題,看了一遍也發現什麼問題。看了下對應日誌,發現是在Redis讀超時之後才出現的該異常,因此懷疑redis client操作邏輯那塊導致的(公司架構組對redis做了一層封裝),發現獲取/釋放redis連接如下代碼:

 1 try {
 2     jedis = jedisPool.getResource();
 3     // jedis業務讀寫操作
 4 } catch (Exception e) {
 5     // 異常處理
 6 } finally {
 7     if (jedis != null) {
 8         // 歸還給連接池
 9         jedisPool.returnResourceObject(jedis);
10     }
11 }

初步認定原因為:發生了讀寫超時的連接,直接歸還給連接池,下次使用該連接時讀取到了上一次Redis返回的數據。因此本地驗證下,示例代碼如下:

 1 @Data
 2 @NoArgsConstructor
 3 @AllArgsConstructor
 4 static class Person implements Serializable {
 5     private String name;
 6     private int age;
 7 }
 8 @Data
 9 @NoArgsConstructor
10 @AllArgsConstructor
11 static class Dog implements Serializable {
12     private String name;
13 }
14 
15 public static void main(String[] args) throws Exception {
16     JedisPoolConfig config = new JedisPoolConfig();
17     config.setMaxTotal(1);
18     JedisPool jedisPool = new JedisPool(config, "192.168.193.133", 6379, 2000, "123456");
19 
20     Jedis jedis = jedisPool.getResource();
21     jedis.set("key1".getBytes(), serialize(new Person("luoxn28", 26)));
22     jedis.set("key2".getBytes(), serialize(new Dog("tom")));
23     jedisPool.returnResourceObject(jedis);
24 
25     try {
26         jedis = jedisPool.getResource();
27         Person person = deserialize(jedis.get("key1".getBytes()), Person.class);
28         System.out.println(person);
29     } catch (Exception e) {
30         // 發生了異常之後,未對該連接做任何處理
31         System.out.println(e.getMessage());
32     } finally {
33         if (jedis != null) {
34             jedisPool.returnResourceObject(jedis);
35         }
36     }
37 
38     try {
39         jedis = jedisPool.getResource();
40         Dog dog = deserialize(jedis.get("key2".getBytes()), Dog.class);
41         System.out.println(dog);
42     } catch (Exception e) {
43         System.out.println(e.getMessage());
44     } finally {
45         if (jedis != null) {
46             jedisPool.returnResourceObject(jedis);
47         }
48     }
49 }

連接超時時間設置2000ms,為了方便測試,可以在redis服務器上使用gdb命令斷住redis進程(如果redis部署在Linux系統上的話,還可以使用iptable命令在防火牆禁止某個回包),比如在執行 jedis.get("key1".getBytes() 代碼前,對redis進程使用gdb命令斷住,那麼就會導致讀取超時,然後就會觸發如下異常:

Person cannot be cast to Dog

既然已經知道了該問題原因並且本地復現了該問題,對應解決方案是,在發生異常時歸還給連接池時關閉該連接即可(jedis.close內部已經做了判斷),代碼如下:

 1 try {
 2     jedis = jedisPool.getResource();
 3     // jedis業務讀寫操作
 4 } catch (Exception e) {
 5     // 異常處理
 6 } finally {
 7     if (jedis != null) {
 8         // 歸還給連接池
 9         jedis.close();
10     }
11 }

至此,該問題解決。注意,因為使用了hessian序列化(其包含了類型信息,類似的有Java本身序列化機制),所有會報類轉換異常;如果使用了json序列化(其只包含對象屬性信息),反序列化時不會報異常,只不過因為不同類的屬性不同,會導致反序列化后的對象屬性為空或者屬性值混亂,使用時會導致問題,並且這種問題因為沒有報異常所以更不容易發現。

 

既然說到了Redis的連接,要知道的是,Redis基於RESP(Redis Serialization Protocol)協議來通信,並且通信方式是停等方式,也就說一次通信獨佔一個連接直到client讀取到返回結果之後才能釋放該連接讓其他線程使用。小夥伴們可以思考一下,Redis通信能否像dubbo那樣使用單連接+序列號(標識單次通信)通信方式呢?理論上是可以的,不過由於RESP協議中並沒有一個”序列號”的字段,所以直接靠原生的通信方法來實現是不現實的。不過我們可以通過echo命令傳遞並返回”序列號”+正常的讀寫方式來實現,這裏要保證二者執行的原子性,可以通過lua腳本或者事務來實現,事務方式如下:

MULTI
ECHO "唯一序列號"
GET key1
EXEC

然後客戶端收到的結果是一個 [ "唯一序列號", "value1" ]的列表,你可以根據前一項識別出這是你發送的哪個請求。

為什麼Redis通信方式並沒有採用類似於dubbo這種通信方式呢,個人認為有以下幾點:

  • 使用停等這種通信方式實現簡單,並且協議字段盡可能緊湊;
  • Redis都是內存操作,處理性能較強,停等協議不會造成客戶端等待時間較長;
  • 目前來看,通信方式這塊不是Redis使用上的性能瓶頸,這一點很重要。

 

推薦閱讀:

  • 別再問我ConcurrentHashMap了
  • 分佈式鎖設計與實現

  • ConcurrentHashMap竟然也有死循環問題?

  • 你的ThreadLocal線程安全么

 歡迎小夥伴掃描以下二維碼閱讀更多精彩好文。

 

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

高級Java工程師必備 —– 深入分析 Java IO (一)BIO

BIO編程

最原始BIO

網絡編程的基本模型是C/S模型,即兩個進程間的通信。

服務端提供IP和監聽端口,客戶端通過連接操作想服務端監聽的地址發起連接請求,通過三次握手連接,如果連接成功建立,雙方就可以通過套接字進行通信。

傳統的同步阻塞模型開發中,ServerSocket負責綁定IP地址,啟動監聽端口;Socket負責發起連接操作。連接成功后,雙方通過輸入和輸出流進行同步阻塞式通信。
最原始BIO通信模型圖:

存在的問題:

  • 同一時間,服務器只能接受來自於客戶端A的請求信息;雖然客戶端A和客戶端B的請求是同時進行的,但客戶端B發送的請求信息只能等到服務器接受完A的請求數據后,才能被接受。(acceptor只有在接受完client1的請求后才能接受client2的請求)
  • 由於服務器一次只能處理一個客戶端請求,當處理完成並返回后(或者異常時),才能進行第二次請求的處理。很顯然,這樣的處理方式在高併發的情況下,是不能採用的。

一請求一線程BIO

那有沒有方法改進呢? ,答案是有的。改進后BIO通信模型圖:

此種BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監聽客戶端的連接,它接收到客戶端連接請求之後為每個客戶端創建一個新的線程進行鏈路處理沒處理完成后,通過輸出流返回應答給客戶端,線程銷毀。即典型的一請求一應答通宵模型。

代碼演示

服務端:

package demo.com.test.io.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

import demo.com.test.io.nio.NioSocketServer;

public class BioSocketServer {
   //默認的端口號  
   private static int DEFAULT_PORT = 8083;  

   public static void main(String[] args) {
       ServerSocket serverSocket = null;
       try {
           System.out.println("監聽來自於"+DEFAULT_PORT+"的端口信息");
           serverSocket = new ServerSocket(DEFAULT_PORT);
           while(true) {
               Socket socket = serverSocket.accept();
               SocketServerThread socketServerThread = new SocketServerThread(socket);
               new Thread(socketServerThread).start();
           }
       } catch(Exception e) {

       } finally {
           if(serverSocket != null) {
               try {
                   serverSocket.close();
               } catch (IOException e) {
                   // TODO Auto-generated catch block
                   e.printStackTrace();
               }
           }
       }

        //這個wait不涉及到具體的實驗邏輯,只是為了保證守護線程在啟動所有線程后,進入等待狀態
       synchronized (NioSocketServer.class) {
           try {
               BioSocketServer.class.wait();
           } catch (InterruptedException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
           }
       }
   }
}  

class SocketServerThread implements Runnable {
   private Socket socket;
   public SocketServerThread (Socket socket) {
       this.socket = socket;
   }
   @Override
   public void run() {
       InputStream in = null;
       OutputStream out = null;
       try {
           //下面我們收取信息
           in = socket.getInputStream();
           out = socket.getOutputStream();
           Integer sourcePort = socket.getPort();
           int maxLen = 1024;
           byte[] contextBytes = new byte[maxLen];
           //使用線程,同樣無法解決read方法的阻塞問題,
           //也就是說read方法處同樣會被阻塞,直到操作系統有數據準備好
           int realLen = in.read(contextBytes, 0, maxLen);
           //讀取信息
           String message = new String(contextBytes , 0 , realLen);

           //下面打印信息
           System.out.println("服務器收到來自於端口:" + sourcePort + "的信息:" + message);

           //下面開始發送信息
           out.write("回發響應信息!".getBytes());
       } catch(Exception e) {
           System.out.println(e.getMessage());
       } finally {
           //試圖關閉
           try {
               if(in != null) {
                   in.close();
               }
               if(out != null) {
                   out.close();
               }
               if(this.socket != null) {
                   this.socket.close();
               }
           } catch (IOException e) {
               System.out.println(e.getMessage());
           }
       }
   }
}

客戶端:

package demo.com.test.io.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URLDecoder;
import java.util.concurrent.CountDownLatch;

public class BioSocketClient{
   public static void main(String[] args) throws Exception {
       Integer clientNumber = 20;
       CountDownLatch countDownLatch = new CountDownLatch(clientNumber);

       // 分別開始啟動這20個客戶端,併發訪問
       for (int index = 0; index < clientNumber; index++, countDownLatch.countDown()) {
           ClientRequestThread client = new ClientRequestThread(countDownLatch, index);
           new Thread(client).start();
       }

       // 這個wait不涉及到具體的實驗邏輯,只是為了保證守護線程在啟動所有線程后,進入等待狀態
       synchronized (BioSocketClient.class) {
           BioSocketClient.class.wait();
       }
   }
}



/**
* 一個ClientRequestThread線程模擬一個客戶端請求。
* @author keep_trying
*/
class ClientRequestThread implements Runnable {


   private CountDownLatch countDownLatch;

   /**
    * 這個線程的編號
    * @param countDownLatch
    */
   private Integer clientIndex;

   /**
    * countDownLatch是java提供的同步計數器。
    * 當計數器數值減為0時,所有受其影響而等待的線程將會被激活。這樣保證模擬併發請求的真實性
    * @param countDownLatch
    */
   public ClientRequestThread(CountDownLatch countDownLatch , Integer clientIndex) {
       this.countDownLatch = countDownLatch;
       this.clientIndex = clientIndex;
   }

   @Override
   public void run() {
       Socket socket = null;
       OutputStream clientRequest = null;
       InputStream clientResponse = null;

       try {
           socket = new Socket("localhost",8083);
           clientRequest = socket.getOutputStream();
           clientResponse = socket.getInputStream();

           //等待,直到SocketClientDaemon完成所有線程的啟動,然後所有線程一起發送請求
           this.countDownLatch.await();

           //發送請求信息
           clientRequest.write(("這是第" + this.clientIndex + " 個客戶端的請求。 over").getBytes());
           clientRequest.flush();

           //在這裏等待,直到服務器返回信息
          System.out.println("第" + this.clientIndex + "個客戶端的請求發送完成,等待服務器返回信息");
           int maxLen = 1024;
           byte[] contextBytes = new byte[maxLen];
           int realLen;
           String message = "";
           //程序執行到這裏,會一直等待服務器返回信息(注意,前提是in和out都不能close,如果close了就收不到服務器的反饋了)
           while((realLen = clientResponse.read(contextBytes, 0, maxLen)) != -1) {
               message += new String(contextBytes , 0 , realLen);
           }
           //String messageEncode = new String(message , "UTF-8");
           message = URLDecoder.decode(message, "UTF-8");
           System.out.println("第" + this.clientIndex + "個客戶端接收到來自服務器的信息:" + message);
       } catch (Exception e) {

       } finally {
           try {
               if(clientRequest != null) {
                   clientRequest.close();
               }
               if(clientResponse != null) {
                   clientResponse.close();
               }
           } catch (IOException e) {

           }
       }
   }
}   

存在的問題:

  • 雖然在服務器端,請求的處理交給了一個獨立線程進行,但是操作系統通知accept()的方式還是單個的。也就是,實際上是服務器接收到數據報文後的“業務處理過程”可以多線程,但是數據報文的接受還是需要一個一個的來(acceptor只有在接受完client1的請求后才能接受client2的請求),下文會驗證。
  • 在linux系統中,可以創建的線程是有限的。我們可以通過cat /proc/sys/kernel/threads-max命令查看可以創建的最大線程數。當然這個值是可以更改的,但是線程越多,CPU切換所需的時間也就越長,用來處理真正業務的需求也就越少。
  • 另外,如果您的應用程序大量使用長連接的話,線程是不會關閉的。這樣系統資源的消耗更容易失控。

偽異步I/O編程

為了改進這種一連接一線程的模型,我們可以使用線程池來管理這些線程,實現1個或多個線程處理N個客戶端的模型(但是底層還是使用的同步阻塞I/O),通常被稱為“偽異步I/O模型“。

偽異步I/O模型圖:

代碼演示

只給出服務端,客戶端和上面相同

package demo.com.test.io.bio;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import demo.com.test.io.nio.NioSocketServer;

public class BioSocketServerThreadPool {
   //默認的端口號  
   private static int DEFAULT_PORT = 8083;  
   //線程池 懶漢式的單例  
   private static ExecutorService executorService = Executors.newFixedThreadPool(60);  

   public static void main(String[] args) {
       ServerSocket serverSocket = null;
       try {
           System.out.println("監聽來自於"+DEFAULT_PORT+"的端口信息");
           serverSocket = new ServerSocket(DEFAULT_PORT);
           while(true) {
               Socket socket = serverSocket.accept();
               //當然業務處理過程可以交給一個線程(這裏可以使用線程池),並且線程的創建是很耗資源的。
               //最終改變不了.accept()只能一個一個接受socket的情況,並且被阻塞的情況
               SocketServerThreadPool socketServerThreadPool = new SocketServerThreadPool(socket);
               executorService.execute(socketServerThreadPool);
           }
       } catch(Exception e) {

       } finally {
           if(serverSocket != null) {
               try {
                   serverSocket.close();
               } catch (IOException e) {
                   // TODO Auto-generated catch block
                   e.printStackTrace();
               }
           }
       }

        //這個wait不涉及到具體的實驗邏輯,只是為了保證守護線程在啟動所有線程后,進入等待狀態
       synchronized (NioSocketServer.class) {
           try {
               BioSocketServerThreadPool.class.wait();
           } catch (InterruptedException e) {
               // TODO Auto-generated catch block
               e.printStackTrace();
           }
       }
   }
}  

class SocketServerThreadPool implements Runnable {
   private Socket socket;
   public SocketServerThreadPool (Socket socket) {
       this.socket = socket;
   }
   @Override
   public void run() {
       InputStream in = null;
       OutputStream out = null;
       try {
           //下面我們收取信息
           in = socket.getInputStream();
           out = socket.getOutputStream();
           Integer sourcePort = socket.getPort();
           int maxLen = 1024;
           byte[] contextBytes = new byte[maxLen];
           //使用線程,同樣無法解決read方法的阻塞問題,
           //也就是說read方法處同樣會被阻塞,直到操作系統有數據準備好
           int realLen = in.read(contextBytes, 0, maxLen);
           //讀取信息
           String message = new String(contextBytes , 0 , realLen);

           //下面打印信息
           System.out.println("服務器收到來自於端口:" + sourcePort + "的信息:" + message);

           //下面開始發送信息
           out.write("回發響應信息!".getBytes());
       } catch(Exception e) {
           System.out.println(e.getMessage());
       } finally {
           //試圖關閉
           try {
               if(in != null) {
                   in.close();
               }
               if(out != null) {
                   out.close();
               }
               if(this.socket != null) {
                   this.socket.close();
               }
           } catch (IOException e) {
               System.out.println(e.getMessage());
           }
       }
   }
}

服務器端的執行效果

在 Socket socket = serverSocket.accept(); 處打了斷點,有20個客戶端同時發出請求,可服務端還是一個一個的處理,其它線程都處於阻塞狀態

推薦博客

  程序員寫代碼之外,如何再賺一份工資?

阻塞的問題根源

 那麼重點的問題並不是“是否使用了多線程、或是線程池”,而是為什麼accept()、read()方法會被阻塞。API文檔中對於 serverSocket.accept() 方法的使用描述:

Listens for a connection to be made to this socket and accepts it. The method blocks until a connection is made.

服務器線程發起一個accept動作,詢問操作系統 是否有新的socket套接字信息從端口xx發送過來。

注意,是詢問操作系統。也就是說socket套接字的IO模式支持是基於操作系統的,那麼自然同步IO/異步IO的支持就是需要操作系統級別的了。如下圖:

 如果操作系統沒有發現有套接字從指定的端口xx來,那麼操作系統就會等待。這樣serverSocket.accept()方法就會一直等待。這就是為什麼accept()方法為什麼會阻塞:它內部的實現是使用的操作系統級別的同步IO。

  • 阻塞IO 和 非阻塞IO
    這兩個概念是程序級別的。主要描述的是程序請求操作系統IO操作后,如果IO資源沒有準備好,那麼程序該如何處理的問題:前者等待;後者繼續執行(並且使用線程一直輪詢,直到有IO資源準備好了)
  • 同步IO 和非同步IO
    這兩個概念是操作系統級別的。主要描述的是操作系統在收到程序請求IO操作后,如果IO資源沒有準備好,該如何處理相應程序的問題:前者不響應,直到IO資源準備好以後;後者返回一個標記(好讓程序和自己知道以後的數據往哪裡通知),當IO資源準備好以後,再用事件機制返回給程序。

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

集成學習總結

1 基本概念

  • 集成學習的主要思路是先通過一定的規則生成多個學習器,再採用某種集成策略進行組合,最後綜合判斷輸出最終結果。一般而言,通常所說的集成學習中的多個學習器都是同質的”弱學習器”。基於該弱學習器,通過樣本集擾動、輸入特徵擾動、輸出表示擾動、算法參數擾動等方式生成多個學習器,進行集成后獲得一個精度較好的”強學習器”。
  • 目前集成學習算法大多源於bagging、boosting、stacking三種思想。

2 bagging

  • 一種提高分類模型的方法。
    • (1) 從訓練集\(S\)中有放回的隨機選取數據集\(M\)\((∣M∣ < ∣S∣)\);
    • (2) 生成一個分類模型\(C\);
    • (3) 重複以上步驟\(m\)次,得到\(m\)個分類模型\(C_1,C_2,…,C_m\);
    • (4)對於分類問題,每一個模型投票決定,少數服從多數原則;
    • (5)對於回歸問題,取平均值。
  • 注意:這種抽樣的方式會導致有的樣本取不到,大約有\(\lim_{n \to \infty}(1-\frac{1}{n})^n\) = \(36.8%\)的樣本取不到,這部分可用來做測試集。

  • 優點: 通過減少方差來提高預測結果。
  • 缺點: 失去了模型的簡單性

2.1 Random Forest

  • 是一種基於樹模型的bagging算法改進的模型。假定數據集中有\(M\)個特徵和 \(N\)個觀測值。每一個樹有放回的隨機抽出\(N\)個觀測值\(m\)(\(m=M\)或者\(m=logM\))個特徵。把每一個單一決策樹的結果綜合起來。

  • 優點:
    • (1) 減少了模型方差,提高了預測準確性。
    • (2) 不需要給樹做剪枝。
    • (3) 在大規模數據集,尤其是特徵較多的情況下,依然可以保持高效率。
    • (4) 不用做特徵選擇,並且可以給出特徵變量重要性的排序估計。
  • 缺點:
    • (1) 隨機森林已經被證明在某些噪音較大的分類或回歸問題上會過擬合
    • (2) 對於有不同取值的屬性的數據,取值劃分較多的屬性會對隨機森林產生更大的影響,所以隨機森林在這種數據上產出的屬性權值是不可信的。

3 boosting

  • 每一輪根據上一輪的分類結果動態調整每個樣本在分類器中的權重,訓練得到k個弱分類器,他們都有各自的權重,通過加權組合的方式得到最終的分類結果(綜合所有的基模型預測結果)。主要算法有AdaBoost/GBDT/Xgboost/LightGBM。

3.1 Adboost

  • 給定數據集\(S\),它包含\(n\)個元組\((X_1,y_1),(X_2,y_2),…,(X_n,y_n)(X_1,y_1), (X_2,y_2), …, (X_n,y_n)\),其中\(y_i\)是數據對象\(X_i\)的類標號。
  • (1) 開始時,Adaboost對每個訓練元組賦予相等的權重\(1/n\)。組合分類器包含\(T\)個基本分類器。
  • (2) 針對第\(t\)個分類器\(M_t\)
    • 首先,從S中的元組進行抽樣,形成大小為\(n\)的訓練集\(S_t\),此處抽樣方式為有放回的抽樣,抽樣過程中,每個元組被選中的機會由它的權重決定;
    • 然後,根據\(S_t\)導出(訓練出)分類器\(M_t\),使用\(S_t\)檢驗分類器\(M_t\)的分類誤差,並計算該分類器的“表決權”的權重;
    • 最後,訓練元組的權重根據分類器\(M_t\)的分類情況調整。
    • 如果元組被錯誤分類,則它的權重增加。
    • 如果元組被正確分類,則它的權重減少。
    • 元組的權重反映元組被分類的困難程度——權重越高,被錯誤分類的可能性越高。然後,使用這些權重,為下一輪分類器(下一個分類器)產生訓練樣本。
  • 其基本的思想是,當建立分類器時,希望它更關註上一輪分類器(上一個分類器)錯誤分類的元組。整個分類過程中,某些分類器對某些“困難”元組的分類效果可能比其他分類器好。這樣,建立了一個互補的分類器系列。
  • 用於二分類或多分類的應用場景。
  • 優點
    • (1) 很好的利用了弱分類器進行級聯。
    • (2)可以將不同的分類算法作為弱分類器。
    • (3)AdaBoost具有很高的精度。
    • (4) 相對於bagging算法和Random Forest算法,AdaBoost充分考慮的每個分類器的權重。
  • 缺點:
    • (1) AdaBoost迭代次數也就是弱分類器數目不太好設定,可以使用交叉驗證來進行確定。
    • (2) 數據不平衡導致分類精度下降。
    • (3) 訓練比較耗時,每次重新選擇當前分類器最好切分點。

3.2 GBDT

  • 採用決策樹作為弱分類器的Gradient Boosting算法被稱為GBDT,有時又被稱為MART(Multiple Additive Regression Tree)。GBDT中使用的決策樹通常為CART。
  • 用一個很簡單的例子來解釋一下GBDT訓練的過程,如圖下圖所示。模型的任務是預測一個人的年齡,訓練集只有A、B、C、D 4個人,他們的年齡分別是14、16、24、26,特徵包括了 “月購物金額”、”上網時長”、”上網歷史” 等。
  • 下面開始訓練第一棵樹:
    • 訓練的過程跟傳統決策樹相同,簡單起見,我們只進行一次分枝。訓練好第一棵樹后,求得每個樣本預測值與真實值之間的殘差。
    • 可以看到,A、B、C、D的殘差分別是−1、1、−1、1。
    • 這時我們就用每個樣本的殘差訓練下一棵樹,直到殘差收斂到某個閾值以下,或者樹的總數達到某個上限為止。
  • 由於GBDT是利用殘差訓練的,在預測的過程中,我們也需要把所有樹的預測值加起來,得到最終的預測結果。

  • 優點:
    • (1)預測階段的計算速度快,樹與樹之間可并行化計算。
    • (2)在分佈稠密的數據集上,泛化能力和表達能力都很好,這使得GBDT在Kaggle的眾多競賽中,經常名列榜首。
    • (3)採用決策樹作為弱分類器使得GBDT模型具有較好的解釋性和魯棒性,能夠自動發現特徵間的高階關係,並且也不需要對數據進行特殊的預處理如歸一化等。
  • 缺點:
    • (1)GBDT在高維稀疏的數據集上,表現不如支持向量機或者神經網絡。
    • (2)GBDT在處理文本分類特徵問題上,相對其他模型的優勢不如它在處理數值特徵時明顯。
    • (3)訓練過程需要串行訓練,只能在決策樹內部採用一些局部并行的手段提高訓練速度。

3.3 Xgboost

  • XGBoost是陳天奇等人開發的一個開源機器學習項目,高效地實現了GBDT算法並進行了算法和工程上的許多改進。
  • 目標函數:
    \[L^{(t)} = \sum_{i=1}^{n}l(y_i, \hat{y}_i^{(t)}) + \Omega(f_t) \]
  • 優點:
    • (1)計算效率高,使用了二階導。
    • (2)有正則化,減少過擬合。
    • (3)列特徵抽樣減少過擬合,同時有利於并行計算。
  • 缺點:
    • (1)每次迭代時都要遍歷整個數據集。
    • (2)內存佔用大。

3.4 GBDT與XGboost聯繫與區別

  • (1) GBDT是機器學習算法,XGBoost是該算法的工程實現。
  • (2) 在使用CART作為基分類器時,XGBoost顯式地加入了正則項來控制模型的複雜度,有利於防止過擬合,從而提高模型的泛化能力
  • (3) GBDT在模型訓練時只使用了代價函數的一階導數信息,XGBoost對代價函數進行二階泰勒展開,可以同時使用一階和二階導數。
  • (4) 傳統的GBDT採用CART作為基分類器,XGBoost支持多種類型的基分類器,比如線性分類器。
  • (5) 傳統的GBDT在每輪迭代時使用全部的數據,XGBoost則採用了與隨機森林相似的策略,支持對數據進行採樣
  • (6) 傳統的GBDT沒有設計對缺失值進行處理,XGBoost能夠自動學習出缺失值的處理策略

3.5 LightGBM

  • LightGBM也是一種基於決策樹的梯度提升算法,相比XGboost有做了許多改進。
    在樹分裂計算分裂特徵的增益時,xgboost 採用了預排序的方法來處理節點分裂,這樣計算的分裂點比較精確。但是,也造成了很大的時間開銷。為了解決這個問題,Lightgbm 選擇了基於 histogram 的決策樹算法。相比於pre-sorted算法,histogram在內存消耗和計算代價上都有不少優勢。
  • Histogram算法簡單來說,就是先對特徵值進行裝箱處理,形成一個一個的bins。在Lightgbm中默認的#bins為256(1個字節的能表示的長度,可以設置)。具體如下:
    • (1) 把連續的浮點特徵值離散化成N個整數,構造一個寬度為N的直方圖;對於分類特徵,則是每一種取值放入一個bin,且當取值的個數大於max_bin數時,會忽略那些很少出現的category值。
    • (2) 遍曆數據時,根據離散化后的值作為索引在直方圖中累積統計量。
    • (3) 一次遍歷后,直方圖累積了需要的統計量,然後根據直方圖的離散值,遍歷尋找最優的分割點。
  • Level-wise 和 Leaf-wise
    • 相對於xgboost的level—wise的生長策略,lightgbm使用了leaf-wise樹生長策略。由於level-wise在分裂時,部分增益小的樹也得到了增長,雖然容易控制誤差,但是分裂有時是不合理的,而lightgbm使用level-wise,只在增益大的樹上分裂生長,甚至對Feature f如果分裂無收益,那麼後續也將不會對f計算。體現在參數上,xgboost使用max_depth,而lightgbm使用num_leaves控制過擬合。
    • Level-wise過一次數據可以同時分裂同一層的恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子,容易進行多線程優化,也好控制模型複雜度,不容易過擬合。但實際上Level-wise是一種低效的算法,因為它不加區分的對待同一層的恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子,帶來了很多沒必要的開銷,因為實際上很多恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子的分裂增益較低,沒必要進行搜索和分裂
    • Leaf-wise則是一種更為高效的策略,每次從當前所有恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子中,找到分裂增益最大的一個恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子,然後分裂,如此循環。因此同Level-wise相比,在分裂次數相同的情況下,Leaf-wise可以降低更多的誤差,得到更好的精度。Leaf-wise的缺點是可能會長出比較深的決策樹,產生過擬合。因此LightGBM在Leaf-wise之上增加了一個最大深度的限制,在保證高效率的同時防止過擬合。

3.6 Xgboost與LightGBM對比

3.6.1 切分算法(切分點的選取)

  • 佔用的內存更低,只保存特徵離散化后的值,而這個值一般用8位整型存儲就足夠了,內存消耗可以降低為原來的1/8
  • 降低了計算的代價:預排序算法每遍歷一個特徵值就需要計算一次分裂的增益,而直方圖算法只需要計算k次(k可以認為是常數),時間複雜度從O(#data#feature)優化到O(k#features)。(相當於LightGBM犧牲了一部分切分的精確性來提高切分的效率,實際應用中效果還不錯)
  • 空間消耗大,需要保存數據的特徵值以及特徵排序的結果(比如排序后的索引,為了後續快速計算分割點),需要消耗兩倍於訓練數據的內存
  • 時間上也有較大開銷,遍歷每個分割點時都需要進行分裂增益的計算,消耗代價大
  • 對cache優化不友好,在預排序后,特徵對梯度的訪問是一種隨機訪問,並且不同的特徵訪問的順序不一樣,無法對cache進行優化。同時,在每一層長樹的時候,需要隨機訪問一個行索引到恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子索引的數組,並且不同特徵訪問的順序也不一樣,也會造成較大的cache miss。
  • XGBoost使用的是pre-sorted算法(對所有特徵都按照特徵的數值進行預排序,基本思想是對所有特徵都按照特徵的數值進行預排序;然後在遍歷分割點的時候用O(#data)的代價找到一個特徵上的最好分割點最後,找到一個特徵的分割點后,將數據分裂成左右子節點。優點是能夠更精確的找到數據分隔點;但這種做法有以下缺點
  • LightGBM使用的是histogram算法,基本思想是先把連續的浮點特徵值離散化成k個整數,同時構造一個寬度為k的直方圖。在遍曆數據的時候,根據離散化后的值作為索引在直方圖中累積統計量,當遍歷一次數據后,直方圖累積了需要的統計量,然後根據直方圖的離散值,遍歷尋找最優的分割點;優點在於

3.6.2 決策樹生長策略

  • XGBoost採用的是帶深度限制的level-wise生長策略,Level-wise過一次數據可以能夠同時分裂同一層的恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子,容易進行多線程優化,不容易過擬合;但不加區分的對待同一層的恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子,帶來了很多沒必要的開銷(因為實際上很多恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子的分裂增益較低,沒必要進行搜索和分裂)
  • LightGBM採用leaf-wise生長策略,每次從當前所有恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子中找到分裂增益最大(一般也是數據量最大)的一個恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子,然後分裂,如此循環;但會生長出比較深的決策樹,產生過擬合(因此 LightGBM 在leaf-wise之上增加了一個最大深度的限制,在保證高效率的同時防止過擬合)。
  • Histogram做差加速。一個容易觀察到的現象:一個恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子的直方圖可以由它的父親節點的直方圖與它兄弟的直方圖做差得到。通常構造直方圖,需要遍歷該恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子上的所有數據,但直方圖做差僅需遍歷直方圖的k個桶。利用這個方法,LightGBM可以在構造一個恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子的直方圖后,可以用非常微小的代價得到它兄弟恭弘=恭弘=恭弘=叶 恭弘 恭弘 恭弘子的直方圖,在速度上可以提升一倍。
  • 直接支持類別特徵:LightGBM優化了對類別特徵的支持,可以直接輸入類別特徵,不需要額外的0/1展開。並在決策樹算法上增加了類別特徵的決策規則。

3.6.3 分佈式訓練方法上(并行優化)

  • 在特徵并行算法中,通過在本地保存全部數據避免對數據切分結果的通信;
  • 在數據并行中使用分散規約(Reducescatter)把直方圖合併的任務分攤到不同的機器,降低通信和計算,並利用直方圖做差,進一步減少了一半的通信量。基於投票的數據并行(ParallelVoting)則進一步優化數據并行中的通信代價,使通信代價變成常數級別。
  • 特徵并行的主要思想是在不同機器在不同的特徵集合上分別尋找最優的分割點,然後在機器間同步最優的分割點。
  • 數據并行則是讓不同的機器先在本地構造直方圖,然後進行全局的合併,最後在合併的直方圖上面尋找最優分割點。
  • Cache命中率優化
  • 基於直方圖的稀疏特徵優化
  • DART(Dropout + GBDT)
  • GOSS(Gradient-based One-Side Sampling):一種新的Bagging(row subsample)方法,前若干輪(1.0f /gbdtconfig->learning_rate)不Bagging;之後Bagging時, 採樣一定比例g(梯度)大的樣本。

4 stacking

  • stacking的思想是將每個基模型的輸出組合起來作為一個特徵向量,重新進行訓練。可以理解為:將訓練好的所有基模型對整個訓練集進行預測,第j個基模型對第i個訓練樣本的預測值將作為新的訓練集中第i個樣本的第j個特徵值,最後基於新的訓練集進行訓練。同理,預測的過程也要先經過所有基模型的預測形成新的測試集,最後再對測試集進行預測。
  • 具體算法如下(為方便理解我舉例說明)
    • (1) 訓練集大小\(400\times10\),400個樣本,每個樣本10個特徵,(如果算上target有11列)。測試集大小為\(120\times10\)
    • (2) 首先對訓練集4折劃分:\(S_1\),\(S_2\),\(S_3\),\(S_4\),每個\(S_i\)的大小都收是\(100\times10\)。模型\(M_1\)第一次用\(S_1\),\(S_2\),\(S_3\)訓練,用\(S_4\)預測得到預測結果\(100\times1\)。重複訓練步驟,直到每一個\(S_i\)都有對應的預測結果\(100\times1\)。合併所有預測結果得到\(P_1\)\(400\times1\)。用\(M_1\)預測得到原始測試集的預測結果\(T_1\)\(120\times1\)
    • (3) 模型\(M_2\)用4折叫交叉得到訓練集的預測結果:\(P_2\)\(400\times1\);得到測試集的預測結果:\(T_2\)\(120\times1\)
    • (4) 模型\(M_3\)用4折叫交叉得到訓練集的預測結果:\(P_3\)\(400\times1\);得到測試集的預測結果:\(T_3\)\(120\times1\)
    • (5) 綜合(2)(3)(4)底層模型得到的訓練集的預測結果\(P_1\)\(P_2\)\(P_3\),得到上層模型的訓練集\(P_{train}\)\(400\times3\);得到上層模型的測試集\(T_{test}\)\(120\times3\)
    • (6) 用(5)得到的訓練集和測試集進行上層模型的訓練。
  • 優點:學習了底層模型之間的關係
  • 缺點:對於數據量要求比較大,因為要平衡第一層和第二層

    5 參考

  • 《百面機器學習》
  • https://zhuanlan.zhihu.com/p/26890738
  • https://www.imooc.com/article/29530
  • https://blog.csdn.net/anshuai_aw1/article/details/83040541

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!