6 способов предотвратить повторную отправку данных (очень просто)!

Java
6 способов предотвратить повторную отправку данных (очень просто)!

Однажды друг неожиданно спросил брата Лэя:Какое самое простое решение для предотвращения дублирования коммитов в Java?

Это предложение содержит две ключевые части информации, во-первых:предотвратить дублирование отправки;второй:самый легкий.

Поэтому брат Лэй спросил его, это автономная среда или распределенная среда?

По отзывам, которые я получил, это была автономная среда, так что это было легко, поэтому Brother Lei начал устанавливать *.

Без лишних слов, давайте сначала воспроизведем проблему.

Моделирование пользовательских сценариев

По отзывам друзей, общая сцена выглядит так, как показано на следующем рисунке:

重复提交-01.gif
Упрощенный фиктивный код выглядит следующим образом (на основе 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-страницы. Например, после того, как пользователь нажимает кнопку «Отправить», мы можем сделать кнопку недоступной или скрытой.

Эффект выполнения показан на следующем рисунке:

前台拦截.gif

Код реализации фронтального перехвата:

<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 перехвата заключается в том, чтобы определить, было ли выполнено дело до выполнения метода, если оно было выполнено, то оно не будет выполняться, иначе будет выполняться нормально.

Сохраняем запрошенный бизнес-идентификатор в памяти и добавляем мьютекс для обеспечения безопасности выполнения программы в условиях многопоточности.Общая идея реализации показана на следующем рисунке:

image.png

Однако самый простой способ хранить данные в памяти — это использовать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 "执行成功!";
    }
}

Эффект реализации показан на следующем рисунке:

最终效果.gif

существующие проблемы: Эта реализация имеет фатальную проблему, потому что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"Подпишитесь, чтобы узнать больше.