JAVA 通過(guò)Redis伍伤、注解和切面的形式實(shí)現(xiàn)接口冪等

一并徘、先看場(chǎng)景:

  1. 填寫(xiě)完頁(yè)面表單數(shù)據(jù),手抖或者惡意在極短的時(shí)間內(nèi)連續(xù)多次調(diào)用保存操作扰魂,表中出現(xiàn)了業(yè)務(wù)數(shù)據(jù)完全重復(fù)的數(shù)據(jù)麦乞,只有ID不一樣。
  2. 老生常談的付款操作劝评,正常操作姐直,我們只觸發(fā)一次扣款操作,即使遇到其他的情況發(fā)生了多次扣款蒋畜,但是也只應(yīng)該扣款一次声畏。
  3. ...

不同的場(chǎng)景,需要不同的冪等操作方式實(shí)現(xiàn)百侧。
今天主要針對(duì)砰识,上述第一種場(chǎng)景能扒,通過(guò)注解+Redis+aop切面的形式處理佣渴。

二、擼碼

廢話(huà)不多說(shuō)初斑,直接擼碼辛润。

定義注解

package com.aida.annotation.common.annotation;

import com.aida.annotation.common.aspect.em.VariableProvider;

import java.lang.annotation.*;

/**
 * 防止重復(fù)提交
 *
 * @author Mr.SoftRock
 * @Date 2021/7/13 17:14
 **/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {

    /**
     * 參數(shù)的提供方式
     * @return
     */
    VariableProvider variableProvider() default VariableProvider.PATH_VARIABLE;

    /**
     * 待校驗(yàn) 屬性或者變量名稱(chēng)
     * @return
     */
    String variableName();

    /**
     * 參數(shù)變量位置
     * @return
     */
    int variablePosition() default 0;

    /**
     * 要切的資源名稱(chēng),用于描述接口功能
     * @return
     */
    String name()  default "";

    /**
     * key 前綴
     * @return
     */
    String prefix() default "";

    /**
     * 時(shí)間顯示
     * 這個(gè)參數(shù)见秤,我們可以隨便設(shè)定砂竖,默認(rèn)單位是 秒
     * 可以根據(jù)不通的業(yè)務(wù)要求去設(shè)定
     * @return
     */
    int period();

}

變量提供方式枚舉VariableProvider
這個(gè)地方,可以去根據(jù)自己的實(shí)際業(yè)務(wù)去擴(kuò)展鹃答。

package com.aida.annotation.common.aspect.em;

/**
 * 變量提供方式
 *
 * @author Mr.SoftRock
 * @Date 2021/7/13 17:23
 **/
public enum VariableProvider {
    /**
     * 通過(guò)PATH路徑提供
     * url/{p}
     */
    PATH_VARIABLE,

    /**
     * 通過(guò)請(qǐng)求參數(shù)提供
     * url?p=1
     */
    REQUEST_PARAMETER,

    /**
     * 通過(guò)請(qǐng)求體提供
     */
    REQUEST_BODY,
}

定義切面類(lèi):

package com.aida.annotation.common.aspect;

