前言
今天想聊一聊冪等相關的知識,以及實作一個冪等公共組件需要重點涉及和思考的點,
概念
首先,什么是冪等,在實際代碼生產程序中有什么作用呢?
在編程中一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同,
舉個例子,假如有個方法,用于修改一個訂單的狀態為已完成,只改一個狀態欄位,要達到冪等的效果我們可以這樣:
- 每次執行都正真執行更新陳述句介面,結果都是狀態保持已完成
- 每次執行先判斷訂單狀態是否已經是已更新狀態,如果是就回傳,如果不是就執行更新陳述句,也過也是保持已完成狀態
所以,一個擁有冪等性的業務代碼,就可以保證外部重復呼叫的結果和單次呼叫的結果一致,保證這一點在實際代碼生產中的一些場景中是非常重要的,以下做一些例舉:
- 客戶端并發重復提交,這種比較常見,用戶連續點擊按鈕即可觸發重復提交
- 微服務架構中Http或RPC請求呼叫失敗觸發重試
- 訊息中間件重復消費,訊息中間件本身就是通過重復消費達到業務解耦和一致性的,所以使用訊息是必然需要考慮冪等情況的
- 呼叫方定時任務重復呼叫或者上游觸發歷史業務請求,從不信任外部的角度看,有時也需要考慮
總結一下:
At least once + 冪等 = exactly once
邏輯
一下是使用較多的冪等方案的流程圖如下:

- 一個微服務系統中在進入業務執行前必須要保證拿到分布式鎖,這樣才能屏蔽掉并發重復請求
- 執行業務和冪等標記的存盤需要保證原子性,才能保證不會出現冪等標記和業務變更的資料不一致情況的發生,否則這個冪等標記就沒有意義了
公共組件
公共組件的例子以Spring為基礎,其中會使用到Spring相關的組件,
設計
根據前面的流程圖,使用AOP非常適合實作一個公共組件,

代碼
定義注解提供給業務使用:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
/**
* Name used to determine the target idempotent key prefix
*/
String name();
/**
* support Spring Expression Language (SpEL)
*/
String key();
/**
* set idempotent key expire time, default 0 second
*/
long idempotentExpire() default 0;
/**
* set lock expire time default 60 seconds
*/
long lockExpire() default 60;
}
- 注解中的資訊包含冪等key,鎖過期時間,冪等保護時間,注意這里的key支持SpEL,這樣就可以非常方便的可以把方法中的引數作為key的一部分
通過注解切面,核心代碼如下:
public <T> T execute(IdempotentRequest request, Supplier<T> processSupplier, Supplier<T> failSupplier) {
String idempotentKey = request.getKey();
long idempotentExpire = request.getIdempotentExpire();
long lockExpire = request.getLockExpire() == 0 ? DEFAULT_LOCK_TIME : request.getLockExpire();
IdempotentRecordStorage idempotentRecordStorage = getIdempotentRecordStorage(idempotentExpire);
try {
boolean locked = redisLockService.lock(idempotentKey, LOCK_VALUE, lockExpire, true);
if (locked) {
if (idempotentRecordStorage.hasKey(idempotentKey)) {
return failSupplier.get();
}
T result = processSupplier.get();
idempotentRecordStorage.setKey(idempotentKey, idempotentExpire);
return result;
} else {
return failSupplier.get();
}
} finally {
redisLockService.unlock(idempotentKey, LOCK_VALUE, true);
}
}
看一下Oracle 的實作例子:
public class OracleStorage implements IdempotentRecordStorage {
private JdbcTemplate jdbcTemplate;
public OracleStorage(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public void setKey(String key, long expire) {
Date expireDate = expire == 0 ? null : new Date(System.currentTimeMillis() + expire * 1000);
String sql = "insert into AAP_IDEMPOTENT_RECORD(ID, KEY, CREATE_TIME, EXPIRE_TIME) values(IDEMPOTENT_RECORD_SEQUENCE.nextval, ?,?,?)";
jdbcTemplate.update(sql, key, new Date(), expireDate);
}
@Override
public boolean hasKey(String key) {
String sql = "select count(1) from AAP_IDEMPOTENT_RECORD WHERE KEY = ?";
Integer value = https://www.cnblogs.com/killbug/p/jdbcTemplate.queryForObject(sql, Integer.class, key);
return value > 0;
}
@Override
public StorageTypeEnum getType() {
return StorageTypeEnum.ORACLE;
}
}
思考
在實際coding的程序中有幾個有意思的點:
- 1,想把冪等記錄操作和業務操作放入一個事務內,才能保證前面圖中的原子操作,而一般我們會使用@Transaction注解,這個注解也和我們一樣使用AOP實作,所以問題就來了,兩個切面的順序性需要做準確的調整,因為我的例子專案里沒有設定@Transaction切面的
order,所以默認是Integer.MAX_VALUE,自定義的切面也默認是Integer.MAX_VALUE,所以就出現了@Transaction注解在內層,導致變成兩個事務的提交,而不能保證原子性,調整順序方式:@EnableTransactionManagement(order = Ordered.LOWEST_PRECEDENCE - 100) - 2,當我以為業務操作和冪等操作在一個事務的時候我產生了一個疑惑,冪等操作自己提前會先提交嗎?如果會的話,那又保證不了原子了,這里注意使用的是
jdbcTemplate,底層還是會和@Transaction注解一樣拿到相同的Connection,所以可以達到一起提交的能力, - 3,如果使用Redis做冪等資料的操作,那么就需要額外考慮保證原子性的方法,比如在setKey的位置實際執行成功,但是回傳網路問題拋出例外,前面業務操作的事會被回滾,但是冪等資料實際已經存在的問題,為了解決這個問題,更傾向于提供給使用方決定何種情況下需要清楚冪等資料,這里代碼沒有提供,需要補充,
以上是個人的一些思考,實作代碼放在github,歡迎交流:
https://github.com/dchack/crab
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/502888.html
標籤:Java
上一篇:JSP基礎知識總結
下一篇:Java---Lambda
