1.基礎版——HashMap
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, Integer> reqCache = new HashMap<>(); @RequestMapping("/add") public String addUser(String id) { // 非空判斷(忽略)... synchronized (this.getClass()) { // 重復請求判斷 if (reqCache.containsKey(id)) { // 重復請求 System.out.println("請勿重復提交!!!" + id); return "執行失敗"; } // 存儲請求 ID reqCache.put(id, 1); } // 業務代碼... System.out.println("添加用戶ID:" + id); return "執行成功!"; } }
存在的問題:此實現方式有一個致命的問題,因為 HashMap
是無限增長的,因此它會占用越來越多的內存,並且隨着 HashMap
數量的增加查找的速度也會降低,所以我們需要實現一個可以自動“清除”過期數據的實現方案。
2.優化版——固定大小的數組
此版本解決了 HashMap
無限增長的問題,它使用數組加下標計數器(reqCacheCounter)的方式,實現了固定數組的循環存儲。
當數組存儲到最后一位時,將數組的存儲下標設置 0,再從頭開始存儲數據,實現代碼如下:
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 "執行失敗"; } // 記錄請求 ID if (reqCacheCounter >= reqCache.length) reqCacheCounter = 0; // 重置計數器 reqCache[reqCacheCounter] = id; // 將 ID 保存到緩存 reqCacheCounter++; // 下標往后移一位 } // 業務代碼... System.out.println("添加用戶ID:" + id); return "執行成功!"; } }
3.擴展版——雙重檢測鎖(DCL)
上一種實現方法將判斷和添加業務,都放入 synchronized
中進行加鎖操作,這樣顯然性能不是很高,於是我們可以使用單例中著名的 DCL(Double Checked Locking,雙重檢測鎖)來優化代碼的執行效率,實現代碼如下:
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()) { // 雙重檢查鎖(DCL,double checked locking)提高程序的執行效率 if (Arrays.asList(reqCache).contains(id)) { // 重復請求 System.out.println("請勿重復提交!!!" + id); return "執行失敗"; } // 記錄請求 ID if (reqCacheCounter >= reqCache.length) reqCacheCounter = 0; // 重置計數器 reqCache[reqCacheCounter] = id; // 將 ID 保存到緩存 reqCacheCounter++; // 下標往后移一位 } // 業務代碼... System.out.println("添加用戶ID:" + id); return "執行成功!"; } }
注意:DCL 適用於重復提交頻繁比較高的業務場景,對於相反的業務場景下 DCL 並不適用。
4.完善版——LRUMap
上面的代碼基本已經實現了重復數據的攔截,但顯然不夠簡潔和優雅,比如下標計數器的聲明和業務處理等,但值得慶幸的是 Apache 為我們提供了一個 commons-collections 的框架,里面有一個非常好用的數據結構 LRUMap
可以保存指定數量的固定的數據,並且它會按照 LRU 算法,幫你清除最不常用的數據。
小貼士:LRU 是 Least Recently Used 的縮寫,即最近最少使用,是一種常用的數據淘汰算法,選擇最近最久未使用的數據予以淘汰。
首先,我們先來添加 Apache commons collections 的引用:
<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 { // 最大容量 100 個,根據 LRU 算法淘汰數據的 Map 集合 private LRUMap<String, Integer> reqCache = new LRUMap<>(100); @RequestMapping("/add") public String addUser(String id) { // 非空判斷(忽略)... synchronized (this.getClass()) { // 重復請求判斷 if (reqCache.containsKey(id)) { // 重復請求 System.out.println("請勿重復提交!!!" + id); return "執行失敗"; } // 存儲請求 ID reqCache.put(id, 1); } // 業務代碼... System.out.println("添加用戶ID:" + id); return "執行成功!"; } }
使用了 LRUMap
之后,代碼顯然簡潔了很多。
5.最終版——封裝
以上都是方法級別的實現方案,然而在實際的業務中,我們可能有很多的方法都需要防重,那么接下來我們就來封裝一個公共的方法,以供所有類使用:
import org.apache.commons.collections4.map.LRUMap; /** * 冪等性判斷 */ public class IdempotentUtils { // 根據 LRU(Least Recently Used,最近最少使用)算法淘汰數據的 Map 集合,最大容量 100 個 private static LRUMap<String, Integer> reqCache = new LRUMap<>(100); /** * 冪等性判斷 * @return */ public static boolean judge(String id, Object lockClass) { synchronized (lockClass) { // 重復請求判斷 if (reqCache.containsKey(id)) { // 重復請求 System.out.println("請勿重復提交!!!" + id); return false; } // 非重復請求,存儲請求 ID reqCache.put(id, 1); } 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 "執行成功!"; } }