Java冪等性的4種解決方案實(shí)戰(zhàn)講解(附通俗案例)
1. 什么是冪等?
冪等性(Idempotence):
在分布式、高并發(fā)場景中,同一操作無論執(zhí)行一次還是執(zhí)行多次,其對(duì)系統(tǒng)的最終影響是一樣的。
通俗解釋:
你點(diǎn)了個(gè)外賣,點(diǎn)了之后系統(tǒng)扣了你賬戶的錢。網(wǎng)絡(luò)延遲了,你又點(diǎn)了一次。
如果系統(tǒng)不控制冪等,平臺(tái)就會(huì)給你扣兩次錢,要求商家送兩份外賣。
所以,后端系統(tǒng)要判斷:
“這是不是重復(fù)請(qǐng)求?是不是已經(jīng)處理過了?”
如果是,直接返回成功;如果不是,才繼續(xù)處理。
Tips:“冪等性”和 “我想點(diǎn)多份外賣”到底有什么區(qū)別?
這兩個(gè)看起來都可能發(fā)送多個(gè)請(qǐng)求,但業(yè)務(wù)語義和系統(tǒng)處理方式完全不同。
一句話區(qū)別:
| 概念 | 是重復(fù)請(qǐng)求? | 用戶想干嘛? | 后端該處理幾次? |
|---|---|---|---|
| 冪等性 | 是(用戶誤操作或系統(tǒng)重試) | 用戶只想下一個(gè)訂單 | 只處理一次 |
| 我要點(diǎn)多份外賣 | 否(用戶有意多次下單) | 用戶想下多個(gè)訂單 | 處理多次 |
場景對(duì)比解釋
冪等性場景(系統(tǒng)要"去重")
你點(diǎn)了一次外賣按鈕,但由于網(wǎng)絡(luò)慢,或者手抖點(diǎn)了兩下,系統(tǒng)收到了兩個(gè)一模一樣的下單請(qǐng)求。
但你心里是想買一份,不是兩份。
系統(tǒng)這時(shí)必須判斷:“這些請(qǐng)求是不是重復(fù)的?是不是我們已經(jīng)處理過了?”
如果是重復(fù)的,就不能再扣一次錢,不能再發(fā)一份外賣。
這就是冪等性要解決的核心問題:重復(fù)請(qǐng)求,只處理一次。
用戶主動(dòng)下多個(gè)訂單(系統(tǒng)不能去重)
你今天太餓了,就是想點(diǎn)兩份外賣:一份給自己吃,一份留著晚上吃。
所以你在 APP 上點(diǎn)了一次 → 下了一單;又點(diǎn)了一次 → 又下一單。
這兩次請(qǐng)求雖然是一樣的商品、一樣的地址,但它們是你主動(dòng)發(fā)出的兩個(gè)下單請(qǐng)求。
系統(tǒng)這時(shí)不能把這兩次請(qǐng)求當(dāng)成“重復(fù)”來過濾掉。
每次都要扣一次錢,生成一個(gè)訂單,通知商家送餐。
再簡單點(diǎn)說
| 比喻 | 冪等性 | 多份外賣 |
|---|---|---|
| 行為動(dòng)機(jī) | “我點(diǎn)了一次,但系統(tǒng)誤以為我點(diǎn)了多次” | “我自己就點(diǎn)了兩次” |
| 你心里的目標(biāo) | 只想買一份 | 就是要買兩份 |
| 系統(tǒng)正確做法 | 識(shí)別重復(fù)請(qǐng)求,只處理一次 | 每次都要處理,不能去重 |
一句話總結(jié)
冪等性是系統(tǒng)幫你“防止你不小心多下單”;
而“我要點(diǎn)多份外賣”是你故意多下單,系統(tǒng)必須每次都處理。
2. 實(shí)際例子:外賣平臺(tái)“確認(rèn)收貨”
我們來用“外賣平臺(tái)確認(rèn)收貨按鈕”這個(gè)更容易理解的例子:
業(yè)務(wù)背景:
用戶下單外賣,騎手送到之后,點(diǎn)擊“確認(rèn)收貨”按鈕。
后端的處理邏輯大致如下:
1. 更新訂單狀態(tài)為“已完成”
2. 給騎手發(fā)放配送提成
3. 給商家結(jié)算費(fèi)用
4. 給用戶發(fā)放優(yōu)惠券
問題來了:
用戶點(diǎn)了多次“確認(rèn)收貨”怎么辦?或者說用戶網(wǎng)絡(luò)不好點(diǎn)了2次?甚至 App 自動(dòng)重發(fā)了請(qǐng)求怎么辦?
如果沒處理冪等,就會(huì)導(dǎo)致:
- 訂單狀態(tài)多次更新
- 騎手提成發(fā)多次
- 商家收到多筆錢
- 用戶領(lǐng)好幾張券
→ 系統(tǒng)直接崩了,賬全亂了。
3. 數(shù)據(jù)庫設(shè)計(jì)
我們先準(zhǔn)備兩張表模擬場景:
-- 用戶表
CREATE TABLE t_user (
id VARCHAR(50) PRIMARY KEY,
name VARCHAR(50),
coupon_count INT DEFAULT 0
);
-- 訂單表
CREATE TABLE t_order (
id VARCHAR(50) PRIMARY KEY,
user_id VARCHAR(50),
status VARCHAR(20), -- "待收貨"、"已完成"
version BIGINT DEFAULT 0
);
初始化數(shù)據(jù):
-- 用戶張三,沒有優(yōu)惠券
INSERT INTO t_user VALUES ('u1', '張三', 0);
-- 一筆待收貨訂單
INSERT INTO t_order VALUES ('o1', 'u1', '待收貨', 0);
4. 冪等的四種解決方案實(shí)戰(zhàn)講解
方案一:UPDATE帶條件字段控制(最常用)
原理:
利用數(shù)據(jù)庫行級(jí)鎖 + WHERE status = '待收貨' 來保證只有一次能成功更新狀態(tài)。
實(shí)現(xiàn)步驟:
@Transactional
public String confirmOrder(String orderId) {
// 查詢訂單狀態(tài)
Order order = orderMapper.selectById(orderId);
if ("已完成".equals(order.getStatus())) {
return "SUCCESS";
}
// 核心:只更新待收貨的訂單
int rows = orderMapper.updateStatusIfUnfinished(orderId, "已完成", "待收貨");
if (rows == 1) {
// 發(fā)優(yōu)惠券
userMapper.addCoupon(order.getUserId(), 1);
return "SUCCESS";
} else {
throw new RuntimeException("系統(tǒng)繁忙,請(qǐng)稍后重試");
}
}
并發(fā)測試:
100 并發(fā)同時(shí)確認(rèn)訂單
最終結(jié)果:
- 訂單狀態(tài):已完成
- 用戶優(yōu)惠券數(shù)量:1(不會(huì)重復(fù)發(fā))
方案二:樂觀鎖version控制(適用于高并發(fā))
原理:
每次操作都要帶上 version 字段,只有匹配時(shí)才更新。
實(shí)現(xiàn)步驟:
@Transactional(rollbackFor = Exception.class)
public String handleRecharge(String rechargeId) {
// 根據(jù)rechargeId去找充值記錄,如果已處理過,則直接返回成功
RechargePO rechargePo = rechargeMapper.selectById(rechargeId);
if (rechargePo == null) {
throw new IllegalArgumentException("充值記錄不存在");
}
// 充值記錄已處理過,直接返回成功
if (rechargePo.getStatus() == 1) {
return "SUCCESS";
}
// 開啟Spring事務(wù)(由 @Transactional 控制)
// 在where后面要加 status = 0 這個(gè)條件;count表示影響行數(shù)
int count = rechargeMapper.updateStatusToProcessed(rechargeId, 0, 1);
// count = 1,表示上面sql執(zhí)行成功
if (count != 1) {
// 走到這里,說明有并發(fā),直接拋出異常
throw new RuntimeException("系統(tǒng)繁忙,請(qǐng)重試");
} else {
// 給賬戶加錢
accountMapper.increaseBalance(rechargePo.getAccountId(), rechargePo.getPrice());
}
// 提交Spring事務(wù)(由 @Transactional 控制)
return "SUCCESS";
}
并發(fā)測試:
最終結(jié)果:
- 狀態(tài):已完成
- 優(yōu)惠券:1
方案三:唯一約束表控制冪等(通用方案)
原理:
在操作前插入一條帶有唯一索引的冪等 key,插入失敗說明是重復(fù)請(qǐng)求。
新增輔助表:
CREATE TABLE t_idempotent (
id VARCHAR(50) PRIMARY KEY,
idempotent_key VARCHAR(100) NOT NULL,
UNIQUE KEY uq_idempotent_key (idempotent_key)
);
實(shí)現(xiàn)步驟:
@Transactional
public String confirmOrderWithUniqueKey(String orderId) {
String idempotentKey = "order_confirm:" + orderId;
try {
idempotentMapper.insert(idempotentKey);
} catch (DuplicateKeyException e) {
return "SUCCESS";
}
Order order = orderMapper.selectById(orderId);
if ("已完成".equals(order.getStatus())) return "SUCCESS";
orderMapper.updateStatus(orderId, "已完成");
userMapper.addCoupon(order.getUserId(), 1);
return "SUCCESS";
}
并發(fā)測試:
最終結(jié)果:
- 插入
t_idempotent成功 1 次 - 優(yōu)惠券發(fā)放一次
- 插入
方案四:分布式鎖(適合跨服務(wù)或非數(shù)據(jù)庫操作)
原理:
利用 Redis 鎖來控制并發(fā),只允許一個(gè)線程進(jìn)入。
實(shí)現(xiàn)步驟(偽代碼):
public String confirmOrderWithRedisLock(String orderId) {
String lockKey = "lock:order:confirm:" + orderId;
if (!redisLock.tryLock(lockKey, 5秒)) {
throw new RuntimeException("系統(tǒng)繁忙,請(qǐng)稍后重試");
}
try {
// 冪等處理邏輯
...
} finally {
redisLock.release(lockKey);
}
}
并發(fā)測試:
- Redis 鎖只允許 1 個(gè)線程進(jìn)入
- 其余請(qǐng)求返回“系統(tǒng)繁忙”,防止重復(fù)處理
5. 四種方案對(duì)比總結(jié)
| 方案 | 是否通用 | 可靠性 | 成本 |
|---|---|---|---|
| 方案1:UPDATE + 條件 | 僅 DB 操作 | 高 | 低 |
| 方案2:樂觀鎖 version | 僅 DB 操作 | 高 | 中 |
| 方案3:唯一約束表 | 通用 | 高 | 中 |
| 方案4:Redis 鎖 | 非 DB 操作場景 | 高 | 較高 |
6. 實(shí)戰(zhàn)建議 & 常見套路
- 接口冪等性建議統(tǒng)一封裝,如方案3可以做成注解 + 攔截器模式
支付回調(diào)、確認(rèn)訂單、接口重試場景、MQ消費(fèi)等業(yè)務(wù)必須加冪等控制- 數(shù)據(jù)庫層冪等優(yōu)先,跨服務(wù)冪等使用分布式鎖或唯一冪等表
總結(jié)
到此這篇關(guān)于Java冪等性的4種解決方案的文章就介紹到這了,更多相關(guān)Java冪等性解決方案內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Boot 2 實(shí)戰(zhàn):自定義啟動(dòng)運(yùn)行邏輯實(shí)例詳解
這篇文章主要介紹了Spring Boot 2 實(shí)戰(zhàn):自定義啟動(dòng)運(yùn)行邏輯,結(jié)合實(shí)例形式詳細(xì)分析了Spring Boot 2自定義啟動(dòng)運(yùn)行邏輯詳細(xì)操作技巧與注意事項(xiàng),需要的朋友可以參考下2020-05-05
SpringBoot結(jié)合Redis實(shí)現(xiàn)緩存
本文主要介紹了SpringBoot結(jié)合Redis實(shí)現(xiàn)緩存,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06
java實(shí)現(xiàn)excel導(dǎo)出合并單元格的步驟詳解
這篇文章主要介紹了java實(shí)現(xiàn)excel導(dǎo)出合并單元格,通過使用Apache POI庫,我們可以方便地創(chuàng)建Excel文件、填充數(shù)據(jù)、合并單元格和導(dǎo)出Excel文件,需要的朋友可以參考下2023-04-04
java網(wǎng)絡(luò)編程之socket網(wǎng)絡(luò)編程示例(服務(wù)器端/客戶端)
這篇文章主要介紹了java socket網(wǎng)絡(luò)編程的示例,分為服務(wù)器端和客戶端,大家參考使用吧2014-01-01
Java多線程中的ThreadPoolExecutor使用解析
這篇文章主要介紹了Java多線程中的ThreadPoolExecutor使用解析,作為線程池的緩沖,當(dāng)新增線程超過maximumPoolSize時(shí),會(huì)將新增線程暫時(shí)存放到該隊(duì)列中,需要的朋友可以參考下2023-12-12

