6 ways to prevent repeated data submission (super simple)

A friend suddenly asked Dongge one day: what is the simplest solution to prevent repeated submission in Java?

This sentence contains two key messages: first, prevent repeated submission; Second: the simplest.

So Dongge asked him, is it a stand-alone environment or a distributed environment?

The feedback was that it was a stand-alone environment, so it was simple, so Dongge began to install *.

Without much to say, let's repeat the problem first.

Simulate user scenarios

According to the feedback from friends, the general scenario is as follows, as shown in the figure below:

The simplified simulation code is as follows (based on Spring Boot):

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

@RequestMapping("/user")
@RestController
public class UserController {
   /**
     * Method requested repeatedly
     */
    @RequestMapping("/add")
    public String addUser(String id) {
        // Business code
        System.out.println("Add user ID:" + id);
        return "Execution succeeded!";
    }
}
Copy code
 Copy code

So Dongge thought: solve the problem of repeated data submission by intercepting the front and back ends respectively.

Scan VX for Java data, front-end, test, python and so on

Front end interception

Front end interception refers to intercepting repeated requests through HTML pages. For example, after users click the "submit" button, we can set the button to unavailable or hidden status.

The execution effect is shown in the following figure:

Implementation code of front-end interception:

<html>
<script>
    function subCli(){
        // Button set to unavailable
        document.getElementById("btn_sub").disabled="disabled";
        document.getElementById("dv1").innerText = "The button was clicked~";
    }
</script>
<body style="margin-top: 100px;margin-left: 100px;">
    <input id="btn_sub" type="button"  value=" Submit "  onclick="subCli()">
    <div id="dv1" style="margin-top: 80px;"></div>
</body>
</html>
Copy code
 Copy code

However, there is a fatal problem in front-end interception. If a knowledgeable programmer or illegal user can directly bypass the front-end page and repeatedly submit the request by simulating the request. For example, he recharged 100 yuan and repeatedly submitted 10 times, which became 1000 yuan (he immediately found a good way to get rich).

Therefore, in addition to the normal misoperation of the front-end interception, the back-end interception is also essential.

Backend interception

The implementation idea of back-end interception is to judge whether the business has been executed before the method is executed. If it has been executed, it will not be executed, otherwise it will be executed normally.

We store the requested service ID in memory and add a mutex to ensure the safety of program execution under multithreading. The general implementation idea is shown in the figure below:

However, the simplest way to store data in memory is to use HashMap storage or Guava Cache. Obviously, HashMap can realize functions faster, so let's first implement an anti duplication version of HashMap.

1. Basic version - HashMap

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

import java.util.HashMap;
import java.util.Map;

/**
 * Normal Map version
 */
@RequestMapping("/user")
@RestController
public class UserController3 {

    // Cache ID collection
    private Map<String, Integer> reqCache = new HashMap<>();

    @RequestMapping("/add")
    public String addUser(String id) {
        // Non null judgment (ignore)
        synchronized (this.getClass()) {
            // Repeat request judgment
            if (reqCache.containsKey(id)) {
                // Repeat request
                System.out.println("Please do not submit again!!!" + id);
                return "Execution failed";
            }
            // Storage request ID
            reqCache.put(id, 1);
        }
        // Business code
        System.out.println("Add user ID:" + id);
        return "Execution succeeded!";
    }
}
Copy code
 Copy code

The implementation effect is shown in the figure below:

Existing problems: this implementation method has a fatal problem. Because HashMap grows infinitely, it will occupy more and more memory, and the search speed will decrease with the increase of the number of hashmaps. Therefore, we need to implement an implementation scheme that can automatically "clear" expired data.

2. Optimized version - fixed size array

This version solves the problem of unlimited growth of HashMap. It uses the method of array plus subscript counter (reqCacheCounter) to realize the circular storage of fixed array.

When the array is stored to the last bit, set the storage subscript of the array to 0, and then store the data from the beginning. The implementation code is as follows:

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]; // Request ID store collection
    private static Integer reqCacheCounter = 0; // Request counter (indicating where the ID is stored)

    @RequestMapping("/add")
    public String addUser(String id) {
        // Non null judgment (ignore)
        synchronized (this.getClass()) {
            // Repeat request judgment
            if (Arrays.asList(reqCache).contains(id)) {
                // Repeat request
                System.out.println("Please do not submit again!!!" + id);
                return "Execution failed";
            }
            // Record request ID
            if (reqCacheCounter >= reqCache.length) reqCacheCounter = 0; // Reset counter
            reqCache[reqCacheCounter] = id; // Save ID to cache
            reqCacheCounter++; // The subscript moves back one bit
        }
        // Business code
        System.out.println("Add user ID:" + id);
        return "Execution succeeded!";
    }
}
Copy code
 Copy code

