Произнеся на одном дыхании четыре идемпотентных решения, интервьюер показал тетушке улыбку~

Java

Что такое идемпотентность?

идемпотентЭто концепция в математике и информатике.Когда некоторая операция над элементом является идемпотентной в математике, ее эффект на любой элемент дважды будет таким же, как результат ее воздействия один раз.

В компьютерном программировании характеристика идемпотентной операции заключается в том, что любое ее многократное выполнение имеет тот же эффект, что и однократное выполнение.

Идемпотентная функция или идемпотентный метод — это функция, которая может многократно выполняться с одними и теми же параметрами и достигать одного и того же результата. Эти функции не влияют на состояние системы, и не нужно беспокоиться, что повторное выполнение вызовет изменения в системе.

Что такое идемпотентность интерфейса?

существуетHTTP/1.1В определяется идемпотентность.Он описывает, что один и несколько запросов к ресурсу должны иметь одинаковый результат для самого ресурса.(За исключением таких проблем, как тайм-аут сети), то есть первый запрос имеет побочный эффект для ресурса, но последующие запросы не будут иметь никакого побочного эффекта для ресурса.

Побочным эффектом здесь является не разрушение или получение непредсказуемых результатов. Другими словами, любое многократное выполнение оказывает такое же влияние на сам ресурс, как и одно выполнение.

Зачем нужно реализовывать идемпотентность?

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

  1. Фронтенд повторная отправка формы: при заполнении некоторых форм пользователь заполняет и завершает отправку. Много раз из-за колебаний сети пользователь не отвечает вовремя на отправку, заставляя пользователя думать, что отправка не удалась, а затем продолжать нажав кнопку отправки, в это время произойдет повторная отправка формы запроса.

  2. Пользователи злонамеренно смахивают заказы: Например, при реализации функции пользовательского голосования, если пользователь неоднократно отправляет голос за пользователя, это приведет к тому, что интерфейс получит информацию о голосовании, неоднократно отправленную пользователем, что сделает результат голосования серьезно не соответствующим фактам. .

  3. Время ожидания интерфейса истекло, и он повторно отправляется: Во многих случаях инструменты HTTP-клиента по умолчанию включают механизм повтора тайм-аута, особенно когда третья сторона вызывает интерфейс, чтобы предотвратить сбой запроса, вызванный колебаниями сети и тайм-аутом, будет добавлен механизм повтора, в результате чего запрос подается несколько раз.

  4. Сообщения используются повторно: при использовании промежуточного программного обеспечения сообщений MQ, если в промежуточном программном обеспечении сообщений возникает ошибка, информация о потреблении не отправляется вовремя, что приводит к повторному потреблению.

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

Каково влияние на систему введения идемпотентности?

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

  1. Функция параллельного выполнения изменена на последовательное выполнение, что снижает эффективность выполнения.

  2. Добавлена ​​дополнительная бизнес-логика для контроля идемпотентности, усложняющая бизнес-функции;

Поэтому необходимо учитывать необходимость введения идемпотентности при его использовании.Согласно конкретному анализу реального бизнес-сценария, помимо особых требований бизнеса, идемпотентность интерфейса, как правило, не требуется.

Насколько идемпотентным является интерфейс Restful API?

Среди нескольких методов интерфейса HTTP, рекомендуемых популярным Restful, есть идемпотентные строки и методы, которые не могут гарантировать идемпотентность, а именно:

  1. идемпотент
  2. xне идемпотент
  3. -Он может быть или не быть идемпотентным, в зависимости от фактической бизнес-логики.

Решение 1. Как добиться идемпотентности для уникального первичного ключа базы данных?

база данныхуникальный первичный ключРеализация в основном использует характеристики ограничения уникальности первичного ключа в базе данных.Вообще говоря, уникальный первичный ключ больше подходит для идемпотентности «вставки», которая может гарантировать, что может быть только одна запись с уникальным первичным ключ в таблице.

При использовании уникального первичного ключа базы данных для завершения идемпотентности следует отметить, что первичный ключ, как правило, является не самоинкрементным первичным ключом в базе данных, а распределенным идентификатором в качестве первичного ключа, чтобы обеспечить глобальную уникальность. ИД в распределенной среде секс.

Применимые операции

  • операция вставки
  • удалить операцию

ограничения использования

  • Необходимо сгенерировать глобально уникальный идентификатор первичного ключа;

Основной процесс

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

  1. Клиент выполняет запрос на создание и вызывает интерфейс сервера.

  2. Сервер выполняет бизнес-логику и генерирует распределенныйID, используйте идентификатор в качестве первичного ключа вставляемых данных, а затем

