2013-07-12 10 views
8

Mam dwie bardzo proste tabele, nazwijmy je [UserData1] i [UserData2]. Oba mają kolumnę [UserId] jako klucz podstawowy. Używam dwóch typów zapytań przeciwko tym dwóm tabelom. Jednym z nich jest SELECT, która zwraca dane kombinowane dla danego użytkownika:Jak uniknąć zakleszczenia podczas wybierania z połączonych tabel?

SELECT <a subset of columns from both tables> 
FROM [UserData1] ud1 
    FULL OUTER JOIN [UserData2] ud2 ON ud1.[UserId] = ud2.[UserId] 
WHERE 
    ud1.[UserId] = @UserId OR ud2.[UserId] = @UserId 

druga jest transakcją, która aktualizuje dane użytkownika w obu tabelach dla danego użytkownika:

BEGIN TRANSACTION 

UPDATE [UserData1] 
SET <new values> 
WHERE [UserId] = @UserId 

UPDATE [UserData2] 
SET <new values> 
WHERE [UserId] = @UserId 

COMMIT TRANSACTION 

Problemem tutaj jest kolejność pozyskiwania wspólnych blokad tabel w instrukcji SELECT jest nieokreślona, ​​co może (i faktycznie powoduje) prowadzić do klasycznej sytuacji zakleszczenia, jeśli SQL Server zdecyduje się zablokować [UserData2] przed [UserData1]. Jaki byłby najlepszy sposób na uniknięcie zakleszczeń w tym przypadku?

Scal te tabele w jeden stół, prawda? Chciałbym, żeby to było takie łatwe. Załóżmy, że istnieje powód, aby je rozdzielić.

CZYTAĆ NIEKOMPITOWANĄ/NOLOCK wskazówkę? Załóżmy, że brudne odczyty nie mogą być tolerowane.

SNAPSHOT poziom izolacji? To rozwiązałoby problem, ale nie jestem pewien co do związanego z tym obciążenia.

Tak więc pytanie sprowadza się do: czy istnieje sposób na zagwarantowanie kolejności, w której zamki na połączonych stołach są nabywane?

Początkowo sądziłem, że można to osiągnąć za pomocą wskazówki do zapytania FORCE ORDER, ale potem dowiedziałem się, że niekoniecznie wymusza kolejność blokowania tabel. Innym rozwiązaniem w tym przypadku byłoby wydawanie osobnych zapytań SELECT dla każdej tabeli, a następnie łączenie dwóch jednorzędowych zestawów rekordów w warstwie aplikacji, ale na wypadek, gdyby kiedykolwiek musiałem wykonać zapytanie dla wielu użytkowników, wolałbym uzyskać wszystkie wyniki w jednym zestawie rekordów.

UPDATE:

Jest to fragment śladu impasu:

