從單體架構升級到微服務,在程式碼層面應注意的一些問題
由於近年來的移動端的發展和 2C模式 的紅利,一些在風口的企業的業務得到爆發式增長。從架構層面來說,業務驅動技術的變革,所以微服務架構的概念得到很多企業的青睞,因為可以解決服務的大流量和高併發以及穩定性的要求。
但是任何架構設計不是一蹴而就的,不能從起步就開始使用微服務,一般都是先通過單體架構來快速實現需求和搶佔市場,然後再迭代式擴充套件。不能一口氣吃個胖子。
這幾年自己有經歷從單體到微服務的架構演變,也有直接參與到已經落地的微服務架構的專案中。見過好的架構設計,也見過一些孬的設計。好的架構設計,程式碼結構優雅,分層清晰,業務邊界劃分明朗,業務開發人員職責清晰。不好的設計就會導致程式碼混亂難以維護,對新需求無法快速應變,開發人員容易在補丁上打補丁,最後造成積重難返不得不重構。
架構師需要從業務層面和未來業務發展有個全面的規劃,讓架構高可用,易擴充套件,靈活易使用,隱藏其複雜性。好的架構會讓下面的業務開發人員按照既定的模式“傻瓜式”程式設計。
既然第一步是單體架構,那麼好的單體架構設計,為我們後期的微服務拆分會有事半功倍的效果。避免重複勞動和過多的重寫。我們可以從這些方面進行一些有效的設計。
劃清業務邊界
如果對未來的架構有微服務的考慮,那麼在單體架構的時候就需要理清業務邊界的問題,常見的簡單劃分就是以業務區分,例如:使用者,商品,訂單,支付,許可權等等,具體的拆分程度可根據自身業務量和需要做劃分。
當前流行的 DDD(領域驅動設計)可以作為一個指導原則,但是 DDD 比較偏向於理論,需要執行人員有良好的專業能力才能實施的比較好。
程式碼層次結構
業務區分好之後,就是專案程式碼模組的設計。在程式碼層我們需要根據MVC的模式,建議的程式碼設計層次如下:
├─demo-common │ │ demo-common.iml │ │ pom.xml │ │ │ └─src │ ├─main │ │ ├─java │ │ └─resources │ └─test │ └─java ├─demo-dao │ │ demo-dao.iml │ │ pom.xml │ │ │ └─src │ ├─main │ │ ├─java │ │ └─resources │ └─test │ └─java ├─demo-service │ │ demo-service.iml │ │ pom.xml │ │ │ └─src │ ├─main │ │ ├─java │ │ └─resources │ └─test │ └─java └─demo-web │ demo-web.iml │ pom.xml │ └─src ├─main │ ├─java │ └─resources └─test └─java
主要包含4個 module 模組
- demo-common:基礎模組,列舉,常亮類,工具類,配置類。
- demo-dao:Dao層,mapper介面,mapper.xml。
- demo-service:服務介面提供層,業務service介面。
- demo-web:web層,Controller類,服務介面,與外部進行互動。
各模組之間的依賴關係為:
專案 Module 模組設計完成之後,每個模組的內部 package 包如何設計呢?通常有兩種劃分模式:根據業務模組然後內部按MVC劃分,根據MVC模式然後內部按業務劃分。
1、根據業務模組劃分,就是將每個業務模組作為一個 package,然後每個package裡面有自己的 MVC,這樣就做到業務模組的隔離。
2、根據 MVC 模式劃分,先根據 MVC模式劃分不同的包,service,serviceImpl,dto等,然後再是各個業務自己的模型和服務介面。
針對上述的兩個劃分模式,個人的選擇是根據業務模式劃分,這樣的包設計與後期微服務拆分有良好的匹配度,拆分的時候只需要將每個業務包下的程式碼 Copy 到新的微服務中就行了,易遷移變動小。每個模組中對不同的業務通過 package 包名進行劃分,例如:com.example.jajian.service.order、com.example.jajian.service.user等。
└─src
├─main
│ ├─java
│ │ └─com
│ │ └─example
│ │ └─jajian
│ │ ├─common
│ │ │ BaserService.java
│ │ │
│ │ └─service
│ │ ├─order
│ │ │ ├─dto
│ │ │ │ OrderDto.java
│ │ │ │
│ │ │ └─service
│ │ │ │ OrderService.java
│ │ │ │
│ │ │ └─impl
│ │ │ OrderServiceImpl.java
│ │ │
│ │ ├─pay
│ │ │ ├─dto
│ │ │ │ PayDto.java
│ │ │ │
│ │ │ └─service
│ │ │ │ PayService.java
│ │ │ │
│ │ │ └─impl
│ │ │ PayServiceImpl.java
│ │ │
│ │ └─user
│ │ ├─dto
│ │ │ UserDto.java
│ │ │
│ │ └─service
│ │ │ UserService.java
│ │ │
│ │ └─impl
│ │ UserServiceImpl.java
│ │
│ └─resources
└─test
└─java
這樣劃分有什麼好處?我們單體架構的時候這樣開發,當需要拆分成微服務的時候就可以直接將業務包拆分出去,因為每個業務包裡面就已經包含了所有的當前業務的關聯業務類。
避免多邊界業務的關聯查詢
單表關聯由於業務需要而且簡單方便易使用,所以多表關聯查詢在單體服務中是普遍存在的,如果我們後期不需要做服務拆分則可以不需要考慮這方面的限制。
但是如果後期有微服務的規劃,那麼單體服務的時候如果沒有做這個方面的限制,mybatis 的 mapper.xml中有過多的多表關聯查詢,這些關聯查詢會嚴重影響服務拆分的進度和複雜度。
如果同屬於一個業務領域則可以使用關聯查詢,而那些微服務拆分後屬於不同領域的業務則應避免使用多表關聯查詢,因為不同的業務領域後期會被隔離拆分到不同的服務當中,即資料庫表都是分佈在不同的伺服器上,所有服務之間都是通過RPC方式進行通訊,關聯查詢這時是無法處理的。
Controller層儘量不做業務邏輯處理
常看到很多 coder 會在Controller 層做一些業務處理,個人認為這是很不規範的。Controller層是控制層,是和前端進行資料轉換的,這裡我們應該只做請求的接受和返回,也無需做一些異常的try...catch...的捕獲,異常可以通過全域性通用攔截器統一進行攔截然後返回給前端異常提示語,提升程式碼的簡潔性。
所有的引數校驗也放到 service層,因為如果服務內部呼叫也可以使用提高程式碼的共用度。當然分層領域模型最好也能區分開,
- DO(Data Object):此物件與資料庫表結構--對應,通過DAO層向上傳輸資料來源物件。
- DTO(Date Transfer Object):資料傳輸物件,service或Manager向外傳輸的物件。
- VO(View Object):顯示層的物件,通常是Web向模板渲染引擎層傳輸的物件。
這樣區分開的好處是,當你需要對展示層資料進行特殊定製化的時候可以靈活變通,例如針對使用者隱私資訊身份號,手機號碼脫敏處理,或者使用者ID加密顯示等。
最後就是統一通用返回類了,通過這種格式的封裝我們將資料格式進行全域性格式化,這裡的狀態碼可以自己設計的更詳細一點。
public class CommonResult<T> {
public static final String CODE_SUCCESS = "0";
public static final String CODE_FAILED = "9999";
private String code;
private T data;
private String msg;
private CommonResult(String code, T data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
}
public boolean isSuccess() {
return CODE_SUCCESS.equals(code);
}
public static <T> CommonResult<T> success() {
return new CommonResult<>(CODE_SUCCESS, null, null);
}
public static <T> CommonResult<T> success(T data) {
return new CommonResult<>(CODE_SUCCESS, data, null);
}
public static <T> CommonResult<T> success(T data, String msg) {
return new CommonResult<>(CODE_SUCCESS, data, msg);
}
public static <T> CommonResult<T> failed() {
return new CommonResult<>(CODE_FAILED, null, null);
}
public static <T> CommonResult<T> failed(String errorCode, String msg) {
return new CommonResult<>(errorCode, null, msg);
}
public static <T> CommonResult<T> failed(String msg) {
return new CommonResult<>(CODE_FAILED, null, msg);
}
public static <T> CommonResult<T> failed(T data, String msg) {
return new CommonResult<>(CODE_FAILED, data, msg);
}
public static <T> CommonResult<T> failed(String errorCode, T data, String msg) {
return new CommonResult<>(errorCode, data, msg);
}
// 省略 setter、getter
}
後記
以上只是列舉了單體服務未來規劃做微服務時需要注意的一部分簡單內容,每個人在做單體架構拆分成微服務的時候都會踩到各種各樣的坑,這些坑成了我們的開發經驗,有了這些坑就會形成注意點,在我們下次開發時就會具有指導意義。也許我們程式設計師就是在踩坑和填坑的過程中成長壯大起來的