После выполнения операции вставки данных запустите соответствующийSQLутверждение.

  1. Сервер вставляет часть данных в базу данных, и если вставка прошла успешно, это означает, что интерфейс не вызывается повторно. Если выдается дублирующее исключение первичного ключа, это означает, что запись уже существует в базе данных, и клиенту возвращается сообщение об ошибке.

Вариант 2. Как оптимистическая блокировка базы данных обеспечивает идемпотентность?

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

Таким образом, каждый раз при обновлении данных таблицы в базе данных в качестве условия будет использоваться идентификатор версии, а значением будет значение идентификатора версии в данных, которые будут обновляться в последний раз.

Применимые операции

  • операция обновления

ограничения использования

  • Необходимо добавить дополнительные поля в бизнес-таблицу, соответствующую базе данных

Пример описания

Например, в следующей таблице данных:

Чтобы предотвратить повторные обновления каждый раз, когда выполняется обновление, и чтобы определить, что обновляемый контент должен быть обновлен, мы обычно добавляемversionПоле записывает текущую версию записи, так что при обновлении значения информация в соответствующей версии может быть определена до тех пор, пока выполняется операция обновления.

Таким образом, каждый раз, когда выполняется обновление, необходимо указывать номер обновляемой версии, и следующие операции могут точно обновитьversion=5Информация:

UPDATE my_table SET price=price+50,version=version+1 WHERE id=1 AND version=5

вышеWHEREс последующим условиемid=1 AND version=5После казни,id=1изversionбыл обновлен до6, поэтому, если оператор SQL выполняется повторно, он не вступит в силу, потому чтоid=1 AND version=5Данных больше не существует, так что можно поддерживать идемпотентность обновления, а множественные обновления не повлияют на результат.

Решение 3. Как добиться идемпотентности для токенов защиты от повторов?

Эту операцию можно использовать для непрерывных кликов на стороне клиента или тайм-аута вызывающего абонента и повторной попытки, например, для отправки заказа.TokenРеализован механизм предотвращения дублирования отправки.

Проще говоря, вызывающая сторона сначала запрашивает глобальную переменную у серверной части при вызове интерфейса.ID(Token), перенесите это глобальное значение при запросеIDпросьба вместе(Tokenлучше всего поставить егоHeaders), серверная часть должнаTokenв видеKey, информация о пользователе какValueприбытьRedisпроверка содержания ключ-значение вKeyсуществует иValueЕсли он совпадает, выполните команду удаления, а затем выполните следующую бизнес-логику в обычном режиме. Если нет соответствующегоKeyилиValueЕсли совпадения нет, возвращается сообщение об ошибке для повторного выполнения, что обеспечивает идемпотентную операцию.

Применимые операции

  • операция вставки
  • операция обновления
  • удалить операцию

ограничения использования

  • Нужно генерировать глобально уникальныеTokenнить
  • Требует использования сторонних компонентовRedisвыполнить проверку данных

Основной процесс:

  1. Сервер предоставляет интерфейс для получения Токена, который может быть серийным номером или распределеннымIDилиUUIDнить.

  2. Клиент вызывает интерфейс для получения токена, а сервер в это время генерирует строку токена.

  3. Затем сохраните строку в базе данных Redis и используйте токен в качестве ключа Redis (обратите внимание на установку срока действия).

  4. Верните Токен клиенту, после того, как клиент его получит, он должен быть сохранен в скрытом поле формы.

  5. Когда клиент выполняет форму отправки, он сохраняет токен вHeaders, выполнить бизнес-запрос сHeaders.

  6. После того, как сервер получит запрос,HeadersПолучите токен из токена, а затем найдите Redis в соответствии с токеном.keyон существует.

  7. В зависимости от того, существует ли сервер в Rediskeyсделать вывод, если он существует, он будетkeyУдалите, затем выполните бизнес-логику в обычном режиме. Генерирует исключение, если оно не существует, и возвращает сообщение об ошибке повторной отправки.

Обратите внимание, что в случае параллелизма выполнение поиска и удаления данных Redis должно обеспечивать атомарность, в противном случае очень вероятно, что идемпотентность не может быть гарантирована при параллелизме. Его метод реализации может использовать распределенные блокировки или использоватьLuaвыражение для отмены запросов и операций удаления.

Решение 4. Как добиться идемпотентности путем передачи уникального серийного номера вниз по течению?

