龙空技术网

SpringBoot系列——防重放与操作幂等

风后奇门柳坤生 1184

前言:

而今咱们对“ajaxget方式等幂”可能比较关心,大家都想要分析一些“ajaxget方式等幂”的相关资讯。那么小编同时在网摘上搜集了一些关于“ajaxget方式等幂””的相关内容,希望大家能喜欢,兄弟们快快来了解一下吧!

  前言

  日常开发中,我们可能会碰到需要进行防重放与操作幂等的业务,本文记录SpringBoot实现简单防重与幂等

  防重放,防止数据重复提交

  操作幂等性,多次执行所产生的影响均与一次执行的影响相同

  解决什么问题?

  表单重复提交,用户多次点击表单提交按钮

  接口重复调用,接口短时间内被多次调用

  思路如下:

  1、前端页面表提交钮置灰不可点击+js节流防抖

  2、Redis防重Token令牌

  3、数据库唯一主键 + 乐观锁

  具体方案

  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驱动依赖 -->        <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 '业务数据',  `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=";><head>  <meta charset="UTF-8" />  <title>防重放与操作幂等</title>  <!-- 引入静态资源 -->  <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}"/>    <!-- 业务数据 -->    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">节流测试,点我</button>  <br/>  <button id="btn2">防抖测试,点我</button></body><script>  /*  //插入  for (let i = 0; i < 5; i++) {    $.get("张三"+i+"&version=1",null,function (data){      console.log(data);    });  }  //修改  for (let i = 0; i < 5; i++) {    $.get("李四"+i+"&version=1",null,function (data){      console.log(data);    });  }  //删除  for (let i = 0; i < 5; i++) {    $.get(";,null,function (data){      console.log(data);    });  }  //查询  for (let i = 0; i < 5; i++) {    $.get(";,null,function (data){      console.log(data);    });  }  //test表单测试  for (let i = 0; i < 5; i++) {    $.get("张三"+i+"&version=1",null,function (data){      console.log(data);    });  }  //节流测试  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('节流测试 helloworld');  }, 1000)  // 节流:给定一个时间,不管这个时间你怎么点击,点上天,这个时间内也只会执行一次  // 节流函数  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)  // 防抖:给定一个时间,不管怎么点击按钮,每点一次,都会在最后一次点击等待这个时间过后执行  // 防抖函数  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响应后再恢复按钮状态

  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节流、防抖

  节流:给定一个时间,不管这个时间你怎么点击,点上天,这个时间内也只会执行一次

  document.getElementById('btn').onclick = throttle(function () {    console.log('节流测试 helloworld');  }, 1000)  // 节流:给定一个时间,不管这个时间你怎么点击,点上天,这个时间内也只会执行一次  // 节流函数  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)  // 防抖:给定一个时间,不管怎么点击按钮,每点一次,都会在最后一次点击等待这个时间过后执行  // 防抖函数  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令牌

  跳转前端表单页面时,设置一个UUID作为token,并设置在表单隐藏域

    /**     * 跳转页面     */    @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}"/>    <!-- 业务数据 -->    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缓存,允许表单业务正常进行;如果token缓存已经存在,拒绝表单业务

  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();            }            //打印测试数据            System.out.println(token+","+id+","+msg+","+version);            return "操作成功!";        }else{            return "操作失败,表单已被提交...";        }    }

  for循环测试中,5个操作只有一个执行成功!

  数据库

  唯一主键 + 乐观锁

  查询操作自带幂等性

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

  查询没什么好说的,只要数据不变,查询条件不变的情况下查询结果必然幂等

  唯一主键可解决插入操作、删除操作

    /**     * 插入操作,使用唯一主键实现幂等性     */    @Override    public Object insert(Idem idem) {        String msg = "操作成功!";        try{            idemMapper.insert(idem);        }catch (DuplicateKeyException e){            msg = "操作失败,id:"+idem.getId()+",已经存在...";        }        return msg;    }    /**     * 删除操作,使用唯一主键实现幂等性     * PS:使用非主键条件除外     */    @Override    public Object delete(Idem idem) {        String msg = "操作成功!";        int deleteById = idemMapper.deleteById(idem.getId());        if(deleteById == 0){            msg = "操作失败,id:"+idem.getId()+",已经被删除...";        }        return msg;    }

  利用主键唯一的特性,捕获处理重复操作

  乐观锁可解决更新操作

    /**     * 更新操作,使用乐观锁实现幂等性     */    @Override    public Object update(Idem idem) {        String msg = "操作成功!";        // UPDATE table SET [... 业务字段=? ...], 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()+",已经被更新...";        }        return msg;    }

  执行更新sql语句时,where条件带上version版本号,如果执行成功,除了更新业务数据,同时更新version版本号标记当前数据已被更新

UPDATE table SET [... 业务字段=? ...], version = version+1 WHERE (id = ? AND version = ?)

  执行更新操作前,需要重新执行插入数据

  以上for循环测试中,5个操作同样只有一个执行成功!

  后记

  redis、乐观锁不要在代码先查询后if判断,这样会存在并发问题,导致数据不准确,应该把这种判断放在redis、数据库

  错误示例:

//获取最新缓存String redisToken = template.opsForValue().get(token);//为空则放行业务if(redisToken == null){    //设置缓存    template.opsForValue().set(token, "1", 60 * 5, TimeUnit.SECONDS);    //业务处理}else{    //拒绝业务}

  错误示例:

//获取最新版本号Integer version = idemMapper.selectById(idem.getId()).getVersion();//版本号相同,说明数据未被其他人修改if(version == idem.getVersion()){    //正常更新}else{    //拒绝更新}

  防重与幂等暂时先记录到这,后续再进行补充

  代码开源

  代码已经开源、托管到我的GitHub、码云:

  GitHub:

  码云:

版权声明

作者:huanzi-qch

出处:

若标题中有“转载”字样,则本文版权归原作者所有。若无转载字样,本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利.

标签: #ajaxget方式等幂