问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

最简单的6种防止数据重复提交的方法!(干货)

创作时间:
作者:
@小白创作中心

最简单的6种防止数据重复提交的方法!(干货)

引用
1
来源
1.
https://www.pianshen.com/article/32671613239/

防止数据重复提交是Web开发中常见的需求,特别是在表单提交、支付等场景中。本文将介绍在Java单机环境下实现这一功能的6种方法,从简单的前端拦截到复杂的后端处理方案,帮助开发者全面理解并解决这一问题。

模拟用户场景

假设有一个用户添加接口,需要防止用户在短时间内多次提交相同的请求。具体场景如下:

对应的Spring Boot控制器代码如下:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/user")
@RestController
public class UserController {
   /**
    * 被重复请求的方法
    */
    @RequestMapping("/add")
    public String addUser(String id) {
        // 业务代码...
        System.out.println("添加用户ID:" + id);
        return "执行成功!";
    }
}

前端拦截

前端拦截是最简单直接的方式,通过在用户点击提交按钮后,将按钮设置为不可用或隐藏状态,可以有效防止正常操作下的重复提交。

执行效果如下:

但是,前端拦截存在一个致命问题:懂行的程序员或非法用户可以直接绕过前端页面,通过模拟请求来重复提交。因此,后端拦截是必不可少的。

后端拦截

后端拦截的实现思路是在方法执行之前,先判断此业务是否已经执行过,如果执行过则不再执行,否则就正常执行。将请求的业务ID存储在内存中,并通过添加互斥锁来保证多线程下的程序执行安全。

1. 基础版 - HashMap

使用HashMap存储请求ID,代码如下:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;

/**
 * 普通 Map 版本
 */
@RequestMapping("/user")
@RestController
public class UserController3 {

    // 缓存 ID 集合
    private Map<String, Boolean> reqCache = new HashMap<>();

    @RequestMapping("/add")
    public String addUser(String id) {
        synchronized (this.getClass()) {
            if (reqCache.containsKey(id)) {
                System.out.println("请勿重复提交!!!" + id);
                return "执行失败";
            }
            reqCache.put(id, true);
        }
        System.out.println("添加用户ID:" + id);
        return "执行成功!";
    }
}

但是,HashMap会无限增长,占用越来越多的内存,因此需要优化。

2. 优化版 - 固定大小的数组

使用固定大小的数组加下标计数器的方式,实现代码如下:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;

@RequestMapping("/user")
@RestController
public class UserController {

    private static String[] reqCache = new String[100]; // 请求 ID 存储集合
    private static Integer reqCacheCounter = 0; // 请求计数器(指示 ID 存储的位置)

    @RequestMapping("/add")
    public String addUser(String id) {
        synchronized (this.getClass()) {
            if (Arrays.asList(reqCache).contains(id)) {
                System.out.println("请勿重复提交!!!" + id);
                return "执行失败";
            }
            if (reqCacheCounter >= reqCache.length) reqCacheCounter = 0; // 重置计数器
            reqCache[reqCacheCounter] = id; // 将 ID 保存到缓存
            reqCacheCounter++; // 下标往后移一位
        }
        System.out.println("添加用户ID:" + id);
        return "执行成功!";
    }
}

3. 扩展版 - 双重检测锁(DCL)

使用DCL优化代码执行效率,实现代码如下:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;

@RequestMapping("/user")
@RestController
public class UserController {

    private static String[] reqCache = new String[100]; // 请求 ID 存储集合
    private static Integer reqCacheCounter = 0; // 请求计数器(指示 ID 存储的位置)

    @RequestMapping("/add")
    public String addUser(String id) {
        if (Arrays.asList(reqCache).contains(id)) {
            System.out.println("请勿重复提交!!!" + id);
            return "执行失败";
        }
        synchronized (this.getClass()) {
            if (Arrays.asList(reqCache).contains(id)) {
                System.out.println("请勿重复提交!!!" + id);
                return "执行失败";
            }
            if (reqCacheCounter >= reqCache.length) reqCacheCounter = 0; // 重置计数器
            reqCache[reqCacheCounter] = id; // 将 ID 保存到缓存
            reqCacheCounter++; // 下标往后移一位
        }
        System.out.println("添加用户ID:" + id);
        return "执行成功!";
    }
}

4. 完善版 - LRUMap

使用Apache Commons Collections的LRUMap,代码如下:

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-collections4</artifactId>
  <version>4.4</version>
</dependency>
import org.apache.commons.collections4.map.LRUMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/user")
@RestController
public class UserController {

    private LRUMap<String, Boolean> reqCache = new LRUMap<>(100);

    @RequestMapping("/add")
    public String addUser(String id) {
        if (reqCache.containsKey(id)) {
            System.out.println("请勿重复提交!!!" + id);
            return "执行失败";
        }
        reqCache.put(id, true);
        System.out.println("添加用户ID:" + id);
        return "执行成功!";
    }
}

5. 最终版 - 封装

封装一个公共工具类,代码如下:

import org.apache.commons.collections4.map.LRUMap;

public class IdempotentUtils {

    private static LRUMap<String, Boolean> reqCache = new LRUMap<>(100);

    public static boolean judge(String id, Class<?> clazz) {
        if (reqCache.containsKey(id)) {
            return false;
        }
        reqCache.put(id, true);
        return true;
    }
}

调用代码如下:

import com.example.idempote.util.IdempotentUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/user")
@RestController
public class UserController4 {
    @RequestMapping("/add")
    public String addUser(String id) {
        if (!IdempotentUtils.judge(id, this.getClass())) {
            return "执行失败";
        }
        System.out.println("添加用户ID:" + id);
        return "执行成功!";
    }
}

扩展知识 - LRUMap 实现原理分析

LRUMap的本质是持有头结点的环回双链表结构,当使用元素时,就将该元素放在双链表header的前一个位置,在新增元素时,如果容量满了就会移除header的后一个元素。

总结

本文介绍了防止数据重复提交的6种方法,包括前端拦截和多种后端处理方案。这些方法各有优劣,开发者可以根据具体场景选择合适的技术方案。需要注意的是,本文的内容仅适用于单机环境下的重复数据拦截,分布式环境下的解决方案将涉及数据库或Redis等技术。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号