2012-08-24 12 views
8

Taki scenariusz: pewna ilość danych do wstawienia do tabeli, gdy osiągnie próg już się nie wstawia, zasymulowałem ten scenariusz, w przypadku wielowątkowego (np. asp.net) pojawiły się problemy współbieżne.Jak rozwiązać konflikt współbieżności w ado.net

Moje pytanie brzmi, jak rozwiązać od jednoczesnego problemu, nie użyć obudowy lock

void Main() 
{ 

    Enumerable.Range(0,20).ToList().ForEach(i=>{ 
     MockMulit(); 
    }); 

} 
//Start a certain number of threads for concurrent simulation 
void MockMulit() 
{ 
    int threadCount=100; 

    ClearData();//delete all data for test 


    var tasks=new List<Task>(threadCount); 
    Enumerable.Range(1,threadCount).ToList().ForEach(i=>{ 
    var j=i; 
    tasks.Add(Task.Factory.StartNew(()=>T3(string.Format("Thread{0}-{1}",Thread.CurrentThread.ManagedThreadId,j)))); 
    }); 
    Task.WaitAll(tasks.ToArray()); 

    CountData().Dump();//show that the result 
} 

metoda jeden - współbieżność bardzo poważny

void T1(string name) 
{ 
     using(var conn=GetOpendConn()) 
     { 
      var count=conn.Query<int>(@"select count(*) from dbo.Down").Single(); 
      if(count<20) 
      { 
       conn.Execute(@"insert into dbo.Down (UserName) values (@UserName)",new{UserName=name}); 
      } 
     } 

} 

Sposób drugi - umieścić sql razem może zmniejszyć równoczesne, ale nadal istnieją:

void T2(string name) 
{ 
    using(var conn=GetOpendConn()) 
    { 
     conn.Execute(@" 
         if((select count(*) from dbo.Down)<20) 
         begin 
          --WAITFOR DELAY '00:00:00.100'; 
          insert into dbo.Down (UserName) values (@UserName) 
         end",new{UserName=name}); 
    } 
} 

metoda trzy - z blokadą d estroy jednoczesnego, ale nie sądzę, że jest to najlepsze rozwiązanie

private static readonly object countLock=new object(); 
void T3(string name) 
{ 
    lock(countLock) 
    { 
     using(var conn=GetOpendConn()) 
     { 
      var count=conn.Query<int>(@"select count(*) from dbo.Down").Single(); 
      if(count<20) 
      conn.Execute(@"insert into dbo.Down (UserName) values (@UserName)",new{UserName=name}); 
     } 
    } 
} 

inna metoda pomaga

//delete all data 
void ClearData() 
{ 
    using(var conn=GetOpendConn()) 
    { 
     conn.Execute(@"delete from dbo.Down"); 
    } 
} 
//get count 
int CountData() 
{ 
    using(var conn=GetOpendConn()) 
    { 
     return conn.Query<int>(@"select count(*) from dbo.Down").Single(); 
    } 
} 
//get the opened connection 
DbConnection GetOpendConn() 
{ 
    var conn=new SqlConnection(@"Data Source=.;Integrated Security=SSPI;Initial Catalog=TestDemo;"); 
    if(conn.State!=ConnectionState.Open) 
     conn.Open(); 
    return conn; 
} 
+0

Jaki rodzaj bazy danych używasz, że nie może obsłużyć wstawiania danych? – Jake1164

+0

Scenariusz, który próbujesz chronić, jest bardzo niejasny. Czy możesz powtórnie zmienić to, co próbujesz zrobić? –

+0

@MarcGravell Właściwie zaprojektowałem działanie "Spike Goods" na naszej stronie e-commerce. Na przykład, tylko 20 towarów może kupić, ponieważ asp.net jest wielowątkowy, 21 rekordów pojawia się w naszej bazie danych! :( – JeffZhnn

Odpowiedz

4

Brzmi jak chcesz tylko wstawić w dół, gdy istnieją mniej niż 20 wierszy. Jeśli tak: make, że pojedyncza operacja:

insert dbo.Down (UserName) 
select @UserName 
where (select count(1) from dbo.Down) < 20 

select @@rowount -- 1 if we inserted, 0 otherwise 

Ewentualnie, jeśli * potrzeba * trzeba będzie użyć transakcji, idealnie „Serializable”, aby uzyskać kluczową-range-lock - być może nawet dodając (UPDLOCK) do początkowej liczby, aby upewnić się, że wymaga ona szybkiego blokowania zapisu (lub bloków, a nie zakleszczenia). Ale: pojedyncza operacja TSQL (jak już przedstawiono korzystne Można nawet sprawić, że bardziej paranoidalny (choć nie jestem pewien, że go potrzebuje).

declare @count int 

begin tran 

insert dbo.Down (UserName) 
select @UserName 
where (select count(1) from dbo.Down (UPDLOCK)) < 20 

set @count = @@rowount 

commit tran 

select @count -- 1 if we inserted, 0 otherwise 
+0

Byłem napisać test (https://github.com/dushouke/ConcurrencyTest), sprawiają, że pojedyncza operacja nie może rozwiązać problemu, ale z 'UPDLOCK' może zniszczyć równoczesne – JeffZhnn

+0

@jefferydu nie,' UPDLOCK' nie "niszczy współbieżnego" - to tylko *** poprawnie *** wskazuje, że planujemy zmienić te dane w tej samej transakcji (a nie tylko ją przeczytać). Czy próbowałeś drugiej wersji, którą opublikowałem? –

+0

Tak, testowałem drugą wersję, nie ma już żadnych rekordów. Zrzuty ekranu z wyników http://i.stack.imgur.com/155ev.jpg – JeffZhnn

1

rozważyć robi to w odwrotnej kolejności i będzie prawdopodobnie znacznie . prostsze na przykład:

  • Utwórz tabelę z indeksem autoinkrementacja nazywają to „Up” lub „RequestOrdering”, czy cokolwiek
  • Get żądań klienta w dowolnej kolejności Dla każdego żądania:...
    • Włóż nowy rząd w górę.
    • Uzyskaj ostatni wstawiony identyfikator rzędu.
    • Jeśli ostatni identyfikator wkładki < = 20, zrób prawdziwą wkładkę.
  • Wyrzuć swój stół, kiedy skończysz.

Jeśli twoja baza danych obsługuje klucze wielokolumnowe z jednym z nich, automatycznie zwiększając (IIRC, MyISAM tabele), możesz ponownie użyć tabeli "Up" w wielu ofertach produktów.


Jeszcze łatwiejsze wdrożenie byłoby wstawić do 20 wierszy „w dół” i usunąć jeden na życzenie. Sprawdź liczbę dotkniętych wierszy (powinno być 1), aby sprawdzić, czy użytkownik odniósł sukces. To ładnie gra ze specjalnością wielu produktów.Na przykład:

delete * from Down where down_special_id = Xxx limit 1 

Prawdopodobnie najłatwiejszy, a także do śledzenia, który „wygrał”, utworzyć jeden wiersz na produkt i mieć każdą aktualizację obsługi jedno (i tylko jedno) wiersz. Ponownie sprawdzić liczbę wierszy, aby zobaczyć, czy były one udane:

update Up 
set 
    user_name = @user_name 
where 
    user_name is null 
    and product_id = @product_id 
limit 1 
+0

Wybaczcie składnię MySQL. Myślę, że SQL Server i inne podobne "TOP" vs "LIMIT". – EthanB

Powiązane problemy