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

Добрый день.

Имеется следующая проблема. Есть сервисный класс 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 просмотра
Пригласить эксперта
Ответы на вопрос 4
@dborovikov
> Но при этом сильно проседает производительность, потому что нет возможности создать заказ на другую компанию.

Не верю. У вас что, 100 миллионная аудитория?
Ответ написан
Комментировать
Использовать СУБД. Это только первые грабли, на которые вы начинаете наступать. Ваше решение плохо поведет себя, если программа упадет. Может получиться так, что деньги спишутся, а заказ не запомнится или наоборот. Причем нужна СУБД с поддержкой транзакций, модные нынче 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 выполняются быстро. Иначе ваш код будет только и ждать освобождения блокировки, совершенно бездарно тратя циклы ЦП.
  • многопроцессорная система. Нет смысла ждать освобождения блокировки сделанной на другом потоке.
Ответ написан
Ваш ответ на вопрос

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

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