ASP.NET: проблема с расой в бизнес-логике - PullRequest
0 голосов
/ 27 апреля 2010

У меня следующая проблема:

В нашей системе есть продукты, которые только после выпуска могут быть куплены X раз. При покупке центральный алгоритм покупки проверяет, сколько существует заказов и, если ниже X, приступает к покупке.

В псевдоишем C # код:

public class OrderMethods
{

    public static Purchase(Product product, Client client)
    {

        int purchases = /* count order records of this product */;

        if(purchases>=MAX_ORDERS) throw PurchaseException();

        /* perform purchase by inserting order record in database */

     }

}

Проблема в том, что иногда, когда существует высокий спрос на определенный продукт, одновременно происходит много запросов и регистрируется больше, чем MAX_ORDERS. Это происходит примерно раз в год: (.

Какое лучшее решение для решения этой проблемы? Я использую ASP.NET/C#, Ling2SQL и MSSQL. У нас 1000> заказов в день. Важно, чтобы заказы обрабатывались в том порядке, в котором они запрашиваются.

Решения, с которыми я уже столкнулся:

  • Один глобальный мьютекс?

  • Один мьютекс на продукт, сохраненный в хеш-таблице с функцией доступа, такой как:

    private Mutex GetPurchaseMutex(Guid productId)
    {
    
    if (mutexTbl[productId] == null)
        {
            mutexTbl[productId] = new Mutex();
        }
        return (Mutex)mutexTbl[productId];
    }
    

Где mutexTbl - это Hashtable. Здесь я не понимаю, как отбросить старые мьютексы хорошим способом.

  • Использование триггера T-SQL INSERT в таблице Order, которая проверяет, сколько существует заказов:

    CREATE TRIGGER Triggers_OrderInsertTrigger ON Orders ПОСЛЕ ВСТАВКИ КАК ЕСЛИ / * проверить, есть ли много заказов * / НАЧАТЬ RAISERROR («Слишком много заказов», 16, 1); ROLLBACK TRANSACTION; ВЕРНУТЬ END;

Но мне не очень нравится ни одно из этих решений. Как бы вы решили это?

Ответы [ 3 ]

4 голосов
/ 27 апреля 2010

Я бы сказал, перенесите эту логику на уровень базы данных, когда она может быть защищена транзакциями.

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

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

BEGIN TRANSACTION

DECLARE @OrderCount int
SELECT @OrderCount = COUNT (ID) FROM [Order] WHERE ProductID = '234323'

IF (@OrderCount < @Threshold)
BEGIN
    INSERT INTO [Order] (ID, ProductID, ...)
    VALUES (NEWID(), '234323', ...)

    COMMIT TRANSACTION
    RETURN 0
END
ELSE
    ROLLBACK TRANSACTION
    RETURN 1
END
3 голосов
/ 27 апреля 2010

Эта проблема звучит очень похоже на проблему «только двух непоставленных заказов», которую я описал здесь . (Внимание, статья довольно длинная, но очень информативная). Но я думаю, что ваши варианты в основном таковы:

  • Поместите эту полную бизнес-логику (включая проверку) в вашу базу данных (так же, как написал Developer Art).
  • Используйте сериализуемую транзакцию и запишите эту логику на C # на вашем бизнес-уровне, включая .Count().
  • Ничего не делай, и пусть он один раз в год терпит неудачу, а потом наводит порядок.

Хотя последний вариант может показаться забавным, на самом деле это серьезный вариант. Устранить проблему параллелизма такого рода сложно и может быть повсеместно. Это может заставить вас сделать большие архитектурные изменения. Кроме того, эффективность решения COUNT(*) зависит от индексов в этой конкретной таблице. Добавление индексов в базу данных (по соображениям производительности) может случайно изменить эффективность и правильность этого решения. Таким образом, с точки зрения бизнеса, может быть намного дешевле исправлять проблему в вашей базе данных один раз в год. Конечно, я не могу догадаться, каковы затраты, каждый раз, когда клиент может купить больше заказов, чем ваш бизнес. Это решать вам (или вашему бизнесу).

1 голос
/ 27 апреля 2010

Если вы не хотите обрабатывать это на уровне БД, я бы создал класс, в котором будет храниться счетчик покупок для каждого продукта, хранится этот счетчик в таблице и блокируется в вашем методе покупки. Это похоже на ваш метод мьютекса, но вы разрешаете .NET обрабатывать блокировку для вас

public class PurchaseCounter(
{
    public Guid Product {get; set; } 
    public int MaxOrders {get; set; } 
    public int int CurrentOrders {get; set; } 
 }

public static bool Purchase(Product product, Client client)
{

    PurchaseCounter counter = purchaseCounterDictionary[product.ProductGuid];

    lock(counter)
    {
        if( counter.CurrentOrders < counter.MaxOrders )
        {
             //do logic to place order
             counter.CurrentOrders++;
             return true;  
        } 
        else
        {
             return false;
        }
    }

 }

}

...