網際網路架構實踐心得:業務程式碼究竟難不難寫?

java人生發表於2019-02-18

最近我一直在思考幾個問題:


  • 業務程式碼究竟難不難寫?

  • 一直開發業務程式碼是不是完全學不到東西?

  • 5年+開發經驗的老程式設計師的價值在哪裡?

  • 如何通過面試來區分業務程式碼開發的水平?


其實,這幾個問題或多或少是相互關聯的。有的時候大家也會自嘲說,“程式設計師接手的程式碼永遠是爛攤子,然後自己繼續在這個爛攤子上產出程式碼,留給又一波後人接手”。十幾年來經歷過十來個公司,我看了不少差的程式碼,也看了不少好的程式碼,自己產出過垃圾程式碼,也帶領團隊實現過一些自認為不錯的程式碼。


你可能會說,業務程式碼就是增刪改查,和框架程式碼的難度不能比,完全是機械勞動,其實我覺得不完全是這樣,甚至完全不是這樣,我個人認為寫出能跑的業務程式碼不難,但要寫出好的業務程式碼其實是挺難的,更重要的是如果系統設計的足夠好,在很長一段時間內系統的可維護性是可控的,只需要簡單擴充套件即可,如果基礎打的不夠好,那麼專案可能就是一次性專案,下面我列出業務系統我關注的一些點,你想想是不是有道理。

如果你對技術提升很感興趣,可以加入Java高階技術來交流學習:856443934,裡面都是同行,有資源分享和技術進階思維導圖,其中:(分散式架構、高可擴充套件、高效能、高並 發、Jvm效能調優、Spring,MyBatis,Nginx原始碼分析,Redis,ActiveMQ、、Mycat、Netty、Kafka、Mysql 、Zookeeper、Tomcat、Docker、Dubbo、Nginx)。歡迎一到五年的工程師加入,合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!


標準化


標準的專案結構

我自己非常注重搭建專案結構的起步過程,模組的劃分、目錄(包)的命名,我覺得非常重要,如果做的足夠好,別人匯入專案後可能只需要10分鐘就可以大概瞭解結構了。


1、有些名詞是約定俗成的,大家一眼就能看出是啥東西的,比如:controllers、services、configs、utils、commons,還有jobs。比較重要的是確定先進行分類再分業務,還是先分業務再分類,在程式碼裡混用這兩種風格的結構就會很混亂:


  • controllers

    • order

    • user

  • jobs

    • order

    • user

  • order

    • services

    • mappers

  • user

    • services

    • mappers


對於直筒的三層架構的純資料表驅動的程式碼,我建議第一層是分類,第二層是業務功能:看一眼controllers目錄我們知道專案對外的Api能力,再看一眼services目錄我們知道專案的邏輯複雜度,最後再看一眼mappers目錄我們知道專案的表結構。


對於有一些專案,不一定每一個邏輯都涉及到三層架構,資料流量比較複雜,我建議是按照業務功能先來分,下一層視情況也不一定完全是需要按照元件型別分二級目錄,也可以是按功能來分:


  • core

    • storage

    • service

  • dispatcher

    • engine

    • context

  • callback

    • gateway

    • handler

  • notification

    • sms

    • push


對於這種目錄結構一眼望去就能知道大概專案資料流和架構了,core對內,dispatcher做分發的感覺,callback是外部來的回撥資料,notification是通知外部的資料流。這種資料流向複雜的專案,使用這種結構會比前一種合理的多,因為我們需要先關注資料流,而不是三層結構的層次,甚至對於core、dispatcher、notification我們知道其實是沒有controller的。


2、有些名詞可能就需要內部有一個統一,比如不同的層次面向資料庫,面向業務,面向UI,面向RPC需要有不同的POJO,我們需要明確一套對應的命名,能明確就好,比如下面的這些POJO我們其實挺難分辨其用途的,需要進行規範,並且放置於匹配的目錄結構中:


  • CreateOrderRequest / CreateOrderResponse

  • CreateOrderParam / CreateOrderDto


我們可以約定第一組用於服務本身訪問外部(的Rpc服務也好,REST服務也好),第二組用於服務本身對外提供的Web Api,比如:


  • controllers

    • queryOrder()

    • createOrder()

    • OrderController

    • QueryOrderParam

    • QueryOrderDto

    • CreateOrderParam

    • CreateOrderDto

  • rpcs

    • login()

    • register()

    • UserService

    • LoginRequest

    • LoginResponse

    • RegisterRequest

    • RegisterResponse

  • services

    • OrderService

    • OrderServiceImpl

    • OrderEntity

  • storages

    • OrderMapper

    • OrderModel


總之,雖然可能10+人在維護相同的專案,目錄結構的風格、命名、專用名詞的使用一定要統一。


統一的框架

