Как стать автором
Обновить

Комментарии 26

НЛО прилетело и опубликовало эту надпись здесь
«При старте клиентское приложение асинхронно отправляет «пачку» запросов к API» — это ситуация, когда авторизация уже пройдена или вообще отсутсвует.
В примерах Client — это сущность устройства пользователя, оно может меняться при тех же учетных данных.

НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Обработка таких исключений является антипаттерном, как с точки зрения производительности, так и с точки зрения стиля кода. Более того, лишние запросы к БД создают накладные расходы, что тоже не очень хорошо. При нескольких потоках, стоимость обработки таких исключений увеличивается в разы.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь

Надо сделать ClientId праймари ключом, тогда достаточно селекта + insert (или find + saveorupdate), в случае constraint violation — просто вернуть аргументы процедуры, т.к. сущность уже создана. Второй вариант — дописать ON CONFLICT/ON DUPLICATE KEY/IGNORE или подобное прямо в процедуре.


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


Не знаю как там дальше используется ClientId, но нужен ли он вообще как отдельная сущность?

Действительно можно сделать так, а если реализация репозитория делается в ручную, то можно при insert использовать конструкции вида «ON CONFLICT» / «ON DUPLICATE KEY». Единственное, при данном подходе будет кратно больше запросов к БД, что не всегда хорошо на нагруженных сервисах. Но конкретно в нашей ситуации мы пошли в сторону синхронизации даже не по этому — проблемным местом в создании сущности Client являлась необходимость обращения к нескольким другим сервисам до коммита, и без синхронизации мы бы порождали дополнительную нагрузку еще и на нах.
НЛО прилетело и опубликовало эту надпись здесь

Назовём это XA. Обращаемся в какой-то сервис, если все ок — коммитим. Если там какой-то запрет — роллбэк. Так же было бы, если второй сервис заменить на вторую бд.

НЛО прилетело и опубликовало эту надпись здесь

Спасибо за статью!
Интереса ради — сколько времени у вас съедает блокировка на базе которая еще и по сети распределена ?

Сколько точно съедает сейчас уже, к сожалению, не припомню, но было в допустимых пределах. Наибольшая трудоемкость была не при работе с базой как таковой, а в обращении в сторонние сервисы в ходе создания
Хорошая статья. Спасибо
Полезно. Спасибо!
А просто synchronizeOnSession вам бы не помог?

А что делать в случае нескольких нод? В этом случае всегда нужен какой-то внешний арбитр.

Кстати, никогда не заглядывал в код для проверки того, как конкретно синхронизация на сессиях устроена. Интересно, как она себя поведёт в случае распределённого хранилища сессий?
Если синхронизироваться на сессиях, то есть еще такой неприятный момент — когда на api прилетит первая пачка запросов от одного клиента, они должны все «получить» одну сессию. Для того, чтобы это сделать, нужен специальный метод, который вызывается клиентом первым и который создает сессию, а далее остальные методы просто уже использую ее идентификатор. В приложениях с явной авторизацией этот метод всегда есть — это собственно метод авторизации. Если в приложении нет авторизации или она проходит один раз, а не при каждом старте, заставлять клиент сначала создать на сервере сессию, а только потом запрашивать бизнес методы приведет к увеличению времени запуска приложения.

В продакшене это не работает: там как минимум два сервера, а данная синхронизация работает только для одной JVM. Правильных решений три: переделать клиента, переделать API, использовать транзакции на стороне DB.

Решение 1

А пардон, где написано, что ваш сервис singleton, чтобы синхронизироваться по this? Тогда уж поставьте syncronized(MyService.class). Не будет работать в кластере.


Решение 2
Менеджмент «протухания» clientId ложится в данном случае на плечи GС.

Вообще жесть! Зачем вы вводите людей в заблуждение? Где заявлено про "протухание" и GC? String.intern() складывает строки в constant pool, который лежит в permanent generation. И скоро вместо протухания вы получите снижение производительности и как финал OutOfMemory.
all: никогда так не делайте и вообще забудьте про intern()!


Решение 3

Не работает в кластере.


Решение 4

Лочить по сети N нод для каждого getUserById() — ну, ну. Кроме того, привлекается какое-то левое решение.


Решение 0


Единственно правильное — использовать старый добрый механизм транзакций базы данных с повтором. Например так:
https://www.baeldung.com/spring-retry


@Retryable(value = { SQLException.class }, maxAttempts = 3, backoff = @Backoff(delay = 1000))
@Transactional
public Client getOrCreateUser(String clientId)
сервис singleton

Сервис singleton — см. исходники. В случае использования scope prototype синхронизироваться по this действительно нельзя.

И скоро вместо протухания вы получите снижение производительности и как финал OutOfMemory

Согласен, данное решение на практике неприменимо.

Не работает в кластере

Да, я об этом указал в статье: «решения 1-3 вполне подойдут для небольших одноинстансных сервисов».

Лочить по сети N нод для каждого getUserById()

Блокировка будет не на каждый getUserById, а только на операцию создания Client и блокироваться будут только запросы конкретного клиента.

Решение 0 Единственно правильное

Если задача именно решить проблему со вставкой, то вполне можно использовать Ваш вариант. Основная цель статьи больше показать способы синхронизации запросов, возможно просто пример с integrity constraint violation не самый подходящий, так как эта проблема имеет решения и без синхронизации.
Основная цель статьи больше показать способы синхронизации запросов, возможно просто пример с integrity constraint violation не самый подходящий

Проблема синхронизации к Spring-у вообще не имеет отношения. Если у вас под низом rdbms, то самое натуральное — это использовать ее средства синхронизации (напр. select for update). Даже если операция ничего не сохраняет в базу, но требует консистенции, проще будет создать таблицу mutex-ов и синхронизироваться по ней. Если же база распределенная и без acid, а запросы делаются асинхронно, то тут универсального решения нет — требуется адаптировать архитектуру под преследуемые цели, и в любом случае это будет сложнее и хуже.

В моем проекте сначала попытался словить ConstraintIntegrityViolation exception и после пробовал получить entity ещё раз. Из-за производительности сильно не переживал т.к. у Hibernate есть first-level caching вшитый.


Потом перешёл на Retryable mechanism, более чисто и понятней.

Hibernate есть first-level caching вшитый

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