3. Extended version - double detection lock (DCL)

In the previous implementation method, the judgment and added services are put into synchronized for locking. Obviously, the performance is not very high. Therefore, we can use the famous DCL (Double Checked Locking) in the single example to optimize the execution efficiency of the code. The actual modern code is as follows:

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]; // Request ID store collection
    private static Integer reqCacheCounter = 0; // Request counter (indicating where the ID is stored)

    @RequestMapping("/add")
    public String addUser(String id) {
        // Non null judgment (ignore)
        // Repeat request judgment
        if (Arrays.asList(reqCache).contains(id)) {
            // Repeat request
            System.out.println("Please do not submit again!!!" + id);
            return "Execution failed";
        }
        synchronized (this.getClass()) {
            // Double checked locking (DCL) improves the execution efficiency of programs
            if (Arrays.asList(reqCache).contains(id)) {
                // Repeat request
                System.out.println("Please do not submit again!!!" + id);
                return "Execution failed";
            }
            // Record request ID
            if (reqCacheCounter >= reqCache.length) reqCacheCounter = 0; // Reset counter
            reqCache[reqCacheCounter] = id; // Save ID to cache
            reqCacheCounter++; // The subscript moves back one bit
        }
        // Business code
        System.out.println("Add user ID:" + id);
        return "Execution succeeded!";
    }
}
Copy code
 Copy code

Note: DCL is applicable to business scenarios where repeated submissions are frequent and high. DCL is not applicable to opposite business scenarios.

Scan VX for Java data, front-end, test, python and so on

4. Improved version - LRUMap

The above code has basically realized the interception of duplicate data, but it is obviously not concise and elegant, such as subscript counter declaration and business processing. Fortunately, Apache provides us with a framework of Commons collections, which has a very useful data structure LRUMap, which can store a specified amount of fixed data, And it will help you clear the least commonly used data according to the LRU algorithm.

Tip: LRU is the abbreviation of Least Recently Used, that is, Least Recently Used. It is a common data elimination algorithm. Select the most recently unused data to eliminate.

First, let's add a reference to Apache commons collections:

 <!-- Collection tool class apache commons collections -->
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-collections4</artifactId>
  <version>4.4</version>
</dependency>
Copy code
 Copy code

The implementation code is as follows:

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 {

    // The maximum capacity is 100, and the Map set of data is eliminated according to LRU algorithm
    private LRUMap<String, Integer> reqCache = new LRUMap<>(100);

    @RequestMapping("/add")
    public String addUser(String id) {
        // Non null judgment (ignore)
        synchronized (this.getClass()) {
            // Repeat request judgment
            if (reqCache.containsKey(id)) {
                // Repeat request
                System.out.println("Please do not submit again!!!" + id);
                return "Execution failed";
            }
            // Storage request ID
            reqCache.put(id, 1);
        }
        // Business code
        System.out.println("Add user ID:" + id);
        return "Execution succeeded!";
    }
}
Copy code
 Copy code

After using LRUMap, the code is obviously much simpler.

5. Final version - packaging

The above are implementation schemes at the method level. However, in the actual business, we may have many methods that need anti duplication. Next, we will encapsulate a public method for all classes:

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

/**
 * Idempotent judgment
 */
public class IdempotentUtils {

    // According to the LRU(Least Recently Used) algorithm, eliminate the Map set of data, with a maximum capacity of 100
    private static LRUMap<String, Integer> reqCache = new LRUMap<>(100);

    /**
     * Idempotent judgment
     * @return
     */
    public static boolean judge(String id, Object lockClass) {
        synchronized (lockClass) {
            // Repeat request judgment
            if (reqCache.containsKey(id)) {
                // Repeat request
                System.out.println("Please do not submit again!!!" + id);
                return false;
            }
            // Non duplicate request, store request ID
            reqCache.put(id, 1);
        }
        return true;
    }
}
Copy code
 Copy code

The calling code is as follows:

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) {
        // Non null judgment (ignore)
        // --------------Idempotent call (start)--------------
        if (!IdempotentUtils.judge(id, this.getClass())) {
            return "Execution failed";
        }
        // --------------Idempotent call (end)--------------
        // Business code
        System.out.println("Add user ID:" + id);
        return "Execution succeeded!";
    }
}
Copy code
 Copy code

