Однажды друг неожиданно спросил брата Лэя:Какое самое простое решение для предотвращения дублирования коммитов в Java?
Это предложение содержит две ключевые части информации, во-первых:предотвратить дублирование отправки;второй:самый легкий.
Поэтому брат Лэй спросил его, это автономная среда или распределенная среда?
По отзывам, которые я получил, это была автономная среда, так что это было легко, поэтому Brother Lei начал устанавливать *.
Без лишних слов, давайте сначала воспроизведем проблему.
Моделирование пользовательских сценариев
По отзывам друзей, общая сцена выглядит так, как показано на следующем рисунке:
Упрощенный фиктивный код выглядит следующим образом (на основе 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 "执行成功!";
}
}
Итак, брат Лэй подумал: решить проблему повторной отправки данных, перехватив соответственно переднюю и заднюю части.
Фронтальный перехват
Внешний перехват относится к перехвату повторяющихся запросов через HTML-страницы. Например, после того, как пользователь нажимает кнопку «Отправить», мы можем сделать кнопку недоступной или скрытой.
Эффект выполнения показан на следующем рисунке:
Код реализации фронтального перехвата:
<html>
<script>
function subCli(){
// 按钮设置为不可用
document.getElementById("btn_sub").disabled="disabled";
document.getElementById("dv1").innerText = "按钮被点击了~";
}
</script>
<body style="margin-top: 100px;margin-left: 100px;">
<input id="btn_sub" type="button" value=" 提 交 " onclick="subCli()">
<div id="dv1" style="margin-top: 80px;"></div>
</body>
</html>
Однако существует фатальная проблема с перехватом интерфейса.Если вы знающий программист или нелегальный пользователь, вы можете напрямую обойти страницу интерфейса и повторно отправить запрос, симулируя запросы.Мгновенно обнаружил отличный способ разбогатеть ).
Следовательно, в дополнение к перехвату некоторых обычных ошибочных операций, перехват серверной части также важен.
внутренний перехват
Идея реализации back-end перехвата заключается в том, чтобы определить, было ли выполнено дело до выполнения метода, если оно было выполнено, то оно не будет выполняться, иначе будет выполняться нормально.
Сохраняем запрошенный бизнес-идентификатор в памяти и добавляем мьютекс для обеспечения безопасности выполнения программы в условиях многопоточности.Общая идея реализации показана на следующем рисунке:
Однако самый простой способ хранить данные в памяти — это использоватьHashMap
хранилище или использование Guava Cache будет иметь тот же эффект, но, очевидно,HashMap
Функции могут быть реализованы быстрее, поэтому давайте сначала реализуем однуHashMap
Сверхмощная (защищенная от дублирования) версия .
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 — это аббревиатура наименее недавно использованного, то есть наименее использовавшегося в последнее время.
Во-первых, давайте добавим ссылку на коллекции Apache commons:
<!-- 集合工具类 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>
Код реализации выглядит следующим образом:
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 "执行成功!";
}
}
Советы: в целом код написан здесь, но этого можно достичь, если вы хотите быть более кратким.Вы можете использовать пользовательские аннотации для записи бизнес-кода в аннотации.Метод, который вам нужно вызвать, должен написать только строку аннотации , Это может предотвратить повторную отправку данных.Старые утюги могут попробовать это сами (если вам нужна статья Лэй Гэ, оставьте сообщение 666 в области комментариев).
Расширенные знания - анализ принципа реализации LRUMap
теперь, когдаLRUMap
Так мощно, давайте посмотрим, как это реализовано.
LRUMap
Суть заключается в структуре петлевого двусвязного списка, который содержит головной узел, а структура его хранения выглядит следующим образом:
AbstractLinkedMap.LinkEntry entry;
При вызове метода запроса используемый элемент будет помещен в предыдущую позицию заголовка двусвязного списка.Исходный код выглядит следующим образом:
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.");
}
}
Если элемент добавлен, следующий элемент заголовка будет удален, когда емкость будет заполнена.Исходный код выглядит следующим образом:
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);
}
}
Исходный код для оценки емкости:
public boolean isFull() {
return size >= maxSize;
}
** Добавьте данные напрямую, если емкость не заполнена:
super.addMapping(hashIndex, hashCode, key, value);
Если вместимость заполнена, позвонитеreuseMapping
Метод использует алгоритм LRU для очистки данных.
В целом:LRUMap
Суть состоит в структуре петлевого двусвязного списка, который содержит головной узел.Когда элемент используется, этот элемент помещается в двусвязный список.header
Предыдущая позиция при добавлении элемента будет удалена, если емкость заполненаheader
следующий элемент.
Суммировать
В этой статье рассказывается о 6 методах предотвращения повторной отправки данных.Первый — это перехват интерфейса, который можно использовать для блокировки повторных отправок при нормальной работе путем скрытия и установки недоступности кнопок. Однако, чтобы избежать повторных отправок через аномальные каналы, мы внедрили пять версий внутреннего перехвата: версия HashMap, версия с фиксированным массивом, версия массива блокировки двойного обнаружения, версия LRUMap и упакованная версия LRUMap.
Особое примечание: весь контент в этой статье применим только к перехвату дубликатов данных в автономной среде. Если это распределенная среда, ее необходимо реализовать с помощью базы данных или Redis. Для ветеранов, которые хотят видеть распределенные дубликаты данных перехват, пожалуйста, дайте брату Лею "отличный",еслиБолее 100 лайков,мыОбновление схемы обработки дубликатов данных в распределенной среде,Спасибо.
Ссылки и благодарности
blog.CSDN.net/Буря приближается/Ах…
Обратите внимание на общественный номер»Сообщество китайского языка Java"Подпишитесь, чтобы узнать больше.