Database concurrency ensures that when multiple operations are occurring at once, the final result is still in agreement — that they concur. This agreement typically depends on a set of rules and constraints that coordinate the behaviors of transactions, making sure that different operations will play nicely together.
Having considered the attributes of your transactions that you need to protect, the following sections consider the types of things that can happen if you let transactions have a free-for-all environment — one where all the different transactions don’t regard each other’s boundaries, where isolation is completely ignored.
The problems described in this three-part article only occur when multiple sessions are occurring at once in your database, including lost updates, dirty reads and non-repeatable reads.This is typical behavior, I’m sure, but it’s worth noting that in a single-session environment, these problems won’t happen.
You can run the examples in this article against the AdventureWorks2012 database.
A lost update occurs when two processes read the same data and then try to update the data with a different value. Consider a scenario in which you and your partner have the romantic notion of a joint bank account. On pay day, your respective employers both deposit your salaries into the joint account. To perform the update, each process reads the data. At the time of the payments, all is well in the world and you have an outstanding balance of $10,000. Each process therefore reads $10,000 as its starting point. Your employer attempts to update the $10,000 figure with your monthly salary of $2,000, but at the same time your partner’s employer updates the sum with his or her salary of $4,000. Your partner’s salary is added just before yours, updating the $10,000 balance to $14,000. Your payment then runs and updates the $10,000 balance to $12,000. A look at the ATM shows $12,000. The first update has been lost, and even worse, it represented the bigger update!
This situation is one that the SQL Server platform handles automatically, regardless of the isolation level. However, database developers can introduce this behavior themselves by performing an update in two steps, rather than one. Consider this example:
/* SESSION 1*/ USE AdventureWorks2012; DECLARE @SafetyStockLevel int = 0 ,@Uplift int = 5; BEGIN TRAN; SELECT @SafetyStockLevel = SafetyStockLevel FROM Production.Product WHERE ProductID = 1; SET @SafetyStockLevel = @SafetyStockLevel + @Uplift; WAITFOR DELAY '00:00:05.000'; UPDATE Production.Product SET SafetyStockLevel = @SafetyStockLevel WHERE ProductID = 1; SELECT SafetyStockLevel FROM Production.Product WHERE ProductID = 1;
Does it look OK? The developer has wrapped the read and the write in an explicit transaction, but all this scenario needs is for some concurrent activity and a lost update will occur. The WAITFORis only present to make it easier to detonate the code. In a separate session, have the following code ready:
/* SESSION 2*/ USE AdventureWorks2012; DECLARE @SafetyStockLevel int = 0 ,@Uplift int = 100; BEGIN TRAN; SELECT @SafetyStockLevel = SafetyStockLevel FROM Production.Product WHERE ProductID = 1; SET @SafetyStockLevel = @SafetyStockLevel + @Uplift; UPDATE Production.Product SET SafetyStockLevel = @SafetyStockLevel WHERE ProductID = 1; SELECT SafetyStockLevel FROM Production.Product WHERE ProductID = 1; COMMIT TRAN;
Now run Session 1; and then as soon as you have executed it, click over to Session 2 and execute that code. Session 2 should come back almost immediately showing that the transaction has raised the safety stock level from 1,000 to 1,100 (see Figure 1). If you return to Session 1, you should now be able to see that this transaction has also completed, except that the Safety Stock Level has gone from 1,000 to 1,005 (see Figure 2 ). The design of the transaction is flawed, causing an update to be lost.
What caused this loss? The developer wrote the transaction in such a way that both sessions are able to read the data and store the stock level in a variable. Consequently, when the update is made, both transactions start with the same value. This is a situation that should be avoided through more careful coding. Even raising the isolation level does not resolve this particular problem, which should be addressed by performing the addition as part of the update operation, as shown here:
UPDATE Production.Product SET SafetyStockLevel += @Uplift WHERE ProductID = 1;
We know you are all too smart to code your transactions in a way that could allow lost updates, but it does show what can happen when insufficient consideration is given to the transaction design. Interestingly, SQL Server enables the syntax to support this behavior using the NOLOCK hint, although it is largely ignored.