Делать POST-запросы идемпотентными - PullRequest
0 голосов
/ 18 октября 2019

Я искал способ спроектировать мой API, чтобы он был идемпотентным, то есть что-то из этого, чтобы сделать мои маршруты POST-запросов идемпотентными, и я наткнулся на эту статью .

(Если я понял что-то не так, пожалуйста, поправьте меня!)

В этом есть хорошее объяснение общей идеи. но не хватает некоторых примеров того, как он сам это реализовал.

Кто-то спросил автора статьи, как он может гарантировать атомарность? поэтому автор добавил пример кода.

По существу, в его примере кода есть два случая,

поток, если все идет хорошо:

  • Открыть транзакцию в БД, которая содержит данные, которые необходимо изменить с помощью запроса POST
  • Внутри этой транзакции выполните необходимое изменение
  • Установите ключ Idempotency-key и значение, который является ответом для клиента, внутри хранилища Redis
  • Установите время истечения для этого ключа
  • Передайте транзакцию

поток, если что-то внутри кодаидет не так:

  • и происходит исключение внутри потока функции.
  • выполняется откат к транзакции

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

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

Но что произойдет, если машина, на которой выполняется код, вылетит, пока она находится в состоянии, когда она уже выполнила Set expire time to that key и теперь он собирается выполнить фиксацию транзакции?

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

Мне нужно спроектировать API таким способомчто если изменение в данных или настройке ключа и значения в redis не удастся, то они оба откатятся.

Каково решение этой проблемы?

Как я могу гарантироватьатомарность изменения необходимых данных в одной базе данных, и в то же время установка ключа и необходимого ответа в redis, и если какой-либо из них не удается, откатить их обоих? (Включая случай, когда во время действий происходит сбой машины)

Пожалуйста, добавьте пример кода при ответе! Я использую те же технологии, что и в статье (nodejs,Redis, Монго - для самих данных)

Спасибо:)

1 Ответ

2 голосов
/ 01 ноября 2019

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

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

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

Вы все еще можетеиспользуйте Redis, поскольку он даст более быстрые результаты, чем ваша база данных.

Ниже приведен пример кода, но было бы полезно подумать о том, насколько важен сбой между вставкой в ​​Redis и базой данных для вашего бизнеса (может ли это бытьлечиться с другой стратегией?), чтобы избежать чрезмерного проектирования.

async function execute(idempotentKey) {
  try {
    // append to the query statement an insert into executions table.
    // this will be persisted with the transaction
    query = ```
        UPDATE firsttable SET ...;
        UPDATE secondtable SET ...;
        INSERT INTO executions (idempotent_key, success) VALUES (:idempotent_key, true);
    ```;

    const db = await dbConnection();
    await db.beginTransaction();
    await db.execute(query);

    // we're setting a key on redis with a value: "false".
    await redisClient.setAsync(idempotentKey, false, 'EX', process.env.KEY_EXPIRE_TIME);

    /*
      if server crashes exactly here, idempotent key will be on redis with false as value.
      in this case, there are two possibilities: commit to database suceeded or not.
      if on next request redis provides a false value, query database to verify if transaction was executed.
    */

    await db.commit();

    // you can now set key value to true, meaning commit suceeded and you won't need to query database to verify that.
    await redis.setAsync(idempotentKey, true);
  } catch (err) {
    await db.rollback();
    throw err;
  }
}
...