import com.aida.annotation.common.annotation.RepeatSubmit;
import com.aida.annotation.common.aspect.em.VariableProvider;
import com.aida.annotation.common.redis.CommonRedisCache;
import com.aida.annotation.common.utils.ServletUtils;
import com.aida.annotation.support.security.AccountPrincipalUtils;
import com.aida.annotation.support.security.userdetails.AccountPrincipal;
import com.baomidou.mybatisplus.core.toolkit.SystemClock;
import com.google.common.collect.ImmutableList;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 1乎澄、自定義業(yè)務(wù)防止重復(fù)提交切面 在controller層注入,標(biāo)記key變量獲取的方式和變量名稱(chēng)
 * 2测摔、本切面主要是用來(lái)識(shí)別解析得到的key
 * 3置济、將獲取到的key,根據(jù)業(yè)務(wù)規(guī)則去執(zhí)行相應(yīng)的處理
 * 4锋八、如果判斷重復(fù)操作浙于,直接斷言出異常
 *
 * @author Mr.SoftRock
 * @Date 2021/7/13 17:28
 **/
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {

    public final String CACHE_REPEAT_KEY = "repeatSubmitData:";
    public final String REPEAT_TIME = "repeatTime";
    public final String REPEAT_PARAMS = "repeatParams";

    @Autowired
    CommonRedisCache redisCache;

    @Before("@annotation(repeatSubmit)")
    public void repeatSubmitCheck(JoinPoint joinPoint, RepeatSubmit repeatSubmit) {
        AccountPrincipal handler = AccountPrincipalUtils.getCurrentHandler();

        String aopTarget = this.getAopTarget(joinPoint);
        HttpServletRequest request = ServletUtils.getRequest();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method signatureMethod = signature.getMethod();
        RepeatSubmit limit = signatureMethod.getAnnotation(RepeatSubmit.class);
        int period = limit.period();

        //ImmutableList是一個(gè)不可變、線(xiàn)程安全的列表集合挟纱,它只會(huì)獲取傳入對(duì)象的一個(gè)副本羞酗。
        ImmutableList<Object> keys = ImmutableList.of(StringUtils.join(limit.prefix(),
                "_", limit.name(), "_", handler.getUserId(), request.getRequestURI().replaceAll("/", "_")));

        //redis key
        //我們使用 (前綴)+ 用戶(hù)標(biāo)識(shí) + 調(diào)用url 做redis的key,將防重冪等的粒度縮小紊服。
        String redisKey = keys.toString();
        //這個(gè)地方檀轨,我只使用了其中一種胸竞,可以根據(jù)自己的實(shí)際需求去調(diào)整。
        if (Objects.equals(VariableProvider.REQUEST_BODY, repeatSubmit.variableProvider())) {

            //如果參數(shù)提供者是REQUEST_BODY,則直接按照參數(shù)位置獲取
            Object arg = this.getArg(joinPoint.getArgs(), repeatSubmit.variablePosition());
            if (Objects.isNull(arg)) {
                log.error(String.format("無(wú)法執(zhí)行重復(fù)提交的判斷:請(qǐng)求類(lèi)[%s]切片參數(shù)配置錯(cuò)誤裤园,無(wú)法獲取指定位置的參數(shù)對(duì)象", aopTarget));
            }
            Assert.notNull(arg, String.format("無(wú)法執(zhí)行重復(fù)提交的判斷:請(qǐng)求[%s]切片參數(shù)配置有誤撤师,無(wú)法獲取指定位置的參數(shù)對(duì)象", aopTarget));
            Assert.isTrue(arg.getClass().getName().equals(repeatSubmit.variableName()), String.format("無(wú)法執(zhí)行重復(fù)提交的判斷:請(qǐng)求類(lèi)[%s]切片參數(shù)配置錯(cuò)誤,所配置的參數(shù)類(lèi)與指定位置的參數(shù)類(lèi)實(shí)際不一致", aopTarget));
            //拿到接口傳參
            String strArg = arg.toString();
            log.info("切面?zhèn)鲄?-->{},redisKey==>{}", strArg, redisKey);

            Map<String, Object> nowDataMap = new HashMap<>();
            nowDataMap.put(REPEAT_PARAMS, strArg);
            nowDataMap.put(REPEAT_TIME, SystemClock.now());

            Object cacheObject = redisCache.getCacheObject(CACHE_REPEAT_KEY);
            if (Objects.nonNull(cacheObject)) {
                Map<String, Object> cacheObjMap = (Map<String, Object>) cacheObject;
                if (cacheObjMap.containsKey(redisKey)) {
                    Map<String, Object> preDataMap = (Map<String, Object>) cacheObjMap.get(redisKey);
                    boolean result = compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, period);
                    Assert.isTrue(!result, "您提交過(guò)快,稍后再試");
                }
            }
            Map<String, Object> cacheMap = new HashMap<>();
            cacheMap.put(redisKey, nowDataMap);
            redisCache.setCacheObject(CACHE_REPEAT_KEY, cacheMap, period, TimeUnit.SECONDS);
        }
    }

    /**
     * 獲取切片目標(biāo)信息
     *
     * @param joinPoint
     * @return
     */
    private String getAopTarget(JoinPoint joinPoint) {
        String method = joinPoint.getSignature().getName();
        String clazz = joinPoint.getTarget().getClass().getName();
        return String.join("#", clazz, method);
    }

    /**
     * 根據(jù)位置序號(hào)獲取請(qǐng)求參數(shù)
     *
     * @param args
     * @param position
     * @return
     */
    private Object getArg(Object[] args, int position) {
        if (args == null || position + 1 > args.length) {
            return null;
        }
        return args[position];
    }

    /**
     * 判斷參數(shù)是否相同
     */
    private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) {
        String nowParams = (String) nowMap.get(REPEAT_PARAMS);
        String preParams = (String) preMap.get(REPEAT_PARAMS);
        return nowParams.equals(preParams);
    }

    /**
     * 判斷兩次間隔時(shí)間
     */
    private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int period) {
        long time1 = (Long) nowMap.get(REPEAT_TIME);
        long time2 = (Long) preMap.get(REPEAT_TIME);
        return (time1 - time2) < (period * 1000);
    }
}