這個需要在開展專案之前明確下來,我見過有專案中同時使用了Spring MVC和Jersey做Web API,同時使用了Spring Scheduler和Quartz做任務排程。最好是專案開展前明確框架的版本並且搭建好專案腳手架,大概涉及:


  • Web API / Web MVC

  • Job Scheduler

  • Micro Service

  • Config Center

  • Redis Client

  • Data Access

  • Entity Mapper

  • MQ Client


當然,我們也可以獨立出依賴管理的專案,專門由獨立模組進行依賴版本管理。最差也要在Wiki上進行明確。


統一的API

如果專案涉及到對外提供API,那麼非常有必要在初期就規範API的框架定義,涉及到包裝類Result<T>的定義(見過一個專案用了三種包裝類的)以及遇到錯誤的情況下,HTTP狀態碼的體現比如這樣的包裝類格式:


public class ApiResult<T> {boolean success; String code; String message; String path; long time; T data;}複製程式碼


我們可以這麼和客戶端的開發來明確:


  • 即使遇到錯誤,HTTP狀態碼還是200,HTTP狀態碼如果是500或是404的話那一定是閘道器層面的錯誤了,這個錯誤不是後端服務返回的;

  • 在HTTP狀態碼還是200的時候代表收到了後端的返回,前端去按照ApiResult以Json格式反序列化HTTP Body的報文;

  • 然後檢視success(如果沒success也行,我們可以約定code是200就是成功),如果是success代表後端服務成功處理了請求,如果不成功,則根據後端給的錯誤程式碼對映表根據code進行處理或直接提示message中的內容。注意,這裡的success只代表後端是否成功處理了請求,不代表請求代表的業務邏輯是否成功處理。舉一個例子,如果這個請求是非同步支付請求,那麼success==true代表前端給的引數都正確,後端正確接受了支付申請,不代表支付成功;

  • 在success==true的情況下再去解析data中的內容,拿取客戶端需要的資訊,還是前面的例子,data裡可以是{"orderStatus":"PROCESSING", "orderId":"1234"},這個才是真正業務邏輯的資料和狀態,success並不代表訂單支付操作就是成功的,也可能是處理中的狀態


所以這是幾個層次的事情,Http Status->ApiResult.status->ApiResult.data.orderStatus


  • 加解密規範和簽名規範Api的加密解密以及簽名最好在設計的時候就考慮進去,而且要仔細斟酌,否則以後很難變更,特別Api的使用方是客戶端的情況,客戶端很難輕易強更。如果做SAAS服務,建議參考大廠的規範,比如亞馬遜AWS的API規範或阿里雲API的規範,不建議自己造輪子,大廠做的API規範都是經過安全方面的專家深度思考的。

  • 版本管理規範(比如Url path路由還是Http header路由)如果使用了老版本的話,是否需要在返回內容中提示新版本的Url、版本號、老版本最後維護時間呢?這個就不展開了


所以統一Api這個事情不僅僅是Api的格式還涉及到安全處理、版本處理、客戶端操作方式等等。對於一些需要服務端驅動客戶端的業務(UI邏輯動態)來說,我們可以定義一套更復雜的ApiResult,讓服務端告知客戶端這個時候應該是提示還是跳轉還是返回等等。

如果你對技術提升很感興趣,可以加入Java高階技術來交流學習:856443934,裡面都是同行,有資源分享和技術進階思維導圖,其中:(分散式架構、高可擴充套件、高效能、高並 發、Jvm效能調優、Spring,MyBatis,Nginx原始碼分析,Redis,ActiveMQ、、Mycat、Netty、Kafka、Mysql 、Zookeeper、Tomcat、Docker、Dubbo、Nginx)。歡迎一到五年的工程師加入,合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!

統一的原始碼工作模式

現在大家都使用Git,分支如何管理每一個公司(在Gitflow的基礎上)都會略有不同,也需要和大家明確:


  • 分支的定義(master、develop、release、hotfix、feature);

  • 分支命名規範;

  • checkout、merge request流程;

  • 提測流程;

  • 上線流程;

  • Hotfix流程;


別小這個,雖然這個和程式碼質量和架構無關,但是梳理清楚可以:


  • 提高開發和測試的工作效率,人多也亂;

  • 減少甚至杜絕程式碼管理導致的線上事故;

  • 讓專案管理者和架構師可以明確什麼程式碼現在在哪裡;

  • 方便運維處理髮布和回滾;

  • 讓專案的開發可以靈活適應多變的需求;

容錯性

見過一些專案在實現業務程式碼的時候是不考慮任何異常處理、事務處理、鎖處理的,在流量小無併發的情況下,這些專案不容易爆發出嚴重的問題,基本能用。但是對於高併發的專案或將來可能會高速發展的專案,如果不考慮這些問題會死的很難看。


我們來想一下,如果現在在設計一個訂單服務,如果因為網路問題、併發問題導致資料錯亂、服務中斷的可能在千分之一,如果一個業務一天只有1000次請求,1天才遇到這樣1次問題,即使遇到了問題使用者也不一定會來反饋,即使來反饋往往客服也能通過後臺取消訂單等操作來處理,這個問題不會爆發出來,如果一天的單量是1000萬,那麼每天可能就會有10000單異常的訂單,這個可能就超過了客服的處理能力了。


很少有專案真正100%完全做好了所有細節,只不過往往是因為量小得不到大家的重視罷了。但我們想一下,如果遇到資料庫或中介軟體級別大規模故障的情況下,如果一致性處理的不好,那麼資料庫恢復後可能會留下一大堆異常資料需要修復,如果處理的好,業務資料不會錯亂,資料庫恢復後業務馬上可以恢復。在遇到事故的時候,系統這方面的設計功力就體現出來了。


一致性處理

在實現程式碼的時候需要考慮如下事情:


  • 本地事務處理:見過一些程式碼完全不考慮事務,或者是隻是知其然使用@Transactional,但是方法內部完全catch了所有異常的情況。

    • 事務包含的方法塊;

    • 巢狀事務、事務傳播;

    • 什麼時候遇到什麼異常應該回滾;

    • @Transactional是否真正生效了?


  • 外部服務呼叫的事務問題:

    • 呼叫外部服務出現異常的時候,本地事務怎麼處理;

    • 呼叫的外部服務是否允許重試(冪等呼叫);

    • 呼叫外部服務出現未知結果後,怎麼進行補償;

    • 補償是否有上限,是否存在死信資料卡死補償的情況?

    • 如果有2+外部服務連同本地資料庫儲存都需要有事務性,怎麼實現?


  • 資料重複和順序問題:

    • 先本地事務提交還是先呼叫外部介面(如果先呼叫外部介面,可能會遇到外部回撥的時候本地事務還沒提交找不到資料的情況);

    • 從MQ收到的訊息順序問題怎麼解決?

    • 重新入MQ的延遲訊息或重試訊息亂序是否會有問題?

    • 對外提供的Api或回撥方法是否支援冪等?


  • 鎖的問題

    • 哪個層面做鎖?服務層分散式鎖還是資料庫層面鎖?

    • 樂觀鎖還是悲觀鎖?

    • 你確信你的Redis鎖方案是可靠的嗎?

    • 你是否知道多少請求再排隊等待,又是為什麼?


這些要做好真的很難,每一步都需要認證考慮,但是很遺憾見過的很多具有複雜業務的程式碼,在Service中一連串呼叫了N個外部服務進行寫操作,方法內也實現了N個表的寫操作,即不考慮外部服務的事務和補償問題,本地也沒有事務控制,出了錯只是列印了堆疊然後客戶端看到的是一個伺服器忙。


異常處理

異常處理不僅僅是狹義上遇到了Exception怎麼去處理,還有各種業務邏輯遇到錯誤的時候我們怎麼去處理。就拿記日誌這一件事情來說:


  • WARN和ERROR的選擇需要好好考慮,WARN一般我傾向於記錄可自恢復但值得關注的錯誤,ERROR代表了不能自己恢復的錯誤。對於業務處理遇到問題用ERROR不合理,對於catch到了異常也不是全用ERROR。

  • 記錄哪些資訊,最好列印一定的上下文(使用者Id、訂單Id、外部傳來的關鍵資料)而不僅僅是列印執行緒棧。

  • 記錄了上下問資訊,是否要考慮日誌脫敏問題?可以在框架層面實現,比如自定義實現logback的ClassicConverter


我們知道catch到了異常或遇到了業務錯誤,我們除了記錄日誌還有很多選擇,也需要認真考慮什麼時候應該做什麼:


  • 直接返回

  • 丟擲異常

  • 重試處理

  • 恢復處理

  • 熔斷處理

  • 降級處理

  • 甚至關閉業務


這又涉及到了彈力設計的話題,我們的系統往往會對接各種外部服務、Api,大部分服務都不會有SLA,即使有在大併發下我們也需要考慮外部服務不可用對自己的影響,會不會把自己拖死。我們總是希望:


  • 儘可能以小的代價通過嘗試讓業務可以完成

  • 如果外部服務基本不可用,而我們又同步呼叫外部服務的話,我們需要進行自我保護直接熔斷,否則在持續的併發的情況下自己就會垮了

  • 如果外部服務特別重要,我們往往會考慮引入多個同型別的服務,根據價格、服務標準做路由,在出現問題的時候自動降級



架構設計


表的設計

