進程知多少?

目錄

  • 進程為什麼出現?
  • 進程的組成
  • 如何競爭資源(調度算法)
    • FCFS
    • RR
    • SPN
    • SRT
    • HRRN
    • FB
  • 進程狀態
    • 三態圖
    • 五態圖
    • 七態圖
  • 進程關係
    • 父子關係
    • 殭屍進程
    • 孤兒進程
  • 執行模式
  • 進程間通訊
    • 管道(Pipe)
    • 流管道(Flow Pipe)
    • 有名管道(Named Pipe)
    • 信號量(Semophore)
    • 信號(Signal)
    • 消息隊列(Message Queue)
    • 共享內存(Shared Memory)
    • 套接字(Socket)
  • 總結

文章首發:進程知多少?

Java 多線程系列文章第 1 篇

要講線程,一般都得講一講進程,進程是何方神聖呢?下面來簡單介紹一下。

先通過任務管理器看看 Windows 系統下的進程。

從圖片來看,每一個進程都佔有 CPU、內存、磁盤、網絡等資源。站在操作系統的角度,進程是分配資源的基本單位,也是最小單位

進程為什麼出現?

引入進程的目的:為了使多個程序能併發執行,以提高資源的利用率和系統的吞吐量。怎麼理解這句話呢?一個程序在運行過程中會涉及很多操作,利用 CPU 計算、通過磁盤 IO 進行數據傳輸等等,我們知道當程序在進行磁盤 IO 的時候,因為速度問題,會比較慢,所在在這個過程中 CPU 會空閑下來,這會造成資源的浪費,正因為引入進程,在 A 進程進行磁盤 IO 的時候,會讓出 CPU 給 B 進程,合理地利用了 CPU 資源,使得程序之間可以併發執行。

從 CPU 角度,執行過程是這樣子的:CPU 一直在負責執行指令,進程之間互相競爭 CPU 資源,下圖有 A 和 B 進程,在一個時間點,CPU 只執行一個進程的指令,因為 CPU 運行很快,所以在咱們看起來,像是多個進程在同時跑。這就是進程帶來的好處:提高資源利用率,併發地執行多個程序

當然引入進程也不是有益無害,它增加了系統的時間空間開銷。空間開銷這個好理解,進程有自己的組成部分(下面會講),這個就佔用了空間。時間開銷則是進程切換需要時間。

進程的組成

進程由 3 個部分組成,分別是程序代碼數據集、棧進程控制塊(Process Control Block)

各自的作用如下:

  1. 程序代碼:描述了進程需要完成的功能。
  2. 數據集、棧:程序在執行時所需要的數據和工作區。
  3. 進程控制塊:包含進程的描述信息和控制信息,它是進程存在的唯一標識。

如何競爭資源(調度算法)

進程之間需要競爭資源,一般都是競爭 CPU 資源,因為 CPU 運行速度太快了,其他介質都趕不上。有了競爭就需要有規則,就像遊戲一樣,每個遊戲都需要規則,不同規則會有不同的側重點,這個看過“最強大腦”這個節目的朋友就非常清楚,每道題都有不同的考核側重點,有些是側重空間思維、有些側重邏輯推算等等。下面我們就簡單地一一講解競爭資源的遊戲規則。

FCFS

First In First Out(先來先服務):最先進入就緒隊列的進程,先運行,運行到完成或者阻塞時,再重新調度。一般情況下,這種調度算法會和優先級策略結合,比如每個優先級一條隊列,每條隊列中的調度都使用 FCFS。

特點:簡單、比較偏於長進程、相對於其他調度算法平均周轉時間長

RR

Round Robin(輪轉):進程按提交順序存在就緒隊列,依次輪流佔用 CPU 資源,運行一段固定的時間,時間到后如果還沒執行完,就繼續進入就緒隊列隊尾,排隊等待下次執行。

特點:公平、對進程的響應時間較短

SPN

Shortest Process Next(最短進程優先):將預期佔用運行時間最短的進程優先執行,直到運行完成或阻塞時,再重新調度。

特點:有利於短進程

SRT

Shortest Remaining Time(最短剩餘時間優先):新進程進來時,如果新進程的預計運行時間比當前進程的剩餘運行時間更短,就搶佔當前進程,

特點:有利於短進程,和 SPN 的差別在於搶佔這個一點,因為搶佔,所以效率會比 SPN 好一些。

HRRN

Highest Response Ratio Next(最高響應比優先):當前運行的進程完成或者阻塞時發生調度,每次調度前,計算所有就緒進程的響應比,響應比高的進程優先運行。

響應比公式如下所示:

特點:有利於短進程服務時間相同的進程,先來的服務會優先執行長進程因為在等待的過程中,優先級越來越高,所以不會一直不執行

FB

Feedback (反饋):由多個就緒隊列組成的反饋機制,它有如下規則:

  1. 在同一個隊列的進程,按 FCFS 算法調度,最後一個就緒隊列按 RR 算法調度;
  2. 優先級越高的隊列,時間片越小;
  3. 進程在一個時間片內未運行完,則降到下一個隊列末尾;
  4. 只有上級隊列無就緒進程時,才運行本級就緒隊列,本級就緒隊列無進程時,才運行下級就緒隊列,以此類推

進程執行過程如下圖所示

特點:短進程有非常大的優勢,排在前面的隊列都是時間較短的

以上就是幾個搶佔資源的調度算法的說明。

進程狀態

上面我們講到,進程之間是在競爭資源,得到資源就運行,沒得到就等待,這個需要有狀態來維護,像很多系統一樣,需要一個狀態機。

三態圖

三態圖也是描述進程狀態最簡單最基礎的圖,它包含了進程的最基本的 3 個狀態,分別是:就緒態、運行態和阻塞態。

Read(就緒態):進程已得到除 CPU 以外的其他所需資源。
Running(運行態):進程的指令正被執行。
Blocked(阻塞態):進程正等待資源或某事件發生。

就緒態的進程在被調度的時候,進入了運行態,如果時間片運行完或者有更高級別進程搶佔資源,則變成就緒態等待再次被調度;如果發生事件(比如 IO 事件),則從運行態轉到阻塞態,進入阻塞態的進程只能等待事件解除重新進入就緒態

五態圖

基於三態圖,新增了 2 個狀態,分別是:新建態和退出態。

New(新建態):進程正被創建。分配內存后將被設為就緒態。

