Как обеспечить производительность в многопоточной среде?

Добрый день.

Имеется следующая проблема. Есть сервисный класс OrderService. У него есть метод create(Order order, Long companyId), который перед созданием заказа проверяет не превышает ли сумма заказа баланс компании. Код класса примерно следующий.
public class OrderService {
   public Order create(Order order, Long companyId) {
      Company company = companyService.get(companyId);
      checkCompanyBalance(company.getBalance(), order.getSum());
      return create(order);
   }
}


Существует вероятность того, что два пользователя создадут два заказа на компанию одновременно, при этом превысив баланс компании. Чтобы избежать этого делаем наш метод synchronized и все. Но при этом сильно проседает производительность, потому что нет возможности создать заказ на другую компанию.
Какие есть решения у этой проблемы?

Мне приходит в голову только создать пул айдишников компаний, на которые сейчас делают заказы.
public class CompanyPool {
   private Set<Long> companyIds = new HashSet<Long>();

   public synchronized containsAndPut(Long companyId) {
      if(companyIds.contains(companyId)) {
         return false;
      } else {
         companyIds.add(companyId);
         return true;
      }
   }

   public synchronized remove(Long companyId) {
      companyIds.remove(companyId);
   }
}


и переписать метод OrderService#create() следующим образом
public class OrderService {
   private CompanyPool companyPool = new CompanyPool();

   public Order create(Order order, Long companyId) {
      if(companyPool.containsAndPut(companyId)) {
         Company company = companyService.get(companyId);
         checkCompanyBalance(company.getBalance(), order.getSum());
         Order newOrder = create(order);
         companyPool.remove(companyId);
         return newOrder;
      } else {
         // wait till other thread will stop creating order for specified company
      }
}

Насколько жизнеспособно подобное решение? Товарищи, может кто-то решал сходные задачи?
  • Вопрос задан
  • 3082 просмотра
Пригласить эксперта
Ответы на вопрос 3
Использовать СУБД. Это только первые грабли, на которые вы начинаете наступать. Ваше решение плохо поведет себя, если программа упадет. Может получиться так, что деньги спишутся, а заказ не запомнится или наоборот. Причем нужна СУБД с поддержкой транзакций, модные нынче noSQL решения не подойдут или вы напишите поверх них свой транзакционный движок (в документации к mongodb есть пример).

Ну или, если очень хочется, надо переписать
checkCompanyBalance(company.getBalance(), order.getSum());
так, чтобы он входил в критическую секцию (захватывал бы мьютекс, принадлежащий объекту компании), проверял баланс, если денег достаточно, вычитал из баланса сумму, прописывал в список транзакций сообщение, за что было начато списание денег, после чего записывал бы всю эту информацию в энергонезависимую, резервируемую на несколько машин память и, наконец, отпускал мьютекс. Затем создаем заказ, записываем его на такое же надежное хранилище. И, после этого, снова лезем в объект компании и помечаем транзакцию как выполненную.
Ответ написан
@Moxa
можно сделать synchronized по компании… но это если на каждую компанию есть только один инстанс…
Ответ написан
Комментировать
knott
@knott
Полностью согласен c товарищем gvsmirnov.
Однако, мне кажется, что производительность проседает из-за оверхеда на планировщик, при неудачном взятии потоком блокировки. Поэтому, мне кажется что здесь имеет место быть спинлок на основании AtomicBoolean.

public class OrderService {
  
   private AtomicBoolean locked = new AtomicBoolean(false);

   public Order create(Order order, Long companyId) {
      Order result;

      // Пробовать зайти в крит. секцию.
      while (!locked.compareAndSet(false, true)) {
          Company company = companyService.get(companyId);
          checkCompanyBalance(company.getBalance(), order.getSum());
          result = create(order);

          // Открыть путь другим потокам.
          locked.set(false);
          break;
      }
      return result;
   }
}


Однако этот подход годится только если:
  • методы checkCompanyBalance и create выполняются быстро. Иначе ваш код будет только и ждать освобождения блокировки, совершенно бездарно тратя циклы ЦП.
  • многопроцессорная система. Нет смысла ждать освобождения блокировки сделанной на другом потоке.
Ответ написан
Ваш ответ на вопрос

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

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