欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringBoot系列教程之防重放與操作冪等

 更新時(shí)間:2022年04月12日 10:25:08   作者:huanzi-qch  
同一條數(shù)據(jù)被用戶點(diǎn)擊了多次,導(dǎo)致數(shù)據(jù)冗余,需要防止弱網(wǎng)絡(luò)等環(huán)境下的重復(fù)點(diǎn)擊,下面這篇文章主要給大家介紹了關(guān)于SpringBoot系列教程之防重放與操作冪等的相關(guān)資料,需要的朋友可以參考下

前言

日常開(kāi)發(fā)中,我們可能會(huì)碰到需要進(jìn)行防重放與操作冪等的業(yè)務(wù),本文記錄SpringBoot實(shí)現(xiàn)簡(jiǎn)單防重與冪等

防重放,防止數(shù)據(jù)重復(fù)提交

操作冪等性,多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同

解決什么問(wèn)題?

表單重復(fù)提交,用戶多次點(diǎn)擊表單提交按鈕

接口重復(fù)調(diào)用,接口短時(shí)間內(nèi)被多次調(diào)用

思路如下:

  1、前端頁(yè)面表提交鈕置灰不可點(diǎn)擊+js節(jié)流防抖

  2、Redis防重Token令牌

  3、數(shù)據(jù)庫(kù)唯一主鍵 + 樂(lè)觀鎖

具體方案

pom引入依賴

<!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- thymeleaf模板 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!--添加MyBatis-Plus依賴 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>

        <!--添加MySQL驅(qū)動(dòng)依賴 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

一個(gè)測(cè)試表

