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

目录
  • 前言
  • 具体方案
  • 前端页面
  • Redis
  • 数据库
  • 后记
  • 代码开源
  • 总结

前言

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

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

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

总结

到此这篇关于SpringBoot系列教程之防重放与操作幂等的文章就介绍到这了,更多相关SpringBoot防重放与操作幂等内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Springboot+redis+Interceptor+自定义annotation实现接口自动幂等

    前言: 在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同.按照这个含义,最终的含义就是对数据库的影响只能是一次性的,不能重复处理.如何保证其幂等性,通常有以下手段: 1:数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据 2:token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断toke

  • Spring Boot接口设计防篡改、防重放攻击详解

    本示例主要内容 请求参数防止篡改攻击 基于timestamp方案,防止重放攻击 使用swagger接口文档自动生成 API接口设计 API接口由于需要供第三方服务调用,所以必须暴露到外网,并提供了具体请求地址和请求参数,为了防止被别有用心之人获取到真实请求参数后再次发起请求获取信息,需要采取很多安全机制. 需要采用https方式对第三方提供接口,数据的加密传输会更安全,即便是被破解,也需要耗费更多时间 需要有安全的后台验证机制,达到防参数篡改+防二次请求(本示例内容) 防止重放攻击必须要保证请求

  • SpringBoot实现接口幂等性的4种方案

    一.什么是幂等性 幂等是一个数学与计算机学概念,在数学中某一元运算为幂等时,其作用在任一元素两次后会和其作用一次的结果相同. 在计算机中编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同.幂等函数或幂等方法是指可以使用相同参数重复执行,并能获得相同结果的函数.这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变. 二.什么是接口幂等性 在HTTP/1.1中,对幂等性进行了定义.它描述了一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外),

  • Spring Boot接口幂等插件用法示例解析

    幂等概述 幂等性原本是数学上的概念,即使公式:f(x)=f(f(x)) 能够成立的数学性质.用在编程领域,则意为对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的. 幂等性是分布式系统设计中十分重要的概念,具有这一性质的接口在设计时总是秉持这样的一种理念:调用接口发生异常并且重复尝试时,总是会造成系统所无法承受的损失,所以必须阻止这种现象的发生. 实现幂等的方式很多,目前基于请求令牌机制适用范围较广.其核心思想是为每一次操作生成一个唯一性的凭证,也就是 token.一

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

    目录 前言 具体方案 前端页面 Redis 数据库 后记 代码开源 总结 前言 日常开发中,我们可能会碰到需要进行防重放与操作幂等的业务,本文记录SpringBoot实现简单防重与幂等 防重放,防止数据重复提交 操作幂等性,多次执行所产生的影响均与一次执行的影响相同 解决什么问题? 表单重复提交,用户多次点击表单提交按钮 接口重复调用,接口短时间内被多次调用 思路如下: 1.前端页面表提交钮置灰不可点击+js节流防抖 2.Redis防重Token令牌 3.数据库唯一主键 + 乐观锁 具体方案 p

  • SpringBoot系列教程JPA之基础环境搭建的方法

    JPA(Java Persistence API)Java持久化API,是 Java 持久化的标准规范,Hibernate是持久化规范的技术实现,而Spring Data JPA是在 Hibernate 基础上封装的一款框架.JPA作为标准,实际上并没有说局限于某个固定的数据源,事实上mysql,mongo, solr都是ok的.接下来我们将介绍下springboot结合jpa 来实现mysql的curd以及更加复杂一点的sql支持 jpa系列教程将包含以下几块 环境搭建 基础的插入.修改.删除

  • JQuery 无废话系列教程(一) jquery入门 [推荐]

    本文的宗旨, 授人鱼不如授人渔. 我只会讲关键部分,不可能JQuery的每个函数我都讲,因为有很多函数贝壳自己在实际应用中都从未使用过. 但当我们已经会渔了还用担心鱼吗!? BTW:贝壳假设你已经掌了基本的JavaScript运用能力及基础的CSS知识. JQuery是什么        JQuery只是一个JS文件 人对新鲜事业总是报着好奇与排斥的情感,贝壳刚开始学的时候也一样. 想掌握又怕自己学不会. 其实JQuery很简单.       JQuery其实就是一个工具包,很多常用的功能已经被

  • SpringBoot 入门教程之引入数据传输层的方法

    我们现在操作的数据直接就使用了实体类,假如我们输出的用户对象需要包含用户拥有的权限列表,那么我们怎么封装呢? 直接封装进实体类中显然不行,因为实体类的职责就是提供与数据库表字段一一对应的映射关系,所以我们还需要封装出一层数据传输层 DTO(Data Transfer Object),通过它暴露给外部,这样我们就能高度定制化我们需要输出的数据而不会影响到实体类. 在项目中创建 dto 目录,新建 UserDto.java 文件: package com.foxescap.wxbox.dto; im

  • Spring Security系列教程之会话管理处理会话过期问题

    目录 前言 一. 会话过期 1. 会话过期概念 2. Session的超时时间 3. 会话过期时的处理策略 二. 会话过期时的处理策略(一) 1. 配置会话过期时间 2. 定义测试接口 3. 配置跳转到某个URL 4. 启动测试 三. 会话过期时的处理策略(二) 1. 自定义MyInvalSessionStrategy类 2. 配置自定义过期策略 前言 在上一章节中,一一哥 给各位讲解了HTTP协议.会话.URL重新.会话固定攻击等概念,并且实现了对会话固定攻击的防御拦截. 在Spring Se

  • MySQL系列教程小白数据库基础

    目录 1.基础概念与基础命令 1)基础概念 2)基础命令 2.SQL语句的书写顺序和执行顺序 1)一个完整SQL查询语句的书写顺序 2)一个完整的SQL语句执行顺序 3)关于select和having执行顺序谁前谁后的说明 这里介绍的MySQL相关知识,并不是针对那些想学习DBA的同学,而是针对那些想转行.从事数据分析行业的同学. 下面这些知识点,是我学习MySQL数据库时,觉得有必要掌握的一些知识点. 1.基础概念与基础命令 1)基础概念 ① 数据库 按照一定数据结构,存储和管理数据的仓库.

  • SpringBoot系列之MongoDB Aggregations用法详解

    目录 1.前言 2.什么是聚合? 3.环境搭建 4.数据initialize 5.例子应用 参考资料 1.前言 在上一章的学习中,我们知道了Spring Data MongoDB的基本用法,但是对于一些聚合操作,还是不熟悉的,所以本博客介绍一些常用的聚合函数 2.什么是聚合? MongoDB 中使用聚合(Aggregations)来分析数据并从中获取有意义的信息.在这个过程,一个阶段的输出作为输入传递到下一个阶段 常用的聚合函数 聚合函数 SQL类比 描述 project SELECT 类似于s

  • rabbitmq学习系列教程之消息应答(autoAck)、队列持久化(durable)及消息持久化

    目录 一.前言 二.autoAck参数的讨论 三.rabbitmq队列持久化操作 四.2019.11.04问题补充 五.2019.11.07消息的持久化 六.2022.02.09增加队列持久化说明 结语 一.前言 Boolean autoAck = false; channel.basicConsume(queue_name, autoAck ,consumer); 在simple queue 和 work queue(轮询) 处理中,我们设置的消费者的消息监听都采用 channel.basic

  • SpringBoot实战教程之新手入门篇

    目录 1.SpringBoot简介 2.SpringBoot安装 2.1.基于Maven的安装 2.2.基于Gradle的安装 3.创建HelloWorld应用 4.代码结构 5.Configuration类 5.1.导入其它配置 6.自动化配置 6.1.逐渐替换自动化配置 6.2.禁止特定的自动化配置 7.使用@SpringBootApplication注解 总结 1. SpringBoot 简介 Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化 Spri

  • SpringBoot+Redis布隆过滤器防恶意流量击穿缓存

    目录 什么是恶意流量穿透 怎么防 布隆过滤器的另一个用武场景 给Redis安装BloomFilter 在Redis里使用BloomFilter 结合SpringBoot使用 搭建springboot工程 使用压测工具喂120万条数据进入RedisBloomfilter看实际效果 本文主要介绍了SpringBoot+Redis布隆过滤器防恶意流量击穿缓存,具体如下: 什么是恶意流量穿透 假设我们的Redis里存有一组用户的注册email,以email作为Key存在,同时它对应着DB里的User表的

随机推荐