Советы и приемы, которые помогут сделать ваши приложения, использующие Sequelize, более надежными и свободными от условий гонки.

Недавно я столкнулся с особенно пагубной проблемой с Sequelize при попытке доступа к базе данных PostgreSQL в параллельном приложении, таком как экспресс-сервер Node.js. Эта конкретная проблема привела к неприятному состоянию гонки в коде, важном для безопасности. Sequelize предлагает транзакции и блокировки, чтобы убедиться, что в нашем приложении не происходит ничего фальшивого, но эти функции сложны в использовании и плохо документированы. По этой причине я хотел бы предоставить вам несколько идей и приемов, чтобы сделать ваши приложения, использующие Sequelize, более надежными и свободными от условий гонки.

Пример кода для проблемы, с которой я столкнулся, приведен ниже. Маршрут принимает номер телефона, который необходимо проверить. Сначала мы проверяем, был ли номер телефона уже отправлен в течение последних 15 минут для проверки. Если это так, мы увеличиваем количество инициаций, чтобы пользователь не мог инициировать одно и то же электронное письмо более 10 раз в заданном временном окне. А если нет, то вставляем новую запись в базу. В любом случае мы впоследствии отправляем код подтверждения по SMS и возвращаем идентификатор подтверждения в приложение.

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

Чтобы решить эту проблему, мы надеемся использовать функции транзакций и блокировок Sequelize, как показано в следующем примере кода. В большинстве случаев использование блокировок, предоставляемых Sequelize, прекрасно работает. Но блокировки работают только с уже существующими строками, а не с теми, которые мы можем захотеть создать в будущем. А поскольку запись в таблице для проверки телефона может не существовать в начале маршрута, мы не сможем ее заблокировать.

Одним из способов решения этой проблемы было бы настроить Sequelize для использования SERIALIZABLE в качестве уровня изоляции. И хотя это работает хорошо, это будет вызывать исключение каждый раз, когда нарушается изоляция. Об этом нужно позаботиться в коде, и запрос должен выполняться повторно, если происходит исключение. Так как это требует *множества* изменений в коде и создает всевозможные новые проблемы, для меня это не было подходящим решением.

Второй способ, который я выбрал, — просто заблокировать всю таблицу. Теперь Sequelize позволяет выполнять блокировки только на уровне строк. Это означает, что вы можете заблокировать только определенную строку (и эта строка уже должна существовать). Но, к счастью, с помощью Sequelize мы можем выполнять сырые запросы. Этот патч просто требует, чтобы мы блокировали таблицу в начале транзакции, и как только транзакция будет зафиксирована (или откатана), блокировка будет снята. Окончательный рабочий код вы можете найти внизу.

И последнее предупреждение: вы должны экономно использовать блокировки таблиц! Они замедлят работу кода вашего приложения, так как блокируют доступ ко всей таблице и, если вы не будете осторожны, могут даже привести к взаимоблокировкам. В большинстве случаев вы захотите использовать либо блокировки на уровне строк, которые полностью поддерживаются Sequelize, либо рекомендательные блокировки, если вы используете PostgreSQL.

Первоначально опубликовано на https://schneider-lukas.com.

Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn и Discord.