Na het lezen van allerlei info over IsolationLevels en verschillende soorten locks kom ik helaas nog niet uit mijn probleem. Het betreft hier een C#-applicatie die documenten (images+metadata) verwerkt door achter elkaar een reeks van Jobs uit te voeren. Deze applicatie gebruikt een MS SQL Server database om de data op te slaan, wat er ook voor zou moeten zorgen dat met meerdere clients tegelijk gewerkt kan worden. Dit laatste gaat echter bij hoge volumes mis, er beginnen dan deadlocks te ontstaan in SQL Server welke dan een van de queries/transacties afbreekt. Het probleem is op dit moment reproduceerbaar op SQL Server 2008 R2 maar treedt volgens mij ook op op 2005 en 2008 (2000 ondersteunt onze applicatie niet vanwege het gebruik van bepaalde taalconstructies die later geintroduceerd zijn).
Hier volgt wat data definitie, waarbij ik irrelevante kolommen zoveel mogelijk heb weggelaten. De tabel 'Document' representeert 1 document en bevat een verwijzing naar de laatste Job waarin deze verwerkt is en de volgende Job waarin deze verwerkt moet worden en een verwijzing naar een Sessie. Een Sessie is gekoppeld aan 1 client. Een client verwerkt steeds 1 Job tegelijk en verwerkt hierin achter elkaar alle Documenten die klaarstaan voor deze Job. Wanneer een client een Document wil verwerken doet hij dit door in de session_id kolom de id van zijn sessie te zetten. Als het Document klaar is in de betreffende Job wordt de session_id kolom door de client weer op NULL gezet zodat deze in een volgende Job weer door dezelfde of een andere client opgepakt kan worden. Vaak zijn Jobs erg snel klaar zodat er achter elkaar queries worden afgevuurd.
Naast deze tabel is er ook een DocumentStatistics tabel. Deze bevat informatie over aantallen verwerkte documenten. Deze wordt niet rechtstreeks aan een Job gekoppeld, maar aan de GUID van de Job, omdat Jobs weggegooid kunnen worden en vervangen door andere Jobs, maar we op dat moment de historische informatie wel graag willen bewaren.
Om deze tabel bij te werken wordt een stored procedure gebruikt. Deze bekijkt of er al een record is voor de huidige maand. Zo ja, dan wordt de counter hierin opgehoogd, zo niet dan wordt een nieuw record gemaakt.
De query om een Document aan de huidige client te koppelen is de volgende:
De query om een Document weer vrij te geven is de volgende:
De stored procedure en de beide getoonde queries worden uitgevoerd in een transactie met IsolationLevel 'ReadUncommitted'. Het idee hierachter was dat de performance hiervan het beste is omdat zo min mogelijk locks gebruikt worden. In de stored procedure en query 1 wordt vervolgens de hint 'WITH (UPDLOCK, ROWLOCK)' meegegeven om te zorgen dat dezelfde rij niet toch door 2 clients tegelijk gebruikt kan worden.
In het bovenstaande verhaal zitten helaas wat onvolkomenheden, want we lopen bij het processen met 5 clients tegelijk tegen deadlocks aan. Deze treden op in de volgende situaties, waarbij ik met SQL Server Profiler een log hiervan heb gemaakt en het bijbehorende schema erbij post:
1. Query 1 wordt door een client uitgevoerd en query 2 wordt tegelijk op een andere client uitgevoerd:

