SpringBoot系列教程之防重放与操作幂等

作者:huanzi-qch 时间:2021-12-07 11:51:12 

前言

日常开发中,我们可能会碰到需要进行防重放与操作幂等的业务,本文记录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>

SpringBoot系列教程之防重放与操作幂等

一个测试表

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="http://www.thymeleaf.org">
<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("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);
   });
 }

//节流测试
 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;
 }

SpringBoot系列教程之防重放与操作幂等

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)
     }
   }
 }

SpringBoot系列教程之防重放与操作幂等

防抖:给定一个时间,不管怎么点击按钮,每点一次,都会在最后一次点击等待这个时间过后执行

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)
   }
 }

SpringBoot系列教程之防重放与操作幂等

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 "操作失败,表单已被提交...";
       }
   }

SpringBoot系列教程之防重放与操作幂等

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

SpringBoot系列教程之防重放与操作幂等

SpringBoot系列教程之防重放与操作幂等

SpringBoot系列教程之防重放与操作幂等

数据库

唯一主键 + 乐观锁

查询操作自带幂等性

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

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

SpringBoot系列教程之防重放与操作幂等

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

/**
    * 插入操作,使用唯一主键实现幂等性
    */
   @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;
   }

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

SpringBoot系列教程之防重放与操作幂等

SpringBoot系列教程之防重放与操作幂等

SpringBoot系列教程之防重放与操作幂等

SpringBoot系列教程之防重放与操作幂等

乐观锁可解决更新操作

/**
    * 更新操作,使用乐观锁实现幂等性
    */
   @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 = ?)

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

SpringBoot系列教程之防重放与操作幂等

SpringBoot系列教程之防重放与操作幂等

以上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:https://github.com/huanzi-qch/springBoot

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

来源:https://www.cnblogs.com/huanzi-qch/p/16118316.html

标签:springboot,防重放,幂等
0
投稿

猜你喜欢

  • Android之使用Bundle进行IPC详解

    2023-09-27 22:44:56
  • Java中的Struts2框架拦截 器之实例代码

    2023-06-21 19:04:03
  • 获取Java线程转储的常用方法(推荐)

    2023-05-15 02:30:19
  • SpringBoot如何使用Fastjson解析Json数据

    2023-11-25 11:55:58
  • java线程池ThreadPoolExecutor类使用小结

    2021-09-10 16:22:05
  • 一篇文章带你入门Java Script

    2023-09-12 06:09:24
  • obix协议在java中的配置和使用详解

    2023-11-25 20:59:42
  • SpringBoot + validation 接口参数校验的思路详解

    2023-10-09 11:59:12
  • 一篇文章带你了解JVM垃圾回收

    2023-08-06 23:42:20
  • Spring Security保护用户密码常用方法详解

    2023-01-24 17:06:18
  • 使用Logback日志保存到相对路径的操作

    2021-10-11 16:42:30
  • Mybatis返回插入的主键问题解决方案

    2023-05-06 02:58:03
  • Spring整合MyBatis图示过程解析

    2023-11-13 11:45:09
  • Java实战入门之双色球彩票小游戏

    2023-05-12 04:07:13
  • 基于ReentrantLock的实现原理讲解

    2023-11-23 22:43:23
  • spring中的BeanFactory与FactoryBean的讲解

    2023-03-13 11:18:09
  • java序列化和java反序列化示例

    2023-11-24 04:01:03
  • 详解Java注解的实现与使用方法

    2023-10-31 12:33:20
  • Android 实现签到足迹功能

    2023-06-21 15:16:24
  • idea输入sout无法自动补全System.out.println()的问题

    2023-11-28 21:34:03
  • asp之家 软件编程 m.aspxhome.com