1. 程式人生 > 實用技巧 >介面冪等性解決方案

介面冪等性解決方案

介面冪等性解決方案

假如有個服務提供了一個訂單支付介面(服務為負載微服務),使用者在前端呼叫時一不小心點選了兩次,生成了兩次支付請求,然後這一筆訂單進行了兩次支付,扣了兩次錢,這就是介面沒有保證冪等性的結果;

冪等性概念

冪等(idempotent、idempotence)是一個數學與計算機學概念,常見於抽象代數中。

在程式設計中,一個冪等操作的特點是其任意多次執行所產生的的影響均與一次執行的影響相同,

冪等函式,冪等方法,是指可以使用相同引數重複執行,並能獲得相同結果的函式

更復雜的操作冪等保證是利用唯一交易號(流水號)實現.

冪等就是一個操作,不論執行多少次,產生的效果和返回的結果都是一樣的

保證冪等性的核心
  • 每個請求都需要有一個唯一的標識
  • 每次處理完請求之後,必須有一個記錄標識這個請求已經被處理過了
  • 每次接收請求之後,處理請求之前,判斷當前請求是否已經被處理過了
技術方案
  • 查詢操作:查詢操作天生就是冪等性操作。查詢一次和查詢多次,在查詢條件不變的情況下,查詢結果是一樣的
  • 刪除操作:刪除操作天生就是冪等性操作。刪除一次和刪除多次, 在刪除條件不變的情況下,刪除的返回結果可能不同,但是最終的資料庫刪除結果是相同的(第一次刪除成功,第二次會返回刪除失敗,最終結果還是刪除成功,第一次刪除失敗,第二次刪除失敗,最終結果依然是刪除失敗)
  • 新增操作:新增操作可以通過資料庫中的唯一值進行校驗,從而保證只能新增一次
  • 更新操作:更新操作可以通過在資料庫中設定遞增的版本號來實現,如果版本號已被更改,那麼就不能就不能進行第二次更新操作
常見解決方案
  • 業務表內使用演算法生成唯一索引
  • 業務表內設定一個註冊機機制
  • 業務表內增加version(mybatis plus已實現),進行基於版本號的更新
  • 基於mysql/redis的機制進行解決
基於redis的keyvalue機制實現

建立一個註解,用在需要進行防止重複點選的方法介面上


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
 * 冪等性註解,預設五秒重複點選
 * @author windrunner9527
 */
// 該註解可以放在方法上
@Target(ElementType.METHOD)
// 該註解程式執行時也在
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotency {

    String value() default "5";

}

對這個註解進行切面


import com.windrunner9527.idempotency.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 對冪等性註解進行切面
 * @author windrunner9527
 */
// 日誌
@Slf4j
// 切面
@Aspect
// 新增spring容器
@Component
// 開啟代理
@EnableAspectJAutoProxy
public class IdempotencyAspect {

    /**
     * 獲取重入鎖
     */
    Lock lock = new ReentrantLock();


    /**
     * 對使用了冪等性註解的方法進行方法切面
     */
    @Pointcut("@annotation(Idempotency)")
    public void cupPoint(){

    }


    /**
     * 對上面的方法進行環繞
     * @param point
     * @return
     * @throws Throwable
     */
    @Around("cupPoint()")
    public Object idempotency(ProceedingJoinPoint point) throws Throwable {
        // 獲取servlet上下文資訊屬性
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        // 獲取request和response
        HttpServletRequest request = requestAttributes.getRequest();
        HttpServletResponse response = requestAttributes.getResponse();

        // 獲取當前執行的方法的資訊
        Method method = ((MethodSignature) point.getSignature()).getMethod();
        // 獲取當前方法上的idmpotency註解
        Idempotency annotation = method.getAnnotation(Idempotency.class);
        // 獲取當前方法上的idmpotency的值
        String time = annotation.value();
        log.info("當前方法設定重複點選冪等性時間為:"+time+"秒");

        // 從請求頭中獲取認證的token
        String authentication = request.getHeader("Authentication");
        // 如果請求頭中沒有獲取token,那麼就返回Message
        if (authentication == null){
            return "當前請求頭中沒有攜帶token,請登入後使用本功能";
        }
        // 獲取當前請求呼叫的是哪個請求路徑
        String servletPath = request.getServletPath();
        log.info("當前使用者請求的路徑引數:"+servletPath);

        // 拼接token和當前的請求路徑
        String res = authentication + servletPath;

        //上鎖,防止多次獲取值
        lock.lock();
        // 如果當前有這個key,那麼說明這個方法已經被這個token的攜帶者點選過了,進行冪等性判斷,不能連續點選
        if (RedisUtils.hasKey(res)){
            log.warn("點選速度過快,請稍後重試");
            return "點選速度過快,請稍後重試";
        }
        // 如果當前沒有這個key,那麼將這個key放到redis中,並加上過期時間
        RedisUtils.set(res,null,Long.parseLong(time));
        // 解鎖
        lock.unlock();

        // 執行被切面的方法
        Object proceed = point.proceed();

        // 返回方法執行後的值
        return proceed;
    }

}

當需要使用防止重複點選時,將這個註解方法放置到需要使用的controller上,就可以控制時效和點選頻率