6 Ways of Doing Locking in .NET (Pessimistic and Optimistic)

Introduction

This article talks about 6 ways of locking in. NET. It starts with concurrency problems and then discusses 3 ways of doing optimistic locking. As optimistic locking does not solve the concurrency issues from roots, it introduces pessimistic locking. It then moves ahead to explain how isolation levels can help us implement pessimistic locking. Each isolation level is explained with a sample demonstration to make concepts clearer.

This is a small Ebook for all my .NET friends which covers topics like WCF, WPF, WWF, Ajax, Core .NET, SQL, Entity Framework, Design patterns, Agile, etc you can download the same from here, or else you can catch me on my daily free training @ from here

Why do we need locking?

In a multi-user environment, it's possible that multiple users can update the same record at the same time causing confusion between users. This issue is termed concurrency.

Multi-user environment

How can we solve concurrency problems?

Concurrency problems can be solved by implementing a proper "Locking strategy". Locks prevent action on a resource from being performed when some other resource is already performing some action on it.

What kind of confusion is caused by concurrency?

There are 4 kinds of major problems caused because of concurrency, the table below shows the details.

Problems Short description Explanation
Dirty reads "Dirty Read" occurs when one transaction is reading a record, which is part of a half-finished work of another transaction.
  • User A and user B see seeing value as "5".
  • User B changes the value "5" to "2".
  • User A is still seeing the value as "5" A dirty read has happened.
Unrepeatable read In every data read if you get different values then it's an "Unrepeatable Read" problem.
  • User A sees value as "5".
  • User B changes the value"5" to "2".
  • User A refreshes to see value "5", he is surprised....unrepeatable read has happened.
Phantom rows If "UPDATE" and "DELETE" SQL statements do not affect the data then it can be a "Phantom Rows" problem.
  • User A updates all values "5' to "2".
  • User B inserts a new record with the value "2".
  • User A selects all records with value "2' if all the values have changed, he is surprised to still find value "2" records.....Phantom rows have been inserted.
Lost updates "Lost Updates" are scenarios where one updates that is successfully written to the database are overwritten with other updates of other transactions.
  • User A updates all value-form "5" to "2".
  • User B comes and updates all "2" values to "5".
  • User A has lost all his updates.


So how can we solve the above problems?

By using optimistic or pessimistic locking, this article discusses the same.

Pessimistic locking

What is Optimistic locking?

Optimistic locking

As the name suggests "optimistic" assumes that multiple transactions will work without affecting each other. In other words, no locks are enforced while doing optimistic locking. The transaction just verifies that no other transaction has modified the data. In case of modification, the transaction is rolled back.

How does optimistic lock work?

You can implement optimistic locking in numerous ways but the fundamentals of implementing optimistic locking remain the same. It's a 5 step process as shown below.

  • Record the current timestamp.
  • Start changing the values.
  • Before updating check whether anyone else has changed the values by checking the old time stamp and new timestamp.
  • If it's not equal rollbacks or else commits.

Optimistic lock work

What are the different solutions by which we can implement optimistic locking?

There are 3 primary ways by which we can implement optimistic locking-in. NET.

  • Datasets: Datasets by default implement optimistic locking. They do a check of old values and new values before updating.
  • Timestamp Data type: Create a timestamp data type in your table and while updating check if the old timestamp is equal to the new timestamp.
  • Check old and new values: Fetch the values, make the changes and while doing the final updates check if the old value and current values in the database are equal. If they are not equal then rollback or else commit the values.

Solution number 1. Datasets

As said in the previous section, the dataset handles optimistic concurrency by itself. Below is a simple snapshot where we held the debug point on the Adapter's update function and then changed the value from the SQL Server. When we ran the "update" function by removing the breakpoint it threw a "Concurrency" exception error as shown below.

Datasets

If you run the profiler at the back end you can see it fires the update statement checking the current values and the old values are then the same.