так называемыйзапросить серийный номер, по сути, каждый раз, когда делается запрос к серверу, прикрепляется уникальный и не повторяющийся через короткий промежуток времени серийный номер.Серийный номер может бытьаккуратныйID, также может бытьномер заказа, который обычно генерируется нижестоящим сервером.При вызове интерфейса вышестоящего сервера к нему добавляются серийный номер и код аутентификации.ID.

Когда вышестоящий сервер получает информацию о запросе, он извлекаетсерийный номери вниз по течениюИдентификатор аутентификацииВ сочетании образуютKey, а затем перейдите в Redis, чтобы проверить, есть ли соответствующийKeyПара ключ-значение в соответствии с ее результатом:

  1. если он существует, это означает, что нижестоящий запрос серийного номера был обработан, и на сообщение об ошибке повторного запроса можно ответить напрямую.

  2. если не существует, так чтоKeyВ качестве ключа Redis используйте информацию о нисходящем ключе в качестве сохраненного значения (например, некоторую информацию о бизнес-логике, переданную нижестоящим продавцом), сохраните пару ключ-значение в Redis, а затем выполните соответствующую бизнес-логику в обычном режиме.

Применимые операции

  • операция вставки
  • операция обновления
  • удалить операцию

ограничения использования

  • потребовать от третьего лица передать уникальный серийный номер;
  • Необходимо использовать сторонний компонент Redis для проверки данных;

Основной процесс

  1. Генерация нисходящих услуг распределенаIDв качестве серийного номера, затем выполните запрос на вызов вышестоящего интерфейса суникальный серийный номерс запрошеннымИдентификатор учетных данных для аутентификации.

  2. Восходящая служба выполняет проверку безопасности, чтобы определить, существуют ли параметры, переданные нижестоящей.серийный номериИдентификатор учетных данных.

  3. Восходящий сервис обращается к Redis, чтобы проверить, существует ли соответствующийсерийный номериИдентификатор аутентификациисостоит изKey, если он существует, сбросить информацию об исключении повторного выполнения, а затем ответить на соответствующую информацию об ошибке ниже по течению. Если его нет, используйтесерийный номериИдентификатор аутентификациив сочетании какKey, используя информацию о нисходящем ключе какValue, а затем сохраните его в Redis, а затем выполните входящую бизнес-логику в обычном режиме.

Срок действия должен быть установлен при вставке данных в Redis на вышеуказанных шагах. Это гарантирует, что в пределах этого временного диапазона, если интерфейс вызывается повторно, могут быть выполнены оценка и идентификация. Если время истечения срока действия не установлено, это может привести к тому, что в Redis будет храниться неограниченный объем данных, что приведет к неправильной работе Redis.

Пример реализации идемпотентности интерфейса

Здесь используется схема антиповторного токена, которая может обеспечить идемпотентность при разных действиях запроса, логику реализации можно посмотреть в схеме "антиповторный токен", написанной выше, а затем написать код для реализации этой логики.

1. Maven вводит связанные зависимости

использовать здесьMavenИнструменты для управления зависимостями здесьpom.xml введен вSpringBoot,Redis,lombokродственные зависимости.

<dependencies>
        <!--springboot web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--springboot data redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

2. Настройте параметры для подключения к Redis

существуетapplicationНастройте подключение в конфигурационном файлеRedisпараметры следующим образом:

spring:
  redis:
    ssl: false
    host: 127.0.0.1
    port: 6379
    database: 0
    timeout: 1000
    password:
    lettuce:
      pool:
        max-active: 100
        max-wait: -1
        min-idle: 0
        max-idle: 20

3. Создайте и проверьте служебный класс Token.

Создайте класс службы для работы с токенами, и есть методы создания и проверки токенов, в том числе:

  1. TokenМетод создания: использоватьUUIDсоздание инструментаTokenстрока, установленная на“idempotent_token:“+“Token串”в видеKey, используя информацию о пользователе какValue, сохраните информацию в Redis.

  2. TokenМетод проверки: получить параметр строки токена, добавить префикс ключа в формуKey, затем пройти вvalueзначение, выполнитьLuaвыражение(LuaВыражение может обеспечить атомарность выполнения команды) для нахождения соответствующегоKeyи удалить операции. Проверьте возвращаемый результат команды после завершения выполнения.Если результат не пустой и ненулевой, проверка прошла успешно, в противном случае — ошибка.

@Slf4j
@Service
public class TokenUtilService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 存入 Redis 的 Token 键的前缀
     */
    private static final String IDEMPOTENT_TOKEN_PREFIX = "idempotent_token:";

    /**
     * 创建 Token 存入 Redis,并返回该 Token
     *
     * @param value 用于辅助验证的 value 值
     * @return 生成的 Token 串
     */
    public String generateToken(String value) {
        // 实例化生成 ID 工具对象
        String token = UUID.randomUUID().toString();
        // 设置存入 Redis 的 Key
        String key = IDEMPOTENT_TOKEN_PREFIX + token;
        // 存储 Token 到 Redis,且设置过期时间为5分钟
        redisTemplate.opsForValue().set(key, value, 5, TimeUnit.MINUTES);
        // 返回 Token
        return token;
    }

    /**
     * 验证 Token 正确性
     *
     * @param token token 字符串
     * @param value value 存储在Redis中的辅助验证信息
     * @return 验证结果
     */
    public boolean validToken(String token, String value) {
        // 设置 Lua 脚本,其中 KEYS[1] 是 key,KEYS[2] 是 value
        String script = "if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end";
        RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        // 根据 Key 前缀拼接 Key
        String key = IDEMPOTENT_TOKEN_PREFIX + token;
        // 执行 Lua 脚本
        Long result = redisTemplate.execute(redisScript, Arrays.asList(key, value));
        // 根据返回结果判断是否成功成功匹配并删除 Redis 键值对,若果结果不为空和0,则验证通过
        if (result != null && result != 0L) {
            log.info("验证 token={},key={},value={} 成功", token, key, value);
            return true;
        }
        log.info("验证 token={},key={},value={} 失败", token, key, value);
        return false;
    }

}

4. Создайте класс Controller для теста

Создать для тестированияControllerкласс, который имеет доступ кTokenИнтерфейс с тестовой идемпотентностью интерфейса, содержание следующее:

@Slf4j
@RestController
public class TokenController {

    @Autowired
    private TokenUtilService tokenService;

    /**
     * 获取 Token 接口
     *
     * @return Token 串
     */
    @GetMapping("/token")
    public String getToken() {
        // 获取用户信息(这里使用模拟数据)
        // 注:这里存储该内容只是举例,其作用为辅助验证,使其验证逻辑更安全,如这里存储用户信息,其目的为:
        // - 1)、使用"token"验证 Redis 中是否存在对应的 Key
        // - 2)、使用"用户信息"验证 Redis 的 Value 是否匹配。
        String userInfo = "mydlq";
        // 获取 Token 字符串,并返回
        return tokenService.generateToken(userInfo);
    }

    /**
     * 接口幂等性测试接口
     *
     * @param token 幂等 Token 串
     * @return 执行结果
     */
    @PostMapping("/test")
    public String test(@RequestHeader(value = "token") String token) {
        // 获取用户信息(这里使用模拟数据)
        String userInfo = "mydlq";
        // 根据 Token 和与用户相关的信息到 Redis 验证是否存在对应的信息
        boolean result = tokenService.validToken(token, userInfo);
        // 根据验证结果响应不同信息
        return result ? "正常调用" : "重复调用";
    }

}

окончательное резюме

Идемпотентность является очень распространенным и важным требованием в разработке, особенноплатить,ЗаказОсобенно важно обеспечить идемпотентность интерфейса для услуг, связанных с деньгами. В реальной разработке нам нужно гибко выбирать реализацию идемпотентности для разных бизнес-сценариев:

  1. Для заказов с уникальными первичными ключами вы можете использовать "Уникальная схема первичного ключа"Способ достижения.

  2. Для связанных операций обновления сцены, таких как статус заказа обновления, используйте "оптимистичная схема блокировки«Это проще реализовать.

  3. Для восходящих и нисходящих запросов восходящие восходящие службы могут использовать "Схема уникального серийного номера нисходящей передачи"Более разумно.

  4. похожий наОтправка дубликатов внешнего интерфейса,Повторные заказы,Нет уникального идентификационного номерасценарии, которые могут бытьTokenиRedisкооператив"Схема токена защиты от перегрузки«Это быстрее реализовать.

Вышеприведенное просто дает некоторые предложения. Еще раз подчеркнем, для достижения идемпотентности вам необходимо сначала понять свои собственные бизнес-потребности и реализовать их в соответствии с бизнес-логикой. Разумно разобраться с деталями каждого узла и улучшить общий проектирование бизнес-процессов.Хорошо для обеспечения нормальной работы системы. Наконец, делается краткое резюме, а затем этот пост в блоге заканчивается здесь следующим образом:

Статья добавлена ​​на GitHub:github.com/JavaFamily