A deadlock occurs when two or more sessions permanently block each other
by each session having a lock on a resource that the other session(s) are trying to lock.
For example:
Transaction A acquires a shared lock on row 1.
Transaction B acquires a shared lock on row 2.
Transaction A now requests an exclusive lock on row 2,
and is blocked until transaction B finishes and releases the shared lock it has on row 2.
Transaction B now requests an exclusive lock on row 1,
and is blocked until transaction A finishes and releases the shared lock it has on row 1.
We can group the deadlocks into the following categories:
- Reader-Writer deadlocks
- Writer-Writer Deadlocks
- Key Lookup Deadlocks
- Parallelism-related deadlocks
- Range Scans and serializable deadlocks
- Partition escalation deadlocks
Now let`s see what we can do to minimize and/or completely prevent the deadlocks:1)
Access the shared objects/tables in the same order. Ensure that all the resources are accessed in the same order all the time.
For example, if two concurrent transactions both started by requesting
the lock for row 1 and later on requesting the lock for row 2.
This will simply be a blocking situation rather than a deadlock because
transaction 1 will never be deadlocking transaction 2 as resource locks
will never be held out of order.
2)
Reduce a query's lock footprint by making the query as efficient as possible. Make sure the involved queries use row lock and not page/table locks.
Try to read less data, avoid index scans.
3)
In case of Key Lookups deadlocks,
there are 2 possible solutions:
a) Include columns to the existing non-clustered index, which will eliminate the lookup.
b) In case the clustered index is small enough,
Use
FORCESCAN hint on the SELECT to force a clustered index scan
Key Lookup deadlocks are the most common ones, here is an example:
The SELECT uses the NC index to find a qualifying row(s)
While holding a Shared lock on the NC index, it needs to jump over
to the clustered index and retrieve some columns that aren't part of the NC index.
While it's doing this, the UPDATE is busy doing a seek on the CL index.
It finds a row, locks it and modifies it. But because one of the columns
being modified is a key column in the NC index, it then has to move to the NC index
and update that index, too. This requires a second X key lock on the NC index.
So, the SELECT ends up blocked waiting for the UPDATE to release his X lock
on the clustered index, while the UPDATE winds up blocked and waiting for the
SELECT to release his S lock on the NC index.
4)
Force the blocking to occur at an earlier point Sometimes the best way to prevent a deadlock is to force the blocking to occur
at an earlier point in one of the two transactions.
For example, if you force process A to be blocked by process B at the very
beginning of A’s transaction, it may not have a chance to acquire the lock
resource that later ends up blocking process B.
HOLDLOCK and
UPDLOCK hints can be useful for this.
5) Deadlock caused by parallelism
Happens on parallel query plan executions,
you will see "parallelism exchange event" in the deadlock graph/xml.
One of the possible solution here is to add MAXDOP 1 hint to force the queries go serial,
Another solution is to see if add an index can help to reduce the query cost
and make the query to be executed in a single thread.
6) Deadlock between two MERGE statements
Try to add HOLDLOCK hint to the MERGE statement,
If it doesn't help, try to rewrite the MERGE to INSERT/UPDATE statements
7) Use a lower isolation level
Check if the transaction can run at a lower isolation level and
use the minimum necessary transaction isolation level.
Under read committed, SQL Server will hold shared locks for a shorter duration than
a higher isolation level, such as repeatable read/serializable.
8) Use RCSI
Snapshot isolation uses row versioning, which does not use shared locks during read operations.
Snapshot isolation levels can minimize deadlocks that occur between read and write operations.
9)
Implement your own manual locking (sp_getapplock) If possible, consider to lock/unlock objects manually by yourself,
This can be done by using
sp_getapplock procedure.
Add sp_getapplock to the 2 involved stored procedures or queries
Something like below:
begin tran
sp_getapplock @Resource = 'MyTran', @LockMode = 'Exclusive'
... your code ...
commit
Only one client will be able to run this code at a time and it will be serialized.
Don't forget to use sp_releaseapplock