Exit(退出態):進程已正常結束或出現異常結束。回收資源。

新進程剛創建還沒有分配資源的時候是新建態,等到分配了資源,被加載后就進入就緒態。當進程運行完后,就從運行態進入退出態

七態圖

基於五態圖,新增了 2 種掛起態,分別是就緒掛起態和阻塞掛起態。

就緒掛起態:另叫外存就緒態。由於內存容量有限,將原位於內存的就緒進程轉存到外存(磁盤)上。

阻塞掛起態:另叫外存阻塞態。一樣因為內存容量有限,將原位於內存的阻塞進程轉存到外存(磁盤)上。

我們可以看出,圖中新增了解除掛起的狀態轉換過程,一般是由於掛起進程優先級比較高或者內存空間足夠,把位於外存(磁盤)的進程轉存到內存中。

進程關係

進程之間其實比較獨立,比如我們在日常使用的 QQ 和微信,它們運行起來的進程有什麼關係么?其實除了互相競爭資源之外,沒有任何關係。

父子關係

雖然上面說的進程之間沒有關係,但是有一個特殊關係需要講,就是父子關係

先做個試驗,驗證進程的父子關係。操作步驟:

  1. 打開 CMD 命令行程序,將當前的窗口設置為 Father,在 Father 窗口通過命令start cmd啟動另一個 CMD 命令行程序;
  2. 將新開的 CMD 命令行程序的窗口設置為 Son,在 Son 窗口通過命令start cmd啟動另一個 CMD 命令行程序;
  3. 將新開的 CMD 命令行程序的窗口設置為 Grandson。

操作過程如下圖所示。