2. Query 1 wordt door twee clients tegelijk uitgevoerd:
3. De stored procedure wordt door twee clients tegelijk uitgevoerd:
3. De stored procedure wordt door vier clients tegelijk uitgevoerd?:
4.
De vraag is nu dus: hoe kom ik van de deadlocks af en houd/krijg ik een goede performance?
Hier volgt wat data definitie, waarbij ik irrelevante kolommen zoveel mogelijk heb weggelaten. De tabel 'Document' representeert 1 document en bevat een verwijzing naar de laatste Job waarin deze verwerkt is en de volgende Job waarin deze verwerkt moet worden en een verwijzing naar een Sessie. Een Sessie is gekoppeld aan 1 client. Een client verwerkt steeds 1 Job tegelijk en verwerkt hierin achter elkaar alle Documenten die klaarstaan voor deze Job. Wanneer een client een Document wil verwerken doet hij dit door in de session_id kolom de id van zijn sessie te zetten. Als het Document klaar is in de betreffende Job wordt de session_id kolom door de client weer op NULL gezet zodat deze in een volgende Job weer door dezelfde of een andere client opgepakt kan worden. Vaak zijn Jobs erg snel klaar zodat er achter elkaar queries worden afgevuurd.
MSSQL:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| CREATE TABLE [Document] ( [document_id] INTEGER NOT NULL IDENTITY CONSTRAINT PK__Document PRIMARY KEY NONCLUSTERED, [last_Job_id] INTEGER NOT NULL CONSTRAINT [FK__Document_last_Job_id] FOREIGN KEY REFERENCES [Job] ([job_id]), [next_Job_id] INTEGER NULL CONSTRAINT [FK__Document_next_Job_id] FOREIGN KEY REFERENCES [Job] ([job_id]), [session_id] INTEGER NULL CONSTRAINT [FK__Document_session_id] FOREIGN KEY REFERENCES [Session] ([session_id]) ON DELETE SET NULL) CREATE NONCLUSTERED INDEX [FK__Document_last_Job_id] ON [Document]([last_Job_id]) CREATE NONCLUSTERED INDEX [FK__Document_next_Job_id] ON [Document]([next_Job_id]) CREATE NONCLUSTERED INDEX [FK__Document_session_id] ON [Document]([session_id]) |
Naast deze tabel is er ook een DocumentStatistics tabel. Deze bevat informatie over aantallen verwerkte documenten. Deze wordt niet rechtstreeks aan een Job gekoppeld, maar aan de GUID van de Job, omdat Jobs weggegooid kunnen worden en vervangen door andere Jobs, maar we op dat moment de historische informatie wel graag willen bewaren.
MSSQL:
1
2
3
4
5
6
7
8
| CREATE TABLE [DocumentStatistics] ( [documentStatistics_id] INTEGER NOT NULL IDENTITY CONSTRAINT PK__DocumentStatistics PRIMARY KEY NONCLUSTERED, [job_s] [Ts] NOT NULL, [year_int] [Tint] NOT NULL, [month_int] [Tint] NOT NULL, [documentCount_int] [Tint] NOT NULL, [pageCount_int] [Tint] NOT NULL) |
Om deze tabel bij te werken wordt een stored procedure gebruikt. Deze bekijkt of er al een record is voor de huidige maand. Zo ja, dan wordt de counter hierin opgehoogd, zo niet dan wordt een nieuw record gemaakt.
MSSQL:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| CREATE PROCEDURE UpdateDocumentStatistics(@job_id int, @pageCount_int int) AS DECLARE @jobIdentifier_s varchar(255) SELECT @jobIdentifier_s = identifier_s FROM [Job] WHERE job_id = @job_id DECLARE @id int SELECT @id = documentStatistics_id FROM [DocumentStatistics] WITH (UPDLOCK, ROWLOCK) WHERE job_s = @jobIdentifier_s AND year_int = DATEPART(yyyy, GETDATE()) AND month_int = DATEPART(mm, GETDATE()) IF @id > 0 BEGIN UPDATE [DocumentStatistics] SET documentCount_int = documentCount_int + 1, pageCount_int = pageCount_int + @pageCount_int WHERE documentStatistics_id = @id END ELSE BEGIN INSERT INTO [DocumentStatistics] (job_s, year_int, month_int, documentCount_int, pageCount_int) VALUES (@jobIdentifier_s, DATEPART(yyyy, GETDATE()), DATEPART(mm, GETDATE()), 1, @pageCount_int) END |
De query om een Document aan de huidige client te koppelen is de volgende:
MSSQL:
1
2
3
4
5
6
7
8
9
10
| -- query 1 UPDATE [Document] SET session_id = @currentSession_id OUTPUT INSERTED.document_id WHERE document_id = (SELECT TOP 1 document_id FROM Document WITH (UPDLOCK, ROWLOCK) WHERE next_Job_id = @currentJob_id AND session_id IS NULL ORDER BY document_id ASC) |
De query om een Document weer vrij te geven is de volgende:
MSSQL:
1
2
3
4
| -- query 2 UPDATE Document SET last_job_id = @last_job_id, next_job_id = @next_job_id, session_id = NULL WHERE document_id = @document_id |
De stored procedure en de beide getoonde queries worden uitgevoerd in een transactie met IsolationLevel 'ReadUncommitted'. Het idee hierachter was dat de performance hiervan het beste is omdat zo min mogelijk locks gebruikt worden. In de stored procedure en query 1 wordt vervolgens de hint 'WITH (UPDLOCK, ROWLOCK)' meegegeven om te zorgen dat dezelfde rij niet toch door 2 clients tegelijk gebruikt kan worden.
In het bovenstaande verhaal zitten helaas wat onvolkomenheden, want we lopen bij het processen met 5 clients tegelijk tegen deadlocks aan. Deze treden op in de volgende situaties, waarbij ik met SQL Server Profiler een log hiervan heb gemaakt en het bijbehorende schema erbij post:
1. Query 1 wordt door een client uitgevoerd en query 2 wordt tegelijk op een andere client uitgevoerd:

2. Query 1 wordt door twee clients tegelijk uitgevoerd:
3. De stored procedure wordt door twee clients tegelijk uitgevoerd:
3. De stored procedure wordt door vier clients tegelijk uitgevoerd?:
4.
De vraag is nu dus: hoe kom ik van de deadlocks af en houd/krijg ik een goede performance?
- Is IsolationLevel 'ReadUncommitted' helemaal fout en moet ik bijvoorbeeld 'Serializable' gebruiken? Of is dat fataal voor de performance?
- Moeten we de stored procedure en de DocumentStatistics tabel dan maar zo aanpassen dat er gewoon een record per pagina ingeschoten wordt (dit worden er wel erg veel...) of is er een 'best practice' voor het uitvoeren van een 'update als er al een record is, anders insert je er een' query?
- Kan query 1 herschreven worden zodat er geen subquery nodig is? Oftewel een manier om te zeggen 'update maximaal 1 record dat aan bepaalde criteria voldoet'?