EXEC sp_executesql N'
    UPDATE [tbl_items] 
    SET [AuthorName] = @p1 
    WHERE (
        [Id] = @p2 
        AND ((@p3 = 1 AND [ItemName] IS NULL) OR ([ItemName] = @p4))
        AND ((@p5 = 1 AND [Type] IS NULL) OR ([Type] = @p6))
        AND ((@p7 = 1 AND [AuthorName] IS NULL) OR ([AuthorName] = @p8))
        AND ((@p9 = 1 AND [Vendor] IS NULL) OR ([Vendor] = @p10))
    )',
    N'
    @p1 nvarchar(11), 
    @p2 int, 
    @p3 int, 
    @p4 nvarchar(4), 
    @p5 int, 
    @p6 int, 
    @p7 int, 
    @p8 nvarchar(18), 
    @p9 int, 
    @p10 nvarchar(2)
    ',
    @p1 = N'this is new',
    @p2 = 2,
    @p3 = 0,
    @p4 = N'1001',
    @p5 = 0,
    @p6 = 3,
    @p7 = 0,
    @p8 = N'This is Old Author',
    @p9 = 0,
    @p10 = N'kk'

In this scenario we were trying to change the field value "AuthorName" to "This is new" but while updating it makes a check with the old value "This is the old author". Below is the downsized code snippet of the above SQL which shows the comparison with the old value.

,@p8 = N'This is Old Author'

Solution number 2. Use timestamp data type

The other way of doing optimistic locking is by using the 'TimeStamp' data type of SQL Server. Timestamp automatically generates a unique binary number every time you update the SQL Server data. Timestamp data types are for versioning your record updates.

TimeStamp

To implement optimistic locking we first fetch the old 'TimeStamp' value and when we are trying to update we check if the old timestamp is equal to the current time stamp as shown in the below code snippet.

UPDATE tbl_items
SET itemname = @itemname
WHERE CurrentTimestamp = @OldTimeStamp

We then check if any updates have happened, in case updates have not happened then we raise a serious error '16' using SQL Server 'raise error' as shown in the below code snippet.

IF (@@ROWCOUNT = 0)
BEGIN
    RAISEERROR('Hello some else changed the value', 16, 10)
END

If any concurrency violation takes place you should see the error propagated when you call 'ExecuteNonQuery' to the client side as shown in the below figure.

ExecuteNonQuery

Solution number 3. Check old values and new values

Many times we would like to check concurrency on only certain fields and omit fields like identity etc. For that kind of scenario, we can check the old value and the new value of the updated fields as shown in the below code snippet.

UPDATE tbl_items 
SET itemname = @itemname 
WHERE itemname = @OldItemNameValue

But it looks like by using optimistic locking concurrency problems are not really solved.

Yes, you said right. By using optimistic locking you only detect the concurrency problem. To solve concurrency issues from the roots themselves we need to use pessimistic locking. Optimistic is like prevention while pessimistic locking is actually the cure.

What is pessimistic locking?

Pessimistic locking

Pessimistic locking assumes that concurrency/collision issues will happen so a lock is placed on the records and then data is updated.

How can we do pessimistic locking?

We can do pessimistic locking by specifying "IsolationLevel" in SQL Server stored procedures, ADO.NET level, or by using transaction scope object.

What kind of locks can be acquired by using pessimistic locking?

There are 4 kinds of locks you can acquire Shared, Exclusive, Update, and intent. The first two are actual locks while the other two are hybrid locks and markers.

  When to use? Reads Allowed Writes Allowed
Shared lock When you want only to read and you do not want any other transactions to do an update. Yes No
Exclusive When you want to modify data and you do not want anyone to read the transaction, neither you want anyone to update. No No
Update lock This is a hybrid lock. This lock is used when you want to do an update operation that passes through multiple phases before the actual update happens. It first starts with a shared lock in the read phase and then on the actual update, it acquires an exclusive lock.    
  Read phase Yes No
  Manipulating phase Yes No
  Update phase No No
Intent Lock ( Demand locks) Intent lock is for lock hierarchy. This lock is used when you want to lock resources down in the hierarchy. For example, a shared intent lock on a table means shared locks are placed on pages and rows with the table. Na Na
Schema locks When you are changing the table structure. No No
Bulk update locks Used when you are doing bulk updates Table level No Table level No


The update lock is confusing can you explain in detail?

The other locks are pretty straightforward; the updated lock is confusing because of its hybrid nature. Many times before we update we read the record. So during reading the lock is shared and while actually updating we would like to have an exclusive lock. Update locks are more of transient locks.

Update lock

So what are the different types of isolation levels and when should be used?

There are 4 kinds of transaction isolation levels, below is a simple table that shows when to use them and what locks they put.

Isolation Level Read Update Insert
Read Uncommitted Reads data that is yet not committed. Allowed Allowed
Read Committed ( Default) Reads data that is committed. Allowed Allowed
Repeatable Read Reads data that is committed. Not Allowed Allowed
Serializable Reads data that is committed. Not Allowed Not Allowed