Tip: normally, the code ends here, but it is also possible to be more concise. You can write the business code to the annotations by using the custom annotation. The method needs to be written only by writing a line of annotations to prevent data from being submitted repeatedly. The old fellow can try it on their own (a message from the brother's column, 666).

Extended knowledge -- Analysis of LRUMap implementation principle

Since LRUMap is so powerful, let's see how it is implemented.

The essence of LRUMap is a loopback double linked list structure holding head nodes. Its storage structure is as follows:

AbstractLinkedMap.LinkEntry entry;
Copy code
 Copy code

When calling the query method, the element used will be placed in the front of the double linked list header. The source code is as follows:

public V get(Object key, boolean updateToMRU) {
    LinkEntry<K, V> entry = this.getEntry(key);
    if (entry == null) {
        return null;
    } else {
        if (updateToMRU) {
            this.moveToMRU(entry);
        }

        return entry.getValue();
    }
}
protected void moveToMRU(LinkEntry<K, V> entry) {
    if (entry.after != this.header) {
        ++this.modCount;
        if (entry.before == null) {
            throw new IllegalStateException("Entry.before is null. This should not occur if your keys are immutable, and you have used synchronization properly.");
        }

        entry.before.after = entry.after;
        entry.after.before = entry.before;
        entry.after = this.header;
        entry.before = this.header.before;
        this.header.before.after = entry;
        this.header.before = entry;
    } else if (entry == this.header) {
        throw new IllegalStateException("Can't move header to MRU This should not occur if your keys are immutable, and you have used synchronization properly.");
    }

}
Copy code
 Copy code

When adding new elements, the next element of the header will be removed when the capacity is full. The added source code is as follows:

 protected void addMapping(int hashIndex, int hashCode, K key, V value) {
     // Determine whether the container is full	
     if (this.isFull()) {
         LinkEntry<K, V> reuse = this.header.after;
         boolean removeLRUEntry = false;
         if (!this.scanUntilRemovable) {
             removeLRUEntry = this.removeLRU(reuse);
         } else {
             while(reuse != this.header && reuse != null) {
                 if (this.removeLRU(reuse)) {
                     removeLRUEntry = true;
                     break;
                 }
                 reuse = reuse.after;
             }
             if (reuse == null) {
                 throw new IllegalStateException("Entry.after=null, header.after=" + this.header.after + " header.before=" + this.header.before + " key=" + key + " value=" + value + " size=" + this.size + " maxSize=" + this.maxSize + " This should not occur if your keys are immutable, and you have used synchronization properly.");
             }
         }
         if (removeLRUEntry) {
             if (reuse == null) {
                 throw new IllegalStateException("reuse=null, header.after=" + this.header.after + " header.before=" + this.header.before + " key=" + key + " value=" + value + " size=" + this.size + " maxSize=" + this.maxSize + " This should not occur if your keys are immutable, and you have used synchronization properly.");
             }
             this.reuseMapping(reuse, hashIndex, hashCode, key, value);
         } else {
             super.addMapping(hashIndex, hashCode, key, value);
         }
     } else {
         super.addMapping(hashIndex, hashCode, key, value);
     }
 }
Copy code
 Copy code

Source code for judging capacity:

public boolean isFull() {
  return size >= maxSize;
}
Copy code
 Copy code

**Add data directly before the capacity is full:

super.addMapping(hashIndex, hashCode, key, value);
Copy code
 Copy code

If the capacity is full, call the reuseMapping method and use the LRU algorithm to clear the data.

To sum up: the essence of LRUMap is a loopback double linked list structure that holds the head node. When using an element, it will be placed in the previous position of the double linked list header. When adding an element, if the capacity is full, the next element of the header will be removed.

I have written six PDFs < about Java introduction to the great God >, which have spread over 10w + all over the network. After searching the public account of "code farmer attack", I will reply to the PDF in the background and get all the PDFs

Six PDF links

summary

This paper describes six methods to prevent repeated data submission. The first is the front-end interception, which can be used to shield repeated submission under normal operation by hiding and setting the buttons. However, in order to avoid repeated Submission from abnormal channels, we have implemented five versions of back-end interception: HashMap version, fixed array version, array version of double detection lock, LRUMap version and encapsulated version of LRUMap.

Scan VX for Java data, front-end, test, python and so on

Keywords: Java Front-end

Added by drkylec on Thu, 09 Dec 2021 22:39:57 +0200