表的設計和Api的定義類似,屬於那種開頭沒有開好,以後改變需要花10x代價的,我知道,一開始在業務不明確的情況下,設計出良好的一步到位的表結構很困難,但是至少我們可以做的是有一個好的標準:


  • 統一的附加欄位,create_time,update_time,version等

  • 表的命名標準,比如[domain]_[tablename]_[tabletype]

  • 欄位型別、長度標準

  • 雖沒有外來鍵,但是外表關聯欄位和主表欄位的命名標準

  • _id還是_no等欄位命名的區別


除了標準,儘可能需要結合業務以及業務可能的擴充套件思考一下:


  • 1:N的可能性,是有1就足夠了,還是一開始就要設計1:N的層次關係

  • 如果表欄位可能會很多,業務變化多,是否考慮1:1甚至1:N的擴充套件表,把擴充套件欄位從主表分開

  • 表的領域職責,表可能也會分上游、中游、下游,什麼欄位應該在哪裡太重要了(我覺得表的領域相當於之前提到的專案結構中的包的分類,這個最好一開始定義清楚)

  • 關聯表欄位冗餘冗餘到什麼程度,冗餘欄位的同步

  • 列舉的維護方式,是否考慮字典表?


對於表結構文件,我覺得列出欄位型別、長度、說明是不夠的,如果能結合業務程式碼梳理清楚下面這些,那這個文件就是真正有價值的表結構文件:


  • 記錄由哪個業務模組建立

  • 資料重要程度

  • 資料歸檔方案

  • 欄位資料來源頭

  • 欄位會由誰更改

  • 欄位可能會在哪裡快取

設計模式

我想90%的業務專案都是所謂的三層結構,Web層處理引數呼叫Service層做Db層的聚合,Db層基本就是程式碼生成或Orm框架補充少量的手寫SQL。對於這樣的專案,大部分人認為是沒有設計的,也不需要設計。我認為那是因為沒有好好思考:


  • 在我們寫下if-else的時候,我們就可以考慮使用抽象類+具體實現類的方式來替代;

  • 在實現層次化業務處理的時候,就可以考慮使用Filter或職責鏈模式來實現;

  • 在封裝外部Api的時候與其每次都寫一套解析邏輯,我們是否考慮進行動態封裝呢;

  • 在資料改變後我們要記錄改版軌跡,與其複製貼上是否考慮過釋出訂閱模式;


說白了就是利用各種設計模式和OO思想,來儘可能在業務變化需要擴充套件的時候:


  • 只是新增程式碼而不是修改程式碼;

  • 儘可能減少重複程式碼複製貼上;

  • 儘可能讓同類程式碼都呆在一起;

  • 儘可能讓直筒式的程式碼有層次;

往大了說

在一個公司層面,如果有幾十個,幾百個業務專案,我們看這個公司的技術水平到了什麼程度,我個人認為不僅僅是用了什麼新技術,而是是否:


  • 具有統一的開發、服務框架

  • 具有統一的運維、監控、中介軟體、測試平臺

  • 具有清晰的縱向領域劃分

  • 具有清晰的橫向基礎平臺服務和基礎業務服務

  • 具有統一的程式碼工作模式


最簡單的一個例子,一個業務從前到後跨10個事業部,100個服務,實現灰度測試,想想這件事情有多難?整個公司層面要實現步調一致的這些東西還確實很難,不僅僅是技術能力的體現,沒有良好的組織架構,人心不齊,恐怕這些無法實現,實現了也無法推廣,推廣了也無法持續……當然,這些已經超出個人能做的了,作為程式設計師的我們應該從我做起,認真考慮前面提到的這些問題,至少在專案內部做良好的設計。


再來看看文首的問題,你看,雖然只是寫業務程式碼,如果要寫的足夠好,必須要了解設計模式、理解各種彈力設計、理解事務、熟悉框架、瞭解中介軟體原理,怎麼可能學不到東西,要實現健壯的業務程式碼,其實很難,要考慮的東西太多了,如果說寫框架我們需要考慮不同的使用方和使用環境,這很難,寫業務程式碼我們要考慮到千奇百怪的使用行為,要考慮到層次不起的對接方,這不比寫框架簡單。對於5年+經驗豐富的程式設計師應當有能力開一個好頭,或者說願意在老程式碼上去做一些改變,否則你的價值在哪裡呢?

如果你對技術提升很感興趣,可以加入Java高階技術來交流學習:856443934,裡面都是同行,有資源分享和技術進階思維導圖,其中:(分散式架構、高可擴充套件、高效能、高並 發、Jvm效能調優、Spring,MyBatis,Nginx原始碼分析,Redis,ActiveMQ、、Mycat、Netty、Kafka、Mysql 、Zookeeper、Tomcat、Docker、Dubbo、Nginx)。歡迎一到五年的工程師加入,合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!

本文只是展開了一些想到的內容,每一點都有很多東西可以寫,也沒時間一些子展開說太多,這些細節留在今後的文章慢慢展開了。


相關文章