Служба называется дважды - PullRequest
1 голос
/ 09 апреля 2019

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

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

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

Вот псевдокод.

@Transactional
public ServiceResponse update(ServiceRequest serviceRequest) {
....
if (myDao.getStatus() == "COMPLETE") {
 return serviceError;
}
 myDao.insertRow();
 myDao.updateStatus("COMPLETE");
 sendAlert();
}

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

Ответы [ 4 ]

0 голосов
/ 09 апреля 2019

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

Я вижу несколько проблем с этим подходом использования внешнего хранилища данных для координации.Для изоляции по умолчанию « Read Committed » вы столкнетесь с тем, с чем сталкиваетесь сейчас, однако даже если вы можете использовать « Read Un-Committed », у вас возникнет проблема, когда вторая транзакцияпрочитав грязные данные «ЗАВЕРШЕНО», возвращается, но первая транзакция все еще может завершиться неудачно и выполнить откат.

Я предлагаю несколько подходов (конечно, я сделал много предположений)

  1. Идемпотентность: Делая обновления базы данных идемпотентными, вам не нужно беспокоиться о дублированииupdates
  2. Сжатие: если существует вероятность того, что последние записи всегда правильные, вы можете разрешить проходить всем записям, но читать только последние записи, это похоже на то, как Kafka выполняет внутреннее сжатие
0 голосов
/ 09 апреля 2019

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

Вы должны создать другое состояние и использовать операцию проверки и установки:

public ServiceResponse update(ServiceRequest serviceRequest) {
....
while true:
    String status = myDao.getStatus();
    if (status.equals("COMPLETE")) {
        return serviceError;
    }
    else if (status.equals("PROCESSING")) {
        // do whatever you want to do if some other process or thread is
        // already handling this. Maybe also return an error
        return serviceError;
    }
    else if (myDao.testAndUpdateStatus(status, "PROCESSING")) {
        // You would probably want to re-introduce a transaction here, maybe
        // by moving this block to its own method.  Sorry for cutting a
        // corner here trying to just demonstrate the lock idea, which needs
        // to not be in a transaction.
        try {
            myDao.insertRow();
            myDao.updateStatus("COMPLETE");
            sendAlert();
            return serviceOK;
        } catch (Exception ex) {
           // What you do for the failure case will be very app specific. This
           // is mostly to point out that you would want to set the status
           // explicitly in the case of an error, to whatever is appropriate
           myDao.updateStatus("COMPLETE")
           return serviceError;
        }
    }
}

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

Ключевым моментом здесь является то, что testAndUpdateStatus() будет только устанавливать статус и возвращать true, если статус, переданный в качестве первого параметра, является текущим состоянием. В противном случае он ничего не сделает и вернет false. Это решает условие состязания, когда один процесс проверяет состояние, но затем другой процесс также проверяет то же значение, прежде чем вы сможете установить состояние на "PROCESSING", в результате два процесса обрабатывают одно и то же обновление. Один из двух завершится ошибкой, потому что база данных не выполнит операцию, когда состояние больше не будет таким, как было, когда этот процесс прочитал значение.

Обратите внимание, что это будет работать не только в одном процессе, но и между процессами и даже машинами.

0 голосов
/ 09 апреля 2019

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

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

Одним из решений может быть использование ReSTful API с Rest-Entity, который «запрашивает» состояние базы данных в определенном состоянии.

Впример:

  1. Запрос A на ВСТАВКУ-Car-Entity.
  2. В то же время приходит второй запрос B, чтобы ВСТАВИТЬ Car-Entity.
  3. A isвыполнено успешно.
  4. B выполнено успешно.
  5. У нас есть дубликат.

Чтобы предотвратить это, мы могли бы сохранить запрос Car-Exsence-Request.

  1. Car-Existence-Request A направляет Сервер.
  2. Car-Existence-Request B направляет Сервер.
  3. Оба сохраняются в базе данных
  4. Car-Existence-Request A завершается.
  5. Car-Existence-Request B завершается.
  6. Сервер пытается сохранить Car A - успешно.
  7. Server пытается сохранить Car B -fail -duplicate.
  8. Пометить запрос на существование корзины A как успешный.
  9. Пометить запрос на существование корзины B как сбой.

Или просто переключитесь на PostGreSQL

0 голосов
/ 09 апреля 2019

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

...