Deadlock encountered .... Printing deadlock information 
    Wait-for graph 

    Node:1 
    KEY: 17:72057594039173120 (e21762ccf3dc) CleanCnt:3 Mode:X Flags: 0x1 
    Grant List 1: 
     Owner:0x00000020F75B0480 Mode: X  Flg:0x40 Ref:0 Life:02000000 SPID:72 ECID:0 XactLockInfo: 0x00000020EB13ED68 
     SPID: 72 ECID: 0 Statement Type: UPDATE Line #: 1 
     Input Buf: Language Event: (@UserId bigint,@DataColumn2 int)update 
    Requested by: 
    ResType:LockOwner Stype:'OR'Xdes:0x00000020FC98DA40 Mode: S SPID:75 BatchID:0 ECID:0 TaskProxy:(0x00000020DAB38608) Value:0xf75abbc0 Cost:(0/0) 

    Node:2 
    KEY: 17:72057594039107584 (e21762ccf3dc) CleanCnt:9 Mode:S Flags: 0x1 
    Grant List 1: 
     Owner:0x00000020EEBFE580 Mode: S  Flg:0x40 Ref:1 Life:00000000 SPID:75 ECID:0 XactLockInfo: 0x00000020FC98DA80 
     SPID: 75 ECID: 0 Statement Type: SELECT Line #: 1 
     Input Buf: Language Event: (@UserId bigint)select [t].[UserId], t.[DataColumn2], t1.[DataColumn1] 
    Requested by: 
    ResType:LockOwner Stype:'OR'Xdes:0x00000020EB13ED28 Mode: X SPID:72 BatchID:0 ECID:0 TaskProxy:(0x0000001F671C6608) Value:0xf75b5400 Cost:(0/456) 

    Victim Resource Owner: 
    ResType:LockOwner Stype:'OR'Xdes:0x00000020FC98DA40 Mode: S SPID:75 BatchID:0 ECID:0 TaskProxy:(0x00000020DAB38608) Value:0xf75abbc0 Cost:(0/0) 
    deadlock-list 
    deadlock victim=process20fda2ccf8 
    process-list 
    process id=process20fda2ccf8 taskpriority=0 logused=0 waitresource=KEY: 17:72057594039173120 (e21762ccf3dc) waittime=4526 ownerId=3416711 transactionname=SELECT lasttranstarted=2013-07-11T18:42:20.943 XDES=0x20fc98da40 lockMode=S schedulerid=20 kpid=2800 status=suspended spid=75 sbid=0 ecid=0 priority=0 trancount=0 lastbatchstarted=2013-07-11T18:42:20.950 lastbatchcompleted=2013-07-11T18:42:20.950 lastattention=1900-01-01T00:00:00.950 clientapp=.Net SqlClient Data Provider hostname=hostname hostpid=27716 loginname=loginname isolationlevel=read committed (2) xactid=3416711 currentdb=17 lockTimeout=4294967295 clientoption1=671088672 clientoption2=128056 
     executionStack 
     frame procname=adhoc line=1 stmtstart=36 sqlhandle=0x020000001fcbbe1423a0c65cc8411344c6040e879195af3a0000000000000000000000000000000000000000 
    select [t].[UserId], t.[DataColumn2], t1.[DataColumn1] from [UserData1] t1 full outer join [UserData2] t on t1.[UserId]=t.[UserId] where t.[UserId][email protected] or t1.[UserId][email protected] option (force order)  
     frame procname=unknown line=1 sqlhandle=0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 
    unknown  
     inputbuf 
    (@UserId bigint)select [t].[UserId], t.[DataColumn2], t1.[DataColumn1] from [UserData1] t1 full outer join [UserData2] t on t1.[UserId]=t.[UserId] where t.[UserId][email protected] or t1.[UserId][email protected] option (force order)  
    process id=process20fd055498 taskpriority=0 logused=456 waitresource=KEY: 17:72057594039107584 (e21762ccf3dc) waittime=4525 ownerId=3416764 transactionname=user_transaction lasttranstarted=2013-07-11T18:42:20.960 XDES=0x20eb13ed28 lockMode=X schedulerid=9 kpid=6024 status=suspended spid=72 sbid=0 ecid=0 priority=0 trancount=2 lastbatchstarted=2013-07-11T18:42:20.970 lastbatchcompleted=2013-07-11T18:42:20.970 lastattention=1900-01-01T00:00:00.970 clientapp=.Net SqlClient Data Provider hostname=hostname hostpid=27716 loginname=loginname isolationlevel=read committed (2) xactid=3416764 currentdb=17 lockTimeout=4294967295 clientoption1=671088672 clientoption2=128056 
     executionStack 
     frame procname=adhoc line=1 stmtstart=508 sqlhandle=0x02000000c0d74a32597ec460559a2d5dbdc92f7746cdce270000000000000000000000000000000000000000 
    update UserData2 set [LastModified]=getutcdate(), [DataColumn2]=[DataColumn2][email protected] where [UserId][email protected]  
     frame procname=unknown line=1 sqlhandle=0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 
    unknown  
     inputbuf 
    (@UserId bigint,@DataColumn2Increment int)update UserData2 set [LastModified]=getutcdate(), [DataColumn2]=[DataColumn2][email protected] where [UserId][email protected]  
    resource-list 
    keylock hobtid=72057594039173120 dbid=17 objectname=database_name.dbo.UserData1 indexname=1 id=lock20ec75b380 mode=X associatedObjectId=72057594039173120 
     owner-list 
     owner id=process20fd055498 mode=X 
     waiter-list 
     waiter id=process20fda2ccf8 mode=S requestType=wait 
    keylock hobtid=72057594039107584 dbid=17 objectname=database_name.dbo.UserData2 indexname=1 id=lock20ec07f600 mode=S associatedObjectId=72057594039107584 
     owner-list 
     owner id=process20fda2ccf8 mode=S 
     waiter-list 
     waiter id=process20fd055498 mode=X requestType=wait 

