6 implementation options for single-machine idempotence

A friend suddenly asked two days ago: What is the simplest solution to prevent repeated submissions in Java?

This sentence contains two key messages, first: Prevent duplicate submissions; second: Easiest.

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

The feedback I got was that it would be simple if it was a stand-alone environment, so Brother Lei started to install it*.

Without further ado, let’s reproduce this problem first.

Simulating user scenarios

According to feedback from friends, the general scene 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 {
   /**
     * Methods that are repeatedly requested
     */
    @RequestMapping("/add")
    public String addUser(String id) {
        // Business code...
        System.out.println("Add user ID:" + id);
        return "Execution successful!";
    }
}

So Brother Lei came up with the idea of solving the problem of repeated data submission by intercepting data at the front and back ends respectively.

Front-end interception

Front-end interception refers to intercepting repeated requests through HTML pages. For example, after the user clicks the “Submit” button, we can set the button to be unavailable or hidden.

The execution effect is shown in the figure below:

Front-end interception implementation code:

<html>
<script>
    function subCli(){
        //Set the button to be disabled
        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>

But front-end interception has a fatal problem. If you are a knowledgeable programmer or illegal user, you can directly bypass the front-end page and repeatedly submit requests by simulating requests. For example, if you recharge 100 yuan and submit 10 times, the result becomes 1,000 yuan ( Instantly discovered a good way to get rich).

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

Backend interception

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

We store the requested business ID in memory and add a mutex lock to ensure the safety of program execution under multi-threads. The general implementation idea is as shown in the figure below:

However, the simplest way to store data in memory is to use HashMap to store it, or using Guava Cache has the same effect, but obviously HashMap can be faster To implement the function, we first implement an anti-duplication (prevent 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-empty judgment (ignored)...
        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 successful!";
    }
}

The effect is shown in the figure below:

Existing problems: This implementation has a fatal problem, because HashMap grows infinitely, so it will occupy more and more memory, and as As the number of >HashMap increases, the search speed will also decrease, so we need to implement a solution that can automatically “clear” expired data.

2. Optimized version – fixed size array

This version solves the problem of infinite growth of HashMap. It uses the array plus subscript counter (reqCacheCounter) to implement circular storage of fixed arrays.

When the array is stored to the last digit, the storage subscript of the array is set to 0, and the data is stored 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 storage collection
    private static Integer reqCacheCounter = 0; // Request counter (indicates where the ID is stored)

    @RequestMapping("/add")
    public String addUser(String id) {
        // Non-empty judgment (ignored)...
        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 + + ; // Move the subscript back one position
        }
        // Business code...
        System.out.println("Add user ID:" + id);
        return "Execution successful!";
    }
}

3. Extended version – double detection lock (DCL)

The previous implementation method puts both the judgment and the added business into synchronized for locking operations. Obviously, the performance is not very high, so we can use the famous DCL (Double Checked Locking) in the singleton. Double detection lock) to optimize the execution efficiency of the code. 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 storage collection
    private static Integer reqCacheCounter = 0; // Request counter (indicates where the ID is stored)

    @RequestMapping("/add")
    public String addUser(String id) {
        // Non-empty judgment (ignored)...
        // 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, double checked locking) improves program execution efficiency
            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 + + ; // Move the subscript back one position
        }
        // Business code...
        System.out.println("Add user ID:" + id);
        return "Execution successful!";
    }
}

Note: DCL is suitable for business scenarios with high frequency of repeated submissions. DCL is not applicable for opposite business scenarios.

4. Improved version – LRUMap

The above code has basically implemented the interception of duplicate data, but it is obviously not concise and elegant enough, such as the declaration of subscript counters and business processing, but fortunately Apache provides us with a commons-collections framework, which contains a The very easy-to-use data structure LRUMap can save a specified amount of fixed data, and it will help you clear the least commonly used data according to the LRU algorithm.

Tips: LRU is the abbreviation of Least Recently Used, which is the least recently used. It is a commonly used data elimination algorithm that selects the most recent and longest unused data for elimination.

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>

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 collection of data is eliminated according to the LRU algorithm.
    private LRUMap<String, Integer> reqCache = new LRUMap<>(100);

    @RequestMapping("/add")
    public String addUser(String id) {
        // Non-empty judgment (ignored)...
        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 successful!";
    }
}

After using LRUMap, the code is obviously much simpler.

5. Final version – encapsulation

The above are all method-level implementation solutions. However, in actual business, we may have many methods that need to be protected against duplication, so next we will encapsulate a public method for use by all classes:

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

/**
 * Idempotence judgment
 */
public class IdempotentUtils {

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

    /**
     * Idempotence 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;
    }
}

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-empty judgment (ignored)...
        // -------------- 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 successful!";
    }
}

Tips: Under normal circumstances, the code ends here, but if you want to be more concise, you can also write the business code into the annotation through custom annotations. The method that needs to be called only needs to write a line of annotation. This can prevent repeated submission of data. Veterans can try it on their own (if you need Brother Lei to write an article, please leave a message 666 in the comment area).

Extended knowledge – analysis of LRUMap implementation principles

Since LRUMap is so powerful, let’s take a look at how it is implemented.

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

AbstractLinkedMap.LinkEntry entry;

When the query method is called, the used element will be placed at the previous position 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.");
    }

}

If a new element is added and the capacity is full, the next element of the header will be removed. Add the source code 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 & amp; & amp; 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);
     }
 }

Source code to determine capacity:

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

If the capacity is not full, add data directly:

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

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

To sum up: The essence of LRUMap is a looping double linked list structure holding the head node. When an element is used, the element is placed in the double linked list header When adding an element, if the capacity is full, the element after header will be removed.

Summary

This article talks about 6 methods to prevent repeated submission of data. The first is front-end interception. By hiding and setting the button to be disabled, it is used to block repeated submission under normal operation. However, in order to avoid repeated submissions through abnormal channels, we have implemented 5 versions of back-end interception: HashMap version, fixed array version, array version with double detection lock, LRUMap version and LRUMap encapsulated version.

Special note: All the content in this article only applies to duplicate data interception in a stand-alone environment. If it is a distributed environment, it needs to be implemented with a database or Redis.

The knowledge points of the article match the official knowledge files, and you can further learn related knowledge. Java Skill TreeHomepageOverview 139502 people are learning the system