A friend of mine asked Tongo the other day: What’s the easiest way to prevent double commits in Java?
There are two key messages in this statement: first, prevent duplicate submissions; Second: the simplest.
So Tongo asked him, is the standalone environment or distributed environment?
The feedback is the stand-alone environment, that is simple, so east brother began to install *.
Without further ado, let’s revisit the question.
Simulated user scenario
According to my friend’s feedback, the general scene is as follows:
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 {/** * @requestMapping ("/add") public String addUser(String id) {// Business code... System.out.println(" add user ID:" + ID); Return "Execution successful!" ; }} Copy the codeCopy the code
So Dongge thought of solving the problem of repeated data submission by intercepting the front end and the back end respectively.
The front intercept
Front-end interception is the blocking of repeated requests through HTML pages, for example, after the user clicks the “Submit” button, we can make the button unavailable or hidden.
The execution effect is as follows:
Front-end interception implementation code:
< HTML > <script> function subCli(){// Button set to unavailable document.getelementByid ("btn_sub"). Disabled ="disabled"; Document.getelementbyid ("dv1"). InnerText = "The button is clicked ~"; } </script> <body style="margin-top: 100px; margin-left: 100px;" "> <div id="dv1" style="margin-top: 80px; ></div> </body> </ HTML > Copy the codeCopy the code
However, front-end interception has a fatal problem, if it is a savvy programmer or illegal users can directly bypass the front-end page, through the simulation request to repeat submission requests, such as recharge 100 yuan, repeat submission 10 times becomes 1000 yuan (instantly found a good way to get rich).
So in addition to front-end interception part of the normal misoperation, back-end interception is also essential.
The back-end to intercept
The idea of back-end interception is to check whether the service has been executed before the method is executed. If it has been executed, the service will not be executed. Otherwise, the service will be executed normally.
We store the requested business ID in memory, and add mutex to ensure the safe execution of the program under multi-threading. The general implementation idea is shown in the figure below:
However, the easiest way to store data in memory is to use a HashMap, or a Guava Cache would do the same thing, but obviously hashMaps are faster to implement, so let’s implement a dure-proof version of HashMap first.
1. Basic — HashMap
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; /** * Common Map version */ @requestMapping ("/user") @restController Public Class UserController3 {// Cache ID set private Map<String, Integer> reqCache = new HashMap<>(); @requestMapping ("/add") public String addUser(String id) {// RequestMapping("/add") public String addUser(String id) { Synchronized (this.getClass()) {if (reqcache.containsKey (id)) {system.out.println (" Do not resubmit!!" + id); Return "Execute failed "; } // Store request ID reqcache. put(ID, 1); } // Business code... System.out.println(" add user ID:" + ID); Return "Execution successful!" ; }} Copy the codeCopy the code
The realization effect is shown in the figure below:
The problem: This implementation has a fatal problem because the HashMap grows indefinitely, so it takes up more and more memory, and lookups slow down as the number of HashMaps increases, so we need to implement an implementation that automatically “cleans up” expired data.
2. Optimized — Fixed size arrays
This version solves the problem of infinite growth of the HashMap, which uses array plus subscript counter (reqCacheCounter) for circular storage of fixed arrays.
When the array is stored to the last bit, set the array storage index to 0, and then start to store data, 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]; Private static Integer reqCacheCounter = 0; @requestMapping ("/add") public String addUser(String ID) { Synchronized (this.getClass()) {if (arrays.aslist (reqCache).contains(ID)) {// synchronized (this.getclass ()) System.out.println(" Do not submit twice!! ") + id); Return "Execute failed "; } if (reqCacheCounter >= reqcache.length) reqCacheCounter = 0; ReqCache [reqCacheCounter] = id; // save the ID to cache reqCacheCounter++; // move the subscript back one bit} // business code... System.out.println(" add user ID:" + ID); Return "Execution successful!" ; }} Copy the codeCopy the code
3. Extended version — Double Checked Lock (DCL)
The previous implementation method put both the judgment and addition services into synchronized for Locking operation, which obviously has low performance. Therefore, we can use the famous DCL (Double Checked Locking) in the singleton to optimize the execution efficiency of the code, and 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]; Private static Integer reqCacheCounter = 0; @requestMapping ("/add") public String addUser(String ID) { Array.aslist (reqCache).contains(ID)) {array.out.println (" Do not submit multiple Arrays!!" + id); Return "Execute failed "; } synchronized (this.getclass ()) {// double checked locking (DCL,double checked locking) improves program execution efficiency if (array.aslist (reqCache).contains(ID)) {// Repeat request system.out.println (" Do not repeat submit!!" + id); Return "Execute failed "; } if (reqCacheCounter >= reqcache.length) reqCacheCounter = 0; ReqCache [reqCacheCounter] = id; // save the ID to cache reqCacheCounter++; // move the subscript back one bit} // business code... System.out.println(" add user ID:" + ID); Return "Execution successful!" ; }} Copy the codeCopy the code
Note: DCL is suitable for business scenarios with high frequency of repeated submissions, but not 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 thankfully Apache provides us with a commons-collections framework. There is a very useful data structure LRUMap that can hold a specified amount of fixed data, and it will follow the LRU algorithm to help you remove the least used data.
Tip: LRU is the abbreviation of Least Recently Used, which means Least Recently Used. It is a commonly Used data elimination algorithm. It selects the latest and longest unused data to be eliminated.
First, let’s add a reference to Apache Commons Collections:
<! -- Apache Commons Collections --> <! -- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 --> <dependency> Mons < groupId > org.apache.com < / groupId > < artifactId > Commons - collections4 < / artifactId > < version > 4.4 < / version > < / dependency > Copy the codeCopy the 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 { Private LRUMap<String, Integer> reqCache = new LRUMap<>(100); @requestMapping ("/add") public String addUser(String id) {// RequestMapping("/add") public String addUser(String id) { Synchronized (this.getClass()) {if (reqcache.containsKey (id)) {system.out.println (" Do not resubmit!!" + id); Return "Execute failed "; } // Store request ID reqcache. put(ID, 1); } // Business code... System.out.println(" add user ID:" + ID); Return "Execution successful!" ; }} Copy the codeCopy the code
With LRUMap, the code is obviously much cleaner.
5. Final version — Encapsulation
These are all method-level implementations, but in a real business, we might have many methods that need to be protected from duplication, so we’ll encapsulate a common method for all classes to use:
import org.apache.commons.collections4.map.LRUMap; // public class IdempotentUtils {// Public class IdempotentUtils {// Public class IdempotentUtils { Private static LRUMap<String, Integer> reqCache = new LRUMap<>(100); Public static Boolean judge(String id, String id, String id, String id) Object lockClass {synchronized (lockClass) {if (reqcache.containskey (id)) {// synchronized (lockClass) System.out.println(" Do not submit twice!! ") + id); return false; } reqcache. put(ID, 1); } return true; }} Copy the codeCopy the code
The call 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 "execute failed "; } / / -- -- -- -- -- -- -- -- -- -- -- -- -- -- idempotence call (end) -- -- -- -- -- -- -- -- -- -- -- -- -- -- / / business code... System.out.println(" add user ID:" + ID); Return "Execution successful!" ; }} Copy the codeCopy the code
Tip: in general code write here will be over, but want to also can achieve more concise, you can pass the custom annotation, the business code written notes, you need to call the method only needs to write a line annotations can prevent repeated data submitted, the brother can try (to dong lu an, comment section 666).
Extended knowledge – LRUMap implementation principle analysis
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 with head nodes. Its storage structure is as follows:
AbstractLinkedMap.LinkEntry entry; Copy the codeCopy the code
When the query method is called, the element used will be placed before the double-linked list header, 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 the codeCopy the code
If you add a new element to the header, it will remove the last element of the header.
Protected void addMapping(int hashIndex, int hashCode, K key, V value) {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 the codeCopy the code
Judge capacity source:
public boolean isFull() { return size >= maxSize; } Duplicate codeCopy the code
** Add data directly when the capacity is not full:
super.addMapping(hashIndex, hashCode, key, value); Copy the codeCopy the code
If the capacity is full, reuseMapping is called to clean up the data using the LRU algorithm.
To sum up, the essence of LRUMap is to hold the loopback double-linked structure of the header. When an element is used, it is placed before the double-linked header. When a new element is added, the last element of the header is removed if the capacity is full.
I myself liver six copies of PDF < about Java entry to god >, the whole network spread more than 10W +, search “code farmers attack” after paying attention to the public number, in the background reply PDF, get all PDF
Six PDF links
conclusion
This article describes six methods to prevent repeated submission of data, the first is front-end interception, by hiding and setting the button is not available to shield the normal operation of repeated submission. However, in order to avoid repeated commits from abnormal channels, we implemented five more versions of back-end interception: HashMap version, fixed array version, array version of double detection lock, LRUMap version and LRUMap encapsulation version.
Special note: in this paper, all of the content is only applicable to repeat single environment data interception, if it is a distributed environment need to cooperate with the database or Redis, want to see the brother of distributed duplicate data interception, please give a “like” the east elder brother, if thumb up more than 100, we update the duplicate data processing scheme under distributed environment, thank you.
Reference & acknowledgements
Blog.csdn.net/fenglllle/a…
Follow the public account “code farmers attack” to subscribe for more highlights.