微服务架构及幂等性
微服務架構
微服務架構是一種架構概念,旨在通過將功能分解到各個離散的服務中以實現對解決方案的解耦。它的主要作用是將功能分解到離散的各個服務當中,從而降低系統的耦合性,并提供更加靈活的服務支持。
和?微服務?相對應的,這種方式一般被稱為?單體式開發(Monolithic)。既所有的功能打包在一個 WAR 包里,基本沒有外部依賴(除了容器),部署在一個 JavaEE 容器(Tomcat,JBoss,WebLogic)里,包含了 DO/DAO,Service,UI 等所有邏輯。
單體應用的優缺點:
優點:
- 開發簡單,集中式管理
- 基本不會重復開發
- 功能都在本地,沒有分布式的管理和調用消耗
缺點:
- 效率低:開發都在同一個項目改代碼,相互等待,沖突不斷
- 維護難:代碼功能耦合在一起,新人不知道何從下手
- 不靈活:構建時間長,任何小修改都要重構整個項目,耗時
- 穩定性差:一個微小的問題,都可能導致整個應用掛掉
- 擴展性不夠:無法滿足高并發下的業務需
微服務的優缺點:
優點:
- 它解決了復雜問題。它把可能會變得龐大的單體應用程序分解成一套服務
- 這種架構使得每個服務都可以由一個團隊獨立專注開發
- 微服務架構模式可以實現每個微服務獨立部署
- 微服務架構模式使得每個服務能夠獨立擴展
缺點(挑戰):
- 微服務是一個分布式系統,其使得整體變得復雜。開發人員需要基于RPC或者消息實現微服務之間的調用和通信,而這就使得服務之間的發現、服務調用鏈的跟蹤和質量問題變得的相當棘手。
- 分區的數據庫體系和分布式事務。更新多個業務實體的業務交易相當普遍。這些類型的事務在單體應用中實現非常簡單,因為單體應用往往只存在一個數據庫。但在微服務架構下,不同服務可能擁有不同的數據庫。CAP原理的約束,使得我們不得不放棄傳統的強一致性,而轉而追求最終一致性,這個對開發人員來說是一個挑戰。
- 跨越多服務變更。在單體應用程序中,您可以簡單地修改相應的模塊、整合變更并一次性部署他們。相反,在微服務中您需要仔細規劃和協調出現的變更至每個服務。
- 部署基于微服務的應用程序也是相當復雜的
- 測試
以上問題和挑戰可大體概括為:
- API Gateway
- 服務間調用
- 服務發現
- 服務容錯
- 服務部署
- 數據調用
目前流行的兩種微服務框架解決方案(可以解決以上問題)
- SpringBoot + SpringCloud
- SpringBoot是 Spring 的一套快速配置框架,可以基于spring boot 快速開發單個微服務
- Spring Cloud基于Spring Boot,為微服務體系開發中的架構問題提供了一整套的解決方案——服務注冊與發現,服務消費,服務保護與熔斷,網關,分布式調用追蹤,分布式配置管理等路由網關
- Dubbo + ZooKeeper
- Dubbo是一個阿里巴巴開源出來的一個分布式服務框架,致力于提供高性能和透明化的RPC遠程服務調用方案,以及SOA服務治理方案。
- ZooKeeper用來實現服務的注冊與發現和進行負載均衡
基于微服務架構的應用是分布式系統,增加了系統設計和實現的難度,主要有以下方面:
- 運行環境中,微服務實例的網絡地址是動態分配的,微服務運行的實例是動態變化的,需要一套發現機制,使服務調用者可以獲取正確的服務地址。
- 服務之間存在著復雜的依賴關系,在高并發訪問下,依賴的穩定性影響系統的可用性及性能,因此需要提供容錯機制,當依賴的服務發生故障時,不會使調用方線程被長時間占用不釋放,避免故障在系統中蔓延。
- 需要一個高效的進程間通信機制支持微服務之間的交互。
- 服務實例的啟停及流量的調度和分配需要一套負載監控和負載均衡組件來管理。
微服務架構的基本組件
- 可持續交付的平臺
- 服務注冊與發現組件(ZooKeeper)
- 服務網關
一般微服務在系統內部,通常是無狀態的,用戶登入信息和權限管理最好有一個統一的地方維護管理(OAuth)。(類似SSO單點登入)
API網關 + 服務間通信
- 一般在后臺 N 個服務和 UI 之間一般會一個代理或者叫?API Gateway
- 提供統一服務入口,讓微服務對前臺透明
- 聚合后臺的服務,節省流量,提升性能(PC、Android/IOS端只需記住API網關的IP,API網關會有一個后臺服務IP列表)
- 提供安全,過濾,流控等API管理功能
- 其實這個?API Gateway?可以有很多廣義的實現辦法,可以是一個軟硬一體的盒子,也可以是一個簡單的 MVC 框架,甚至是一個?Node.js?的服務端
所有的微服務都是獨立的 Java 進程跑在獨立的虛擬機上,所以服務間的通信就是 IPC(Inter Process Communication),已經有很多成熟的方案。現在基本最通用的有兩種方式
服務間通信
同步調用(阻塞)
服務間通信:網絡中只有字符串可以穿透防火墻
- REST(JAX-RS,Spring Boot):Http通信
- RPC(Thrift, Dubbo):遠程過程調用?User user = new RPCUser();
同步調用比較簡單,一致性強,但是容易出調用問題,性能體驗上也會差些,特別是調用層次多的時候。一般 REST 基于 HTTP,更容易實現,更容易被接受,服務端實現技術也更靈活些,各個語言都能支持,同時能跨客戶端,對客戶端沒有特殊的要求,只要封裝了 HTTP 的 SDK 就能調用,所以相對使用的廣一些。RPC 也有自己的優點,傳輸協議更高效,安全更可控,特別在一個公司內部,如果有統一個的開發規范和統一的服務框架時,他的開發效率優勢更明顯些。就看各自的技術積累實際條件,自己的選擇了。
(對外REST,對內RPC)
異步消息調用
- Kafka
- Notify
- MessageQueue
異步消息的方式在分布式系統中有特別廣泛的應用,他既能減低調用服務之間的耦合,又能成為調用之間的緩沖,確保消息積壓不會沖垮被調用方,同時能保證調用方的服務體驗,繼續干自己該干的活,不至于被后臺性能拖慢。不過需要付出的代價是一致性的減弱,需要接受數據?最終一致性;還有就是后臺服務一般要實現?冪等性,因為消息送出于性能的考慮一般會有重復(保證消息的被收到且僅收到一次對性能是很大的考驗);最后就是必須引入一個獨立的?Broker(消息隊列的中間服務器)
服務發現
在微服務架構中,一般每一個服務都是有多個拷貝,來做負載均衡。一個服務隨時可能下線,也可能應對臨時訪問壓力增加新的服務節點。服務之間如何相互感知?服務如何管理?
這就是服務發現的問題了。一般有兩類做法,也各有優缺點。基本都是通過 Zookeeper 等類似技術做服務注冊信息的分布式管理。當服務上線時,服務提供者將自己的服務信息注冊到 ZK(或類似框架),并通過心跳維持長鏈接,實時更新鏈接信息。服務調用者通過 ZK 尋址,根據可定制算法,找到一個服務,還可以將服務信息緩存在本地以提高性能。當服務下線時,ZK 會發通知給服務客戶端。
ZooKeeper實現了分布式鎖,解決單點故障問題
服務注冊
Dubbo是一個RPC通信框架;ZooKeeper服務注冊與發現。此處兩者結合使用。
- 基于客戶端的服務注冊與發現
優點是架構簡單,擴展靈活,只對服務注冊器依賴。缺點是客戶端要維護所有調用服務的地址,有技術難度,一般大公司都有成熟的內部框架支持,比如 Dubbo。
此處調用可以是Dubbo,服務注冊為ZooKeeper
- 基于服務端的服務注冊與發現
優點是簡單,所有服務對于前臺調用方透明,一般在小公司在云服務上部署的應用采用的比較多。
LB為負載均衡服務器,由LB去查詢注冊中心,再去調用對應指定的服務
微服務需要考慮的問題:
- API Gateway
- 服務間調用
- 服務發現
- 服務容錯
- 服務部署
- 數據調用
微服務冪等性
分布式系統中的冪等性概念:用戶對于同一操作發起的一次請求或者多次請求的結果是一致的,不會因為多次點擊而產生了副作用。
冪等場景:
可能會發生重復請求或消費的場景,在微服務架構中是隨處可見的。
- 網絡波動:因網絡波動,可能會引起重復請求
-
分布式消息消費:任務發布后,使用分布式消息服務來進行消費
- 用戶重復操作:用戶在使用產品時,可能無意地觸發多筆交易,甚至沒有響應而有意觸發多筆交易
-
未關閉的重試機制:因開發人員、測試人員或運維人員沒有檢查出來,而開啟的重試機制(如Nginx重試、RPC通信重試或業務層重試等)
CRUD操作分析
-
新增類請求:不具備冪等性
-
查詢類動作:重復查詢不會產生或變更新的數據,查詢具有天然冪等性
- 更新類請求:
- 基于主鍵的計算式Update,不具備冪等性,即UPDATE goods SET number=number-1 WHERE id=1
- 基于主鍵的非計算式Update:具備冪等性,即UPDATE goods SET number=newNumber WHERE id=1
- 基于條件查詢的更新,不一定具備冪等性(需要根據實際情況進行分析判斷)
- 刪除類請求:
- 基于主鍵的Delete具備冪等性
- 一般業務層面都是邏輯刪除(即update操作),而基于主鍵的邏輯刪除操作也是具有冪等性的
冪等性的重要性
針對一個微服務架構,如果不支持冪等操作,那將會出現以下情況:
- 電商超賣現象
- 重復轉賬、扣款或付款
- 重復增加金幣、積分或優惠券
超賣現象
? 比如某商品的庫存為1,此時用戶1和用戶2并發購買該商品,用戶1提交訂單后該商品的庫存被修改為0,而此時用戶2并不知道的情況下提交訂單,該商品的庫存再次被修改為-1這就是超賣現象。
? 究其深層原因,是因為數據庫底層的寫操作和讀操作可以同時進行,雖然寫操作默認帶有隱式鎖(即對同一數據不能同時進行寫操作)但是讀操作默認是不帶鎖的,所以當用戶1去修改庫存的時候,用戶2依然可以都到庫存為1,所以出現了超賣現象。
??解決方案A:可以對讀操作加上顯式鎖(即在select …語句最后加上for update)這樣一來用戶1在進行讀操作時用戶2就需要排隊等待了。但問題來了,如果該商品很熱門并發量很高那么效率就會大大的下降,如何解決呢?(解決方案B)
??解決方案B:我們可以有條件有選擇的在讀操作上加鎖,比如可以對庫存做一個判斷,當庫存小于一個量時開始加鎖,讓購買者排隊,這樣一來就解決了超賣現象。
解決方案
- 全局唯一ID
如果使用全局唯一ID,就是根據業務的操作和內容生成一個全局ID,在執行操作前先根據這個全局唯一ID是否存在,來判斷這個操作是否已經執行。如果不存在則把全局ID,存儲到存儲系統中,比如數據庫、Redis等。如果存在則表示該方法已經執行。
使用全局唯一ID是一個通用方案,可以支持插入、更新、刪除業務操作。但是這個方案看起來很美但是實現起來比較麻煩,下面的方案適用于特定的場景,但是實現起來比較簡單。
- 去重表
這種方法適用于在業務中有唯一標識的插入場景中,比如在以上的支付場景中,如果一個訂單只會支付一次,所以訂單ID可以作為唯一標識。這時,我們就可以建一張去重表,并且把唯一標識作為唯一索引,在我們實現時,把創建支付單據寫入去重表,放在一個事務中,如果重復創建,數據庫會拋出唯一約束異常,操作就會回滾。
- 插入或更新
這種方法插入并且有唯一索引的情況,比如我們要關聯商品品類,其中商品的ID和品類的ID可以構成唯一索引,并且在數據表中也增加了唯一索引。這時就可以使用InsertOrUpdate操作。
- 多版本控制
這種方法適合在更新的場景中,比如我們要更新商品的名字,這時我們就可以在更新的接口中增加一個版本號,來做冪等:boolean updateGoodsName(int id,String newName,int version);
在實現時可以如下:update goods set name=#{newName},version=#{version} where id=#{id} and version<${version}
- 狀態機控制
這種方法適合在有狀態機流轉的情況下,比如就會訂單的創建和付款,訂單的付款肯定是在之前,這時我們可以通過在設計狀態字段時,使用int類型,并且通過值類型的大小來做冪等,比如訂單的創建為0,付款成功為100,付款失敗為99。在做狀態機更新時,我們就這可以這樣控制:update goods_order set status=#{status} where id=#{id} and status<#{status}
以上就是保證接口冪等性的一些方法。
總結
冪等性設計不能脫離業務來討論,一般情況下,去重表同時也是業務數據表,而針對分布式的去重ID,可以參考以下幾種方式:
- UUID
- Snowflake
- 數據庫自增ID
- 業務本身的唯一約束
- 業務字段+時間戳拼接
分類:?分布式
總結
- 上一篇: 信用卡还款哪种方式最不靠谱
- 下一篇: 以各国拥有的黄金储备,如果重启金本位,哪