CREATE TABLE `idem`  (
  `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '唯一主鍵',
  `msg` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '業(yè)務(wù)數(shù)據(jù)',
  `version` int(8) NOT NULL COMMENT '樂(lè)觀鎖版本號(hào)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '防重放與操作冪等測(cè)試表' ROW_FORMAT = Compact;

前端頁(yè)面

先寫一個(gè)test頁(yè)面,引入jq

<!DOCTYPE html>
<!--解決idea thymeleaf 表達(dá)式模板報(bào)紅波浪線-->
<!--suppress ALL -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8" />
  <title>防重放與操作冪等</title>

  <!-- 引入靜態(tài)資源 -->
  <script th:src="@{/js/jquery-1.9.1.min.js}" type="application/javascript"></script>
</head>
<body>
  <form>
    <!-- 隱藏域 -->
    <input type="hidden" id="token" th:value="${token}"/>

    <!-- 業(yè)務(wù)數(shù)據(jù) -->
    id:<input id="id" th:value="${id}"/> <br/>
    msg:<input id="msg" th:value="${msg}"/> <br/>
    version:<input id="version" th:value="${version}"/> <br/>

    <!-- 操作按鈕 -->
    <br/>
    <input type="submit" value="提交" onclick="formSubmit(this)"/>
    <input type="reset" value="重置"/>
  </form>
  <br/>

  <button id="btn">節(jié)流測(cè)試,點(diǎn)我</button>
  <br/>
  <button id="btn2">防抖測(cè)試,點(diǎn)我</button>
</body>
<script>
  /*

  //插入
  for (let i = 0; i < 5; i++) {
    $.get("http://localhost:10010/idem/insert?id=1&msg=張三"+i+"&version=1",null,function (data){
      console.log(data);
    });
  }

  //修改
  for (let i = 0; i < 5; i++) {
    $.get("http://localhost:10010/idem/update?id=1&msg=李四"+i+"&version=1",null,function (data){
      console.log(data);
    });
  }

  //刪除
  for (let i = 0; i < 5; i++) {
    $.get("http://localhost:10010/idem/delete?id=1",null,function (data){
      console.log(data);
    });
  }

  //查詢
  for (let i = 0; i < 5; i++) {
    $.get("http://localhost:10010/idem/select?id=1",null,function (data){
      console.log(data);
    });
  }

  //test表單測(cè)試
  for (let i = 0; i < 5; i++) {
    $.get("http://localhost:10010/test/test?token=abcd&id=1&msg=張三"+i+"&version=1",null,function (data){
      console.log(data);
    });
  }

  //節(jié)流測(cè)試
  for (let i = 0; i < 5; i++) {
    document.getElementById('btn').onclick();
  }

  //防抖測(cè)試
  for (let i = 0; i < 5; i++) {
    document.getElementById('btn2').onclick();
  }

   */


  function formSubmit(but){
    //按鈕置灰
    but.setAttribute("disabled","disabled");

    let token = $("#token").val();
    let id = $("#id").val();
    let msg = $("#msg").val();
    let version = $("#version").val();

    $.ajax({
      type: 'post',
      url: "/test/test",
      contentType:"application/x-www-form-urlencoded",
      data: {
        token:token,
        id:id,
        msg:msg,
        version:version,
      },
      success: function (data) {
        console.log(data);

        //按鈕恢復(fù)
        but.removeAttribute("disabled");
      },
      error: function (xhr, status, error) {
        console.error("ajax錯(cuò)誤!");

        //按鈕恢復(fù)
        but.removeAttribute("disabled");
      }
    });

    return false;
  }

  document.getElementById('btn').onclick = throttle(function () {
    console.log('節(jié)流測(cè)試 helloworld');
  }, 1000)
  // 節(jié)流:給定一個(gè)時(shí)間,不管這個(gè)時(shí)間你怎么點(diǎn)擊,點(diǎn)上天,這個(gè)時(shí)間內(nèi)也只會(huì)執(zhí)行一次
  // 節(jié)流函數(shù)
  function throttle(fn, delay) {
    var lastTime = new Date().getTime()
    delay = delay || 200
    return function () {
      var args = arguments
      var nowTime = new Date().getTime()
      if (nowTime - lastTime >= delay) {
        lastTime = nowTime
        fn.apply(this, args)
      }
    }
  }

  document.getElementById('btn2').onclick = debounce(function () {
    console.log('防抖測(cè)試 helloworld');
  }, 1000)
  // 防抖:給定一個(gè)時(shí)間,不管怎么點(diǎn)擊按鈕,每點(diǎn)一次,都會(huì)在最后一次點(diǎn)擊等待這個(gè)時(shí)間過(guò)后執(zhí)行
  // 防抖函數(shù)
  function debounce(fn, delay) {
    var timer = null
    delay = delay || 200
    return function () {
      var args = arguments
      var that = this
      clearTimeout(timer)
      timer = setTimeout(function () {
        fn.apply(that, args)
      }, delay)
    }
  }
</script>
</html>

按鈕置灰不可點(diǎn)擊

點(diǎn)擊提交按鈕后,將提交按鈕置灰不可點(diǎn)擊,ajax響應(yīng)后再恢復(fù)按鈕狀態(tài)

function formSubmit(but){
    //按鈕置灰
    but.setAttribute("disabled","disabled");

    let token = $("#token").val();
    let id = $("#id").val();
    let msg = $("#msg").val();
    let version = $("#version").val();

    $.ajax({
      type: 'post',
      url: "/test/test",
      contentType:"application/x-www-form-urlencoded",
      data: {
        token:token,
        id:id,
        msg:msg,
        version:version,
      },
      success: function (data) {
        console.log(data);

        //按鈕恢復(fù)
        but.removeAttribute("disabled");
      },
      error: function (xhr, status, error) {
        console.error("ajax錯(cuò)誤!");

        //按鈕恢復(fù)
        but.removeAttribute("disabled");
      }
    });

    return false;
  }

js節(jié)流、防抖

節(jié)流:給定一個(gè)時(shí)間,不管這個(gè)時(shí)間你怎么點(diǎn)擊,點(diǎn)上天,這個(gè)時(shí)間內(nèi)也只會(huì)執(zhí)行一次

document.getElementById('btn').onclick = throttle(function () {
    console.log('節(jié)流測(cè)試 helloworld');
  }, 1000)
  // 節(jié)流:給定一個(gè)時(shí)間,不管這個(gè)時(shí)間你怎么點(diǎn)擊,點(diǎn)上天,這個(gè)時(shí)間內(nèi)也只會(huì)執(zhí)行一次
  // 節(jié)流函數(shù)
  function throttle(fn, delay) {
    var lastTime = new Date().getTime()
    delay = delay || 200
    return function () {
      var args = arguments
      var nowTime = new Date().getTime()
      if (nowTime - lastTime >= delay) {
        lastTime = nowTime
        fn.apply(this, args)
      }
    }
  }

防抖:給定一個(gè)時(shí)間,不管怎么點(diǎn)擊按鈕,每點(diǎn)一次,都會(huì)在最后一次點(diǎn)擊等待這個(gè)時(shí)間過(guò)后執(zhí)行

document.getElementById('btn2').onclick = debounce(function () {
    console.log('防抖測(cè)試 helloworld');
  }, 1000)
  // 防抖:給定一個(gè)時(shí)間,不管怎么點(diǎn)擊按鈕,每點(diǎn)一次,都會(huì)在最后一次點(diǎn)擊等待這個(gè)時(shí)間過(guò)后執(zhí)行
  // 防抖函數(shù)
  function debounce(fn, delay) {
    var timer = null
    delay = delay || 200
    return function () {
      var args = arguments
      var that = this
      clearTimeout(timer)
      timer = setTimeout(function () {
        fn.apply(that, args)
      }, delay)
    }
  }

Redis

防重Token令牌

跳轉(zhuǎn)前端表單頁(yè)面時(shí),設(shè)置一個(gè)UUID作為token,并設(shè)置在表單隱藏域

/**
     * 跳轉(zhuǎn)頁(yè)面
     */
    @RequestMapping("index")
    private ModelAndView index(String id){
        ModelAndView mv = new ModelAndView();
        mv.addObject("token",UUIDUtil.getUUID());
        if(id != null){
            Idem idem = new Idem();
            idem.setId(id);
            List select = (List)idemService.select(idem);
            idem = (Idem)select.get(0);
            mv.addObject("id", idem.getId());
            mv.addObject("msg", idem.getMsg());
            mv.addObject("version", idem.getVersion());
        }
        mv.setViewName("test.html");
        return mv;
    }
<form>
    <!-- 隱藏域 -->
    <input type="hidden" id="token" th:value="${token}"/>

    <!-- 業(yè)務(wù)數(shù)據(jù) -->
    id:<input id="id" th:value="${id}"/> <br/>
    msg:<input id="msg" th:value="${msg}"/> <br/>
    version:<input id="version" th:value="${version}"/> <br/>

    <!-- 操作按鈕 -->
    <br/>
    <input type="submit" value="提交" onclick="formSubmit(this)"/>
    <input type="reset" value="重置"/>
  </form>

后臺(tái)查詢r(jià)edis緩存,如果token不存在立即設(shè)置token緩存,允許表單業(yè)務(wù)正常進(jìn)行;如果token緩存已經(jīng)存在,拒絕表單業(yè)務(wù)

PS:token緩存要設(shè)置一個(gè)合理的過(guò)期時(shí)間

/**
     * 表單提交測(cè)試
     */
    @RequestMapping("test")
    private String test(String token,String id,String msg,int version){
        //如果token緩存不存在,立即設(shè)置緩存且設(shè)置有效時(shí)長(zhǎng)(秒)
        Boolean setIfAbsent = template.opsForValue().setIfAbsent(token, "1", 60 * 5, TimeUnit.SECONDS);

        //緩存設(shè)置成功返回true,失敗返回false
        if(Boolean.TRUE.equals(setIfAbsent)){

            //模擬耗時(shí)
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //打印測(cè)試數(shù)據(jù)
            System.out.println(token+","+id+","+msg+","+version);

            return "操作成功!";
        }else{
            return "操作失敗,表單已被提交...";
        }
    }

for循環(huán)測(cè)試中,5個(gè)操作只有一個(gè)執(zhí)行成功!

數(shù)據(jù)庫(kù)

唯一主鍵 + 樂(lè)觀鎖

查詢操作自帶冪等性

/**
     * 查詢操作,天生冪等性
     */
    @Override
    public Object select(Idem idem) {
        QueryWrapper<Idem> queryWrapper = new QueryWrapper<>();
        queryWrapper.setEntity(idem);
        return idemMapper.selectList(queryWrapper);
    }

查詢沒(méi)什么好說(shuō)的,只要數(shù)據(jù)不變,查詢條件不變的情況下查詢結(jié)果必然冪等

唯一主鍵可解決插入操作、刪除操作

/**
     * 插入操作,使用唯一主鍵實(shí)現(xiàn)冪等性
     */
    @Override
    public Object insert(Idem idem) {
        String msg = "操作成功!";
        try{
            idemMapper.insert(idem);
        }catch (DuplicateKeyException e){
            msg = "操作失敗,id:"+idem.getId()+",已經(jīng)存在...";
        }
        return msg;
    }

    /**
     * 刪除操作,使用唯一主鍵實(shí)現(xiàn)冪等性
     * PS:使用非主鍵條件除外
     */
    @Override
    public Object delete(Idem idem) {
        String msg = "操作成功!";
        int deleteById = idemMapper.deleteById(idem.getId());
        if(deleteById == 0){
            msg = "操作失敗,id:"+idem.getId()+",已經(jīng)被刪除...";
        }
        return msg;
    }

利用主鍵唯一的特性,捕獲處理重復(fù)操作

樂(lè)觀鎖可解決更新操作

/**
     * 更新操作,使用樂(lè)觀鎖實(shí)現(xiàn)冪等性
     */
    @Override
    public Object update(Idem idem) {
        String msg = "操作成功!";

        // UPDATE table SET [... 業(yè)務(wù)字段=? ...], version = version+1 WHERE (id = ? AND version = ?)
        UpdateWrapper<Idem> updateWrapper = new UpdateWrapper<>();

        //where條件
        updateWrapper.eq("id",idem.getId());
        updateWrapper.eq("version",idem.getVersion());

        //version版本號(hào)要單獨(dú)設(shè)置
        updateWrapper.setSql("version = version+1");
        idem.setVersion(null);

        int update = idemMapper.update(idem, updateWrapper);
        if(update == 0){
            msg = "操作失敗,id:"+idem.getId()+",已經(jīng)被更新...";
        }

        return msg;
    }

執(zhí)行更新sql語(yǔ)句時(shí),where條件帶上version版本號(hào),如果執(zhí)行成功,除了更新業(yè)務(wù)數(shù)據(jù),同時(shí)更新version版本號(hào)標(biāo)記當(dāng)前數(shù)據(jù)已被更新

UPDATE table SET [... 業(yè)務(wù)字段=? ...], version = version+1 WHERE (id = ? AND version = ?)

執(zhí)行更新操作前,需要重新執(zhí)行插入數(shù)據(jù)

以上for循環(huán)測(cè)試中,5個(gè)操作同樣只有一個(gè)執(zhí)行成功!

后記

redis、樂(lè)觀鎖不要在代碼先查詢后if判斷,這樣會(huì)存在并發(fā)問(wèn)題,導(dǎo)致數(shù)據(jù)不準(zhǔn)確,應(yīng)該把這種判斷放在redis、數(shù)據(jù)庫(kù)

錯(cuò)誤示例:

//獲取最新緩存
String redisToken = template.opsForValue().get(token);

//為空則放行業(yè)務(wù)
if(redisToken == null){
    //設(shè)置緩存
    template.opsForValue().set(token, "1", 60 * 5, TimeUnit.SECONDS);

    //業(yè)務(wù)處理
}else{
    //拒絕業(yè)務(wù)
}

錯(cuò)誤示例:

//獲取最新版本號(hào)
Integer version = idemMapper.selectById(idem.getId()).getVersion();

//版本號(hào)相同,說(shuō)明數(shù)據(jù)未被其他人修改
if(version == idem.getVersion()){
    //正常更新
}else{
    //拒絕更新
}

防重與冪等暫時(shí)先記錄到這,后續(xù)再進(jìn)行補(bǔ)充

代碼開(kāi)源

代碼已經(jīng)開(kāi)源、托管到我的GitHub、碼云:

GitHub:https://github.com/huanzi-qch/springBoot

碼云:https://gitee.com/huanzi-qch/springBoot

總結(jié)

到此這篇關(guān)于SpringBoot系列教程之防重放與操作冪等的文章就介紹到這了,更多相關(guān)SpringBoot防重放與操作冪等內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 解決Mybatis-plus和pagehelper依賴沖突的方法示例

    解決Mybatis-plus和pagehelper依賴沖突的方法示例

    這篇文章主要介紹了解決Mybatis-plus和pagehelper依賴沖突的方法示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2021-04-04
  • 基于MyBatis的數(shù)據(jù)持久化框架的使用詳解

    基于MyBatis的數(shù)據(jù)持久化框架的使用詳解

    Mybatis是一個(gè)優(yōu)秀的開(kāi)源、輕量級(jí)持久層框架,它對(duì)JDBC操作數(shù)據(jù)庫(kù)的過(guò)程進(jìn)行封裝。本文將為大家講解一下基于MyBatis的數(shù)據(jù)持久化框架的使用,感興趣的可以了解一下
    2022-08-08
  • spring boot打jar包發(fā)布的方法

    spring boot打jar包發(fā)布的方法

    這篇文章主要介紹了spring boot打jar包發(fā)布的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-06-06
  • Java算法之位圖的概念和實(shí)現(xiàn)詳解

    Java算法之位圖的概念和實(shí)現(xiàn)詳解

    這篇文章主要介紹了Java算法之位圖的概念和實(shí)現(xiàn)詳解,位圖可以利用每一位來(lái)對(duì)應(yīng)一個(gè)值,比如可以利用int類型的數(shù)去存儲(chǔ)0~31這個(gè)集合的數(shù)字,如果該集合內(nèi)的數(shù)字存在,則把對(duì)應(yīng)的位設(shè)置位1默認(rèn)為0,需要的朋友可以參考下
    2023-10-10
  • springboot war包部署過(guò)程詳解

    springboot war包部署過(guò)程詳解

    這篇文章主要為大家介紹了springboot war包部署過(guò)程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-08-08
  • 關(guān)于使用Mybatisplus自帶的selectById和insert方法時(shí)的一些問(wèn)題

    關(guān)于使用Mybatisplus自帶的selectById和insert方法時(shí)的一些問(wèn)題

    這篇文章主要介紹了關(guān)于使用Mybatisplus自帶的selectById和insert方法時(shí)的一些問(wèn)題,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-08-08
  • springmvc與mybatis集成配置實(shí)例詳解

    springmvc與mybatis集成配置實(shí)例詳解

    這篇文章主要介紹了springmvc與mybatis集成配置實(shí)例詳解的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下
    2016-09-09
  • Java基礎(chǔ)之switch分支結(jié)構(gòu)詳解

    Java基礎(chǔ)之switch分支結(jié)構(gòu)詳解

    這篇文章主要介紹了Java基礎(chǔ)之switch分支結(jié)構(gòu)詳解,文中有非常詳細(xì)的代碼示例,對(duì)正在學(xué)習(xí)java的小伙伴們有很大的幫助,需要的朋友可以參考下
    2021-05-05
  • SpringBoot集成RocketMQ的使用示例

    SpringBoot集成RocketMQ的使用示例

    RocketMQ是阿里巴巴開(kāi)源的一款消息中間件,性能優(yōu)秀,功能齊全,被廣泛應(yīng)用在各種業(yè)務(wù)場(chǎng)景,本文就來(lái)介紹一下SpringBoot集成RocketMQ的使用示例,感興趣的可以了解一下
    2023-11-11
  • SpringBoot在RequestBody中使用枚舉參數(shù)案例詳解

    SpringBoot在RequestBody中使用枚舉參數(shù)案例詳解

    這篇文章主要介紹了SpringBoot在RequestBody中使用枚舉參數(shù)案例詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下
    2021-09-09

最新評(píng)論