大多數程序員,做得最多的事,也不過是寫接口這件事而已。
今天繼續總結下接口設計需要注意的點。盡量每種都給出具體的場景、案例等,希望大家能有所收獲。
1、接口冪等
冪等性:是指一個操作或者一個服務,無論執行多少次,其結果都是一樣的,對系統產生的影響是相同的。
這對于確保系統的穩定性和可靠性至關重要,尤其是在分布式系統、微服務架構以及涉及支付、訂單處理等關鍵業務場景中。
比如對最典型的支付場景而言,就需要避免重復支付的問題:一個訂單,后續的多次付款請求,只能有一次成功。
以下是需要考慮冪等性的幾個典型場景:
- 支付接口:重復的支付請求可能導致用戶被多次扣款。確保支付操作是冪等的,可以避免用戶因網絡延遲或錯誤點擊造成的重復扣費問題。
- 訂單創建與更新:在電商系統中,用戶可能因為網絡問題提交了多次訂單請求。冪等設計可以保證即使請求多次,也只創建或更新一次訂單狀態。
- 消息隊列:消息生產者可能因為網絡波動等原因重發消息,消息消費者需要能夠識別并忽略重復的消息,確保業務邏輯只被執行一次。
- 緩存更新:在更新緩存時,如果因為網絡問題導致更新操作未確認成功而重試,冪等性可以確保緩存不會被錯誤地多次修改。
- 用戶注冊與登錄:雖然這些操作通常不期望是冪等的(例如,注冊不能重復創建賬戶),但在實現過程中,如驗證碼驗證、token刷新等功能應當設計為冪等,以防止因多次請求導致的問題。
- 數據庫寫入操作:特別是那些具有唯一約束的操作,如插入唯一索引的記錄,冪等設計能防止因重復插入而導致的沖突錯誤。
- RESTful API設計:特別是PUT和DELETE方法,按照HTTP規范,它們應該具備冪等性。PUT請求多次應該得到相同的結果,而DELETE請求多次也應該保持資源的刪除狀態不變。
針對上述需要考慮冪等性的場景,可以采取以下幾種策略來設計和實現冪等性:
- 使用事務: 在數據庫操作中,通過ACID(原子性、一致性、隔離性、持久性)屬性確保操作的冪等性。例如,在訂單創建時,通過事務包裹插入操作,即使操作重復執行,數據庫也能保證訂單只被創建一次。
- 唯一鍵約束: 對于可能產生重復數據的寫入操作,利用數據庫中的唯一索引或主鍵約束。當嘗試插入已存在的記錄時,數據庫會阻止重復插入,從而實現冪等。
- 樂觀鎖/悲觀鎖: 在并發更新時,樂觀鎖通過版本號或時間戳字段檢查數據是否被改動過,只有當數據未被其他事務修改時才執行更新;悲觀鎖則是在事務開始時就鎖定資源,阻止其他事務修改,直到當前事務結束。這兩種機制都能確保更新操作的冪等性。
- Token機制: 在處理敏感操作如支付、訂單提交時,先向服務器請求一個一次性Token,之后的請求攜帶此Token進行操作。服務器驗證Token的有效性和使用次數,確保操作只被執行一次。 可用于用戶注冊和登錄的場景。
- 記錄操作日志: 記錄每一次操作的狀態和結果,當接收到重復請求時,首先檢查操作日志,如果發現該操作已經成功執行,則直接返回之前的成功結果,不再重復執行業務邏輯。 比如記錄訂單的狀態,基于訂單的狀態判斷后續操作的可行性。
- 冪等鍵: 在API設計中,特別是在POST和PATCH請求中,客戶端可以提供一個唯一的idempotent key(冪等鍵)。服務端根據這個key判斷請求是否已經處理過,避免重復處理。
- 消息冪等處理: 在消息隊列中,為每條消息分配一個全局唯一的ID,并在消費端記錄已處理的消息ID。當消息重新投遞時,消費端通過檢查ID判斷是否已經處理過,從而避免重復消費。
- 重試策略與去重: 對于可能會因為網絡原因重試的請求,如支付、消息發送等,服務端需要有機制識別并忽略重復的請求。這可以通過記錄請求指紋、設置請求超時和重試次數上限等方式實現。
2、讀寫分離
使用讀寫分離主要是為了減輕主數據庫的壓力,當需要考慮讀寫分離的時,通常涉及數據庫層面的優化。
以下是實現讀寫分離的一些策略和實踐方法:
- 使用數據庫中間件
-
MyCAT、ShardingSphere、dynamic-datasource:這類中間件能夠實現MySQL數據庫的讀寫分離,自動路由讀操作到從庫,寫操作到主庫。通過配置中間件,Java應用只需連接到中間件,而無需直接管理數據庫連接的讀寫分離邏輯。
- 讀寫分離策略
-
主從延遲問題處理:讀寫分離后,從庫的數據同步通常會有一定延遲,需要在應用層考慮這一因素,比如對實時性要求高的查詢盡量走主庫。
-
負載均衡:對于讀操作,可以配置從庫的負載均衡策略,比如輪詢、隨機或者基于權重的分配,以充分利用所有從庫資源。
- 事務管理
- 兩階段提交(2PC):雖然在分布式系統中使用2PC來保證跨庫事務的一致性較為復雜且影響性能,但在某些嚴格要求事務一致性的場景下可能需要考慮。
- 靈活事務策略:對于非嚴格事務場景,可以采用補償事務或者最大努力通知等模式,犧牲一定的強一致性來換取性能。
比如使用多數據源查看數據:
@Service
@DS("master")
public class UserServiceImpl implements UserService {@Autowiredprivate JdbcTemplate jdbcTemplate;public List<Map<String, Object>> selectAll() {return jdbcTemplate.queryForList("select * from user");}@Override@DS("slave")public List<Map<String, Object>> selectByCondition() {return jdbcTemplate.queryForList("select * from user where age >10");}
}
3、數據緩存
Redis主要因為它作為一個高性能的鍵值存儲系統,提供了豐富的數據結構(如字符串、哈希、列表、集合、有序集合等)以及強大的特性(如事務、持久化、Lua腳本、發布/訂閱等),非常適合解決各種緩存、消息隊列、會話存儲、分布式鎖等方面的問題。
下面列舉了一些常見的使用場景:
- 數據緩存:最典型的使用場景之一。將頻繁訪問且不經常改變的數據存儲在Redis中,減少數據庫的負載,提高應用性能。例如,商品詳情、用戶信息、分類樹、組架樹等。
- 會話存儲:可以將用戶的會話信息存儲在Redis中,實現無狀態服務,方便水平擴展。同時利用Redis的持久化機制保證數據不丟失。
- 分布式鎖:利用Redis的原子操作能力,實現分布式環境下的鎖機制,用于控制并發操作,如秒殺、庫存扣減等場景。
- 消息隊列:Redis的發布/訂閱功能可以用來構建簡單的消息隊列系統,實現異步處理和解耦合。雖然不是專門的消息隊列服務,但在某些輕量級場景下非常實用。
- 計數器與限流:利用Redis的incr、decr等原子操作實現高并發下的計數需求,如統計訪問次數、限流(如限制API調用頻率)等。
- 排行榜與Leaderboard:利用有序集合數據類型可以輕松實現各類排行榜,如游戲積分榜、文章熱度排名等,支持快速查詢排名和分數區間。
- 布隆過濾器:可以利用Redis的HyperLogLog或者結合布隆過濾器的庫來實現數據去重、判斷元素是否存在等功能,尤其是在大數據場景下非常高效。
- 實時分析與聚合:雖然Redis主要用于存儲,但其數據結構如有序集合可以支持一些實時分析的需求,比如統計網站在線人數、最近活躍用戶等。
使用緩存的時候,需要注意緩存和數據庫數據的一致性,在修改內容的時候要及時更新緩存,特別是新增加修改接口的時候,不要忘記了更新緩存。
使用spring的Cache組件進行緩存數據:
@CacheEvict(cacheNames = CACHE_NAME_FRONT_CATEGORY, allEntries = true)
public Long saveAndUpdate(CategoryFrontReq req) throws ServiceException {log.info("新增或修改類目開始CategoryReq{}", req);// 業務邏輯log.info("新增或修改類目成功result{}", result);return result;
}
4、接口分頁
如果一個接口對外返回一個數組,千萬注意數據量的大小,新接手項目寫接口的時候,要查一查生產環境的數據量。不要因為一時疏忽,直接導致一個生產環境的OOM事故產生。
分頁容易出現的問題包括:
- 深分頁
- 關聯表過多
5、接口加鎖
回家不鎖門,小心被偷家。既然是獨有的私密空間,就應該自己獨享,那句話怎么說的來著,臥榻之側,豈容他人鼾睡。哈哈,就是這個意思。
以下是一些具體場景:
- 庫存管理:在線購物系統中,當用戶下單時,需要減少商品庫存。由于同一商品可能同時被多個用戶訪問和購買,對庫存的修改操作就需要加鎖,確保不會出現超賣的情況。
- 訂單處理:在處理訂單狀態變更時(如從“未支付”變為“已支付”),為了防止并發操作導致的訂單狀態混亂,對訂單狀態更新的操作需要加鎖。
- 賬戶資金操作:銀行或支付系統中,轉賬、存款、取款等操作涉及到賬戶余額的增減,這些操作需要原子性執行,以確保不會因為并發操作導致賬戶余額計算錯誤。
- 票務系統:如火車票、電影票等票務預訂過程中,座位的鎖定和釋放操作需要加鎖,確保不會出現重復售賣同一座位的問題。
- 消息隊列處理:在處理消息隊列(如任務調度、事件驅動架構)時,消費和確認消息的操作可能需要加鎖,確保消息只被一個消費者正確處理。
- 緩存更新:當有多個線程可能同時讀取并更新緩存中的數據時,如熱點商品的緩存刷新,需要加鎖以防止臟讀或數據覆蓋問題。
- 計數器與統計:如網站訪問計數、點贊數、評論數等,這些統計量的累加操作如果不加以控制,可能會因為并發更新而丟失更新,加鎖可以保證計數的準確性。
- 會話管理:在Web應用中,對用戶會話信息的修改(如登錄狀態、購物車內容)需要加鎖,確保多個請求對同一會話的操作不會沖突。
- 并發任務調度:在復雜的任務調度系統中,分配任務給工作者線程、記錄任務狀態變更等操作可能需要加鎖,以避免任務被重復分配或狀態更新混亂。
- 分布式鎖應用場景:在分布式系統中,對于全局資源的訪問和修改(如分布式緩存、數據庫中的全局序列號生成)也需要通過分布式鎖來協調不同節點間的并發訪問。
在涉及并發控制的場景下使用加鎖機制時,也需要注意一下幾個問題:
- 死鎖:當兩個或多個進程互相等待對方持有的鎖時,就會發生死鎖。例如,進程A持有資源X并等待資源Y,而進程B持有資源Y并等待資源X,如果沒有外部干預,兩個進程都會無限期地等待下去。
- 過度使用鎖:不必要的廣泛使用鎖會導致性能下降。如果對不需要同步的代碼塊也進行了加鎖,會增加線程之間的競爭和上下文切換的開銷。
- 鎖粒度過大:如果一個鎖保護了過多的數據或代碼段,可能會導致不必要的阻塞。理想的狀況是使用細粒度的鎖,只保護必要的資源。
6、事務管理
在Java應用開發中,事務管理是確保數據一致性和完整性的重要環節
- 本地事務
本地事務通常涉及單個數據庫,所有操作都在同一個數據庫連接中完成。在Java應用中,通過JDBC直接操作數據庫或者使用如Spring的@Transactional注解來管理事務。
- 分布式事務
分布式事務涉及跨越多個數據庫或服務的操作,需要在多個節點上保持數據的一致性。Java應用中常用的技術包括兩階段提交(2PC,如JTA+XA)、Saga模式、TCC(Try-Confirm-Cancel)模式等。可用使用seata等分布式事務組件實現。
主要適用于以下場景:
- 跨庫操作:當業務操作需要同時在多個數據庫上執行,并且這些操作必須全部成功或全部失敗時,就需要分布式事務。例如,一個訂單系統可能需要同時更新訂單數據庫、庫存數據庫和用戶積分數據庫。
- 微服務架構:在微服務架構中,一個業務功能往往由多個服務協同完成,每個服務可能有自己的數據庫。在這種情況下,一個業務流程可能跨越多個服務,因此需要分布式事務來保證事務的ACID特性。
- 跨服務調用:如果一個事務操作需要調用多個遠程服務,并且這些服務操作之間存在依賴關系,那么分布式事務是必需的。例如,在電商平臺中,下單操作可能需要調用庫存服務減少庫存、支付服務處理支付、物流服務安排發貨等多個步驟。
- 復雜業務流程:對于那些包含多個步驟,且每個步驟都需要在不同服務或數據庫中持久化數據的長事務或復雜業務流程,分布式事務提供了一種解決一致性問題的方法。
@GlobalTransactional // seata分布式事務注解
public OrderCreateResponse createBookOrder(CardBookReq cardBookReq) throws Exception {// 業務邏輯
}
7、設計模式
設計模式在代碼中的使用也比較常見,能夠對場景進行抽象,然后給出通用的解決方案。
下面是代碼中場景的一些設計模式的使用場景:
- 單例模式:全局唯一的配置項、數據庫連接池、線程池、緩存管理、日志記錄器等。在這些場景中,需要確保無論何時何地都只有一個實例存在,且這個實例易于訪問。
- 工廠模式:當需要創建對象而不希望客戶端知道具體實現時;當需要動態決定創建哪種對象時;當需要封裝對象的創建過程時。例如,創建不同種類的數據庫連接、創建不同操作系統的UI組件等。
- 建造者模式:當需要構建復雜對象,且對象的構建過程需要設置多個選項或屬性時;當這些選項或屬性有多種組合時。例如,創建具有復雜配置的服務器對象、創建具有多個字段的表單對象等。
- 適配器模式:當需要將一個類的接口轉換成客戶端所期望的另一個接口時;當兩個類之間因為接口不兼容而無法協同工作時。例如,舊接口與新系統之間的適配、不同數據庫之間的數據轉換等。
- 觀察者模式:當一個對象的狀態改變時,需要通知其他依賴它的對象時;當需要實現發布-訂閱模型時。例如,用戶界面中的監聽器(如按鈕點擊事件監聽器)、股票價格變動通知等。
- 策略模式:當需要在運行時根據條件選擇不同算法或行為時;當需要將算法與使用該算法的對象分離時。例如,排序算法的選擇(冒泡排序、快速排序等)、支付方式的選擇(信用卡支付、支付寶支付等)。
8、多線程編程
多線程技術在許多業務場景中能夠顯著提升應用的響應速度和處理能力,尤其是在需要同時執行多項任務,或者處理大量并發請求的情況下。
下面是一些需要考慮使用多線程的業務場景:
- 文件上傳/下載:文件上傳或下載過程中,特別是在處理大文件時,可以利用多線程分塊上傳或下載,加快傳輸速度,同時保持用戶界面的響應性。
- 異步處理:在需要執行一些耗時操作(如發送郵件、生成報告等)但又不想阻塞主線程時,可以開啟新線程異步執行這些任務,從而提高用戶體驗。
- 數據批處理:在大數據處理、ETL作業中,通過多線程并行處理數據,可以大大減少處理時間,特別是在數據清洗、轉換、加載到數據庫等步驟。
- 即時通訊服務:即時通訊軟件需要快速處理消息收發、群聊廣播等功能,多線程可以有效應對高并發的消息處理需求,保證消息的實時性。
多線程編程雖然強大,但也存在一些潛在的問題和陷阱,如果不小心處理,可能會導致程序行為異常,數據不一致,甚至死鎖等問題。
以下是使用Java多線程時容易遇到的一些“坑”:
- 線程泄漏:未正確管理線程,導致線程創建后沒有被正確終止或回收,長期累積可能導致系統資源耗盡。
- 濫用線程池:不恰當的線程池配置(如線程數過多或過少)、任務隊列無界導致內存溢出、長時間運行的任務阻礙了短任務快速執行等,都是常見的問題。
- 非線程安全的類誤用:誤將非線程安全的集合類(如ArrayList、HashMap)當作線程安全的使用,也是常見的錯誤。