Je vais essayer d'expliquer mon problème de la manière la plus détaillée possible, et j'apprécierais toute aide/suggestion. Mon problème concerne un blocage causé par deux requêtes (une insertion et une mise à jour). J'utilise MS-SQL server 2008
J'ai deux applications qui utilisent la même base de données :
- Application web (à chaque requête, plusieurs enregistrements sont insérés dans la table Impressions en appelant une procédure stockée)
- Service Windows (calcule toutes les impressions réalisées en une minute, chaque minute, pour la minute précédente et met un drapeau sur chacune des impressions calculées via une procédure stockée également).
L'application web insère les enregistrements d'impressions sans utiliser de transaction, tandis que l'application de service Windows calcule les impressions en utilisant une transaction. IsolationLevel.ReadUncommitted
transaction. La procédure stockée dans l'application de service Windows fait quelque chose comme ceci :
Procédure stockée du service Windows :
Passe en revue toutes les impressions qui ont l'attribut isCalculated
défini à false et date < @now , incrémente un compteur et d'autres données dans une autre table connectée à la table d'impressions, et définit l'attribut isCalculated
à true sur les impressions qui ont une date < @now. Parce que cette procédure stockée est assez grosse, pas la peine de la coller, voici un extrait de code raccourci de ce que fait la proc :
DECLARE @nowTime datetime = convert(datetime, @now, 21)
DECLARE dailyCursor CURSOR FOR
SELECT Daily.dailyId,
Daily.spentDaily,
Daily.impressionsCountCache ,
SUM(Impressions.amountCharged) as sumCharged,
COUNT(Impressions.impressionId) as countImpressions
FROM Daily INNER JOIN Impressions on Impressions.dailyId = Daily.dailyId
WHERE Impressions.isCharged=0 AND Impressions.showTime < @nowTime AND Daily.isActive = 1
GROUP BY Daily.dailyId, Daily.spentDaily, Daily.impressionsCountCache
OPEN dailyCursor
DECLARE @dailyId int,
@spentDaily decimal(18,6),
@impressionsCountCache int,
@sumCharged decimal(18,6),
@countImpressions int
FETCH NEXT FROM dailyCursor INTO @dailyId,@spentDaily, @impressionsCountCache, @sumCharged, @countImpressions
WHILE @@FETCH_STATUS = 0
BEGIN
UPDATE Daily
SET spentDaily= @spentDaily + @sumCharged,
impressionsCountCache = @impressionsCountCache + @countImpressions
WHERE dailyId = @dailyId
FETCH NEXT FROM dailyCursor INTO @dailyId,@spentDaily, @impressionsCountCache, @sumCharged, @countImpressions
END
CLOSE dailyCursor
DEALLOCATE dailyCursor
UPDATE Impressions
SET isCharged=1
WHERE showTime < @nowTime AND isCharged=0
Procédure stockée de l'application Web :
Cette procédure est assez simple, elle insère simplement l'enregistrement dans la table. Voici un extrait de code raccourci :
INSERT INTO Impressions
(dailyId, date, pageUrl,isCalculated) VALUES
(@dailyId, @date, @pageUrl, 0)
Le code
Le code qui appelle ces procédures stockées est assez simple : il crée simplement les commandes SQL en passant les paramètres nécessaires et les exécute.
//i send the date like this
string date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff",
CultureInfo.InvariantCulture);
SqlCommand comm = sql.StoredProcedureCommand("storedProcName",
parameters, values);
Je rencontre très souvent des blocages (les exceptions se produisent dans l'application web, pas dans le service Windows), et après avoir utilisé le SQL-Profiler, j'ai découvert que les blocages se produisent probablement à cause de ces deux requêtes (je n'ai pas beaucoup d'expérience dans l'analyse des données des profileurs).
Les dernières données de trace recueillies à partir du profileur du serveur SQL se trouvent au bas de cette question.
En théorie, ces deux procédures stockées devraient pouvoir fonctionner ensemble car la première insère les enregistrements un par un avec date=DateTime.Now, et la seconde calcule les Impressions qui ont une date < DateTime.Now.
Edit :
Voici le code exécuté dans l'application de service Windows :
SQL sql = new SQL();
DateTime endTime = DateTime.Now;
//our custom DAL class that opens a connection
sql.StartTransaction(IsolationLevel.ReadUncommitted);
try
{
List<string> properties = new List<string>() { "now" };
List<string> values = new List<string>() { endTime.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture) };
SqlCommand comm = sql.StoredProcedureCommannd("ChargeImpressions", properties, values);
comm.Transaction = sql.Transaction;
ok = sql.CheckExecute(comm);
}
catch (Exception up)
{
ok = false;
throw up;
}
finally
{
if (ok)
sql.CommitTransaction();
else
sql.RollbackTransactions();
CloseConn();
}
EDIT :
J'ai ajouté les index sur les deux tables comme suggéré par Martin Smith comme ceci :
CREATE NONCLUSTERED INDEX [IDX_Daily_DailyId] ON [dbo].[Daily]
(
[daily] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
y
CREATE NONCLUSTERED INDEX [IDX_Impressions_isCharged_showTime] ON [dbo].[Impressions]
(
[isCharged] ASC,
[showTime] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO
Pour l'instant, pas d'exception, je ferai un rapport plus tard
Edit :
Malheureusement, cela n'a pas permis de résoudre le problème de blocage. Je vais lancer une trace de deadlock dans le profiler pour voir si les deadlocks sont les mêmes qu'avant.
Edit :
J'ai collé la nouvelle trace (pour moi, elle ressemble à la précédente), je n'ai pas pu capturer un écran du plan d'exécution (il est trop grand) mais voici le xml du plan d'exécution Et voici une capture d'écran du plan d'exécution de la requête d'insertion :
<deadlock victim="process14e29e748">
<process-list>
<process id="process14e29e748" taskpriority="0" logused="952" waitresource="KEY: 6:72057594045071360 (f473d6a70892)" waittime="4549" ownerId="2507482845" transactionname="INSERT" lasttranstarted="2011-09-05T11:59:16.587" XDES="0x15bef83b0" lockMode="S" schedulerid="1" kpid="2116" status="suspended" spid="65" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2011-09-05T11:59:16.587" lastbatchcompleted="2011-09-05T11:59:16.587" clientapp=".Net SqlClient Data Provider" hostpid="2200" isolationlevel="snapshot (5)" xactid="2507482845" currentdb="6" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
<executionStack>
<frame procname="dbo.InsertImpression" line="27" stmtstart="2002" stmtend="2560" sqlhandle="0x03000600550e30512609e200529f00000100000000000000">
INSERT INTO Impressions
(dailyId, languageId, showTime, pageUrl, amountCharged, age, ipAddress, userAgent, portalId, isCharged,isCalculated) VALUES
(@dailyId, @languageId, @showTime, @pageUrl, @amountCharged, @age, @ip, @userAgent, @portalId, 0, 0) </frame>
</executionStack>
<inputbuf>
Proc [Database Id = 6 Object Id = 1362103893] </inputbuf>
</process>
<process id="process6c9dc8" taskpriority="0" logused="335684" waitresource="KEY: 6:72057594045464576 (5fcc21780b69)" waittime="4475" ownerId="2507482712" transactionname="transaction_name" lasttranstarted="2011-09-05T11:59:15.737" XDES="0x1772119b0" lockMode="U" schedulerid="2" kpid="3364" status="suspended" spid="88" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2011-09-05T11:59:15.737" lastbatchcompleted="2011-09-05T11:59:15.737" clientapp=".Net SqlClient Data Provider" hostpid="1436" isolationlevel="read uncommitted (1)" xactid="2507482712" currentdb="6" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
<executionStack>
<frame procname="dbo.ChargeImpressions" line="60" stmtstart="4906" stmtend="5178" sqlhandle="0x03000600e3c5474f0609e200529f00000100000000000000">
UPDATE Impressions
SET isCharged=1
WHERE showTime &lt; @nowTime AND isCharged=0
</frame>
</executionStack>
<inputbuf>
Proc [Database Id = 6 Object Id = 1330103779] </inputbuf>
</process>
</process-list>
<resource-list>
<keylock hobtid="72057594045071360" dbid="6" objectname="dbo.Daily" indexname="PK_Daily" id="lock14c6aab00" mode="X" associatedObjectId="72057594045071360">
<owner-list>
<owner id="process6c9dc8" mode="X"/>
</owner-list>
<waiter-list>
<waiter id="process14e29e748" mode="S" requestType="wait"/>
</waiter-list>
</keylock>
<keylock hobtid="72057594045464576" dbid="6" objectname="dbo.Impressions" indexname="IDX_Impressions_isCharged_showTime" id="lock14c901200" mode="X" associatedObjectId="72057594045464576">
<owner-list>
<owner id="process14e29e748" mode="X"/>
</owner-list>
<waiter-list>
<waiter id="process6c9dc8" mode="U" requestType="wait"/>
</waiter-list>
</keylock>
</resource-list>
</deadlock>
Edit :
Après des suggestions de Jonathan Dickinson :
- J'ai modifié la procédure stockée (j'ai supprimé le curseur),
- J'ai modifié l'IDX_Impressions_isCharged_showTime pour ne pas autoriser les PAGE_LOCKS et les
- J'ai ajouté -1 seconde à la propriété @now dans l'application du service Windows pour éviter les cas limites de blocage.
Mise à jour :
Le temps d'exécution des requêtes a diminué après les dernières modifications, mais pas le nombre d'exceptions.
J'espère que c'est la dernière mise à jour :
Les changements proposés par Martin Smith sont maintenant en ligne, la requête d'insertion utilise maintenant l'index non groupé et en théorie cela devrait résoudre le problème. Pour l'instant, aucune exception n'a été signalée (je croise les doigts).