一年前一個偶然的機會參與了公司的一個重點項目,需要長時間出差,開發團隊規模在20人左右,而且時間緊迫。在異地,少了公司技術團隊的支持,遠程溝通不方便,很多事情都顯得比較困難,碰到問題往往需要自己摸索,自己解決。有句話說,一個開發團隊有的時候就像一台發動機,只要啟動起來,就能有成果和產出。但如果方向存在偏差,發動機越跑越遠,可能收不住腳,最終會導致項目失控。很慶幸,我們這個項目順利上線,順利完成電商大促,以及最后順利交付。但中間的存在一些風險點以及經驗教訓還是值得拿出來總結一下。
通常開發經理或架構師會早於開發人員介入項目,了解項目的需求,系統分析,做相關的技術選型,制定開發計划與開發規范。

在制定技術規格說明階段,開發經理或架構師要協調起所有的開發人員,指定相關的技術規范與開發人員保持溝通,讓開發人員理解他們負責的模塊或者子系統,確保開發人員能夠按照架構意圖實現各項功能。
1 基本編碼規范
這個基本上每個公司都有一份這樣的文檔(如果沒有你基本上可以考慮job-hopping),這個文檔一般跟項目無關,比如命名規范,注釋規范,SQL規范等等。另外,要統一jdk,包括本地開發環境、服務器環境;定好項目名,包名,數據庫名,表名,以及是否每個表需要通用字段(如version樂觀鎖版本號)等等。
這里重點強調2個地方,工程規范和包名目錄規范。
工程規范:要明確定義每個工程模塊的邊界,尤其是在分布式系統中,這一點顯得尤為重要,開發人員要對框架的層次結構非常理解,比如什么時候該定義DTO,什么時候該定義Domain。這個如果沒有搞清楚,在項目的整個生命周期里面,項目的體積越來越龐大,這個問題一旦暴露出來簡直就是災難。
包名目錄規范:包目錄規范要盡可能從頂層開始,開發人員的包目錄權限要盡可能低,簡單的說就是盡可能讓開發人員少去建包,這一點很像日本的外包項目,目的就是統一規范。另外一點,要避免兩個工程模塊出現相同的包路徑,這樣在引用的時候極有可能出現沖突。
2 定義好組件的邊界和職責
系統分解之后,要定義好組件(子系統或者模塊)的邊界和職責,這個是項目初期開發人員最關心的問題,如果這個沒有定義清楚,后面系統就要面臨重構的危險。
比如基礎數據,並不是所有基礎資料、配置信息都放到基礎數據中,只有跨系統、跨服務、模塊的基礎資料、配置信息才屬於基礎數據管理范疇;另外基礎數據服務的接口需要具備一定的通用性,盡可能減少針對某個系統、服務、模塊開放特殊接口;並且不允許基礎數據服務依賴上層服務。
3 項目版本定義
項目初期,除了一些基礎的模塊,比如工具類等公共模塊可能被打包成Release版本,其他的一般都是SNAPSHOT版本,當項目陸陸續續上線之后,比如分布式系統中RPC調用要給客戶端系統提供jar包引用,版本控制的作用就會凸顯出來。這里推薦一批文章《語義化版本2.0.0》,是關於語義化的版本規范,這個規范是由 Gravatars 創辦者兼 GitHub 共同創辦者 Tom Preston-Werner 所建立。
4 svn代碼管理與發布規范
根據項目的開發模型,定好代碼的主干(trunk),分支(branches),基線(tags)的關系,以及相關環境的發布流程規范。
5 自動構建
在分布式系統開發中,沒有自動構建簡直不敢想象,往往一個項目有很多個子項目部署在幾十台甚至幾百台服務器上。如果沒有自動構建,開發人員有可能要花費大量的時間在服務打包發布上,一旦需要馬上發布一個測試bug,都需要很長一段時間,這會另整個團隊精疲力竭。
6 代碼日志規范
項目的日志在生產環境上禁止將日志輸出到Console中。項目代碼中都需要通過Logger對象來輸出日志和異常,禁止使用system.out.println等方法進行輸出,禁止使用e.printStackTrace來輸出異常。日志需要通過日期、大小兩個維度來分割文件,避免日志文件過大無法打開。生產環境的日志級別禁止開啟DEBUG,如果排查確實需要則可以針對特定類打開而不能將整個環境設置為DEBUG,可能會造成系統因為文件鎖卡死。
7 統一異常處理
分布式業務處理系統,系統中存在大量的跨服務調用,並且需要對不同類型的異常做不同級別的處理,處理的方式需要隨着系統的不斷擴展而適應不同類型的異常處理,並且做到跨服務的異常統一定義和快速定位跨服務的異常發生的源頭和原因。一般通過定義全系統統一的異常編碼,並定義其產生的原因,並需要達到識別系統的目的。
8 提供批量更新方法
批量更新必須使用提供的統一批量更新方法,對性能有很大的提升,如果有非常特殊的場景需求無法使用則必須要經過評審,否則這有可能成為一個性能瓶頸。
9 盡早提供基礎服務
如短信服務,郵件服務,這些基礎服務要優先安排開發,因為幾乎每個模塊都有可能涉及到。
10 規范多線程寫法
如果系統中有用到多線程,不要隨意開辟線程,盡可能使用統一的線程池,並封裝公共的調用方法以及返回結果。
11 規范事務的處理
11.1 避免產生一個較長時間鎖定多行數據的事務,盡可能將事務拆解或改為異步。
11.2 分布式事務
在項目盡可能不通過數據庫層面的分布式事務來實現數據的一致性而是通過異步、補償、冪等等方式來實現數據的最終一致性。
l 冪等性:
任何對外提供的服務入口方法都必須實現冪等性,重復使用同樣的參數調用同一方法時總能獲得同樣的結果,而不會造成重復的處理。
查詢服務:查詢本身就是冪等的,所以不需要做額外的處理。
新增服務:通過唯一性約束來控制數據的唯一性避免重復寫入數據
更新服務:通過樂觀鎖來控制數據的冪等性
刪除服務:刪除本身也是冪等的,所以不需要做額外的處理
l 異步消息處理:
跨系統進行事務處理時盡可能的使用異步消息處理來進行,先將數據保存為一個中間狀態,並將消息寫入MQ,由下游系統訂閱處理。下游系統處理完之后再將結果反饋給上游更新狀態。MQ中寫入的數據正常情況下不要使用完整數據,會造成MQ的IO壓力很高而且數據可能是已經過期的,並且會將隊列變成專用而不是通用,可能無法被其他服務訂閱。
l 補償
MQ處理時可能存在消息丟失、故障等問題丟失數據導致流程中斷,所以需要補償措施來保障流程不會長時間中斷。由一個分布式事務的最上游系統提供一個定時補償措施檢測長時間未完成事務的流程,並重新觸發這個流程(重新寫入MQ)。
12 確定基礎數據緩存方案
緩存往往被忽視,等到項目后期碰到性能瓶頸的時候才發現需要重構,那個時候的代價是很大的,所以需要盡早的定好緩存方案,是用ehcache? guava? redis?這個要確定下來並讓開發人員實施下去。
13 規范緩存環境使用
memcached/redis緩存環境規划,要定義好數據結構使用規范,比如不能讓所有人都用key/value這種方式來存儲,最終導致緩存環境臟亂。
14 分布式文件存儲
盡早確定是在物理上做共享磁盤?還是使用分布式的文件系統?
15 單元測試
編寫單元測試要成為一種習慣,另外,單元測試應該是沒有副作用的。定義良好的單元測試在運行多次的情況下,如果沒有其他條件發生變化,那么每一次都應該產生完全相同的結果。比如說,在數據庫中通常會插入一些假數據,然后在測試中驗證這些數據,這種方式的測試不可靠,因為數據庫可能發生變化。可以在單元測試過程中使用內存數據庫或者每次單元測試的數據都是自動生產最后自動刪除的。
許多同事怕寫單元測試的一個主要原因就是依賴太多(遠程服務調用、redis、webservice等),如果一個服務因為種種原因掛掉了,那么這個測試就會失敗。要解決這個痛點,可以引入mock對象可以滿足這些條件的需求,而Mockito正是這樣的一個框架,用Mockito來模擬相關行為,不用費力去准備各種依賴環境,這時只需專注於業務邏輯即可。
