亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

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

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

前言

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

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

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

解決什么問題?

表單重復提交,用戶多次點擊表單提交按鈕

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

思路如下:

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

  2、Redis防重Token令牌

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

具體方案

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ū)動依賴 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

一個測試表

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è)務數(shù)據(jù)',
  `version` int(8) NOT NULL COMMENT '樂觀鎖版本號',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '防重放與操作冪等測試表' ROW_FORMAT = Compact;

前端頁面

先寫一個test頁面,引入jq

<!DOCTYPE html>
<!--解決idea thymeleaf 表達式模板報紅波浪線-->
<!--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è)務數(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é)流測試,點我</button>
  <br/>
  <button id="btn2">防抖測試,點我</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表單測試
  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é)流測試
  for (let i = 0; i < 5; i++) {
    document.getElementById('btn').onclick();
  }

  //防抖測試
  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);

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

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

    return false;
  }

  document.getElementById('btn').onclick = throttle(function () {
    console.log('節(jié)流測試 helloworld');
  }, 1000)
  // 節(jié)流:給定一個時間,不管這個時間你怎么點擊,點上天,這個時間內(nèi)也只會執(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('防抖測試 helloworld');
  }, 1000)
  // 防抖:給定一個時間,不管怎么點擊按鈕,每點一次,都會在最后一次點擊等待這個時間過后執(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>

按鈕置灰不可點擊

點擊提交按鈕后,將提交按鈕置灰不可點擊,ajax響應后再恢復按鈕狀態(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);

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

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

    return false;
  }

js節(jié)流、防抖

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

document.getElementById('btn').onclick = throttle(function () {
    console.log('節(jié)流測試 helloworld');
  }, 1000)
  // 節(jié)流:給定一個時間,不管這個時間你怎么點擊,點上天,這個時間內(nèi)也只會執(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)
      }
    }
  }

防抖:給定一個時間,不管怎么點擊按鈕,每點一次,都會在最后一次點擊等待這個時間過后執(zhí)行

document.getElementById('btn2').onclick = debounce(function () {
    console.log('防抖測試 helloworld');
  }, 1000)
  // 防抖:給定一個時間,不管怎么點擊按鈕,每點一次,都會在最后一次點擊等待這個時間過后執(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)前端表單頁面時,設置一個UUID作為token,并設置在表單隱藏域

/**
     * 跳轉(zhuǎn)頁面
     */
    @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è)務數(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>

后臺查詢redis緩存,如果token不存在立即設置token緩存,允許表單業(yè)務正常進行;如果token緩存已經(jīng)存在,拒絕表單業(yè)務

PS:token緩存要設置一個合理的過期時間

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

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

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

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

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

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

數(shù)據(jù)庫

唯一主鍵 + 樂觀鎖

查詢操作自帶冪等性

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

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

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

/**
     * 插入操作,使用唯一主鍵實現(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;
    }

    /**
     * 刪除操作,使用唯一主鍵實現(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;
    }

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

樂觀鎖可解決更新操作

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

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

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

        //version版本號要單獨設置
        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語句時,where條件帶上version版本號,如果執(zhí)行成功,除了更新業(yè)務數(shù)據(jù),同時更新version版本號標記當前數(shù)據(jù)已被更新

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

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

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

后記

redis、樂觀鎖不要在代碼先查詢后if判斷,這樣會存在并發(fā)問題,導致數(shù)據(jù)不準確,應該把這種判斷放在redis、數(shù)據(jù)庫

錯誤示例:

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

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

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

錯誤示例:

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

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

防重與冪等暫時先記錄到這,后續(xù)再進行補充

代碼開源

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

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

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

總結(jié)

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

相關文章

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

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

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

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

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

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

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

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

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

    springboot war包部署過程詳解

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

    關于使用Mybatisplus自帶的selectById和insert方法時的一些問題

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

    springmvc與mybatis集成配置實例詳解

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

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

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

    SpringBoot集成RocketMQ的使用示例

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

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

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

最新評論