@brainshock

Как изолировать выполнение кода несколькими потоками?

Упрощённый пример:
asp.net mvc приложение
Допустим, пользователь делает заказ
У нас есть класс OrderService - для размещения заказа и UserService для операций над денежным балансом
Размещение заказа делаем осуществляя ajax post запрос с клиента к контроллеру (HomeController) из которого уже дёргается OrderService

public class OrderService
    {
        private readonly UserService _userService;
        private readonly DbContext _dbContext;

        public OrderService(DbContext dbContext, UserService userService)
        {
            _userService = userService;
            _dbContext = dbContext;
        }

        public int AddOrder(decimal totalSum)
        {
            var order = new OrderEntity
            {
                TotalSum = totalSum
            };

            var balance = _userService.GetBalance();
            
            // проверяем баланс
            if (balance < order.TotalSum)
            {
                throw new Exception("Not enough money");
            }

            // списываем деньги
            _userService.OperateBalance(-order.TotalSum);

            _dbContext.Set<OrderEntity>().Add(order);

            _dbContext.SaveChanges();

            return order.Id;
        }
    }


public class UserService
    {
        private readonly DbContext _dbContext;
        public UserService(DbContext dbContext)
        {
            _dbContext = dbContext;
        }

        public decimal GetBalance()
        {
            return _dbContext.Set<UserEntity>().FirstOrDefault(x => x.Id == userId)?.Balance;
        }

        public void OperateBalance(decimal amount)
        {
            var user = _dbContext.Set<UserEntity>().FirstOrDefault(x => x.Id == userId);
            if (user != null)
            {
                user.Balance += amount;
                _dbContext.SaveChanges();
            }
        }
    }


public class HomeController : Controller
    {
        private readonly IOrderService _service;

        public HomeController(IOrderService service)
        {
            _service = service;
        }

        [HttpPost]
        public JsonResult PostOrder(decimal totalSum)
        {
            var result = _service.AddOrder(totalSum);
            
            return Json(result);
        }
    }


$.post('/Home/PostOrder', { totalSum: 150 })
                        .success(function(res) {
                             console.info('res1', res);
                        });


Проблема: если сделать два одновременных запроса, то два потока войдут одновременно в метод AddOrder и оба дадут верную проверку на баланс пользователя

Я вижу только одно решение - statick lock на весь метод

Это вообще нормальная практика? Есть какие-то ещё варианты?

p.s. DbContext управляется Unity через PerRequestLifetimeManager
UserService и OrderService - стандартно (PerResolveLifetimeManager)
  • Вопрос задан
  • 107 просмотров
Решения вопроса 2
@kttotto
пофиг на чем писать
Первое, если OrderService не будет резолвится как синглтон, то на каждый запрос будет создваться свой инстанс сервиса, т.е двух потоков на один метод не будет.

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

Третье, чтобы не было двух одновременных запросов с одного клиента, надо дизейблить кнопку и показывать прелоадер до получения ответа.

Ну и четвертое, если хотите получить баланс до того как другой запрос туда что-то добавит, то в AddOrder используйте транзакции с достаточным уровнем блокировки на изменение.
Ответ написан
hePPer
@hePPer
если используете ОРМ EF - то пробуйте использовать атрибут ConcurrencyCheck для проверки на уровне базы.
ссылка
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
Это вообще нормальная практика? Есть какие-то ещё варианты?

Блокировать потоки-обработчики запросов -- плохая, вредная практика. Потоки -- ресурсы не бесконечные, как сами по себе, так и вычислительные ресурсы, требуемые потоками. У вас просто появляется шанс, что всем запросам не хватит ОЗУ.
1 поток, обрабатывающий заказы по очереди. Это может быть 1 поток на пользователя, может даже не быть потоков как таковых: флаг, запрещающий создавать\обрабатывать заказы, при наличии неотработанного заказа у пользователя. Но чаще всего, это отдельный (микро)сервис, который собственно получает от веб-сервера заказы, складывает в очередь и уже с этой очередью работает.
Основная задача -- организовать очередь.
Ответ написан
Ваш ответ на вопрос

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

Похожие вопросы