目錄
1、介紹
2、樂觀鎖
2.1、核心思想
2.2、實現方式
1. 使用?version?字段(推薦)
2. 使用?timestamp?字段
2.3、如何處理沖突
2.4、樂觀鎖局限性
3、冪等性
3.1、什么是冪等性
3.2、樂觀鎖與冪等性的關系
1. 樂觀鎖如何輔助冪等性?
2. 樂觀鎖的局限性
3.3、如何設計
3.4、order_no?添加唯一約束
1、防止重復創建訂單
2、保證業務邏輯的正確性
3、支持冪等性設計
4、節點故障場景
4.1. 數據庫層面
1、事務的原子性
2、自動提交(Autocommit)
4.2. 應用層
1、重試機制
2、冪等性設計
3、異步消息隊列
4.3. 網絡恢復后的處理
1、客戶端檢測網絡狀態
2、服務端日志與監控
1、介紹
????????在分布式系統中,樂觀鎖、冪等性設計和數據插入失敗處理是保障數據一致性和系統可靠性的三大核心機制,它們共同協作以解決并發沖突、重復請求和網絡異常等問題。
1.樂觀鎖:
????????通過在數據庫中添加?version
?或?timestamp
?字段,確保并發更新時的數據一致性。每次更新時檢查版本號是否匹配。
????????若匹配則更新并遞增版本號,否則拋出異常(如?StaleObjectStateException
)。適用于讀多寫少的場景,減少鎖競爭,但需業務層配合處理沖突重試。
2.冪等性設計:
????????確保同一請求多次執行的結果與一次執行相同,常用于支付、訂單等關鍵業務。通過?唯一業務標識符(如訂單號)、請求ID、數據庫唯一約束?或?緩存記錄?來攔截重復請求。例如,插入訂單前先檢查?order_no
?是否已存在,若存在則直接返回結果,避免重復操作。
3.數據插入失敗的處理:
網絡宕機:
????????若插入操作未提交,數據庫事務會自動回滾;若已提交部分數據,需通過補償機制(如回滾或修復)修正。
重試機制:
????????在網絡恢復后,客戶端可結合?指數退避算法?重試請求,但需確保重試操作是冪等的(如通過唯一約束或請求ID)。
異步隊列:
????????將請求放入消息隊列(如 Kafka、RabbitMQ),確保網絡中斷時消息不丟失,恢復后繼續處理。
典型場景示例:
? ? ? ? 用戶提交支付請求時,系統通過?
order_no
?的唯一約束防止重復訂單,使用樂觀鎖避免并發修改價格,若網絡中斷則通過重試機制重新提交(但依賴冪等性設計避免重復扣款)。
核心目標:
? ? ????通過?樂觀鎖?保證數據一致性,冪等性?防止重復操作,重試與補償?應對網絡異常,三者結合構建高可用、可靠的分布式系統。
2、樂觀鎖
??????????MySQL的樂觀鎖是一種并發控制機制,它假設數據沖突(多個事務同時修改同一數據)的概率較低,因此在讀取數據時不加鎖,而是在更新時檢查數據是否被其他事務修改過。如果沖突發生,事務會失敗并重試。
2.1、核心思想
- 讀取數據時:記錄數據的版本號(或時間戳)。
- 更新數據時:檢查版本號是否一致,如果一致則更新,否則拋出異常(沖突)。
2.2、實現方式
在MySQL中,樂觀鎖通常通過以下方式實現:
1. 使用?version
?字段(推薦)
- 在表中添加一個?
version
?字段(整數類型),每次更新時自動遞增。 - 讀取數據時獲取當前?
version
?值。 - 更新時將?
version
?作為條件,如果匹配則更新并遞增?version
。
示例表結構
CREATE TABLE product (id INT PRIMARY KEY,name VARCHAR(50),price DECIMAL(10,2),version INT DEFAULT 0 -- 樂觀鎖版本號
);
示例操作
1.讀取數據:
SELECT id, name, price, version FROM product WHERE id = 1;
-- 假設返回: id=1, name='Apple', price=10.00, version=5
2.更新數據(帶版本號檢查):
UPDATE product
SET price = 12.00, version = version + 1
WHERE id = 1 AND version = 5;
3.判斷是否更新成功:
????????如果?version=5
?的記錄還存在,則更新成功。
????????如果?version
?已經被其他事務修改為?6
,則更新失敗(影響行數為0),此時需要拋出異常或重試。
2. 使用?timestamp
?字段
- 類似?
version
?字段,但使用?TIMESTAMP
?或?DATETIME
?類型。 - 每次更新時自動更新該字段。
- 更新時檢查?
timestamp
?是否匹配。
1.讀取數據:
SELECT id, name, price, update_time FROM product WHERE id = 1;
-- 假設返回: id=1, name='Apple', price=10.00, update_time='2023-10-01 12:00:00'
2.更新數據(帶時間戳檢查):
UPDATE product
SET price = 12.00, update_time = NOW()
WHERE id = 1 AND update_time = '2023-10-01 12:00:00';
3.判斷是否更新成功:
????????如果?update_time
?匹配,則更新成功。
????????否則更新失敗(影響行數為0)。
2.3、如何處理沖突
當樂觀鎖檢測到沖突時(更新失敗),應用程序需要:
- 拋出異常(如?
StaleObjectStateException
)。 - 重試邏輯:重新讀取數據,重新嘗試更新(可能需要限制重試次數)。
代碼示例(Java)
int retryCount = 0;
while (retryCount < MAX_RETRIES) {Product product = getProductFromDatabase(productId); // 包含 versionproduct.setPrice(newPrice);int rowsUpdated = updateProductInDatabase(product); // 使用 version 條件更新if (rowsUpdated == 1) {break; // 更新成功} else {retryCount++;// 可能需要等待一段時間再重試}
}
if (retryCount >= MAX_RETRIES) {throw new RuntimeException("樂觀鎖重試失敗");
}
小結:
????????它適用于讀多寫少或沖突概率低的場景,能有效提高并發性能,但需要業務層配合實現沖突處理邏輯。
如下圖所示:
對比于悲觀鎖
2.4、樂觀鎖局限性
-
需要業務層配合:必須顯式實現版本號檢查和重試邏輯。
-
無法完全避免沖突:在極端高并發下仍可能發生沖突。
-
不適合復雜事務:如果事務涉及多個表,樂觀鎖可能難以維護一致性。
3、冪等性
通過上面對于樂觀鎖的介紹,感覺是不是可以作為冪等性的處理手段呢?
????????樂觀鎖可以作為處理冪等性問題的一種手段,但它的作用和適用范圍需要結合具體場景來看。
3.1、什么是冪等性
冪等性(Idempotency)是指同一個操作多次執行的結果與執行一次的結果相同。
例如:
- 發送重復的支付請求,不會導致重復扣款。
- 提交重復的訂單,不會生成多個訂單。
- 更新資源時,多次相同請求不會改變最終狀態。
冪等性設計的核心目標:防止因網絡重傳、用戶重復點擊、系統故障等原因導致的重復請求對業務邏輯產生副作用。
3.2、樂觀鎖與冪等性的關系
????????樂觀鎖(Optimistic Locking)主要用于解決并發更新時的數據一致性問題,而冪等性解決的是重復請求對業務邏輯的影響。兩者的結合可以增強系統的健壯性。
1. 樂觀鎖如何輔助冪等性?
????????樂觀鎖通過?版本號(version)或時間戳(timestamp)?保證數據更新的原子性,防止并發沖突。
在某些場景下,它可以間接支持冪等性:
- 場景:更新某個資源時,重復的請求可能因版本號不匹配而失敗,避免重復操作。
- 示例:用戶多次提交更新請求,若第一次請求已修改了數據版本號,后續重復請求會因版本號不一致而失敗,從而避免重復操作。
2. 樂觀鎖的局限性
????????樂觀鎖無法直接解決冪等性問題,因為它不處理“重復請求”的識別和過濾。
例如:
- 如果用戶多次提交相同的請求參數(如相同的訂單號、交易號),樂觀鎖無法識別這是重復請求,只會檢查版本號是否沖突。
- 如果請求參數不同(如不同的版本號),樂觀鎖可能允許更新,但業務邏輯可能需要拒絕重復操作。
3.3、如何設計
在實際開發中,通常需要將樂觀鎖與其他冪等性策略結合使用,例如:
- 唯一業務標識符(Business Key)
- 請求ID(Request ID)
- 數據庫唯一約束
- 緩存記錄已處理的請求
示例:支付接口的冪等性設計
假設用戶發起支付請求,接口需要確保同一筆訂單不會被重復扣款:
-- 表結構
CREATE TABLE orders (id INT PRIMARY KEY,order_no VARCHAR(50) UNIQUE, -- 唯一業務標識符amount DECIMAL(10,2),status VARCHAR(20),version INT DEFAULT 0 -- 樂觀鎖版本號
);
處理流程:
- 客戶端發送請求,包含?
order_no
?和?request_id
(唯一請求ID)。 - 服務端處理:
- 檢查緩存或數據庫,是否存在已處理的?
order_no
?或?request_id
。- 如果存在,直接返回結果(冪等性保障)。
- 如果不存在,繼續處理。
- 執行支付操作時,使用樂觀鎖更新訂單狀態:
- 檢查緩存或數據庫,是否存在已處理的?
UPDATE orders
SET status = 'PAID', version = version + 1
WHERE id = ? AND version = ?;
如果更新失敗(版本號不匹配),說明訂單狀態已被其他事務修改,需重試或報錯。
關鍵點:
- 唯一業務標識符(order_no):直接過濾重復請求。
- 請求ID(request_id):記錄已處理的請求,避免重復消費。
- 樂觀鎖(version):防止并發更新導致的數據不一致。
3.4、order_no
?添加唯一約束
1、防止重復創建訂單
????????假設用戶點擊“提交訂單”按鈕多次,或網絡重傳導致相同請求被多次發送。如果沒有唯一約束,可能會導致以下問題:
- 重復插入訂單:系統生成多個相同?
order_no
?的訂單,浪費資源。 - 業務邏輯混亂:例如,重復扣款、重復發貨等。
-- 假設沒有唯一約束
INSERT INTO orders (order_no, amount) VALUES ('20231001-001', 100);
-- 用戶重復提交相同訂單號
INSERT INTO orders (order_no, amount) VALUES ('20231001-001', 100); -- 會成功插入第二條數據!
后果:系統會認為這是兩個不同的訂單,可能導致重復扣款、庫存異常等問題。
2、保證業務邏輯的正確性
order_no
?是業務的核心標識符,如果允許重復,會導致:
- 數據不一致:無法通過?
order_no
?準確查詢或修改訂單。 - 冪等性失效:重復請求無法被攔截,破壞系統的一致性。
示例:
-- 有唯一約束后,第二次插入會失敗
INSERT INTO orders (order_no, amount) VALUES ('20231001-001', 100); -- 成功
INSERT INTO orders (order_no, amount) VALUES ('20231001-001', 100); -- 報錯:Duplicate entry
3、支持冪等性設計
唯一約束是實現冪等性的關鍵手段之一:
- 冪等性:同一請求多次執行的結果與執行一次的結果相同。
- 唯一約束:通過數據庫層強制攔截重復請求,避免業務邏輯重復執行。
示例:
-- 用戶多次提交相同的訂單號
BEGIN TRANSACTION;-- 嘗試插入訂單INSERT INTO orders (order_no, amount) VALUES ('20231001-001', 100);
COMMIT;-- 如果已經存在相同 order_no,會拋出異常,事務回滾,避免重復操作
4、節點故障場景
????????在插入數據的過程中如果發生網絡宕機,處理方式取決于數據庫的事務機制、應用層的容錯設計以及網絡恢復后的重試策略。
以下是詳細的分析和解決方案:
4.1. 數據庫層面
1、事務的原子性
- 如果插入操作被包裹在事務中(例如使用?
BEGIN TRANSACTION
?和?COMMIT
),且數據庫支持事務(如 MySQL 的 InnoDB 引擎):- 網絡中斷時:事務未提交,數據庫會自動回滾未提交的更改。
- 恢復后:需要重新發送插入請求。
- 示例(MySQL):
BEGIN;
INSERT INTO orders (order_no, amount) VALUES ('20231001-001', 100);
-- 網絡中斷,事務未提交,數據不會寫入數據庫
2、自動提交(Autocommit)
- 如果數據庫處于自動提交模式(默認開啟),每次插入操作會立即提交:
- 網絡中斷時:可能已部分提交數據(如部分字段寫入),導致數據不一致。
- 解決方案:在應用層顯式關閉自動提交,手動控制事務邊界。
4.2. 應用層
1、重試機制
- 重試邏輯:在網絡恢復后,客戶端可以重試插入請求。
- 關鍵點:需確保重試操作是冪等的(見下文)。
- 重試策略:
- 指數退避(Exponential Backoff):重試間隔逐漸增大(如 1s → 2s → 4s → ...),避免網絡擁塞。
- 最大重試次數限制:防止無限循環重試(如最多重試 3 次)。
2、冪等性設計
- 唯一約束:通過數據庫的?
UNIQUE
?約束(如訂單號?order_no
)防止重復插入。- 示例:即使重試,只要?
order_no
?唯一,重復插入會失敗,避免數據冗余。
- 示例:即使重試,只要?
- 請求 ID(Request ID):為每個請求生成唯一 ID,記錄已處理的請求。
- 示例:在插入前檢查請求 ID 是否已存在,若存在則直接返回結果。
3、異步消息隊列
- 可靠性隊列:將插入操作放入消息隊列(如 Kafka、RabbitMQ),確保網絡中斷時消息不丟失。
- 生產者:將插入請求發送到隊列,即使網絡中斷,消息仍保留在隊列中。
- 消費者:網絡恢復后,繼續消費消息并執行插入操作。
- 優點:解耦生產與消費,提高系統魯棒性。
4.3. 網絡恢復后的處理
1、客戶端檢測網絡狀態
- 心跳機制:客戶端定期檢測與數據庫的連接狀態。
- 自動重連:網絡恢復后,客戶端自動重新建立連接并重試未完成的請求。
2、服務端日志與監控
- 記錄失敗請求:在服務端記錄失敗的插入請求(如日志或數據庫表),便于人工介入處理。
- 告警通知:通過監控工具(如 Prometheus、Zabbix)檢測異常,及時通知運維人員。
總結: