Как обеспечить идемпотентность интерфейсов при высоком уровне параллелизма?

Java

предисловие

接口幂等性Проблема для разработчиков является общественной проблемой, которая не имеет ничего общего с языком. В этой статье рассказывается о некоторых очень практических способах решения таких проблем.Большая часть контента, который я практиковал в проекте, является справочной информацией для нуждающихся небольших партнеров.

Я не знаю, сталкивались ли вы с такими сценариями:

  1. Иногда мы заполняем некоторыеform表单Когда я случайно дважды быстро нажал кнопку сохранения, в таблице сгенерировались два повторяющихся данных, но идентификаторы были разными.

  2. Мы в проекте, чтобы решить接口超时проблема, которая обычно приводит重试机制. Первый интерфейс запроса истек по таймауту, и запросчик не смог вовремя получить возвращаемый результат (может быть, в это время удалось), во избежание возврата неправильного результата (в этом случае невозможно вернуть отказ напрямую?) , поэтому повторите запрос несколько раз, это также приведет к дублированию данных.

  3. Когда потребители mq читают сообщения, они иногда читают重复消息(Что касается причины, я не буду говорить об этом здесь. Заинтересованные друзья могут пообщаться со мной в частном порядке.) Если это не будет обработано должным образом, также будут созданы дубликаты данных.

Недавно я случайно получил заметку о чистке, написанную крупным производителем BAT, которая открыла мне сразу вторую линейку Ren и Du, и я все больше чувствую, что алгоритм не так сложен, как я себе представлял.Заметки о чистке, написанные боссом BAT, позвольте мне мягко получить предложение

Да, это все идемпотентные проблемы.

接口幂等性Это означает, что результат одного или нескольких запросов, инициированных пользователем для одной и той же операции, является согласованным, и не будет побочных эффектов из-за нескольких кликов.

Этот тип проблемы возникает в основном в интерфейсе:

  • insertОперация, в данном случае несколько запросов, может генерировать повторяющиеся данные.

  • updateОперация, если это просто обновление данных, например:update user set status=1 where id=1,Нет проблем. Если есть расчеты, например:update user set status=status+1 where id=1, в этом случае несколько запросов могут привести к ошибкам данных.

Так как же обеспечить идемпотентность интерфейса? Эта статья расскажет вам ответ.

1. Выберите перед вставкой

Обычно в интерфейсе сохранения данных, во избежание дублирования данных, мы обычноinsertраньше, по словамnameилиcodeполеselectВзгляните на данные. Если данные уже существуют, выполнитеupdateОперация, если она не существует, выполнить ееinsertработать.

图片

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

2. Добавьте пессимистическую блокировку

В сценарии оплаты баланс счета пользователя А составляет 150 юаней, и он хочет перевести 100 юаней.В нормальных условиях баланс пользователя А составляет всего 50 юаней. В общем, sql такой:

update user amount = amount-100 where id=123;

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

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

Обычно одна строка данных блокируется следующим sql:

select * from user id=123 for update;

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

图片

Конкретные шаги:

  1. Несколько запросов одновременно запрашивают информацию о пользователе на основе идентификатора.

  2. Определите, меньше ли баланса 100, и если баланса недостаточно, верните недостаточный баланс напрямую.

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

  4. Только первый запрос может получить блокировку строки, а другие запросы, которые не получают блокировку, ждут следующей возможности получить блокировку.

  5. После того, как первый запрос получает блокировку, оценивается, меньше ли баланс 100, и если баланс достаточен, выполняется операция обновления.

  6. Если баланс недостаточен, что указывает на то, что запрос повторяется, он сразу вернет успех.

Следует отметить, что если вы используете базу данных mysql, механизм хранения должен использовать innodb, поскольку он поддерживает только транзакции. Кроме того, поле id здесь должно быть первичным ключом или уникальным индексом, иначе будет заблокирована вся таблица.

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

Кроме того, трудно гарантировать одно и то же возвращаемое значение для каждого интерфейса запроса, поэтому он не подходит для сценариев идемпотентного проектирования, но его можно использовать в сценариях антивеса.

Кстати вот,防重设计и幂等设计, разница на самом деле есть. Дизайн антирепликации в основном предназначен для предотвращения дублирования данных, и требований к возврату интерфейса не так много. В дополнение к предотвращению дублирования данных идемпотентный дизайн также требует, чтобы каждый запрос возвращал один и тот же результат.

3. Добавьте оптимистическую блокировку

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

Запросите данные перед их обновлением:

select id,amount,version from user id=123;

Если данные существуют, предполагается, что найденныеversionравный1, повторное использованиеidиversionПоля используются как условия запроса для обновления данных:

update user set amount=amount+100,version=version+1
where id=123 and version=1;

При обновлении данныхversion+1, а потом судить на этот разupdateКоличество строк, затронутых операцией. Если оно больше 0, обновление выполнено успешно. Если равно 0, значит, обновление не изменило данные.

С момента первого запросаversionравный1Это может быть успешным, после успешной операцииversionстать2. В это время, если приходит параллельный запрос, выполняем тот же sql:

 update user set amount=amount+100,version=version+1
where id=123 and version=1;

ДолженupdateОперация реально не обновляет данные, а конечный результат выполнения sql влияет на количество строк0,так какversionстал2в настоящее время,whereсерединаversion=1Конечно, не может выполнить условия. Но чтобы обеспечить идемпотентность интерфейса, интерфейс может напрямую возвращать успех, потому чтоversionЕсли значение было изменено, то предыдущее должно быть успешно выполнено один раз, а последующие являются повторными запросами.

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

Конкретные шаги:

  1. Первый запрос информации о пользователе на основе идентификатора, включая поле версии

  2. По значению полей id и version как параметрам условия where обновить информацию о пользователе, а заодно и version+1

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

  4. Если затронуто 0 строк, это означает, что запрос повторяется, и успех возвращается напрямую.

4. Добавьте уникальный индекс

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

alter table `order` add UNIQUE KEY `un_code` (`code`);

После добавления уникального индекса данные первого запроса могут быть успешно вставлены. Но для того же запроса, который следует, он будет сообщать при вставке данных.Duplicate entry '002' for key 'order.un_codeИсключение, указывающее на конфликт уникальных индексов.

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

еслиjavaПрограмма должна зафиксировать:DuplicateKeyExceptionисключение, если используетсяspringФреймворк также должен охватывать:MySQLIntegrityConstraintViolationExceptionаномальный.

Конкретная блок-схема выглядит следующим образом:

图片

Конкретные шаги:

  1. Пользователь инициирует запрос через браузер, а сервер собирает данные.

  2. вставьте эти данные в mysql

  3. Определите, успешно ли выполнено выполнение, и если да, обработайте другие данные (и, возможно, другую бизнес-логику).

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

5. Создайте антитяжелый стол

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

В этом случае мы можем использовать建防重表решить проблему.

Таблица может содержать только два поля:idи唯一索引, уникальный индекс может быть уникальным идентификатором, который объединяет несколько полей, таких как имя, код и т. д., например: susan_0001.

Конкретная блок-схема выглядит следующим образом:

图片

Конкретные шаги:

  1. Пользователь инициирует запрос через браузер, а сервер собирает данные.

  2. Вставьте данные в антидупликационную таблицу mysql.

  3. Определите, успешно ли выполнено выполнение, в случае успеха выполните другие операции с данными MySQL (и, возможно, другую бизнес-логику).

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

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

6. По государственной машине

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

Если статус заказа с id=123已支付, который теперь становится完成государство.

update `order` set status=3 where id=123 and status=2;

При первом запросе статус заказа已支付, значение2, так чтоupdateОператор может нормально обновлять данные, а количество строк, затронутых результатом выполнения SQL, равно1, статус заказа становится3.

Когда тот же запрос приходит позже, и снова выполняется тот же sql, потому что статус заказа становится3, затем используйтеstatus=2В качестве условия данные, которые необходимо обновить, не могут быть запрошены, поэтому число строк, затронутых окончательным результатом выполнения SQL, равно0, то есть фактически не будет обновлять данные. Но чтобы обеспечить идемпотентность интерфейса, число затронутых строк равно0, интерфейс также может напрямую возвращать успех.

Конкретная блок-схема выглядит следующим образом:

图片

Конкретные шаги:

  1. Пользователь инициирует запрос через браузер, а сервер собирает данные.

  2. В соответствии с идентификатором и текущим состоянием в качестве условий обновить до следующего состояния

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

  4. Если затронуто 0 строк, это означает, что запрос повторяется, и успех возвращается напрямую.

Главное отметить, что схема ограничена теми, которые подлежат обновлению.表有状态字段, и как раз собираюсь обновить状态字段В данном конкретном случае применимы не все сценарии.

7. Добавьте распределенную блокировку

На самом деле упомянутый ранее加唯一索引или加防重表, по существу используя数据库из分布式锁, который также является типом распределенной блокировки. Но из-за数据库分布式锁Производительность не очень хорошая, вместо этого мы можем использовать:redisилиzookeeper.

Ввиду того, что сейчас многие компании используют распределенные центры конфигурации дляapolloилиnacos, использовался редкоzookeeper, мы используемredisВозьмите распределенную блокировку в качестве примера.

В настоящее время существует три основных способа реализации распределенных блокировок в Redis:

  1. команда setNx

  2. установить команду

  3. Редиссон Фреймворк

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

Конкретная блок-схема выглядит следующим образом:

图片

Конкретные шаги:

  1. Пользователь инициирует запрос через браузер, сервер соберет данные и сгенерирует код номера заказа как уникальное бизнес-поле.

  2. Используйте команду set redis, чтобы установить код заказа в redis и установить время ожидания.

  3. Определить успешность настройки.Если настройка прошла успешно, то это означает, что это первый запрос, а затем выполняется операция с данными.

  4. Если настройка не удалась, указывая на то, что запрос повторяется, он сразу вернет успех.

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

8. Получить токен

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

  1. Получить первый запросtoken

  2. Второй запрос с этимtoken, завершите бизнес-операцию.

Конкретная блок-схема выглядит следующим образом:

Первым шагом является получение токена.

图片

Второй шаг – выполнение конкретных бизнес-операций.

图片

Конкретные шаги:

  1. Когда пользователь посещает страницу, браузер автоматически инициирует запрос на получение токена.

  2. Сервер генерирует токен, сохраняет его в Redis и возвращает браузеру.

  3. Когда пользователь инициирует запрос через браузер, токен переносится.

  4. Запросите, существует ли токен в redis, если его нет, значит, это первый запрос, и выполняются последующие операции с данными.

  5. Если он существует, это означает, что запрос повторяется, и он напрямую возвращает успех.

  6. В Redis токен будет автоматически удален по истечении срока действия.

Приведенная выше схема рассчитана на идемпотентность.

Если это антитяжелый дизайн, блок-схему следует изменить:

图片

Важно отметить, что токен должен быть глобально уникальным.

Недавно я случайно получил заметку о чистке, написанную крупным производителем BAT, которая открыла мне сразу вторую линейку Ren и Du, и я все больше чувствую, что алгоритм не так сложен, как я себе представлял.Заметки о чистке, написанные боссом BAT, позвольте мне мягко получить предложение

Последнее слово (пожалуйста, обратите внимание, не проституируйте меня по пустякам)

Если эта статья оказалась для вас полезной или познавательной, отсканируйте QR-код и обратите внимание, ваша поддержка — самая большая мотивация для меня продолжать писать.

Попросите в один клик три ссылки: лайк, вперед и смотреть.

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