Comments 10
1) А уровни изоляции зачем придумали ?
2)
обратите внимание на добавленный оператор FOR UPDATE в SELECT. Он блокирует строку, чтобы другие SELECT запросы ждали завершения транзакции. Это позволяет правильно обрабатывать параллельные запросы.
И получить High load и "мы упёрлись в СУБД" на ровном месте .
Сорри, но дальше не стал читать . Потому, что на обсуждение с разрабами темы "вы зачем используете select for update и потом приходите в отдел администрирования баз данных с жалобами "у нас все тормозит" ? ", было потрачено сколько нервов , времени и бесполезных разговоров, что не хочется опять вспоминать.
Строчкой ниже шло предупреждение именно от автора об этом )
Стало быть поторопился. Просто уже столько раз натыкался на попытки перенести логику СУБД на уровень приложения , что уже стойкая настороженность возникает.
Ок. Попробую дочитать.
Эх , если бы современные разрабы это читали и руководствовались затем в реальной жизни
Что бы вы ни выбрали, обязательно проводите нагрузочные тесты, чтобы увидеть, как ваш код работает под нагрузкой. Это поможет избежать неприятных сюрпризов в продакшене, когда резко возрастет трафик.
По личному опыту - "неприятные сюрпризы в продакшене" возникают в 100% . Если нагрузка на информационную систему средняя или высокая.
Если внутри fn()
вызываемой в теле `runInTx`
случится паника то ваш код не вызовет ни коммит ни роллбэк. Думаю безопаснее делать так
func runInTx(db *sql.DB, fn func(tx *sql.Tx) error) (err error) {
tx, err := db.Begin()
if err != nil {
return
}
defer func() {
rollbackErr := tx.Rollback()
if rollbackErr != nil {
err = errors.Join(err, rollbackErr)
}
}()
err = fn(tx)
if err == nil {
err = tx.Commit()
}
return
}
Теперь благодаря деферу роллбэк всегда гарантированно вызывается, просто если это произошло после коммита то ничего не случится, зато если вылетит паника вы корректно откатите начатую транзакцию.
Транзакции в слое логики (избегайте, если можете)
...
По мере роста логики вы должны тщательно обдумывать, что должно выполняться внутри транзакции, а что — вне её. Откат повлияет на всё, что вы поместите внутрь функции.
Если в рамках одного запроса от пользователя мне требуется проводить какие-то изменения внутри транзакции, а другие - строго вне, то это повод задуматься о переосмыслении api. Но даже если требуется делать именно так, то ведь на то это и слой логики, что я волен определять, какие действия должны быть атомарными, а какие - нет :)
Не говоря уже о странном аргументе tx, который нужно передавать в методы репозитория.
Что думаете насчёт того, чтобы не заморачиваться и просто создавать (где нужно) отдельную функцию, которая принимает на вход tx? В UpdateByID по сути так и происходит, просто вложено в другую функцию.
Паттерн UpdateFn (наше основное решение)
Принцип "на, держи юзера, измени в нём, что нужно, а я пока покурю" интересен. Но, кажется, что не совсем удобный в более сложных случаях. У вас был опыт применения, когда требуются изменения сразу в нескольких таблицах? Как будто в этом случае часть бизнес логики будет проникать в репозиторий...
По своим наблюдениям считаю, что "Транзакции в слое логики" - оптимальное решение, разве что транзакцию прокинуть в контекст для удобства можно и сделать методы репозитория универсальными (работающими как с tx из контекста так и без него). А tx в контекст прокидывать через метод runInTx. Предлагать целевое решение с оговорками, что оно не для хайлоада, ну... Если не хайлоад, то оптимальнее наверное ORM и не думать.
А в целом я считаю, что если понадобились транзакции, то значит транзакция является неотъемлемой частью бизнес логики и не стоит потеть потаясь её куда-то замести под ковер
Транзакции в БД на Go с использованием многослойной архитектуры