8.1 ID транзакций
Обозначается как XID или TxID (если есть разница - подскажите). В качестве TxID можно использовать timestamp'ы, что может пригодиться, если мы захотим восстановить все действия к какому-то моменту времени. Проблема может возникнуть, если timestamp недостаточно гранулярный - тогда транзакции могут получить один и тот же ID.
Поэтому, самый надежный вариант – это генерировать уникальные ID при помощи UUID.
import java.util.UUID;
public class GenerateUUID {
public static void main(String[] args) {
System.out.println(UUID.randomUUID().toString()); // f50ec0b7-f960-400d-91f0-c42a6d44e3d0
System.out.println(UUID.randomUUID().toString()); // d15bed89-c0a5-4a72-98d9-5507ea7bc0ba
}
}
Также можно хэшировать данные, определяющие транзакцию, и использовать этот хэш в качестве TxID.
8.2 Повторные попытки ("retries")
Если мы знаем, что функция или программа идемпотентны, то это означает, что мы можем и должны повторить их вызов в случае ошибки. Мы должны быть готовы к тому, что какая-то операция может завершиться неудачно, учитывая, что современные приложения распределены по сети и железу. Ошибка может быть вызвана падением сервера, ошибками сети или перегруженностью удаленного приложения. Как должно вести себя наше приложение? Правильно, попробовать повторить операцию.
Поскольку один кусочек кода может сказать больше, чем целая страница слов, давайте рассмотрим на примере, как должен работать механизм повторения операции в духе naive retrying.
import java.util.logging.Level;
import java.util.logging.Logger;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.retry.RetryRegistry;
public class DoSomethingUnreliable {
private static final Logger LOGGER = Logger.getLogger(DoSomethingUnreliable.class.getName());
private static final RetryRegistry RETRY_REGISTRY = RetryRegistry.ofDefaults();
private static final Retry DO_SOMETHING_UNRELIABLE_RETRY = RETRY_REGISTRY.retry("doSomethingUnreliableRetry", RetryConfig.custom()
.maxAttempts(5)
.waitDuration(RetryConfig.exponentialWait(1, 4, 10))
.retryOnException(IOException.class)
.onRetry((retryContext) -> LOGGER.log(Level.DEBUG, "Retrying doSomethingUnreliable: attempt {0} of {1}", new Object[]{retryContext.getAttemptCount(), retryContext.getNumberOfAttempts()}))
.build());
public static void main(String[] args) throws IOException {
String result = doSomethingUnreliable();
System.out.println(result);
System.out.println(DO_SOMETHING_UNRELIABLE_RETRY.getMetrics().toString());
}
@Retry(name = "doSomethingUnreliableRetry")
private static String doSomethingUnreliable() throws IOException {
if (random.nextInt(10) > 1) {
throw new IOException("Broken sauce, everything is hosed!");
} else {
return "Awesome sauce!";
}
}
}
Awesome sauce!
Metrics:
name: doSomethingUnreliableRetry
numberOfSuccessfulCalls: 1
numberOfFailedCalls: 0
numberOfRetryAttempts: 0
Как мы видим, повторные попытки можно оформить креативно.
- Можно ограничить попытки по времени (10 секунд) или количеству (5).
- Также можно экспоненциально (то есть, 2 ** некоторое увеличивающееся число n) или фиксированно увеличивать время между отдельными попытками (называется «congestion collapse»).
- Также можно делать повторные попытки только для некоторых видов ошибок.
- Повторные попытки можно предварять или завершать особыми записями в лог.
После курса молодого бойца и изучения основных понятий для работы с транзакциями, давайте познакомимся с двумя методами, которые помогают реализовывать транзакции в распределённых системах.
8.3 Продвинутый инструментарий для любителей транзакций
Дам довольно общие определения темы, которая достойна отдельного уровня.
Two-phase commit (2pc). У 2pc две фазы: подготовка и фиксация. Во время подготовки микросервисы готовятся к изменениям данных, которые можно выполнить атомарно. После этого, во время фиксации, происходят реальные изменения. Для синхронизации нужен глобальный координатор, который блокирует нужные объекты. Если какой-то микросервис не готов к изменениям, координатор прервет транзакцию и начнет процесс отката.
Что представляет собой этот протокол? Он обеспечивает атомарность. Также он гарантирует изоляцию при записи и чтении. Это означает, что изменения одной транзакции не будут видны другим, пока координатор не зафиксирует их. Но эти свойства имеют и минус: поскольку протокол синхронный (блокирующий), он замедляет работу системы (в то время как вызов RPC сам по себе довольно медленный). Также возникает риск взаимной блокировки.
Шаблон Saga использует асинхронные локальные транзакции во всех связанных микросервисах. Микросервисы связаны между собой через шину событий. Если один из микросервисов не может завершить свою транзакцию, другие микросервисы откатят изменения с помощью компенсационных транзакций.
Плюсом Saga является то, что никакие объекты не блокируются. Однако, есть и минусы.
Отладка Saga сложная, особенно когда много микросервисов. Другая проблема с Saga - в нем нет изоляции чтения. То есть, если важны свойства из ACID, то Saga не очень подходит.
Что мы видим? В распределенных системах и БД, которые не предоставляют гарантии ACID, приложение должно быть ответственным за атомарность и изоляцию. То есть, разработчику придется решать конфликты, делать откаты, коммиты и высвобождать место.
8.4 Как понять, когда мне нужны гарантии ACID?
Вероятность того, что несколько пользователей или процессов одновременно будут работать с одними и теми же данными, достаточно высока.
Извините за банальность, но типичным примером являются финансовые транзакции.
Когда порядок выполнения транзакций имеет значение.
Представьте себе, что ваша компания решила перейти с мессенджера FunnyYellowChat на FunnyRedChat, потому что в нём можно отправлять гифки. Но вы не просто меняете мессенджер — вы мигрируете переписку вашей компании из одного мессенджера в другой.
Вы делаете это, потому что программисты не документировали программы и процессы где-то централизованно, а публиковали их в разных каналах мессенджера. Да и продажники публиковали детали переговоров и соглашений там же. Таким образом, вся жизнь компании была там, и поскольку никто не имел времени переносить всё в сервис для документации, а поиск в мессенджерах работал неплохо, вы решили просто скопировать все сообщения в новое место.
Очерёдность сообщений важна, потому что иначе всё может перепутаться, и вы не сможете понять, где находится ответ на какой-либо вопрос.
Кстати, для переписки в мессенджере важно соблюдать последовательность, но если двое пишут одновременно в одном чате, то нет разницы, какое сообщение появится первым. Поэтому для этого сценария не требуется использовать ACID.
Другой возможный пример — это биоинформатика. Я в этом не разбираюсь, но предполагаю, что при расшифровке генома человека порядок важен. Слышал, что биоинформатики используют свои инструменты и БД.
Когда нельзя выдать пользователю или процессу устаревшие данные.
Финансовые транзакции. Не могу придумать другой пример.
Когда незавершённые транзакции связаны со значительными издержками. Представьте себе, какие проблемы могут возникнуть, если врач и медсестра одновременно обновляют карту пациента и перезаписывают изменения друг друга, потому что БД не может изолировать транзакции. Система здравоохранения — это другая область, в которой гарантии ACID, как правило, крайне важны.
8.5 В каких случаях мне не нужны ACID?
Когда пользователи обновляют лишь некие свои приватные данные. Например, пользователь оставляет комментарии или закрепляет заметки к веб-странице. Или редактирует свои личные данные в личном кабинете у провайдера услуг.
Когда пользователи вообще не обновляют данные, а только дополняют новыми (append). Например, приложение для бега, которое сохраняет данные о ваших пробежках: сколько вы пробежали, за какое время, маршрут и т.д. Каждая новая пробежка — новые данные, а старые не редактируются. Возможно, на основании этих данных вы получаете аналитику — и для этого сценария отлично подходит БД NoSQL.
Когда бизнес-логика не определяет необходимость некоего порядка выполнения транзакций. Для блогера на YouTube, который принимает пожертвования для производства нового материала во время прямого эфира, неважно, кто и в какой последовательности дарит деньги.
Когда пользователи пребывают на одной и той же веб-странице или окне приложения несколько секунд или даже минут, они могут видеть устаревшие данные. Такие ситуации могут возникнуть при просмотре новостных онлайн-медиа, YouTube или Хабра. Если вам не важно, что в системе временно хранятся неполные транзакции, вы можете их игнорировать без потерь.
Если вы собираете данные из нескольких источников и они обновляются часто, например данные о свободных парковочных местах в городе, которые изменяются каждые 5 минут, то для вас не будет проблемы, если в один момент транзакция не пройдет. Но, конечно, все зависит от того, что вы хотите делать с этими данными..
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