三拧揽、驗(yàn)證

1剃盾、新建一個(gè)controller 調(diào)用方法

    @RepeatSubmit(variableProvider = VariableProvider.REQUEST_BODY, variableName = "com.aida.annotation.common.controller.dto.Test", period = 5,
            name = "testRepeatSubmit", prefix = "repeat")
    @PostMapping("/repeat")
    public int testRepeatSubmit(@RequestBody Test test) {
        return ATOMIC_INTEGER.incrementAndGet();
    }

2、用到的測(cè)試class類(lèi)對(duì)象

package com.aida.annotation.common.controller.dto;

import lombok.Data;

import java.io.Serializable;
import java.util.List;

/**
 * @author Mr.SoftRock
 * @Date 2021/7/13 19:28
 **/
@Data
public class Test {

    String name;

    Integer age;
    List<String> list;
    Test1 test1;

    @Data
    public static class Test1 implements Serializable {
        private static final long serialVersionUID = -4262288319285897072L;
        String name;

        Integer age;
        List<String> list;
    }
}

3淤袜、啟動(dòng)項(xiàng)目痒谴,通過(guò)postman調(diào)用看下效果

在設(shè)定的5秒內(nèi)調(diào)用一次,可以正常返回铡羡,如下圖:

正常調(diào)用返回

Redis中也存在了對(duì)應(yīng)的key值积蔚,如下圖:
Redis中存在key

如果在設(shè)定的時(shí)間內(nèi)多次操作,則觸發(fā)冪等校驗(yàn)烦周,如下圖:


觸發(fā)冪等校驗(yàn)

總結(jié)

冪等性的問(wèn)題確實(shí)是在很多種場(chǎng)景都會(huì)需要尽爆,實(shí)現(xiàn)的方式有很多種,找一種最合適自己的读慎。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末漱贱,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子夭委,更是在濱河造成了極大的恐慌幅狮,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件株灸,死亡現(xiàn)場(chǎng)離奇詭異崇摄,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)慌烧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén)逐抑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人屹蚊,你說(shuō)我怎么就攤上這事厕氨。” “怎么了淑翼?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵腐巢,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我玄括,道長(zhǎng)冯丙,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮胃惜,結(jié)果婚禮上泞莉,老公的妹妹穿的比我還像新娘。我一直安慰自己船殉,他們只是感情好鲫趁,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著利虫,像睡著了一般挨厚。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上糠惫,一...
    開(kāi)封第一講書(shū)人閱讀 49,741評(píng)論 1 289
  • 那天疫剃,我揣著相機(jī)與錄音,去河邊找鬼硼讽。 笑死巢价,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的固阁。 我是一名探鬼主播壤躲,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼备燃!你這毒婦竟也來(lái)了碉克?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤赚爵,失蹤者是張志新(化名)和其女友劉穎棉胀,沒(méi)想到半個(gè)月后法瑟,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體冀膝,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年霎挟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了窝剖。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡酥夭,死狀恐怖赐纱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情熬北,我是刑警寧澤疙描,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站讶隐,受9級(jí)特大地震影響起胰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜巫延,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一效五、第九天 我趴在偏房一處隱蔽的房頂上張望地消。 院中可真熱鬧,春花似錦畏妖、人聲如沸脉执。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)半夷。三九已至,卻和暖如春迅细,著一層夾襖步出監(jiān)牢的瞬間玻熙,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工疯攒, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留嗦随,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓敬尺,卻偏偏與公主長(zhǎng)得像枚尼,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子砂吞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容