通過 ProcessExplorer 可以很清晰看到這 3 個 CMD 進程之間的關係。(想要 ProcessExplorer 插件可以通過百度網盤下載鏈接:https://pan.baidu.com/s/19531gf5tD_of1CWxpFR9Dg 提取碼:qhc6)

我們看到 Father、Son、Grandson 三個進程呈現出我們預料中的樹形。那麼什麼是父子進程呢?簡單的說就是在進程中創建出新的進程,這個新的進程就是子進程,一個進程可以有多個子進程,但是只能有一個父進程。在 Unix 系統中,父進程通過調用 fork() 創建子進程,父子進程有如下特點:

  1. 父、子進程併發執行;
  2. 父、子進程共享父進程的所有資源;
  3. 子進程複製父進程的地址空間,甚至有相同的正文段和程序計數器 PC 值;
  4. 利用寫時複製(Copy On Write)技術減少不必要的複製:fork 時父子共用父空間,當一方試圖修改時才複製。

這裏重點講一下Copy On Write,使用了這個技術,父進程創建子進程的時候不會複製所有數據到子進程,省了複製的時間以及減少了大量的內存。這個複製不是必要的,因為如果應用程序在進程複製之後立即加載新程序,那之前的複製工作就是浪費時間和內存了。

講了進程父子關係,就免不了提一下殭屍進程孤兒進程,下面分別介紹一下。

殭屍進程

殭屍進程:子進程退出后,父進程沒有調用 wait 或 waitpid 獲取子進程的狀態信息,子進程的進程描述符仍保存在系統中,這種進程叫殭屍進程。

殭屍進程的危害:殭屍進程會一直佔用進程號,系統能使用的進程號又是有限的,如果有大量的殭屍進程,會因為沒有可用進程號導致無法創建新的進程。

孤兒進程

孤兒進程:父進程結束退出,而它的子進程還在運行,這時的子進程就叫做孤兒進程。孤兒進程就被 init 進程(進程號為 1)收養,init 進程將對孤兒進程完成狀態收集工作。

孤兒進程沒有危害,因為被 init 進程託管了,init 進程會處理孤兒進程的收集工作。

執行模式

指令分為特權指令(只能由操作系統內核使用的指令)和非特權指令(只能由用戶程序使用的指令),因為指令有特權和非特權之分,所以 CPU 也分為 2 種執行模式:系統態(可以執行所有指令,使用所有資源以及改變 CPU 狀態)和用戶態(只能執行非特權指令)。

CPU 的系統態和用戶態之間的切換。

進程間通訊

當進程之間需要數據傳輸、共享數據時,進程間就需要互相通訊,通訊方式有如下幾種,這裏只是簡單概括一下,不展開講,咱的重點在於多線程,進程咱們簡單了解一下就可以,感興趣的同學可以根據要點進行深入學習。

管道(Pipe)

管道是半雙工通訊,數據是單向流動,要建立進程間互相通訊,則需要 2 個管道,這種通訊方式只能在親戚關係的進程間使用,比如父子進程。

流管道(Flow Pipe)

流管道是管道進化來的,數據不再是單向流動,可以雙向流動,但是依舊是只能在親戚關係的進程間使用。

有名管道(Named Pipe)

有名管道提供了新的功能,就是給管道設置名字,它改善了上面 2 種管道通訊方式,支持了非親戚關係的進程通訊。

信號量(Semophore)

信號量相當於計數器,利用它來控制多個進程訪問共享資源,當一個進程A在訪問共享資源時,信號量防止其他進程來訪問,只有當進程A不訪問共享資源了,其他進程才能訪問。

信號(Signal)

信號可以在任何時候發給某一進程,不需要知道該進程當前的狀態,如果對方進程未執行,信號會存在內核中,直到進程執行後傳遞給它;如果對方進程是阻塞,則信號會延遲傳遞,等到對方進程阻塞取消后才傳遞給它。

消息隊列(Message Queue)

消息隊列是存放在內核中的鏈表,可以有多個進程對這個鏈表進行寫入和讀取,它解決了信號傳遞信息少、管道只能傳輸無格式字節流和緩衝區大小受限的缺點。目前有 POSIX 消息隊列和 System V 消息隊列。

共享內存(Shared Memory)

共享內存即為一段能被其他進程訪問的內存,多個進程訪問同一個內存,達到了通訊的效果。

套接字(Socket)

套接字就是我們網絡編程裏面的那個套接字,可以通過網絡也可以在本機進行通信,它的好處在於可以跨主機進行通信。

總結

總的來說,進程是程序在一個數據集上的一次執行過程,它就是程序運行起來的表現。這是我們學習多線程的開篇,希望通過這篇文章,讓大家簡單地了解進程是什麼,後面我們再來深入了解多線程。

推薦閱讀

設計模式看了又忘,忘了又看?

公眾號後台回復『設計模式』可以獲取《一故事一設計模式》电子書

覺得文章有用幫忙轉發&點贊,多謝朋友們!

【精選推薦文章】

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

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

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

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

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

如何從現在開始寫博客?

  • 《構建之法》讀書筆記四-關於寫博客的想法

  在前不久知名博主《純潔的微笑》在博客園分享了他寫博客的心得,獲得了讀者圈的廣泛共鳴,同時也引起了一些老生常談的爭議,這樣的討論在我們長沙.NET技術社區內部也經常發生。

1、寫博客的好處

  在《軟技能·代碼之外的生存指南》這本書中,作者提到作為軟件開發人員,最好的推銷自己的媒介就是博客,他認為每個在乎自己職業生涯的軟件開發人員都應該建立一個博客,他把他職業生涯的大部分成功都歸功於他的博客。

  在我之前寫過的一篇博客中,介紹了長沙.NET技術社區的優秀開發者,全棧工程師《ASP.NET企業級開發實戰》的作者鄒瓊俊老師的成長經歷,在他大學畢業後去找工作屢屢碰壁時,有幸加入了由長沙社區組織者周尹老師開設的學習班,並向老師請教如何才能提高自己的技能時,老師告訴他,你去寫博客,堅持寫博客,堅持五年,一定能獲得成功。果不其然,他筆耕不輟、堅持學習和記錄自己發現的問題,並成為了一位出版了兩本開發技能書籍的暢銷書作者。

  除了他之外,還有我在博客園結識的Java全棧工程師,《Web全棧開發進階之路》的作者,沉默王二,也是一位愛好寫博客的人,多年前他在蘇州的日企工作,偶爾會做一些筆記,但自從回到小城洛陽之後,就開始瘋狂的學習技術和寫博客,多年的堅持沒有白費,終於得以打造這本優秀的Java開發者們值得擁有的寶貴書籍。

  在技術社區有一次的閑談中,有人說起一個故事,說他以前有一次和他的同學一起去面試一份工作,然後面試官問你覺得你平時有什麼比較好的學習習慣么?他說基本上回家之後就是看看書,玩玩遊戲什麼的。而他的同學卻說:回家之後會看看書,逛逛博客園、寫寫博客。於是毫無疑問,他的同學得到了面試官的青睞並獲得了這份Offer。或許他的同學以前並沒有寫博客,但是相信得到這份Offer之後,他一定會開始寫博客、並培養自己寫博客的習慣的。

  在《軟技能》這本書裏面也說了,他去給軟件開發人員做演講,每當他問開發者有多少位開了博客並每周更新的請举手時,一百個開發者,頂多只有一位举手。他認為,堅持寫博客、持之以恆的撰寫優質內容,能讓你輕鬆在開發者中脫穎而出。

2、不要給自己太大的壓力、從小問題開始記錄

  許多人說曾經開過博客賬戶,甚至心血來潮使用wordpress\hexo等博客平台搭建過自己的博客網站,但是最終都無疾而終了,主要原因是不知道寫什麼內容,以及擔心自己寫的內容會被人吐槽說質量不好、自己忙於工作,根本沒有時間寫博客等。

  有時候開發者總是會給自己過大的壓力,其實這樣的壓力毫無必要,坦率而言,在大部分技術網站中活躍的優秀開發者,他們剛剛參加工作時,並非每個人都是學霸、一開始就能寫出優秀的博客,他們善於發現和記錄在工作中自己發現的一些點,並持之以恆,最終讓他們脫穎而出。

  每一位開發者本質上沒有那麼大的區別,並不是所有的開發者都有機會經歷大併發等互聯網的牛逼場景,但是我們總有機會發現或親歷一些只有自己才能看到的場景。在開發者這個包容心最強的社會群體中,只要你勇於寫下自己的博客,一定會獲得其他開發者的認同。(當然,有一些網絡噴子,請不要在意他們的說法)

  例如,我個人認為,我們可以這樣嘗試。

  1、不要擔心自己的文筆不夠好、不要過度在意別人的看法,只要寫博客,讓自己滿意,就是一個開端。

  2、學會記錄,形成素材。想到什麼,就記錄什麼,提前頭腦風暴出不同的想法,隨時更新在自己筆記中記錄的點子、並適當的進行擴展,就是一個非常不錯的話題。

  3、無需花費太多時間,每天花半個小時思考和總結問題,一周就有3個半小時,足夠寫出一篇千字以內的總結了。

  4、通過博客與其他人進行討論。如果遇到想不明白的問題,還可以通過博客的形式,分享出來,邀請大家進行討論,這樣的過程簡直不要太開心。

  在《構建之法》這本書中,將寫博客當成是開發者形成良好習慣的開始,並稱之為“做中學”,我們也可以在edu.cnblogs.com這個站點中,看到許多年輕的未來開發者們,他們通過博客園這個平台,將自己軟件工程學習過程中的問題、解決問題的方法和經驗分享出來,不同學校間還可以互相交流,形成了一個非常積極活躍的技術氛圍,我覺得這是一種令人愉悅的體驗。

  當然,我也很遺憾在我讀書的那些年沒有機會體驗這樣的機會,以至於走了一些彎路,到今天我的博客依然寫得比較少,所以這篇博客其實也是寫給自己的一種警醒,提醒自己應該堅持寫博客,不管年紀多大,只要今天開始堅持,堅持五年,總能取得不錯的成功,雖然不一定能像其他人一樣成功,但至少會比今天的自己成功。

3、寫博客的原則

  我曾經針對寫博客的問題,有幸請教過《構建之法》的作者鄒欣老師,我說我最近也寫了一些博客,但是總感覺都是一些毫無乾貨,讀起來感覺沒什麼意思,請問如何才能寫出有乾貨的內容呢?

  • 老師回答:說清楚一個具體問題,解決一個具體問題。

  這是一個充滿哲理的回答,讓我茅塞頓開。寫博客不是寫小說,不用長篇大論,不用引經據典,不用引用華麗的詞藻,只需講清楚一個問題即可。再簡單的問題,也是一個問題,每個人的理解都不一定相同,只需用鍵盤敲下你的理解,就可以成為一篇博客。

 

  在閱讀《浪潮之巔》第二卷的過程中,我看到了一樁關於博客的軼事,說甲骨文收購了Sun公司之後,甲骨文老闆 Ellison這樣吐槽:

  “Sun的工程師團隊是如此優秀,但是他們獲得的指引方向卻異常糟糕,這是導致他們無法成功的原因。花哨的博客並不能取代優秀的微處理器,也不能取代任何軟件,博客文章多頁無法帶來好的銷售業績。”

  Sun是一家曾經是一家優秀的互聯網公司,開創的許多領域時至今日依然讓開發者們收益,但是為什麼十年前卻突然死亡,最終賣身給Oracle?在《浪潮之巔》中有比較深入的闡述,而在Sun破產之前, Schwartz這位Sun的末代領袖,卻試圖通過博客來治理這麼大的公司,經常使用十餘種語言寫博客,實在是一位有意思的商界領袖。(還有那位喜好用推特治國的川普,也挺有意思的,嗯,川普和Schwartz應該是筆友。)

  ps:我是一位.NET開發者,在過去十年間,大概.NET是受Java衝擊最嚴重的開發技術吧,但是創造出Java如此優秀語言的Sun公司,究竟是什麼原因讓他被歷史的浪潮打翻的?真的只是因為CEO愛寫博客嗎?這是一個很有意思的故事,大家也可以去《浪潮之巔》中看看,正好《浪潮之巔》第四版新書也上市了,我已經買了一套了。哈哈。

【精選推薦文章】

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

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

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

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

上周熱點回顧(6.17-6.23)

熱點隨筆:

· 升維打擊,設計之道(Artech)
· 藉助FreeHttp為任意移動端web網頁添加vConsole調試(lulianqi15)
· 你為什麼有那麼多時間寫博客?(純潔的微笑)
· 看完此文,媽媽還會擔心你docker入不了門?(sullivan06)
· 我和 HelloGitHub(削微寒)
· 簡歷上如果出現過於高大上的項目,反而過猶不及:再論如何通過項目引出技術(hsm_computer)
· 程序員與醫生(道友留步`)
· 一個理想主義的程序員(沉默王二)
· 高考完?入門級的開源項目帶你開啟編程之旅(削微寒)
· 短信驗證碼“最佳實踐”(GUOKUN)
· .NET CORE下最快比較兩個文件內容是否相同的方法(WAKU)
· 從CLR GC到CoreCLR GC看.NET Core對雲原生的支持(艾心❤)

熱點新聞:

· “地震波還有61秒到達”,08年籌建的技術,在這次四川地震中立功了
· 微軟,奪回王位
· 我們從未見過它的真面目,直到一群科學家拍了張照片
· 華為自研SSD揭秘:國內唯一殺入全球TOP10
· 無需固態電池,一條假魚靠“血液”續航36小時
· 5G來了,需要更換SIM卡嗎?
· 屠呦呦團隊放“大招”:“青蒿素抗藥性”等研究獲新進展
· 羅永浩再談收購蘋果:還需要一點時間
· 國產最先進X86處理器KX-6000發布:8核3.0GHz 力壓酷睿i5
· 美國欲剝奪華為在美專利權,涉及至少 3195 件專利
· 四川發生強震 成都提前61秒收到預警
· 聯想 ThinkPad P 系列筆記本預裝 Ubuntu 系統

【精選推薦文章】

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

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

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

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

Express:模板引擎深入研究

深入源碼

首先,看下express模板默認配置。

  • view:模板引擎模塊,對應 require(‘./view’),結合 res.render(name) 更好了解些。下面會看下 view 模塊。
  • views:模板路徑,默認在 views 目錄下。
// default configuration
this.set('view', View);
this.set('views', resolve('views'));

騰訊IVWEB前端團隊招前端工程師,2年以上工作經驗,本科以上學歷,有意者可私信、留言,或者郵箱聯繫 2377488447@qq.com,JD可參考這裏

從實例出發

從官方腳手架生成的代碼出發,模板配置如下:

  • views:模板文件在 views 目錄下;
  • view engine:用jade這個模板引擎進行模板渲染;
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

假設此時有如下代碼調用,內部邏輯是如何實現的?

res.render('index');

res.render(view)

完整的 render 方法代碼如下:

/**
 * Render `view` with the given `options` and optional callback `fn`.
 * When a callback function is given a response will _not_ be made
 * automatically, otherwise a response of _200_ and _text/html_ is given.
 *
 * Options:
 *
 *  - `cache`     boolean hinting to the engine it should cache
 *  - `filename`  filename of the view being rendered
 *
 * @public
 */

res.render = function render(view, options, callback) {
  var app = this.req.app;
  var done = callback;
  var opts = options || {};
  var req = this.req;
  var self = this;

  // support callback function as second arg
  if (typeof options === 'function') {
    done = options;
    opts = {};
  }

  // merge res.locals
  opts._locals = self.locals;

  // default callback to respond
  done = done || function (err, str) {
    if (err) return req.next(err);
    self.send(str);
  };

  // render
  app.render(view, opts, done);
};

核心代碼就一句,調用了 app.render(view) 這個方法。

res.render = function (name, options, callback) {
  var app = this.req.app;
  app.render(view, opts, done);
};

app.render(view)

完整源碼如下:

/**
 * Render the given view `name` name with `options`
 * and a callback accepting an error and the
 * rendered template string.
 *
 * Example:
 *
 *    app.render('email', { name: 'Tobi' }, function(err, html){
 *      // ...
 *    })
 *
 * @param {String} name
 * @param {String|Function} options or fn
 * @param {Function} callback
 * @public
 */

app.render = function render(name, options, callback) {
  var cache = this.cache;
  var done = callback;
  var engines = this.engines;
  var opts = options;
  var renderOptions = {};
  var view;

  // support callback function as second arg
  if (typeof options === 'function') {
    done = options;
    opts = {};
  }

  // merge app.locals
  merge(renderOptions, this.locals);

  // merge options._locals
  if (opts._locals) {
    merge(renderOptions, opts._locals);
  }

  // merge options
  merge(renderOptions, opts);

  // set .cache unless explicitly provided
  if (renderOptions.cache == null) {
    renderOptions.cache = this.enabled('view cache');
  }

  // primed cache
  if (renderOptions.cache) {
    view = cache[name];
  }

  // view
  if (!view) {
    var View = this.get('view');

    view = new View(name, {
      defaultEngine: this.get('view engine'),
      root: this.get('views'),
      engines: engines
    });

    if (!view.path) {
      var dirs = Array.isArray(view.root) && view.root.length > 1
        ? 'directories "' + view.root.slice(0, -1).join('", "') + '" or "' + view.root[view.root.length - 1] + '"'
        : 'directory "' + view.root + '"'
      var err = new Error('Failed to lookup view "' + name + '" in views ' + dirs);
      err.view = view;
      return done(err);
    }

    // prime the cache
    if (renderOptions.cache) {
      cache[name] = view;
    }
  }

  // render
  tryRender(view, renderOptions, done);
};

源碼開頭有 cacheengines 兩個屬性,它們在 app.int() 階段就初始化了。

this.cache = {};
this.engines = {};

View模塊源碼

看下View模塊的源碼:

/**
 * Initialize a new `View` with the given `name`.
 *
 * Options:
 *
 *   - `defaultEngine` the default template engine name
 *   - `engines` template engine require() cache
 *   - `root` root path for view lookup
 *
 * @param {string} name
 * @param {object} options
 * @public
 */

function View(name, options) {
  var opts = options || {};

  this.defaultEngine = opts.defaultEngine;
  this.ext = extname(name);
  this.name = name;
  this.root = opts.root;

  if (!this.ext && !this.defaultEngine) {
    throw new Error('No default engine was specified and no extension was provided.');
  }

  var fileName = name;

  if (!this.ext) {
    // get extension from default engine name
    this.ext = this.defaultEngine[0] !== '.'
      ? '.' + this.defaultEngine
      : this.defaultEngine;

    fileName += this.ext;
  }

  if (!opts.engines[this.ext]) {
    // load engine
    opts.engines[this.ext] = require(this.ext.substr(1)).__express;
  }

  // store loaded engine
  this.engine = opts.engines[this.ext];

  // lookup path
  this.path = this.lookup(fileName);
}

核心概念:模板引擎

模板引擎大家不陌生了,關於express模板引擎的介紹可以參考官方文檔。

下面主要講下使用配置、選型等方面的內容。

可選的模版引擎

包括但不限於如下模板引擎

  • jade
  • ejs
  • dust.js
  • dot
  • mustache
  • handlerbar
  • nunjunks

配置說明

先看代碼。

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');

有兩個關於模版引擎的配置:

  1. views:模版文件放在哪裡,默認是在項目根目錄下。舉個例子:app.set('views', './views')
  2. view engine:使用什麼模版引擎,舉例:app.set('view engine', 'jade')

可以看到,默認是用jade做模版的。如果不想用jade怎麼辦呢?下面會提供一些模板引擎選擇的思路。

選擇標準

需要考慮兩點:實際業務需求、個人偏好。

首先考慮業務需求,需要支持以下幾點特性。

  • 支持模版繼承(extend)
  • 支持模版擴展(block)
  • 支持模版組合(include)
  • 支持預編譯

對比了下,jadenunjunks都滿足要求。個人更習慣nunjunks的風格,於是敲定。那麼,怎麼樣使用呢?

支持nunjucks

首先,安裝依賴

npm install --save nunjucks

然後,添加如下配置

var nunjucks = require('nunjucks');

nunjucks.configure('views', {
    autoescape: true,
    express: app
});

app.set('view engine', 'html');

看下views/layout.html

<!DOCTYPE html>
<html>
<head>
    <title>
        {% block title %}
            layout title
        {% endblock %}
    </title>
</head>
<body>
<h1>
    {% block appTitle %}
        layout app title
    {% endblock %}
</h1>
<p>正文</p>

</body>
</html>

看下views/index.html

{% extends "layout.html" %}
{% block title %}首頁{% endblock %}
{% block appTitle %}首頁{% endblock %}

開發模板引擎

通過app.engine(engineExt, engineFunc)來註冊模板引擎。其中

  • engineExt:模板文件後綴名。比如jade
  • engineFunc:模板引擎核心邏輯的定義,一個帶三個參數的函數(如下)
// filepath: 模板文件的路徑
// options:渲染模板所用的參數
// callback:渲染完成回調
app.engine(engineExt, function(filepath, options, callback){

    // 參數一:渲染過程的錯誤,如成功,則為null
    // 參數二:渲染出來的字符串
    return callback(null, 'Hello World');
});

比如下面例子,註冊模板引擎 + 修改配置一起,於是就可以愉快的使用後綴為tmpl的模板引擎了。

app.engine('tmpl', function(filepath, options, callback){

    // 參數一:渲染過程的錯誤,如成功,則為null
    // 參數二:渲染出來的字符串
    return callback(null, 'Hello World');
});
app.set('views', './views');
app.set('view engine', 'tmpl');

res.render(view [, locals] [, callback])

參數說明:

  • view:模板的路徑。
  • locals:對象類型。渲染模板時傳進去的本地變量。
  • callback:回調函數。如果聲明了的話,當渲染工作完成時被調用,參數為兩個,分別是錯誤(如果出錯的話)、渲染好的字符串。在這種情況下,response不會自動完成。當錯誤發生時,內部會自動調用 next(err)

view參數說明:

  • 可以是相對路徑(相對於views設置的目錄),或者絕對路徑;
  • 如果沒有聲明文件後綴,則以view engine設置為準;
  • 如果聲明了文件後綴,那麼Express會根據文件後綴,通過 require() 加載對應的模板引擎來完成渲染工作(通過模板引擎的 __express 方法完成渲染)。

locals參數說明:

locals.cache 啟動模板緩存。在生產環境中,模板緩存是默認啟用的。在開發環境,可以通過將 locals.cache 設置為true來啟用模板緩存。

例子:

// send the rendered view to the client
res.render('index');

// if a callback is specified, the rendered HTML string has to be sent explicitly
res.render('index', function(err, html) {
  res.send(html);
});

// pass a local variable to the view
res.render('user', { name: 'Tobi' }, function(err, html) {
  // ...
});

關於view cache

The local variable cache enables view caching. Set it to true, to cache the view during development; view caching is enabled in production by default.
render(view, opt, callback) 這個方法調用時,Express會根據 view 的值 ,進行如下操作

  1. 確定模板的路徑
  2. 根據模板的擴展性確定採用哪個渲染引擎
  3. 加載渲染引擎

重複調用render()方法,如果 cache === false 那麼上面的步驟每次都會重新做一遍;如果 cache === true,那麼上面的步驟會跳過;

關鍵源代碼:

if (renderOptions.cache) {
  view = cache[name];
}

此外,在 view.render(options, callback) 里,options 也會作為參數傳入this.engine(this.path, options, callback)。也就是說,渲染引擎(比如jade)也會讀取到options.cache這個配置。根據options.cache的值,渲染引擎內部也可能會進行緩存操作。(比如為true時,jade讀取模板後會緩存起來,如果為false,每次都會重新從文件系統讀取)

View.prototype.render = function render(options, callback) {
  debug('render "%s"', this.path);
  this.engine(this.path, options, callback);
};

備註:cache配置對渲染引擎的影響是不確定的,因此實際需要用到某個渲染引擎時,需確保對渲染引擎足夠了解。

以jade為例,在開發階段,NODE_ENV !== 'production',cahce默認是false。因此每次都會從文件系統讀取模板,再進行渲染。因此,在開發階段,可以動態修改模板內容來查看效果。

NODE_ENV === 'production' ,cache 默認是true,此時會緩存模板,提升性能。

混合使用多種模板引擎

根據對源碼的分析,實現很簡單。只要帶上文件擴展名,Express就會根據擴展名加載相應的模板引擎。比如:

  1. index.jade:加載引擎jade
  2. index.ejs:加載引擎ejss
// 混合使用多種模板引擎
var express = require('express');
var app = express();

app.get('/index.jade', function (req, res, next) {
  res.render('index.jade', {title: 'jade'});
});

app.get('/index.ejs', function (req, res, next) {
  res.render('index.ejs', {title: 'ejs'});
});

app.listen(3000);

同樣的模板引擎,不同的文件擴展名

比如模板引擎是jade,但是因為一些原因,擴展名需要採用.tpl

// 同樣的模板引擎,不同的擴展名
var express = require('express');
var app = express();

// 模板採用 tpl 擴展名
app.set('view engine', 'tpl');
// 對於以 tpl 擴展名結尾的模板,採用 jade 引擎
app.engine('tpl', require('jade').__express);

app.get('/index', function (req, res, next) {
  res.render('index', {title: 'tpl'});
});

app.listen(3000);

相關鏈接

Using template engines with Express
http://expressjs.com/en/guide/using-template-engines.html

res.render 方法使用說明
http://expressjs.com/en/4x/api.html#res.render

騰訊IVWEB前端團隊招前端工程師,2年以上工作經驗,本科以上學歷,有意者可私信、留言,或者郵箱聯繫 2377488447@qq.com,JD可參考這裏

【精選推薦文章】

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

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

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

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

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

[WPF自定義控件庫]排序、篩選以及高亮

1. 如何讓列表的內容更容易查找

假設有這麼一個列表(數據源在本地),由於內容太多,要查找到其中某個想要的數據會比較困難。要優化這個列表,無非就是排序、篩選和高亮。

改造過的結果如上。

2. 排序

在WPF中要實現數據排序的功能有很多種,例如用Linq,但這種場景的標準做法是使用CollectionViewSource。

CollectionViewSource是一種數據集合的代理類。它有兩個很重要的屬性:

  • Source 是數據源的集合;

  • View 是經過處理后的數據視圖。

看上去感覺是不是很像數據庫里的Table和View的關係?

在這個例子里使用CollectionViewSource排序的代碼如下:

private readonly CollectionViewSource _viewSource;

public HighlightSample()
{
    InitializeComponent();
    _viewSource = new CollectionViewSource
    {
        Source = Employee.AllExecutives
    };

    _viewSource.View.Culture = new System.Globalization.CultureInfo("zh-CN");
    _viewSource.View.SortDescriptions.Add(new SortDescription(nameof(Employee.FirstName), ListSortDirection.Ascending));
    EmployeeElement.ItemsSource = _viewSource.View;
}

這段代碼為CollectionViewSource的Source賦值后,把CollectionViewSource的View作為ListBox的數據源。其中SortDescriptions用於描述View的排序方式。如果包含中文,別忘記將Culture設置為zh-cn

至此排序的功能就實現了。文檔中還提到CollectionViewSource的其它信息:

您可以將集合視圖作為綁定源集合,可用於導航和显示集合中基於排序、 篩選和分組查詢,而無需操作基礎源集合本身的所有頂層。 如果Source實現INotifyCollectionChanged接口,所做的更改引起CollectionChanged事件傳播到View。

由於View不會更改Source,因此每個Source都可以有多個關聯的View。 使用View,可以通過不同方式显示相同數據。 例如,可能希望在頁面左側显示按優先級排序的任務,而在頁面右側显示按區域分組的任務。

3. 篩選

CollectionViewSource的View屬性類型為ICollectionView接口,它提供了Filter屬性用於實現數據的過濾。在這個例子里實現如下:

_viewSource.View.Filter = (obj) => (obj as Employee).DisplayName.ToLower().Contains(FilterElement.Text);

private void OnFilterTextChanged(object sender, TextChangedEventArgs e)
{
    if (_viewSource != null)
        _viewSource.View.Refresh();
}

這段代碼實現了當輸入框的文字改變時刷新View的功能。其中Refresh方法用於重新創建View,也就是刷新視圖。

ICollectionView還提供了一個DeferRefresh函數,這個函數用於進入延遲循環,該循環可用於將更改合併到視圖並延遲自動刷新,在需要多次操作並刷新數據量大的集合時可以用這個函數。

4. 高亮

<TextBox x:Name="FilterElement"
         TextChanged="OnFilterTextChanged"/>
<ListBox Name="EmployeeElement"
         Grid.Row="1"
         Height="200"
         Margin="0,8,0,0">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding DisplayName}"
                           kino:TextBlockService.HighlightText="{Binding ElementName=FilterElement,Path=Text}" />
            </StackPanel>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

UWP的高亮可以使用TextHighlighter這個類,實現起來很簡單。WPF中的高亮則是使用自定義的TextBlockService.HighlightText附加屬性聲明要高亮的文字,然後將TextBlock的Text替換為處理過的Inlines,使用方式如上。

private static void MarkHighlight(TextBlock target, string highlightText)
{
    var text = target.Text;
    target.Inlines.Clear();
    if (string.IsNullOrWhiteSpace(text))
        return;

    if (string.IsNullOrWhiteSpace(highlightText))
    {
        target.Inlines.Add(new Run { Text = text });
        return;
    }

    while (text.Length > 0)
    {
        var runText = string.Empty;
        var index = text.IndexOf(highlightText, StringComparison.InvariantCultureIgnoreCase);
        if (index > 0)
        {
            runText = text.Substring(0, index);
            target.Inlines.Add(new Run { Text = runText, Foreground = _noHighlightBrush });
        }
        else if (index == 0)
        {
            runText = text.Substring(0, highlightText.Length);
            target.Inlines.Add(new Run { Text = runText });
        }
        else if (index == -1)
        {
            runText = text;
            target.Inlines.Add(new Run { Text = runText, Foreground = _noHighlightBrush });
        }

        text = text.Substring(runText.Length);
    }
}

這是實現代碼。其實用Regex.Split代碼會好看很多,但懶得改了。
本來應該是高亮匹配的文字,但實際使用中發覺把未匹配的文字置灰更好看,就這樣實現了。

5. 結語

這篇文章介紹了使用CollectionViewSource實現的排序、篩選功能,以及使用附加屬性和Inlines實現高亮功能。

不過這樣實現的高亮功能有個問題:不能定義高亮(或者低亮)的顏色,不管在代碼中還是在XAML中。一種可行的方法是參考ToolTipService定義一大堆附加屬性,例如這樣:

<TextBox x:Name="FilterElement" 
         ToolTipService.ToolTip="Filter Text"
         ToolTipService.HorizontalOffset="10"
         ToolTipService.VerticalOffset="10"
         TextChanged="OnFilterTextChanged"/>

這種方式的缺點是這一大堆附加屬性會導致代碼變得很複雜,難以維護。ToolTipService還可以創建一個ToolTip類,把這個類設置為附加屬性的值:

<TextBox x:Name="FilterElement" 
         TextChanged="OnFilterTextChanged">
    <ToolTipService.ToolTip>
        <ToolTip Content="Filter Text"
                 HorizontalOffset="10" 
                 VerticalOffset="10"/>
    </ToolTipService.ToolTip>
</TextBox>

這種方式比較容易維護,但有人可能不明白ToolTipService.ToolTip屬性的值為什麼既可以是文本(或圖片等其它內容),又可以是ToolTip類型,XAML如何識別。關於這一點我在下一篇文章會講解,並且重新實現高亮的功能以支持Style等功能。

也可以參考SearchableTextBlock寫一個高亮的文本框,一了百了,但我希望通過這個有趣的功能多介紹幾種知識。

6. 參考

CollectionViewSource Class (System.Windows.Data) Microsoft Docs

TextBlock.Inlines Property (System.Windows.Controls) Microsoft Docs

A WPF Searchable TextBlock Control with Highlighting WPF

7. 源碼

TextBlockService.cs at master

【精選推薦文章】

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

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

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

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

Jest — ElasticSearch Java 客戶端

1. 介紹

任何使用過Elasticsearch的人都知道,使用基於rest的搜索API構建查詢可能是單調乏味且容易出錯的。

在本教程中,我們將研究Jest,一個用於Elasticsearch的HTTP Java客戶端。Elasticsearch提供了自己原生的Java客戶端,然而 Jest提供了更流暢的API和更容易使用的接口。

2. Maven 依賴

我們需要做的第一件事是導入Jest庫到我們的POM文件中:

<dependency> <groupId>io.searchbox</groupId> <artifactId>jest</artifactId> <version>6.3.1</version> </dependency> 

Jest的版本是遵循Elasticsearch的主版本號的。
這將確保客戶端和服務端之間的兼容性。

通過包含Jest依賴項,相應的[Elasticsearch庫](https://search.maven.org/search?q=g:org.elasticsearch a:elasticsearch)將被包含為傳遞依賴項。

3. 使用Jest Client

在本節中,我們將研究如何使用Jest client執行Elasticsearch中的常見任務。

要使用Jest client,我們只需使用 JestClientFactory 創建一個 JestClient 對象。這些對象的創建開銷很高,而且是線程安全的,因此我們將創建一個可以在整個應用程序中共享的單例實例:

public JestClient jestClient() { JestClientFactory factory = new JestClientFactory(); factory.setHttpClientConfig( new HttpClientConfig.Builder("http://localhost:9200") .multiThreaded(true) .defaultMaxTotalConnectionPerRoute(2) .maxTotalConnection(10) .build()); return factory.getObject(); } 

這裏將創建一個Jest client,該客戶端連接到本地運行的Elasticsearch。雖然這個連接示例很簡單,但是Jest還完全支持代理、SSL、身份驗證,甚至節點發現。

JestClient 類是通用類,只有少數公共方法。我們將使用的一個主要方法是execute,它接受Action接口的一個實例。Jest客戶端提供了幾個構建器類來幫助創建與Elasticsearch交互的不同操作。

所有Jest調用的結果都是JestResult的一個實例。 我們可以通過調用 issucceeded 方法來檢查是否成功。對於失敗的操作,我們可以調用GetErrorMessage方法來獲取更多詳細信息:

JestResult jestResult = jestClient.execute(new Delete.Builder("1").index("employees").build()); if (jestResult.isSucceeded()) { System.out.println("Success!"); } else { System.out.println("Error: " + jestResult.getErrorMessage()); } 

3.1. 管理索引

檢查索引是否存在,我們使用IndicatesExists操作:

JestResult result = jestClient.execute(new IndicesExists.Builder("employees").build()) 

創建一個索引,我們使用CreateIndex操作:

jestClient.execute(new CreateIndex.Builder("employees").build()); 

這將創建一個具有默認設置的索引。我們可以在創建索引時覆蓋特定的設置:

Map<String, Object> settings = new HashMap<>(); settings.put("number_of_shards", 11); settings.put("number_of_replicas", 2); jestClient.execute(new CreateIndex.Builder("employees").settings(settings).build()); 

使用ModifyAliases操作創建或更改別名也很簡單:

jestClient.execute(new ModifyAliases.Builder( new AddAliasMapping.Builder("employees", "e").build()).build()); jestClient.execute(new ModifyAliases.Builder( new RemoveAliasMapping.Builder("employees", "e").build()).build()); 

3.2. 創建文檔

Jest client使用索引操作類索引或創建新文檔變得容易。Elasticsearch中的文檔只是JSON數據,有多種方法可以將JSON數據傳遞給Jest client 進行索引。

對於這個例子,讓我們使用一個虛構的僱員文檔:

{ "name": "Michael Pratt", "title": "Java Developer", "skills": ["java", "spring", "elasticsearch"], "yearsOfService": 2 } 

表示JSON文檔的第一種方法是使用Java字符串。雖然我們可以手動創建JSON字符串,但我們必須注意正確的格式、大括號和轉義引號字符。因此,更容易的方式是使用一個JSON庫(如Jackson)來構建我們的JSON結構,然後將其轉換為字符串:

ObjectMapper mapper = new ObjectMapper(); JsonNode employeeJsonNode = mapper.createObjectNode() .put("name", "Michael Pratt") .put("title", "Java Developer") .put("yearsOfService", 2) .set("skills", mapper.createArrayNode() .add("java") .add("spring") .add("elasticsearch")); jestClient.execute(new Index.Builder(employeeJsonNode.toString()).index("employees").build()); 

我們還可以使用Java Map 來表示JSON數據,並將其傳遞給索引操作:

Map<String, Object> employeeHashMap = new LinkedHashMap<>(); employeeHashMap.put("name", "Michael Pratt"); employeeHashMap.put("title", "Java Developer"); employeeHashMap.put("yearsOfService", 2); employeeHashMap.put("skills", Arrays.asList("java", "spring", "elasticsearch")); jestClient.execute(new Index.Builder(employeeHashMap).index("employees").build()); 

最後,Jest client 可以接受表示要索引的文檔的任何POJO。假設我們有一個Employee類:

public class Employee { String name; String title; List<String> skills; int yearsOfService; } 

我們可以把這個類的一個實例直接傳遞給Index builder:

Employee employee = new Employee(); employee.setName("Michael Pratt"); employee.setTitle("Java Developer"); employee.setYearsOfService(2); employee.setSkills(Arrays.asList("java", "spring", "elasticsearch")); jestClient.execute(new Index.Builder(employee).index("employees").build()); 

3.3. 讀取文檔

使用Jest client從Elasticsearch訪問文檔有兩種主要方法。首先,如果我們知道文檔ID,我們可以使用get操作直接訪問它:

jestClient.execute(new Get.Builder("employees", "17").build()); 

要訪問返回的文檔,我們必須調用其中一個getSource方法。我們可以將結果作為原始JSON獲取,或者將其反序列化為DTO:

Employee getResult = jestClient.execute(new Get.Builder("employees", "1").build()) .getSourceAsObject(Employee.class); 

訪問文檔的其他方法是使用搜索查詢,這種方式在Jest中是通過搜索操作實現的。

Jest client 支持全部的 Elasticsearch query DSL。 與索引操作一樣,查詢被表示為JSON文檔,並且有多種執行搜索的方法。

首先,我們可以傳遞一個表示搜索查詢的JSON字符串。提醒一下,我們必須確保字符串是正確轉義的,並且是有效的JSON:

String search = "{" + " \"query\": {" + " \"bool\": {" + " \"must\": [" + " { \"match\": { \"name\": \"Michael Pratt\" }}" + " ]" + " }" + " }" + "}"; jestClient.execute(new Search.Builder(search).build()); 

與上面的索引操作一樣,我們可以使用Jackson之類的庫來構建JSON查詢字符串。此外,我們還可以使用原生的Elasticsearch查詢操作API。這樣做的一個缺點是,我們的應用程序必須依賴於完整的Elasticsearch庫。

我們可以使用 getSource 方法來訪問搜索操作中匹配的文檔。然而,Jest還提供了Hit類,它包裝了匹配的文檔並提供有關結果的元數據。 使用Hit類,我們可以訪問每個結果的附加元數據:得分、路由和解釋結果,舉幾個例子:

List<SearchResult.Hit<Employee, Void>> searchResults = jestClient.execute(new Search.Builder(search).build()) .getHits(Employee.class); searchResults.forEach(hit -> { System.out.println(String.format("Document %s has score %s", hit.id, hit.score)); }); 

3.4. 更新文檔

Jest為更新文檔提供了一個簡單的Update操作:

employee.setYearOfService(3); jestClient.execute(new Update.Builder(employee).index("employees").id("1").build()); 

它接受與我們前面看到的索引操作相同的JSON表示,這使得在兩個操作之間共享代碼變得很容易。

3.5. 刪除文檔

從索引中刪除文檔是使用Delete操作完成的。它只需要索引名和文檔ID:

jestClient.execute(new Delete.Builder("17") .index("employees") .build()); 

4. 批量操作

Jest client 同樣支持批量操作。 這意味着我們可以通過同時發送多個操作來節省時間和帶寬。

使用批量操作,我們可以將任意數量的請求組合成單個調用。我們甚至可以將不同類型的請求組合在一起:

jestClient.execute(new Bulk.Builder() .defaultIndex("employees") .addAction(new Index.Builder(employeeObject1).build()) .addAction(new Index.Builder(employeeObject2).build()) .addAction(new Delete.Builder("17").build()) .build()); 

5. 異步操作

Jest client 同樣支持異步操作。 這意味着我們可以使用非阻塞I/O執行上述任何操作。

要異步調用操作,只需使用客戶端的executeAsync方法:

jestClient.executeAsync( new Index.Builder(employeeObject1).build(), new JestResultHandler<JestResult>() { @Override public void completed(JestResult result) { // handle result } @Override public void failed(Exception ex) { // handle exception } }); 

注意,除了(本例中是索引)操作之外,異步流還需要一個JestResultHandler。當操作完成時,Jest client 將調用該對象。該接口有兩個方法—完成和失敗—分別允許處理操作的成功或失敗。

6. 結論

在本教程中,我們簡要介紹了Jest client,一個用於Elasticsearch的RESTful Java客戶端。雖然我們只介紹了它的一小部分功能,但很明顯Jest是一個健壯的Elasticsearch客戶端。它的流暢的構建器類和RESTful接口使其易於學習,並且它對Elasticsearch接口的完全支持使其成為原生客戶端的一個有力的替代方案。

和往常一樣,本教程中的所有代碼示例都在我們的Github頁面上。

原文:https://www.baeldung.com/elasticsearch-jest

作者:Michael Pratt

譯者:huowolf/

【精選推薦文章】

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

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

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

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

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銀行會根據處理結果進行回滾。

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

【精選推薦文章】

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

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

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

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

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