<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          瞬間幾千次的重復(fù)提交,我用 SpringBoot+Redis 扛住了~

          共 9403字,需瀏覽 19分鐘

           ·

          2021-06-01 19:22


          上一篇:3600萬(wàn)中國(guó)人在抖音“上清華”

          來(lái)源:http://suo.im/5PaEZI

          在實(shí)際的開(kāi)發(fā)項(xiàng)目中,一個(gè)對(duì)外暴露的接口往往會(huì)面臨,瞬間大量的重復(fù)的請(qǐng)求提交,如果想過(guò)濾掉重復(fù)請(qǐng)求造成對(duì)業(yè)務(wù)的傷害,那就需要實(shí)現(xiàn)冪等!

          我們來(lái)解釋一下冪等的概念:


          任意多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同。按照這個(gè)含義,最終的含義就是 對(duì)數(shù)據(jù)庫(kù)的影響只能是一次性的,不能重復(fù)處理。

          如何保證其冪等性,通常有以下手段:

          1、數(shù)據(jù)庫(kù)建立唯一性索引,可以保證最終插入數(shù)據(jù)庫(kù)的只有一條數(shù)據(jù)
          2、token機(jī)制,每次接口請(qǐng)求前先獲取一個(gè)token,然后再下次請(qǐng)求的時(shí)候在請(qǐng)求的header體中加上這個(gè)token,后臺(tái)進(jìn)行驗(yàn)證,如果驗(yàn)證通過(guò)刪除token,下次請(qǐng)求再次判斷token
          3、
          悲觀鎖或者樂(lè)觀鎖,悲觀鎖可以保證每次for update的時(shí)候其他sql無(wú)法update數(shù)據(jù)(在數(shù)據(jù)庫(kù)引擎是innodb的時(shí)候,select的條件必須是唯一索引,防止鎖全表)
          4、先查詢后判斷,首先通過(guò)查詢數(shù)據(jù)庫(kù)是否存在數(shù)據(jù),如果存在證明已經(jīng)請(qǐng)求過(guò)了,直接拒絕該請(qǐng)求,如果沒(méi)有存在,就證明是第一次進(jìn)來(lái),直接放行。

          redis實(shí)現(xiàn)自動(dòng)冪等的原理圖:



          # 搭建redis的服務(wù)Api


          1、首先是搭建redis服務(wù)器。


          2、引入springboot中到的redis的stater,或者Spring封裝的jedis也可以,后面

          主要用到的api就是它的set方法和exists方法,這里我們使用springboot的封裝好

          的redisTemplate

          /** * redis工具類(lèi) */@ComponentpublicclassRedisService{    @Autowired    privateRedisTemplate redisTemplate;    /**     * 寫(xiě)入緩存     * @param key     * @param value     * @return     */    publicbooleanset(finalString key, Object value) {        boolean result = false;        try{            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();            operations.set(key, value);            result = true;        } catch(Exception e) {            e.printStackTrace();        }        return result;    }    /**     * 寫(xiě)入緩存設(shè)置時(shí)效時(shí)間     * @param key     * @param value     * @return     */    publicboolean setEx(finalString key, Object value, Long expireTime) {        boolean result = false;        try{            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();            operations.set(key, value);            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);            result = true;        } catch(Exception e) {            e.printStackTrace();        }        return result;    }    /**     * 判斷緩存中是否有對(duì)應(yīng)的value     * @param key     * @return     */    publicboolean exists(finalString key) {        return redisTemplate.hasKey(key);    }    /**     * 讀取緩存     * @param key     * @return     */    publicObjectget(finalString key) {        Object result = null;        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();        result = operations.get(key);        return result;    }    /**     * 刪除對(duì)應(yīng)的value     * @param key     */    publicboolean remove(finalString key) {        if(exists(key)) {            Booleandelete= redisTemplate.delete(key);            returndelete;        }        returnfalse;    }}


          # 自定義注解AutoIdempotent


          自定義一個(gè)注解,定義此注解的主要目的是把它添加在需要實(shí)現(xiàn)冪等的方法上

          ,凡是某個(gè)方法注解了它,都會(huì)實(shí)現(xiàn)自動(dòng)冪等。后臺(tái)利用反射如果掃描到這個(gè)

          注解,就會(huì)處理這個(gè)方法實(shí)現(xiàn)自動(dòng)冪等,使用元注解ElementType.METHOD

          表示它只能放在方法上,etentionPolicy.RUNTIME表示它在運(yùn)行時(shí)。

          @Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public@interfaceAutoIdempotent{}


          # token創(chuàng)建和檢驗(yàn)


          1、token服務(wù)接口


          我們新建一個(gè)接口,創(chuàng)建token服務(wù),里面主要是兩個(gè)方法,一個(gè)用來(lái)創(chuàng)建

          token,一個(gè)用來(lái)驗(yàn)證token。創(chuàng)建token主要產(chǎn)生的是一個(gè)字符串,檢驗(yàn)token

          的話主要是傳達(dá)request對(duì)象,為什么要傳request對(duì)象呢?主要作用就是獲取

          header里面的token,然后檢驗(yàn),通過(guò)拋出的Exception來(lái)獲取具體的報(bào)錯(cuò)信息

          返回給前端。
          publicinterfaceTokenService{    /**     * 創(chuàng)建token     * @return     */    public  String createToken();    /**     * 檢驗(yàn)token     * @param request     * @return     */    publicboolean checkToken(HttpServletRequest request) throwsException;}


          2、token的服務(wù)實(shí)現(xiàn)類(lèi)


          token引用了redis服務(wù),創(chuàng)建token采用隨機(jī)算法工具類(lèi)生成隨機(jī)uuid字符串,

          然后放入到redis中(為了防止數(shù)據(jù)的冗余保留,這里設(shè)置過(guò)期時(shí)間為10000秒,

          具體可視業(yè)務(wù)而定),如果放入成功,最后返回這個(gè)token值。checkToken方法

          就是從header中獲取token到值(如果header中拿不到,就從paramter中獲取),

          如若不存在,直接拋出異常。這個(gè)異常信息可以被攔截器捕捉到,然后返回給

          前端。

          @ServicepublicclassTokenServiceImplimplementsTokenService{    @Autowired    privateRedisService redisService;    /**     * 創(chuàng)建token     *     * @return     */    @Override    publicString createToken() {        String str = RandomUtil.randomUUID();        StrBuilder token = newStrBuilder();        try{            token.append(Constant.Redis.TOKEN_PREFIX).append(str);            redisService.setEx(token.toString(), token.toString(),10000L);            boolean notEmpty = StrUtil.isNotEmpty(token.toString());            if(notEmpty) {                return token.toString();            }        }catch(Exception ex){            ex.printStackTrace();        }        returnnull;    }    /**     * 檢驗(yàn)token     *     * @param request     * @return     */    @Override    publicboolean checkToken(HttpServletRequest request) throwsException{        String token = request.getHeader(Constant.TOKEN_NAME);        if(StrUtil.isBlank(token)) {// header中不存在token            token = request.getParameter(Constant.TOKEN_NAME);            if(StrUtil.isBlank(token)) {// parameter中也不存在token                thrownewServiceException(Constant.ResponseCode.ILLEGAL_ARGUMENT, 100);            }        }        if(!redisService.exists(token)) {            thrownewServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);        }        boolean remove = redisService.remove(token);        if(!remove) {            thrownewServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);        }        returntrue;    }}


          # 攔截器的配置


          1、web配置類(lèi),實(shí)現(xiàn)WebMvcConfigurerAdapter,主要作用就是添加

          autoIdempotentInterceptor到配置類(lèi)中,這樣我們到攔截器才能生效,注意使

          用@Configuration注解,這樣在容器啟動(dòng)是時(shí)候就可以添加進(jìn)入context中。

          @ConfigurationpublicclassWebConfigurationextendsWebMvcConfigurerAdapter{    @Resource   privateAutoIdempotentInterceptor autoIdempotentInterceptor;    /**     * 添加攔截器     * @param registry     */    @Override    publicvoid addInterceptors(InterceptorRegistry registry) {        registry.addInterceptor(autoIdempotentInterceptor);        super.addInterceptors(registry);    }}


          2、攔截處理器:主要的功能是攔截掃描到AutoIdempotent到注解到方法,然后

          調(diào)用tokenService的checkToken()方法校驗(yàn)token是否正確,如果捕捉到異常就

          將異常信息渲染成json返回給前端

          /** * 攔截器 */@ComponentpublicclassAutoIdempotentInterceptorimplementsHandlerInterceptor{    @Autowired    privateTokenService tokenService;    /**     * 預(yù)處理     *     * @param request     * @param response     * @param handler     * @return     * @throws Exception     */    @Override    publicboolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throwsException{        if(!(handler instanceofHandlerMethod)) {            returntrue;        }        HandlerMethod handlerMethod = (HandlerMethod) handler;        Method method = handlerMethod.getMethod();        //被ApiIdempotment標(biāo)記的掃描        AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);        if(methodAnnotation != null) {            try{                return tokenService.checkToken(request);// 冪等性校驗(yàn), 校驗(yàn)通過(guò)則放行, 校驗(yàn)失敗則拋出異常, 并通過(guò)統(tǒng)一異常處理返回友好提示            }catch(Exception ex){                ResultVo failedResult = ResultVo.getFailedResult(101, ex.getMessage());                writeReturnJson(response, JSONUtil.toJsonStr(failedResult));                throw ex;            }        }        //必須返回true,否則會(huì)被攔截一切請(qǐng)求        returntrue;    }    @Override    publicvoid postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throwsException{    }    @Override    publicvoid afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throwsException{    }    /**     * 返回的json值     * @param response     * @param json     * @throws Exception     */    privatevoid writeReturnJson(HttpServletResponse response, String json) throwsException{        PrintWriter writer = null;        response.setCharacterEncoding("UTF-8");        response.setContentType("text/html; charset=utf-8");        try{            writer = response.getWriter();            writer.print(json);        } catch(IOException e) {        } finally{            if(writer != null)                writer.close();        }    }}


          # 測(cè)試用例


          1、模擬業(yè)務(wù)請(qǐng)求類(lèi)


          首先我們需要通過(guò)/get/token路徑通過(guò)getToken()方法去獲取具體的token,然后

          我們調(diào)用testIdempotence方法,這個(gè)方法上面注解了@AutoIdempotent,攔截

          器會(huì)攔截所有的請(qǐng)求,當(dāng)判斷到處理的方法上面有該注解的時(shí)候,就會(huì)調(diào)用

          TokenService中的checkToken()方法,如果捕獲到異常會(huì)將異常拋出調(diào)用者,

          下面我們來(lái)模擬請(qǐng)求一下:
          @RestControllerpublicclassBusinessController{    @Resource    privateTokenService tokenService;    @Resource    privateTestService testService;    @PostMapping("/get/token")    publicString  getToken(){        String token = tokenService.createToken();        if(StrUtil.isNotEmpty(token)) {            ResultVo resultVo = newResultVo();            resultVo.setCode(Constant.code_success);            resultVo.setMessage(Constant.SUCCESS);            resultVo.setData(token);            returnJSONUtil.toJsonStr(resultVo);        }        returnStrUtil.EMPTY;    }    @AutoIdempotent    @PostMapping("/test/Idempotence")    publicString testIdempotence() {        String businessResult = testService.testIdempotence();        if(StrUtil.isNotEmpty(businessResult)) {            ResultVo successResult = ResultVo.getSuccessResult(businessResult);            returnJSONUtil.toJsonStr(successResult);        }        returnStrUtil.EMPTY;    }}


          2、使用postman請(qǐng)求


          首先訪問(wèn)get/token路徑獲取到具體到token:



          利用獲取到到token,然后放到具體請(qǐng)求到header中,可以看到第一次請(qǐng)求成功,

          接著我們請(qǐng)求第二次:



          第二次請(qǐng)求,返回到是重復(fù)性操作,可見(jiàn)重復(fù)性驗(yàn)證通過(guò),再多次請(qǐng)求到時(shí)候

          我們只讓其第一次成功,第二次就是失?。?/span>


          # 總結(jié)


          本篇博客介紹了使用springboot和攔截器、redis來(lái)優(yōu)雅的實(shí)現(xiàn)接口冪等,對(duì)于

          冪等在實(shí)際的開(kāi)發(fā)過(guò)程中是十分重要的,因?yàn)橐粋€(gè)接口可能會(huì)被無(wú)數(shù)的客戶端

          調(diào)用,如何保證其不影響后臺(tái)的業(yè)務(wù)處理,如何保證其只影響數(shù)據(jù)一次是非常

          重要的,它可以防止產(chǎn)生臟數(shù)據(jù)或者亂數(shù)據(jù),也可以減少并發(fā)量,實(shí)乃十分有

          益的一件事。而傳統(tǒng)的做法是每次判斷數(shù)據(jù),這種做法不夠智能化和自動(dòng)化,

          比較麻煩。而今天的這種自動(dòng)化處理也可以提升程序的伸縮性。


          ◆  ◆  ◆  ◆  


          看完這篇文章,你有什么收獲?歡迎在留言區(qū)與10w+Java開(kāi)發(fā)者一起討論~

          關(guān)注微信公眾號(hào):互聯(lián)網(wǎng)架構(gòu)師,在后臺(tái)回復(fù):2T,可以獲取我整理的教程,都是干貨。


          猜你喜歡

          1、GitHub 標(biāo)星 3.2w!史上最全技術(shù)人員面試手冊(cè)!FackBoo發(fā)起和總結(jié)

          2、如何才能成為優(yōu)秀的架構(gòu)師?

          3、從零開(kāi)始搭建創(chuàng)業(yè)公司后臺(tái)技術(shù)棧

          4、程序員一般可以從什么平臺(tái)接私活?

          5、37歲程序員被裁,120天沒(méi)找到工作,無(wú)奈去小公司,結(jié)果懵了...

          6、滴滴業(yè)務(wù)中臺(tái)構(gòu)建實(shí)踐,首次曝光

          7、不認(rèn)命,從10年流水線工人,到谷歌上班的程序媛,一位湖南妹子的勵(lì)志故事

          8、15張圖看懂瞎忙和高效的區(qū)別

          9、2T架構(gòu)師學(xué)習(xí)資料干貨分享


          瀏覽 34
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  www.天天操.com | 亚洲激情无码视频 | 特黄特色免费大片 | 久久久久久三级 | 久久久久久电影成人电影 |