Что означает атомарность транзакции в SQL / Spring, а что нет?

Я думаю о следующем случае. Поправьте меня, если я ошибаюсь:

Этот код неверен:

@Transactional
public void voteUp(long fooId) {
    Foo foo = fooMapper.select(fooId); // SELECT * FROM foo WHERE fooId == #{fooId}
    foo.setVotes(foo.getVotes() + 1);
    fooMapper.update(foo); // UPDATE foo SET votes = #{votes} (...) WHERE fooId == #{fooId}
}

Несмотря на то, что это транзакционный, это не означает, что значение «голосов» всегда будет увеличиваться на единицу, если voteUp вызывается одновременно на многих машинах / во многих потоках? Если бы это было так, это означало бы, что одновременно может выполняться только одна транзакция, что приводит к снижению эффективности (особенно, если код голосованияUp выполняет больше операций в транзакции)?

Единственный правильный способ сделать это так (?):

/* not necessarily */ @Transactional 
public void voteUp(long fooId) {
    fooMapper.voteUp(fooId); // UPDATE foo SET votes = votes + 1 WHERE fooId == #{fooId}
}

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

6
Arsen 6 Янв 2016 в 17:57

4 ответа

Лучший ответ

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

На этом уровне изоляции реализация СУБД с управлением параллелизмом на основе блокировок сохраняет блокировки записи (полученные для выбранных данных) до конца транзакции, но блокировки чтения снимаются, как только выполняется операция SELECT (так что явление неповторяемого чтения может происходить на этом уровне изоляции, как обсуждается ниже). Как и на предыдущем уровне, блокировки диапазона не управляются.

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

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

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

5
Community 20 Июн 2020 в 09:12

Речь идет не только об атомарности. Стандартная транзакция БД должна иметь следующие характеристики:

  • Атомный
  • Последовательный
  • Изолированные
  • Прочный

Это «КИСЛОТНЫЕ» требования. То, что вы отметили как «неправильный» , на самом деле все еще атомарно, но не изолированно . чтобы сделать его изолированным (чтобы одновременные обновления по-прежнему давали правильный результат), вы можете либо делегировать обработку параллелизма БД (set vote = vote+1), либо использовать функцию вашей инфраструктуры для правильной обработки изоляции .

https://en.wikipedia.org/wiki/Database_transaction

1
Gergely Bacso 6 Янв 2016 в 15:57

Согласно документации, когда вы аннотируете метод с помощью @Transactional, Spring создает прокси с тем же интерфейсом (интерфейсами), что и ваш аннотированный класс. И когда вы вызываете методы своего объекта, все вызовы проходят через прокси-объект. Прокси-объект обертывает транзакционные методы вашего класса в конструкции try catch. Код вашего исходного объекта:

@Transactional
public void voteUp(long fooId) {
    Foo foo = fooMapper.select(fooId); // SELECT * FROM foo WHERE fooId == #{fooId}
    foo.setVotes(foo.getVotes() + 1);
    fooMapper.update(foo); // UPDATE foo SET votes = #{votes} (...) WHERE fooId == #{fooId}
}

В прокси объект будет выглядеть примерно так:

//It's all approximately just to show you a way how Spring does it. 
public void voteUp(long fooId) {
    EntityTransaction tx = em.getTransaction();
    tx.begin();
    try{
       originalObject.voteUp(fooId);
       tx.commit();
    }catch(Exception e){
       tx.rallback();
       throw e;
    }
 }

Таким образом, даже если voteUp вызывается одновременно на многих машинах / во многих потоках, значение «голосов» всегда будет увеличиваться на единицу. Поскольку транзакция в одном потоке блокирует таблицу для записи данных из других потоков.

Вы правы: если метод voteUp займет много времени, это приведет к снижению эффективности. Это означает, что ваши методы, аннотированные @Transactional, не должны занимать много времени.

И вы правы, вы можете обновлять записи в базе данных без выбора, если ваша библиотека ORM позволяет это.

2
Ken Bekov 6 Янв 2016 в 16:30

@Transactional в этом случае используется для управления транзакциями SQL, он не добавляет безопасности потоков. Диспетчер транзакций Spring на самом деле мало что делает, кроме как запрашивать у базы данных запуск новой транзакции, поэтому вам нужно обратиться к документации вашей СУБД и прочитать о семантике ее транзакций.

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

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

2- Оптимистическая блокировка: Оптимистическая блокировка фактически не использует никаких блокировок. Что вы делаете, так это то, что вы используете значение, которое, как вы точно знаете, будет изменяться при обновлении этой строки. Например, вы можете переписать оператор обновления на:

UPDATE foo SET votes = #{votes} (...) WHERE fooId == #{fooId} AND votes = #{oldNoOfVotes}

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

3
Alexandre H. 6 Янв 2016 в 17:37