| 本文轉(zhuǎn)載自 Fastpay快付 互聯(lián)網(wǎng)平臺架構(gòu)日益成為互聯(lián)網(wǎng)發(fā)展的基石,對于 Java 開發(fā)者和架構(gòu)師而言,只有在了解架構(gòu)背后的原理后,才能寫出更高質(zhì)量的代碼,才能設(shè)計(jì)出更好的方案,才能在錯(cuò)綜復(fù)雜平臺架構(gòu)下產(chǎn)出價(jià)值,才能在各種場景下快速發(fā)現(xiàn)問題、快速定位問題、快速解決問題。 本場 Chat 會帶領(lǐng)大家從支付平臺架構(gòu)設(shè)計(jì)評審入手,講解設(shè)計(jì)評審的核心要點(diǎn),為讀者帶去現(xiàn)實(shí)中的案例,幫助讀者理解設(shè)計(jì)評審的重要性、核心要點(diǎn)和最佳實(shí)現(xiàn)。在這場 Chat 中將學(xué)到如下內(nèi)容: 
 揭秘支付系統(tǒng)中數(shù)據(jù)庫鎖的應(yīng)用實(shí)踐 鎖通常應(yīng)用在多個(gè)線程對一個(gè)共享資源進(jìn)行同時(shí)操作,用來保證操作的有序性和正確性的同步設(shè)施。在筆者看來,鎖的本質(zhì)其實(shí)是排隊(duì),不同的鎖排隊(duì)的空間和時(shí)間不同而已,例如,Java 的 Synchronized 的鎖是在應(yīng)用處理業(yè)務(wù)邏輯的時(shí)候在對象頭上進(jìn)行排隊(duì),數(shù)據(jù)庫的鎖是在數(shù)據(jù)庫上進(jìn)行數(shù)據(jù)庫操作的時(shí)候進(jìn)行排隊(duì),而分布式鎖是在處理業(yè)務(wù)邏輯的時(shí)候在一個(gè)公用的存儲服務(wù)上排隊(duì)。 樂觀鎖樂觀鎖是基于一種具有“樂觀”的思想,假設(shè)數(shù)據(jù)庫操作的并發(fā)非常少,多數(shù)情況下是沒有并發(fā)的,更新是按照順序執(zhí)行的,少有的一些并發(fā)通過版本控制來防止臟數(shù)據(jù)的產(chǎn)生。具體過程為,在操作數(shù)據(jù)庫數(shù)據(jù)的時(shí)候,對數(shù)據(jù)不加顯式的鎖,而是通過對數(shù)據(jù)的版本或者時(shí)間戳的對比來保證操作的有序性和正確性。一般是在更新數(shù)據(jù)之前,先獲取這條記錄的版本或者時(shí)間戳,在更新數(shù)據(jù)的時(shí)候,對比記錄的版本或者時(shí)間戳,如果版本或者時(shí)間戳一樣,則繼續(xù)更新,如果不一樣,則停止更新數(shù)據(jù)記錄,這說明數(shù)據(jù)已經(jīng)被其他線程或者其他客戶端更新過了。這時(shí)候需要獲取最新版本的數(shù)據(jù),進(jìn)行業(yè)務(wù)邏輯的操作,再次進(jìn)行更新。 其偽代碼如下。 int version = executeSql('select version from... where id = $id');// process business logicboolean succ = executeSql('update ... where id = $id and version = $version');if (!succ) {    // try again}樂觀鎖在同一時(shí)刻,只有一個(gè)更新請求會成功,其他的更新請求會失敗,因此,適用于并發(fā)不高的場景,通常是在傳統(tǒng)的行業(yè)里應(yīng)用在 ERP 系統(tǒng),防止多個(gè)操作員并發(fā)修改同一份數(shù)據(jù)。在某些互聯(lián)網(wǎng)公司里,使用樂觀鎖在失敗的時(shí)候再嘗試多次更新,導(dǎo)致并發(fā)量始終上不去,是一個(gè)反模式。而且這種模式是應(yīng)用層實(shí)現(xiàn)的,阻止不了其他程序?qū)?shù)據(jù)庫數(shù)據(jù)的直接更新。 悲觀鎖悲觀鎖是基于一種具有“悲觀”的思想,假設(shè)數(shù)據(jù)庫操作的并發(fā)很多,多數(shù)情況下是有并發(fā)的,在更新數(shù)據(jù)之前對數(shù)據(jù)上鎖,更新過程中防止任何其他的請求更新數(shù)據(jù)而產(chǎn)生臟數(shù)據(jù),更新完成之后,再釋放鎖,這里的鎖是數(shù)據(jù)庫級別的鎖。 通常使用數(shù)據(jù)庫的 for update 語句來實(shí)現(xiàn),代碼如下。 executeSql('select ... where id = $id for update');try {    // process business logic    commit();} catch (Exception e) {    rollback();}悲觀鎖是在數(shù)據(jù)庫引擎層次實(shí)現(xiàn)的,它能夠阻止所有的數(shù)據(jù)庫操作。但是為了更新一條數(shù)據(jù),需要提前對這條數(shù)據(jù)上鎖,直到這條數(shù)據(jù)處理完成,事務(wù)提交,別的請求才能更新數(shù)據(jù),因此,悲觀鎖的性能比較低下,但是由于它能夠保證更新數(shù)據(jù)的強(qiáng)一致性,是最安全的處理數(shù)據(jù)庫的方式,因此,有些賬戶、資金處理系統(tǒng)仍然使用這種方式,犧牲了性能,但是獲得了安全,規(guī)避了資金風(fēng)險(xiǎn)。 行級鎖不是所有更新操作都要加顯示鎖的,數(shù)據(jù)庫引擎本身有行級別的鎖,本身在更新行數(shù)據(jù)的時(shí)候是有同步和互斥操作的,我們可以利用這個(gè)行級別的鎖,控制鎖的時(shí)間窗口最小,一次來保證高并發(fā)的場景下更新數(shù)據(jù)的有效性。 行級鎖是數(shù)據(jù)庫引擎中對記錄更新的時(shí)候引擎本身上的鎖,是數(shù)據(jù)庫引擎的一部分,在數(shù)據(jù)庫引擎更新一條數(shù)據(jù)的時(shí)候,本身就會對記錄上鎖,這時(shí)候即使有多個(gè)請求更新,也不會產(chǎn)生臟數(shù)據(jù),行級鎖的粒度非常細(xì),上鎖的時(shí)間窗口也最少,只有更新數(shù)據(jù)記錄的那一刻,才會對記錄上鎖,因此,能大大減少數(shù)據(jù)庫操作的沖突,發(fā)生鎖沖突的概率最低,并發(fā)度也最高。 通常在扣減庫存的場景下使用行級鎖,這樣可以通過數(shù)據(jù)庫引擎本身對記錄加鎖的控制,保證數(shù)據(jù)庫更新的安全性,并且通過 where 語句的條件,保證庫存不會被減到0以下,也就是能夠有效的控制超賣的場景,如下代碼。 boolean result = executeSql('update ... set amount = amount - 1 where id = $id and amount > 1');if (result) {    // process sucessful logic} else {    // process failure logic}另外一種場景是在狀態(tài)轉(zhuǎn)換的時(shí)候使用行級鎖,例如交易引擎中,狀態(tài)只能從 init 流轉(zhuǎn)到 doing 狀態(tài),任何重復(fù)的從 init 到 doing 的流轉(zhuǎn),或者從 init 到 finished 等其他狀態(tài)的流轉(zhuǎn)都會失敗,代碼如下。 boolean result = executeSql('update ... set status = 'doing' where id = $id and status = 'init'');if (result) {    // process sucessful logic} else {    // process failure logic}行級鎖的并發(fā)性較高,性能是最好的,適用于高并發(fā)下扣減庫存和控制狀態(tài)流轉(zhuǎn)的方向的場景。 但是,有人說這種方法是不能保證冪等的,比如說,在扣減余額場景,多次提交可能會扣減多次,這確實(shí)是實(shí)際存在的,但是,我們是有應(yīng)對方案的,我們可以記錄扣減的歷史,如果有非冪等的場景出現(xiàn),通過記錄的扣減歷史來核對并矯正,這種方法也適用于賬務(wù)歷史等場景,代碼如下。 boolean result = executeSql('update ... set amount = amount - 1 where id = $id and amount > 1');if (result) {    int amount = executeSql('select amount ... where id = $id');    executeSql('insert into hist (pre_amount, post_amount) values ($amount + 1, $amount)');    // process successful logic} else {    // process failure logic}在支付平臺架構(gòu)設(shè)計(jì)評審中,通常對交易和支付系統(tǒng)的流水表的狀態(tài)流轉(zhuǎn)的控制、對賬戶系統(tǒng)的狀態(tài)控制,分賬和退款余額的更新等,都推薦使用行級鎖,而單獨(dú)使用樂觀鎖和悲觀鎖是不推薦的。 如何科學(xué)的設(shè)置線程池 線上高并發(fā)的服務(wù)就像默默的屹立在大江大河旁邊的大堤一樣,隨時(shí)準(zhǔn)備著應(yīng)對洪水帶來了沖擊,線上高并發(fā)服務(wù)的線程池導(dǎo)致的問題也頗多,例如:線程池漲滿、CPU 利用率高、服務(wù)線程掛死等,這些都是因?yàn)榫€程池的使用不當(dāng),或者沒有做好保護(hù)、降級的工作而導(dǎo)致的。 當(dāng)然,有些小伙伴是有保護(hù)線程池的想法的,但是,大家是不是有過這樣的經(jīng)驗(yàn)和印象,線程池的線程有時(shí)候設(shè)置多了性能低,設(shè)置少了還是性能低,到底應(yīng)該怎么設(shè)置線程池呢? 在經(jīng)歷過這些年對小伙伴的設(shè)計(jì)評審,得知小伙伴們都是憑經(jīng)驗(yàn)、憑直覺來設(shè)置線程池的線程數(shù)的,然后根據(jù)線上的情況調(diào)整數(shù)量多少,最后找到一個(gè)最合適的值,這是通過經(jīng)驗(yàn)的,有時(shí)候管用,有時(shí)候不管用,有時(shí)候雖然管用但是犧牲了很大的代價(jià)才找到最佳的設(shè)置數(shù)量。 其實(shí),線程池的設(shè)置是有據(jù)可依的,可以根據(jù)理論計(jì)算來設(shè)置的。 首先,我們看一下理想的情況,也就是所有要處理的任務(wù)都是計(jì)算任務(wù),這時(shí),線程數(shù)應(yīng)該等于 CPU 核數(shù),讓每個(gè) CPU 運(yùn)行一個(gè)線程,不需要線程切換,效率是最高的,當(dāng)然這是理想情況。 這種情況下,如果我們要達(dá)到某個(gè)數(shù)量的 QPS,我們使用如下的計(jì)算公式。 
 舉例說明,假設(shè)目標(biāo) QPS=100,任務(wù)實(shí)際處理時(shí)間 0.2s,100 * 0.2 = 20個(gè)線程,這里的20個(gè)線程必須對應(yīng)物理的20個(gè) CPU 核心,否則將不能達(dá)到預(yù)估的 QPS 指標(biāo)。 但實(shí)際上我們的線上服務(wù)除了做內(nèi)存計(jì)算,更多的是訪問數(shù)據(jù)庫、緩存和外部服務(wù),大部分的時(shí)間都是在等待 IO 任務(wù)。 如果 IO 任務(wù)較多,我們使用阿姆達(dá)爾定律來計(jì)算。 
 舉例說明,假設(shè)4核 CPU,每個(gè)任務(wù)中的 IO 任務(wù)占總?cè)蝿?wù)的80%,4 * (1 + 4) = 20個(gè)線程,這里的20個(gè)線程對應(yīng)的是4核心的 CPU。 線程中除了線程數(shù)的設(shè)置,線程隊(duì)列大小的設(shè)置也很重要,這也是可以通過理論計(jì)算得出,規(guī)則為按照目標(biāo)響應(yīng)時(shí)間計(jì)算隊(duì)列大小。 
 舉例說明,假設(shè)目標(biāo)相應(yīng)時(shí)間為0.4s,計(jì)算阻塞隊(duì)列的長度為20 * (0.4 / 0.2) = 40。 另外,在設(shè)置線程池?cái)?shù)量的時(shí)候,我們有如下最佳實(shí)踐。 
 緩存使用的最佳實(shí)踐 筆者在做設(shè)計(jì)評審的過程中,總結(jié)了一些開發(fā)人員在設(shè)計(jì)緩存系統(tǒng)時(shí)的優(yōu)秀實(shí)踐。 最佳實(shí)踐1緩存系統(tǒng)主要消耗的是服務(wù)器的內(nèi)存,因此,在使用緩存時(shí)必須先對應(yīng)用需要緩存的數(shù)據(jù)大小進(jìn)行評估,包括緩存的數(shù)據(jù)結(jié)構(gòu)、緩存大小、緩存數(shù)量、緩存的失效時(shí)間,然后根據(jù)業(yè)務(wù)情況自行推算未來一定時(shí)間的容量的使用情況,根據(jù)容量評估的結(jié)果來申請和分配緩存資源,否則會造成資源浪費(fèi)或者緩存空間不夠。 最佳實(shí)踐2建議將使用緩存的業(yè)務(wù)進(jìn)行分離,核心業(yè)務(wù)和非核心業(yè)務(wù)使用不同的緩存實(shí)例,從物理上進(jìn)行隔離,如果有條件,則請對每個(gè)業(yè)務(wù)使用單獨(dú)的實(shí)例或者集群,以減少應(yīng)用之間互相影響的可能性。筆者經(jīng)常聽說有的公司應(yīng)用了共享緩存,造成緩存數(shù)據(jù)被覆蓋,以及緩存數(shù)據(jù)錯(cuò)亂的線上事故。 最佳實(shí)踐3根據(jù)緩存實(shí)例提供的內(nèi)存大小推送應(yīng)用需要使用的緩存實(shí)例數(shù)量,一般在公司里會成立一個(gè)緩存管理的運(yùn)維團(tuán)隊(duì),這個(gè)團(tuán)隊(duì)會將緩存資源虛擬成多個(gè)相同內(nèi)存大小的緩存實(shí)例,例如,一個(gè)實(shí)例有 4GB 內(nèi)存,在應(yīng)用申請時(shí)可以按需申請足夠的實(shí)例數(shù)量來使用,對這樣的應(yīng)用需要進(jìn)行分片。這里需要注意,如果我們使用了 RDB 備份機(jī)制,每個(gè)實(shí)例使用 4GB 內(nèi)存,則我們的系統(tǒng)需要大于 8GB 內(nèi)存,因?yàn)?RDB 備份時(shí)使用 copy-on-write 機(jī)制,需要 fork 出一個(gè)子進(jìn)程,并且復(fù)制一份內(nèi)存,因此需要雙份的內(nèi)存存儲大小。 最佳實(shí)踐4緩存一般是用來加速數(shù)據(jù)庫的讀操作的,一般先訪問緩存,后訪問數(shù)據(jù)庫,所以緩存的超時(shí)時(shí)間的設(shè)置是很重要的。筆者曾經(jīng)在一家互聯(lián)網(wǎng)公司遇到過由于運(yùn)維操作失誤導(dǎo)致緩存超時(shí)設(shè)置得較長,從而拖垮服務(wù)的線程池,最終導(dǎo)致服務(wù)雪崩的情況。 最佳實(shí)踐5所有的緩存實(shí)例都需要添加監(jiān)控,這是非常重要的,我們需要對慢查詢、大對象、內(nèi)存使用情況做可靠的監(jiān)控。 最佳實(shí)踐6如果多個(gè)業(yè)務(wù)共享一個(gè)緩存實(shí)例,當(dāng)然我們不推薦這種情況,但是由于成本控制的原因,這種情況經(jīng)常出現(xiàn),我們需要通過規(guī)范來限制各個(gè)應(yīng)用使用的 key 一定要有唯一的前綴,并進(jìn)行隔離設(shè)計(jì),避免緩存互相覆蓋的問題產(chǎn)生。 最佳實(shí)踐7任何緩存的 key 都必須設(shè)定緩存失效時(shí)間,且失效時(shí)間不能集中在某一點(diǎn),否則會導(dǎo)致緩存占滿內(nèi)存或者緩存穿透。 最佳實(shí)踐8低頻訪問的數(shù)據(jù)不要放在緩存中,如我們前面所說的,我們使用緩存的主要目的是提高讀取性能,曾經(jīng)有個(gè)小伙伴設(shè)計(jì)了一套定時(shí)的批處理系統(tǒng),由于批處理系統(tǒng)需要對一個(gè)大的數(shù)據(jù)模型進(jìn)行計(jì)算,所以該小伙伴把這個(gè)數(shù)據(jù)模型保存在每個(gè)節(jié)點(diǎn)的本地緩存中,并通過消息隊(duì)列接收更新的消息來維護(hù)本地緩存中模型的實(shí)時(shí)性,但是這個(gè)模型每個(gè)月只用了一次,所以這樣使用緩存是很浪費(fèi)的,既然是批處理任務(wù),就需要把任務(wù)進(jìn)行分割,進(jìn)行批量處理,采用分而治之、逐步計(jì)算的方法,得出最終的結(jié)果即可。 最佳實(shí)踐9緩存的數(shù)據(jù)不易過大,尤其是 Redis,因?yàn)?Redis 使用的是單線程模型,單個(gè)緩存 key 的數(shù)據(jù)過大時(shí),會阻塞其他請求的處理。 最佳實(shí)踐10對于存儲較多 value 的 key,盡量不要使用 HGETALL 等集合操作,該操作會造成請求阻塞,影響其他應(yīng)用的訪問。 最佳實(shí)踐11緩存一般用于交易系統(tǒng)中加速查詢的場景,有大量的更新數(shù)據(jù)時(shí),尤其是批量處理,請使用批量模式,但是這種場景較少。 最佳實(shí)踐12如果對性能的要求不是非常高,則盡量使用分布式緩存,而不要使用本地緩存,因?yàn)楸镜鼐彺嬖诜?wù)的各個(gè)節(jié)點(diǎn)之間復(fù)制,在某一時(shí)刻副本之間是不一致的,如果這個(gè)緩存代表的是開關(guān),而且分布式系統(tǒng)中的請求有可能會重復(fù),就會導(dǎo)致重復(fù)的請求走到兩個(gè)節(jié)點(diǎn),一個(gè)節(jié)點(diǎn)的開關(guān)是開,一個(gè)節(jié)點(diǎn)的開關(guān)是關(guān),如果請求處理沒有做到冪等,就會造成處理重復(fù),在嚴(yán)重情況下會造成資金損失。 最佳實(shí)踐13寫緩存時(shí)一定寫入完全正確的數(shù)據(jù),如果緩存數(shù)據(jù)的一部分有效,一部分無效,則寧可放棄緩存,也不要把部分?jǐn)?shù)據(jù)寫入緩存,否則會造成空指針、程序異常等。 最佳實(shí)踐14在通常情況下,讀的順序是先緩存,后數(shù)據(jù)庫;寫的順序是先數(shù)據(jù)庫,后緩存。 最佳實(shí)踐15當(dāng)使用本地緩存(如 Ehcache)時(shí),一定要嚴(yán)格控制緩存對象的個(gè)數(shù)及生命周期。由于 JVM 的特性,過多的緩存對象會極大影響 JVM 的性能,甚至導(dǎo)致內(nèi)存溢出等問題出現(xiàn)。 最佳實(shí)踐16在使用緩存時(shí),一定要有降級處理,尤其是對關(guān)鍵的業(yè)務(wù)環(huán)節(jié),緩存有問題或者失效時(shí)也要能回源到數(shù)據(jù)庫進(jìn)行處理。 關(guān)于緩存使用的最佳實(shí)踐和線上案例,請參考《可伸縮服務(wù)架構(gòu):框架與中間件》一書的第4章的內(nèi)容,預(yù)計(jì)在2018年3月份上市。 數(shù)據(jù)庫設(shè)計(jì)要點(diǎn) 索引提起數(shù)據(jù)庫的設(shè)計(jì)要點(diǎn),我們首先要說的就是數(shù)據(jù)庫索引的使用,在線上的服務(wù)中,任何數(shù)據(jù)庫的查詢都要走索引,這個(gè)是底線,不能因?yàn)閿?shù)據(jù)量暫時(shí)較小就不使用索引,久而久之可能數(shù)據(jù)量增大就導(dǎo)致了性能問題,一般每個(gè)開發(fā)者都有建立索引和使用索引的意識,然而,問題出現(xiàn)在開發(fā)者使用索引的方法上。我們要保證建立的索引的有效性,一定要確保線上的查詢最后走到了索引,曾經(jīng)就出現(xiàn)過這樣的一個(gè)低級錯(cuò)誤,某個(gè)場景需要根據(jù) A、B、C 三個(gè)字段聯(lián)合查詢,開發(fā)者分別在 A、B 和 C 上建立了3個(gè)索引,看似也符合規(guī)范,但是實(shí)際上只用了 A 這個(gè)索引,B 和 C 的都沒有用上,后來由于產(chǎn)生了性能問題,代碼走查的時(shí)候才發(fā)現(xiàn)。 我們建議每個(gè)開發(fā)者對使用的 SQL 都要查看執(zhí)行計(jì)劃,另外,SQL 和索引要經(jīng)過 DBA 的審閱才能上線。 另外,對于一般的數(shù)據(jù)庫,>=、BETWEEN、IN、LIKE 等都可以走索引,而 NOT IN 不能走索引,如果匹配的字符以 % 開頭,是不能走索引的,這些必須記住了。 范圍查詢任何針對數(shù)據(jù)庫的范圍查詢,都要有最大結(jié)果集條數(shù)的限制,然后進(jìn)行分頁處理,不能因?yàn)闀簳r(shí)數(shù)據(jù)量小而采用開發(fā)式的 SQL 語句,如果這樣的話,在數(shù)據(jù)上量以后,會導(dǎo)致結(jié)果集太大,而讓應(yīng)用 OOM。 下面是主流數(shù)據(jù)庫限制結(jié)果集大小的方法。 DB2FETCH FIRST 100 ROWS ONLYSELECT id FROM( SELECT ROW_NUMBER() OVER() AS num,id FROM TABLE ) A WHERE A.num>=1 AND A.num<=>=>100MySQLlimit 1, 100OraclerownumSchema 變更對于數(shù)據(jù)庫的 Schema 變更,我們推薦只能增加字段,而不要修改字段,也不要?jiǎng)h除字段,修改和刪除字段的風(fēng)險(xiǎn)太高了,尤其是在應(yīng)用比較復(fù)雜,數(shù)據(jù)庫和應(yīng)用的設(shè)計(jì)都是做加法加上來的,對于使用數(shù)據(jù)庫的應(yīng)用了解不清楚,不要輕易更改原有的數(shù)據(jù)結(jié)構(gòu),修改字段就有可能導(dǎo)致代碼和數(shù)據(jù)庫不兼容的情況。 即使是只允許添加字段,我們也做如下的規(guī)定。 
 要盡量讓新老代碼和新老數(shù)據(jù)庫 Schema 完全兼容,這在數(shù)據(jù)庫升級前、中、后都不會產(chǎn)生問題。 字段枚舉值的增加,或者數(shù)據(jù)庫字段的含義、格式、限制的改變,必須考慮準(zhǔn)生產(chǎn)和線上導(dǎo)致的不一致的行為或者上線過程中新老版本的不一致的行為。曾經(jīng)就出現(xiàn)過,版本更新的時(shí)候增加了枚舉值,由于 Boss 后臺先上線,產(chǎn)生了新的枚舉值,結(jié)果交易程序沒有更新,不認(rèn)識新的枚舉值就出現(xiàn)了處理異常,因此枚舉值要慎用。 事務(wù)經(jīng)常會出現(xiàn)在數(shù)據(jù)庫事務(wù)中調(diào)用遠(yuǎn)程服務(wù),由于遠(yuǎn)程服務(wù)超時(shí)而拉長事務(wù),導(dǎo)致數(shù)據(jù)庫癱瘓的情況,因此,在事務(wù)處理過程中,禁止執(zhí)行可能產(chǎn)生線程阻塞的調(diào)用,例如:鎖等待、遠(yuǎn)程調(diào)用等。 另外,事務(wù)要盡可能保持短事務(wù),一個(gè)事務(wù)中不要有太多的操作,或者做太多的事情,長時(shí)間操作事務(wù)會影響或堵塞其他的請求,累積可造成數(shù)據(jù)庫故障,同一事務(wù)中大量的數(shù)據(jù)操作會引起鎖的范圍和影響擴(kuò)大,易造成數(shù)據(jù)庫的其他操作阻塞而導(dǎo)致短暫的不可用。 因此,如果業(yè)務(wù)允許,要盡可能用短事務(wù)來代替長事務(wù),降低事務(wù)執(zhí)行時(shí)間,減少鎖的時(shí)長,使用最終一致性來保證數(shù)據(jù)的一致性原則。 我們推薦下圖中的這種結(jié)構(gòu)。 一定不能使用如下圖中的這種結(jié)構(gòu)。 SQL 安全所有的 SQL 必須使用參數(shù)化的 SQL,防止 SQL 注入,這是一條不能妥協(xié)的底線原則。 一行代碼引起的“血案” 在做支付平臺的設(shè)計(jì)評審的時(shí)候,我們一定要格外仔細(xì),因?yàn)橐徊蛔⒁饪赡芫蜁霈F(xiàn)問題,甚至導(dǎo)致資金損失,筆者就經(jīng)歷一次增加一行打印日志的代碼導(dǎo)致的“血案”。 在一次查問題的過程中,發(fā)現(xiàn)缺少一個(gè)日志,于是,增加了一行日志。 log.info(... + obj); 很不巧,上線以后應(yīng)用就全面出現(xiàn)問題,交易出現(xiàn)失敗,查看代碼發(fā)現(xiàn)不時(shí)的有 NullPointerException,分析代碼發(fā)現(xiàn),出現(xiàn) NullPointerException 的代碼在 obj.toString() 方法里。 object.toString() 方法代碼如下所示。 private Object fld1; ......public String toString() {    return ... + this.fld1;}我們看見,在 obj.toString() 方法里面,直接使用了本地的變量 fld1,由于返回值是 String 類型,所以,Java 會試圖將 fld1 轉(zhuǎn)化成字符串,但是這個(gè)時(shí)候發(fā)生了 NullPointerException,那么,fld1就一定為 null,查明原因發(fā)現(xiàn),這個(gè)對象是從緩存中反序列化而來的,反序列化的時(shí)候這個(gè)字段就為 null。 因此,我們看到線上的代碼和環(huán)境是十分復(fù)雜的,在做設(shè)計(jì)評審的時(shí)候,一定要考慮到所有的情況,盡可能的將影響想得全面些,充分的降低代碼變更帶來的降低可用性的風(fēng)險(xiǎn)。 冪等和防重 冪等和防重雖然說起來挺復(fù)雜,但是實(shí)現(xiàn)起來很簡單,這也就應(yīng)了筆者的一句話:凡是能夠有效解決問題的方法都是看起來很挫的方法”。 冪等是一個(gè)特性,一個(gè)操作執(zhí)行多次,產(chǎn)生的結(jié)果是一樣的,就成為冪等,用數(shù)學(xué)公式表達(dá)如下。 
 對于某些業(yè)務(wù)具有的特點(diǎn),操作本身就是冪等的,例如:刪除一個(gè)資源、增加一個(gè)資源、獲得一個(gè)資源等。 防重是實(shí)現(xiàn)冪等的一種方法,防重有多種方法。 
 實(shí)現(xiàn)分布式任務(wù)調(diào)度的多種方法 使用成熟的框架可以使用成熟的開源分布式任務(wù)調(diào)用系統(tǒng),例如 TBSchedule、ElasticJob 等等。 詳細(xì)內(nèi)容,請參考《可伸縮服務(wù)架構(gòu):框架與中間件》的第6章的內(nèi)容。 代碼自行實(shí)現(xiàn)如果不喜歡使用成熟的框架,喜歡重復(fù)發(fā)明輪子,或者平臺有要求,不準(zhǔn)引入外部的開源項(xiàng)目,那么這個(gè)時(shí)候就是我們大顯身手的時(shí)候了,我們可以自己開發(fā)一套分布式任務(wù)調(diào)度系統(tǒng)。 其實(shí),分布式任務(wù)調(diào)度系統(tǒng)的核心就是任務(wù)的搶占,這和操作系統(tǒng)的任務(wù)調(diào)度類似,只不過應(yīng)用的場景不同而已,操作系統(tǒng)處理各個(gè)應(yīng)用進(jìn)程提交的任務(wù),而我們的分布式任務(wù)調(diào)度系統(tǒng)處理服務(wù)化系統(tǒng)中的后臺定時(shí)任務(wù)。 假設(shè),我們有4個(gè)后臺定時(shí)的服務(wù)節(jié)點(diǎn),以及4個(gè)任務(wù)存儲在數(shù)據(jù)庫的任務(wù)表中,如下圖所示,所有的任務(wù)都處于空閑狀態(tài),擁有者為空,4臺服務(wù)器都沒有工作可做。 到了某個(gè)時(shí)間點(diǎn),激活服務(wù)節(jié)點(diǎn)的定時(shí)任務(wù),服務(wù)節(jié)點(diǎn)開始搶占任務(wù),搶占任務(wù)需要更新數(shù)據(jù)庫里面的記錄狀態(tài)字段和擁有者,一般會使用數(shù)據(jù)庫的行級別鎖,代碼如下。 boolean result = executeSql('update ... set status = 'occupied' and owner = $node_no where id = $id and status = 'FREE' limit 1');if (result) {    Task t = executeSql('select ... where status = 'occupied' and owner = $node_no');    // process task t    executeSql('update ... set status = 'finished' and owner = null where id = $t.id and status = 'occupied');} 假設(shè)服務(wù)節(jié)點(diǎn)1搶占了任務(wù)號1,服務(wù)節(jié)點(diǎn)2搶占了任務(wù)號2,服務(wù)節(jié)點(diǎn)3搶占了任務(wù)號3,服務(wù)節(jié)點(diǎn)4搶占了任務(wù)號4,如下圖所示,這樣各自開始處理自己的任務(wù),處理后,將任務(wù)狀態(tài)設(shè)置成 finished,其他服務(wù)節(jié)點(diǎn)就不會搶占這個(gè)任務(wù)了。 當(dāng)然,這里描述的只是核心思想,具體實(shí)現(xiàn)的時(shí)候需要詳細(xì)的設(shè)計(jì),要考慮到任務(wù)如何調(diào)度、任務(wù)超時(shí)如何處理等等。 利用 Dubbo 服務(wù)化或者具有負(fù)載均衡的服務(wù)化平臺來實(shí)現(xiàn)假如說平臺規(guī)定不能使用第三方開源組件,自己開發(fā)又比較耗時(shí)耗力,那么還有一種辦法,這種辦法雖然看起來不是最佳的,但是能夠幫助你快速實(shí)現(xiàn)任務(wù)的分片。 我們可以借助 Dubbo 服務(wù)化或者具有負(fù)載均衡的服務(wù)來實(shí)現(xiàn),我們在服務(wù)節(jié)點(diǎn)上開發(fā)兩個(gè)服務(wù),一個(gè)總控服務(wù),用來接受分布式定時(shí)的觸發(fā)事件,總控服務(wù)從數(shù)據(jù)庫里面撈取任務(wù),然后分發(fā)任務(wù),分發(fā)任務(wù)利用 Dubbo 服務(wù)化或者具有負(fù)載均衡的服務(wù)化平臺來實(shí)現(xiàn),也就是調(diào)用服務(wù)節(jié)點(diǎn)的任務(wù)處理服務(wù),通過服務(wù)化的負(fù)載均衡來實(shí)現(xiàn)。 例如,下圖中分布式定時(shí)調(diào)用服務(wù)節(jié)點(diǎn)2的主控服務(wù),主控服務(wù)從數(shù)據(jù)庫里面撈取任務(wù),并且分成4個(gè)分片,然后通過服務(wù)化調(diào)用任務(wù)處理接口,由于服務(wù)化具有負(fù)載均衡的功能,因此,4個(gè)分片會均衡的分布在服務(wù)節(jié)點(diǎn)1、服務(wù)節(jié)點(diǎn)2、服務(wù)節(jié)點(diǎn)3、服務(wù)節(jié)點(diǎn)4上。 當(dāng)然,這種方法需要把后臺的定時(shí)任務(wù)與前臺的服務(wù)相互隔離,不能影響正常的線上服務(wù)是底線。 | 
|  |