How can we specify Isolation?

Isolation levels are features of RDBMS software, in other words, they fundamentally really belong to SQL Server and not to Ado.NET, EF, or LINQ. Said and done you can always set the transaction isolation level from any of these components.

RDBMS software

Middle tier

In the middle tier, you can specify the isolation level using the transaction scope object.

TransactionOptions TransOpt = new TransactionOptions();
TransOpt.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted;
using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required, TransOpt))
{
    // Code inside the transaction scope
}

ADO.NET

You can also specify the transaction isolation level using the "SqlTransaction" object in ADO.NET.

SqlTransaction objTransaction = objConnection.BeginTransaction(System.Data.IsolationLevel.Serializable);

SQL Server

You can also specify the isolation level in TSQL using the 'SET TRANSACTION ISOLATION LEVEL' as shown in the below code snippet.

SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

Which transaction isolation level solves which problems from Concurrency?

Below is a chart that shows which transaction isolation level solves which problems of concurrency.

  Read committed(S) Repeatable read(I) Serializable Read Uncommitted
Dirty reads Solves Solves Solves X
Lost updates X Solves Solves X
Non-repeatable reads X Solves Solves X
Phantom rows X X Solves X


Solution 4. Can we see how dirty reads are solved using Read Committed?

Some important key points about reading committed.

  • It's the default transaction isolation level for SQL Server.
  • It reads only committed data. In other words, any uncommitted data is not read and blocked until the commit happens. the Below figure explains the same in more detail. You can see the update

SQL Server

If you want to see the above things practically do the following.

  • Open 2 Query windows fire an update transaction but do not commit.
  • In the second window try firing the select query it will show a blocked query as shown in the figure below.

Open 2 Query windows

So is Read uncommitted the opposite of Read Committed?

Yes, read uncommitted is the opposite to read committed. When you set the transaction isolation level to read uncommitted, uncommitted data is also read.

Some important key points for reading committed.

  • Uncommitted is see so dirty read possible.
  • No locks held.
  • Useful when locking is not important and more important is concurrency and throughput.

If you want to test the same, fire the below SQL statement which is doing an update, and roll back. The rollback happens after 20 seconds halt. Within that time if you fire a select query you will get the uncommitted data and after 20 seconds you will see the old data this committed data is rolled back.

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

BEGIN TRAN;

UPDATE customer
SET CustomerName = 'Changed'
WHERE CustomerCode = '1001';

WAITFOR DELAY '000:00:20';

ROLLBACK TRAN;

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

SELECT * FROM Customer
WHERE CustomerCode = '1001';

Solution 5. Can we see how lost updates and non-repeatable reads are solved using repeatable reads?

By setting the isolation level to repeatable read no one can read and update the data. Some key points about repeatable read isolation levels are as follows.

  • Only committed data is read when the repeatable transaction isolation level is set for select queries.
  • When you select a record using repeatable read no one other transaction can update the record, but selects are possible.
  • If the repeatable transaction is set in an update query until the transaction finishes no one can read or update the same.
  • When the select and update query is set to repeatable read other transactions can insert new records. In other words, phantom rows are possible.

If you want to test this isolation level, fire the syntax below and try firing select and update queries they will be blocked and after 50 seconds you should see the data.

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

BEGIN TRAN;

UPDATE customer
SET CustomerName = 'Changed'
WHERE CustomerCode = '1001';

WAITFOR DELAY '000:00:50';

ROLLBACK TRAN;

If you fire the select query below in repeatable read mode you will not be able to update for 50 seconds until the transaction finishes.

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

BEGIN TRAN;

SELECT * FROM Customer WHERE CustomerCode = '1001';

WAITFOR DELAY '000:00:50';

COMMIT TRAN;

One important note you can add new records of customer code 1001, in other words, phantom rows are possible.

Solution 6. How are Phantom row problems addressed using Serializable Isolation level?

This is the highest level of isolation level; in this other transactions cannot update, select, and insert records. Some key points for the serializable transaction are,

  • No other transaction can be inserted, updated, deleted, or selected when the isolation level is serializable.
  • Lot of blockings but all concurrency issues are solved.
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

BEGIN TRAN;

SELECT * FROM Customer WHERE CustomerCode = '1001';

WAITFOR DELAY '000:00:50';

COMMIT TRAN;

In what scenarios should we use optimistic and pessimistic locking?

Working

What is a deadlock and how does a shared lock avoid the same?

Still working

What are Lock hints?

Still working