在分布式系統中,接口冪等性是一個非常重要的概念,它保證了在同樣的條件下,同一請求的多次執行所產生的效果都是相同的。在實際開發中,為了防止重復提交或者重復操作帶來的問題,我們需要考慮如何實現接口冪等性。
下面我將介紹如何在 SpringBoot + MySQL + MyBatisPlus + Druid 的環境下實現接口冪等性。
- 什么是接口冪等性?
接口冪等性是指,對于相同的輸入,接口的輸出結果應該相同。換句話說,如果接口已經處理了一個請求并返回了結果,那么在相同的輸入條件下,該接口的后續請求應該返回相同的結果,而不會產生任何新的副作用。
- 如何實現接口冪等性?
要實現接口冪等性,需要考慮以下幾個方面:
- 請求唯一標識:每個請求都應該有一個唯一的標識,可以是請求參數的組合或者是一個單獨的參數。
- 冪等性校驗:每次請求到達服務器時,服務器需要判斷該請求是否已經被處理過,如果已經被處理過,則直接返回處理結果,否則執行請求操作,并記錄請求的唯一標識,以便后續的冪等性校驗。
在 SpringBoot + MySQL + MybatisPlus + Druid 的環境下,我們可以通過以下方式實現接口冪等性:
- 在請求參數中添加一個冪等性校驗碼(比如 UUID),用于唯一標識每個請求。
- 在請求處理前,先查詢冪等性校驗碼是否已經存在于數據庫中,如果存在則說明該請求已經被處理過,直接返回結果。
- 如果冪等性校驗碼不存在于數據庫中,則執行請求操作,并將冪等性校驗碼插入到數據庫中。
下面是實現接口冪等性的示例代碼:
在請求參數中添加一個冪等性校驗碼:
public class RequestDTO {
private String idempotenceKey;
// other request fields and methods
}
在 MybatisPlus 中創建對應的實體類:
@Data
@TableName("idempotence_key")
public class IdempotenceKey {
@TableId(type = IdType.ASSIGN_UUID)
private String id;
private String key;
private Date createTime;
}
在 Controller 中實現冪等性校驗:
@RestController
public class UserController {
@Autowired
private UserService userService;
@PostMApping("/user")
public String createUser(@RequestBody RequestDTO request) {
// 冪等性校驗
if (checkIdempotence(request.getIdempotenceKey())) {
return "success";
}
// 執行請求操作
userService.createUser(request);
// 插入冪等性校驗碼
saveIdempotence(request.getIdempotenceKey());
return "success";
}
}
在 Service 中實現冪等性校驗和插入冪等性校驗碼:
@Service
public class UserService {
@Autowired
private IdempotenceKeyMapper idempotenceKeyMapper;
public void createUser(RequestDTO request) {
// 創建用戶
// ...
}
private boolean checkIdempotence(String key) {
IdempotenceKey idempotenceKey = idempotenceKeyMapper.selectOne(new LambdaQueryWrapper<IdempotenceKey>().eq(IdempotenceKey::getKey, key));
return idempotenceKey != null;
}
private void saveIdempotence(String key) {
IdempotenceKey idempotenceKey = new IdempotenceKey();
idempotenceKey.setKey(key);
idempotenceKey.setCreateTime(new Date());
idempotenceKeyMapper.insert(idempotenceKey);
}
}
這里使用了 MybatisPlus 的 LambdaQueryWrapper 進行查詢,并使用自動生成的 UUID 作為冪等性校驗碼。
全局實現冪等性校驗可以使用AOP(面向切面編程)來實現,在方法執行前先進行冪等性校驗,如果已經執行過該方法,則直接返回結果??梢酝ㄟ^自定義注解來標記需要進行冪等性校驗的方法。
以下是一個簡單的示例代碼:
- 自定義注解 Idempotent:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
long expireSeconds() default 60;
}
- 編寫 AOP 切面,用于攔截帶有 @Idempotent 注解的方法:
@Aspect
@Component
public class IdempotentAspect {
@Autowired
private IdempotenceKeyMapper idempotenceKeyMapper;
@Pointcut("@annotation(com.example.demo.annotation.Idempotent)")
public void idempotentPointcut() {}
@Around("idempotentPointcut()")
public Object idempotentAround(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Idempotent idempotent = method.getAnnotation(Idempotent.class);
String key = getKey(point);
if (StringUtils.isBlank(key)) {
throw new RuntimeException("冪等性校驗碼不能為空");
}
if (checkIdempotence(key)) {
throw new RuntimeException("請勿重復操作");
}
saveIdempotence(key, idempotent.expireSeconds());
return point.proceed();
}
private boolean checkIdempotence(String key) {
IdempotenceKey idempotenceKey = idempotenceKeyMapper.selectOne(new LambdaQueryWrapper<IdempotenceKey>().eq(IdempotenceKey::getKey, key));
return idempotenceKey != null;
}
private void saveIdempotence(String key, long expireSeconds) {
IdempotenceKey idempotenceKey = new IdempotenceKey();
idempotenceKey.setKey(key);
idempotenceKey.setCreateTime(new Date());
idempotenceKey.setExpireTime(new Date(System.currentTimeMillis() + expireSeconds * 1000));
idempotenceKeyMapper.insert(idempotenceKey);
}
private String getKey(ProceedingJoinPoint point) {
Object[] args = point.getArgs();
if (args.length == 0) {
return null;
}
return args[0].toString();
}
}
- 在需要進行冪等性校驗的方法上添加 @Idempotent 注解:
@Service
public class UserService {
@Autowired
private IdempotenceKeyMapper idempotenceKeyMapper;
@Idempotent(expireSeconds = 60)
public void createUser(String username) {
// 創建用戶
// ...
}
}
通過以上方式,在方法執行前會先進行冪等性校驗,如果已經執行過該方法,則直接返回結果,不會再次執行。
在實際應用中,需要考慮一些特殊情況的處理,以提高冪等性校驗的準確性和可靠性。下面列舉一些可能遇到的情況:
- 請求超時處理:由于冪等性校驗碼是有過期時間的,如果客戶端發起的請求在冪等性校驗碼過期后才到達服務器,那么該請求就不應該再被視為重復請求。為了解決這個問題,可以在冪等性校驗碼表中記錄請求的時間戳,并在校驗冪等性校驗碼時進行時間戳比較,以判斷請求是否超時。
在冪等性校驗碼表中添加一個請求時間戳的字段,將請求時間戳一并存儲,以便在校驗冪等性校驗碼時進行時間戳比較。
CREATE TABLE `idempotent_key` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵ID',
`key` varchar(128) NOT NULL COMMENT '冪等性校驗碼',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`expire_time` datetime NOT NULL COMMENT '過期時間',
`request_time` datetime NOT NULL COMMENT '請求時間',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_key` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='冪等性校驗碼表';
在進行冪等性校驗時,需要先判斷冪等性校驗碼是否過期,如果過期則不再進行校驗。
public void processRequest() {
String key = generateIdempotentKey();
LocalDateTime now = LocalDateTime.now();
LocalDateTime expireTime = now.plusMinutes(5);
LocalDateTime requestTime = now;
// 將冪等性校驗碼和請求時間戳存入數據庫中
idempotentKeyDao.insert(key, expireTime, requestTime);
// 判斷請求是否過期
LocalDateTime threshold = now.minusMinutes(5);
if (requestTime.isBefore(threshold)) {
// 請求已經過期,不再進行冪等性校驗
return;
}
// 進行冪等性校驗
boolean success = idempotentKeyDao.checkAndUpdate(key);
if (!success) {
// 冪等性校驗失敗
return;
}
// 執行業務操作
// ...
}
- 高并發下的冪等性校驗:在高并發場景下,多個請求可能同時到達服務器進行冪等性校驗,這時需要保證校驗的準確性和唯一性??梢酝ㄟ^對冪等性校驗碼進行唯一索引的方式來保證每個冪等性校驗碼只會出現一次,避免多個請求同時通過校驗。
在冪等性校驗碼表的 key 字段上添加唯一索引,以保證每個冪等性校驗碼只會出現一次。
ALTER TABLE `idempotent_key` ADD UNIQUE INDEX `uk_key` (`key`);
在進行冪等性校驗時,需要使用數據庫的唯一索引進行校驗。
public boolean checkAndUpdate(String key) {
// 利用數據庫的唯一索引保證冪等性校驗碼的唯一性
int affectedRows = jdbcTemplate.update(
"UPDATE idempotent_key SET request_count = request_count + 1 WHERE key = ?",
key);
return affectedRows == 1;
}
- 冪等性校驗碼的重復利用:在一些場景下,比如一個請求執行失敗需要重試,或者用戶進行了一些撤銷操作后需要再次執行該操作等,冪等性校驗碼可能會被多次使用。為了避免重復利用同一個冪等性校驗碼導致的校驗失效,可以對冪等性校驗碼進行標記,標記該校驗碼已被使用過,避免再次使用。
在冪等性校驗碼表中添加一個 used 字段,標記該冪等性校驗碼是否已被使用過。
在進行冪等性校驗時,需要判斷該冪等性校驗碼是否已經被使用過,如果已經被使用過,則不再進行校驗。
public boolean checkAndUpdate(String key) {
// 判斷冪等性校驗碼是否已經被使用過
boolean used = jdbcTemplate.queryForObject(
"SELECT used FROM idempotent_key WHERE key = ?",
Boolean.class,
key);
if (used) {
// 冪等性校驗碼已經被使用過,不再進行校驗
return true;
}
// 將冪等性校驗碼標記為已使用
int affectedRows = jdbcTemplate.update(
"UPDATE idempotent_key SET used = true WHERE key = ?",
key);
return affectedRows == 1;
}
冪等性校驗碼的生成規則:冪等性校驗碼的生成規則也需要考慮,應該根據業務的特點來確定??梢圆捎秒S機數、UUID、請求參數哈希等方式生成冪等性校驗碼。需要保證冪等性校驗碼在相同的請求條件下生成的結果一致。
在分布式環境下,需要保證不同實例之間共享冪等性校驗碼的狀態??梢允褂?redis 等分布式緩存來存儲冪等性校驗碼狀態。
public boolean checkAndUpdate(String key) {
// 從 Redis 中獲取冪等性校驗碼的狀態
boolean used = redisTemplate.opsForValue().get(key);
if (used) {
// 冪等性校驗碼已經被使用過,不再進行校驗
return true;
}
// 將冪等性校驗碼標記為已使用
redisTemplate.opsForValue().set(key, true);
// 執行業務操作
// ...
return true;
}
需要注意的是,由于 Redis 中存儲的數據可能會被意外刪除或過期,因此在使用 Redis 作為冪等性校驗碼狀態存儲介質時,需要考慮數據丟失或過期的情況,確保系統的可靠性和正確性。