Najwyraźniej proces uruchomiony SELECT nabył zamek [UserData2] stole przed [UserData1], mimo FORCE ORDER podpowiedź.

Odpowiedz

0

Pierwszą rzeczą (myślę) jest to, że klauzula where w pierwszym zapytaniu jest zbędna. Masz to samo w Połączeniu i lepiej jest w złączeniu, ponieważ robisz pełne sprzężenie zewnętrzne.

Jeśli chodzi o unikanie zakleszczeń, to prawdopodobnie ma to związek z tym, jak traktowane jest pierwsze zapytanie w kwestii zrzucenia blokady odczytu. Jeśli aplikacja właśnie czyta dane i nie jest to częścią transakcji użytkownika, to po zakończeniu odczytu drugie zapytanie będzie mogło zostać ukończone, a użytkownik nie otrzyma zakleszczenia.

Tworzysz impas w swoim środowisku lub po prostu zgadujesz, że dostaniesz zakleszczenie. Jeśli publikujesz wykres zakleszczenia, abyśmy mogli zobaczyć, jaki jest rzeczywisty zamek.

+0

Tak, dostaję zakleszczenia. Zaktualizowałem to pytanie za pomocą śladu zakleszczenia. Wygląda na to, że proces z uruchomionym zapytaniem SELECT zatrzymuje się na pierwszej blokadzie ([UserData2]), próbując uzyskać drugą blokadę [UserData1], zamiast upuszczać pierwszą blokadę. –

1

Wybrany użytkownik nie powinien brać udziału w impasie, ponieważ powinien zdobyć tylko jedną blokadę na raz. Blokada może zostać zwolniona natychmiast po przeczytaniu zablokowanego wiersza.

Zdecydowanie zaleca się włączenie izolacji migawki. To rozwiąże problem.Zapoznaj się z trzema związanymi z tym kosztami: zwiększonym rozmiarem wiersza, tempdb i drobnym odczytem narzutów. Przez większość czasu nie mają one znaczenia.

+0

Zgadzam się, że izolacja migawki rozwiąże problem, a prawdopodobnie narzut będzie znikomy. Jestem ciekawy, czy można to rozwiązać innymi sposobami. Na przykład, czy można go ustawić tak, aby połączone stoły były blokowane pojedynczo? Z zapisu zakleszczenia (dodanego do treści pytania) wynika, że ​​blokada na jednej tabeli ([UserData2]) nie została zwolniona, podczas gdy SELECT próbuje zablokować drugą tabelę. –

+0

Blokady X nigdy nie są wydawane, więc gwarantowane jest działanie przywracania. Jestem bardziej zdziwiony, dlaczego selekcja zajmie więcej niż jedną blokadę w tym samym czasie? SQL Server zwykle blokuje wiersze, a nie tabele. Powinieneś to debugować. Czy inne transakcje są częścią wybranych transakcji? Dodaj "USTAWIĆ POZIOM IZOLACJI TRANSAKCJI CZYTAŁ ZAANGAŻOWANY". Możesz to naprawić poprzez 'TABLOCKX''ing tabel w aktualizacjach, ale to jest straszne dla współbieżności. Być może trzeba również zablokować X w tej samej kolejności w zapytaniu do wyboru. – usr

Powiązane problemy