@Lite_stream

Many-to-many как избежать Race conditions?

Есть 3 таблицы: Users, Subscriptions, UsersSubscriptions(join table). При добавлении новой записи в UsersSubscriptions нужно проверить число подписок пользователя, если оно превышает определённое значение, то не делать insert.

Псевдокод:
If(!usersSubscriptions.userCanSubscribe()) throw new Exception('Subscriptions exceeded!'); /** ( SELECT COUNT(*) FROM UsersSubscriptions WHERE userId = currentUserId ) < maxSubscriptions */
UsersSubscriptions.addRecord(); /** INSERT INTO UsersSubscriptions  (subscriptionId, userId)
VALUES (currentSubscriptionId, urrentUserId); */


Race Conditions: Если поток №1 и поток №2 успешно прошли условие if (первую строчку кода), а в количество подписок пользователя на тот момент было, скажем, 4 из 5 возможных, то оба потока выполнят UsersSubscriptions.addRecord() и кол-во подписок пользователя станет больше положенного. Как можно избежать этого ?

P.S.: в NoSQL базах данных, например, MongoDB, можно было бы не делать join table UsersSubscriptions, а просто в таблице пользователей разместить массив id подписок и в таблице подписок разместить массив id пользователей. Тогда в операцию update можно было бы добавить условие, что длина массива меньше maxSubscriptions, то есть что-то вроде
Users.updateOne( {_id: userId, $expr: { $lt: [ { $size: '$subscriptions'}, maxSubscriptions] } } }, updateObject );

И то же самое, только для Subscriptions. Но при таком подходе потребуется добавлять транзакции, так как нужно модифицировать более 1-го документа.
  • Вопрос задан
  • 154 просмотра
Решения вопроса 2
@yayashitoya
Как можно избежать этого ?


Все уже придумано до нас. Лет эдак за 50.
Делать в одной транзакции. И проверку и добавление подписки.
Ответ написан
Rsa97
@Rsa97
Для правильного вопроса надо знать половину ответа
Блокировка таблицы или SQL-запрос, составленный так, чтобы возникала ошибка при попытке добавлении лишней записи.
Например, для MySQL
INSERT 
  INTO `users_subscriptions` (`user_id`, `subscription_id`)
  SELECT IF(`count` < 5, :userId, NULL), :subscriptionId
    FROM (
      SELECT COUNT(*) AS `count`
        FROM `users_subscriptions`
        WHERE `user_id` = :userId
    ) AS `t`
Ответ написан
Пригласить эксперта
Ответы на вопрос 2
Вижу два решения - использовать очереди или выполнять проверку на уровне триггеров БД.
Ответ написан
Адекватным решением видится создать прослойку, которая будет обрабатывать обращения потоков друг за другом, т.е. некое бутылочное горлышко, которое не будет позволять потокам работать, как потоки. Тогда и race condition будет невозможен.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы