Locking and Blocking, Family Management.
For any parents out there of multiple children, you are acutely aware of locking and blocking in the real world. Who gets to use the bathroom first? Hearing the phrase “I saw that first”. I want that. Fighting over a toy. Each parent has their own methods of how to resolve these disputes.
Just as in a home with siblings, locking and blocking are absolutely a part of life in SQL Server and most RDBMS. Understanding how locking and blocking occurs and then minimizing the affects in accessing our precious information is a high priority.
Database Engines treat the data as children in this respect. Keeping the data as orderly as possible, without too much fighting.
First let’s take a look at some basic principles.
ACID
ACID is an acronym that stands for Atomicity Consistency Isolation Durability. This is a principle that Database professionals will look to when evaluating basics of a database. This basically means that the data can be trusted.
- Atomicity – A transaction is an entire unit of work. All or NONE of the transaction is committed.
- Consistency – All of the data is in a state of equilibrium. Indexes, materialized views and anything else affected by the transaction is consistent and in a zen state.
- Isolation – The idea that the data modifications are left alone until they are finished. Transactions are kept away from each other. Like two children being sent to their rooms to clean.
- Durability – The RDBMS keeps track of these changes and that after the transaction is complete, that data is recorded in the database permanently and can be recovered in the event of a system failure.
Isolation Levels
In SQL Server, and other RDBMS, the engine implements the configured isolation level which will affect the ACIDity when reading data. The isolation level will isolate the process and protect it from other transactions. To do this, locking is required. The locking is determined by the isolation level. Low levels of isolation will give high levels of concurrency and shared use of the data, but can result in lower levels of consistency with dirty or phantom reads.
SQL Server will use isolation levels to command the read behavior of the database/transaction.
- Read Uncommited
- Read data from other transactions yet completed
- Dirty Reads
- Uncommitted updates
- Read Committed
- SQL Server Default
- Prevents reading Data from uncommitted transactions
- Can result in Phantom Reads/Repeatable Reads
- Repeatable Read
- This will ensure that if data is reread within a transaction, that the data does not change
- No transaction can modify the data until the transaction reading the data is complete
- This comes at the cost that all shared locks in the read will hold until the transaction is completed
- This will eliminate Dirty and Non-Repeatable reads, but Phantom reads may still occur.
- Serializable
- This is putting the transactions in a single line.
- This is the most consistent and best way to ensure for ACIDity.
- A read/write lock will be issued for all of the records affected by the transaction
- This includes Repeatable Read isolation and will also eliminate Phantom reads.
- Snapshot
- This will read the data as it is at the beginning of the transaction.
- This will come at a high cost to tempdb as the data for the snapshot is stored in tempdb.
- This will eliminate Dirty, Phantom, Non-Repeatable Read and Lost updates
Lower isolation levels offer greater concurrency, meaning more connections may view the data, but the following concurrency affects may occur:
- Lost Updates – Transaction A changes a value, then commits. Transaction B then also changes that value and commits. Transaction A then reads the data after Transaction B commits. Transaction A is expecting the first value, but obtains the second
- Dirty Reads – Reading data when another process is changing the data and the original process is reading uncommitted data.
- Non-repeatable Reads – This occurs when a process reads the same data multiple times within a session. Between reads of the data, another process may change the data and therefore different values are returned.
- Phantom Reads – This is similar to Non-Repeatable reads, but instead of changing(updating) the data, another process adds or removes records between the reads of the same data during a process.
Locking Explained
Locking is a mechanism used by the SQL Server Database Engine to synchronize access by multiple users to the same piece of data at the same time. Locks are managed internally by a part of the Database Engine called the lock manager.
Each transaction will lock levels of resources such as rows, pages or tables for which the transaction is dependent. The transaction will release the lock when it either commits or rolls back. The amount of time the lock is held is dependent on the isolation level.
The two basic types of locks are read locks and write locks.
- Read Locks
- Shared –While a shared lock is being held other transactions can read but cannot modify locked data. The lock is released after the locked data has been read unless the isolation level is at or higher than Repeatable Read or a locking hint such as READCOMMITED or READCOMMITTEDLOCK is used.
- Concurrent – This is when you read data using read data held by exclusive lock only by specifying a NOLOCK hint or using a read uncommitted isolation level.
- Write Locks
- Update – Update locks are a mix of shared and exclusive locks. When an update statement is executed, SQL Server has to find the data it wants to modify first, so to avoid lock conversion deadlocks an update lock is used. Only one update lock can be held on the data at one time, similar to an exclusive lock. The update lock itself can’t modify the underlying data, when the data is modified, it is transferred to an exclusive lock.
- Exclusive – Exclusive locks are used to lock data being modified by one transaction thus preventing modifications by other concurrent transactions. A transaction always gets an exclusive lock on any data it modifies, and holds that lock until the transaction completes, regardless of the isolation level set for that transaction.
Lock Granularity/Escalation
Lock Escalations are an optimization technique used by SQL Server to control the amount of locks that are held within the Lock Manager of SQL Server.
Lock granularity consists of DATABASE, TABLE, PAGE and ROW locks. As soon as you connect to a SQL Server database, a shared lock is issued on the database level itself. This prevents another user from performing an action such as “DROP DATABASE” while there is a connection to it.
SQL Server will perform it’s lock granularity from top down. First, it will check if a table lock is necessary, then page then row. An Intent(Shared or Exclusive), dependent if the operation is read or write, is issued on the table and page and a shared lock on the row. If the amount of data necessary is more than a row and on a page. Once SQL Server needs more than 5,000 row locks, it will escalate(by default) to a table lock.
You can alter these defaults by either using query hints such as ROWLOCK, PAGLOCK or TABLOCK. You can also alter the lock escalation of each table by using the following:
ALTER TABLE MyTable SET LOCK_ESCALATION = <AUTO, TABLE, DISABLE>
This is generally not recommended as it is best to allow the Database Engine to escalate locks accordingly. Lower level locks increase concurrency, but consume more memory.
Fun Scripts
So, we now know that locking and blocking are part of the natural occurrence of processing transactions. When it comes to blocking the objective is to lessen the affects and hold the blocks as little time as possible. Below, you will find two scripts. The first will list was locking is occurring within your database. The second will list your current processes and list the ones blocking first. These may help in ascertaining what issues need to be addressed regarding blocking.
Current processes, showing blocking first
if object_id('tempdb..#tmpdm_exec_sessions') is not null drop table #tmpdm_exec_sessions if object_id('tempdb..#tmpdm_exec_requests') is not null drop table #tmpdm_exec_requests if object_id('tempdb..#tmpdm_exec_connections') is not null drop table #tmpdm_exec_connections if object_id('tempdb..#tmpsysprocesses') is not null drop table #tmpsysprocesses if object_id('tempdb..#tmpdm_os_waiting_tasks') is not null drop table #tmpdm_os_waiting_tasks select * into #tmpdm_exec_sessions from sys.dm_exec_sessions select * into #tmpdm_exec_requests from sys.dm_exec_requests select * into #tmpdm_exec_connections from sys.dm_exec_connections select * into #tmpsysprocesses from sys.sysprocesses select * into #tmpdm_os_waiting_tasks from sys.dm_os_waiting_tasks select distinct * from ( SELECT s.session_id AS spid , s.[status] , s.login_name AS loginName , s.[host_name] AS hostName , DB_NAME(s2.dbid) AS dbName , r.command , s.cpu_time AS cpuTime , s.reads + s.writes AS diskIO , s.last_request_end_time AS lastBatch , s.[program_name] AS programName , s.session_id , r.request_id , CASE s.transaction_isolation_level WHEN 0 THEN 'Unspecified' WHEN 1 THEN 'ReadUncommitted' WHEN 2 THEN 'ReadCommitted' WHEN 3 THEN 'Repeatable' WHEN 4 THEN 'Serializable' WHEN 5 THEN 'Snapshot' END AS transactionIsolationLevel , OBJECT_NAME(t.objectid) AS objectName , SUBSTRING (t.text,r.statement_start_offset/2, ( CASE WHEN r.statement_end_offset = -1 THEN LEN(CONVERT(NVARCHAR(MAX),t.text)) * 2 ELSE r.statement_end_offset END - r.statement_start_offset )/2 ) as IndividualQuery , t.[text] AS ParentQuery , w.blocking_session_id , r.percent_complete , r.estimated_completion_time , r.status as request_status , r.wait_type FROM #tmpdm_exec_sessions AS s LEFT JOIN #tmpdm_exec_requests AS r ON r.session_id = s.session_id LEFT JOIN #tmpdm_exec_connections AS c ON c.session_id = s.session_id CROSS APPLY sys.dm_exec_sql_text(c.most_recent_sql_handle) AS t inner join #tmpsysprocesses s2 on s2.spid = s.session_id LEFT join #tmpdm_os_waiting_tasks w on w.session_id = s.session_id WHERE s.is_user_process = 1 ) a where a.[status] != 'sleeping' order by a.blocking_session_id desc
Locks currently held within the database
set nocount on /***************************************************************************************************** ** *****************************************************************************************************/ declare @dm_tran_locks table ( SPID int , DBName varchar(256) , LockedResource nvarchar(60) , resource_associated_entity_id bigint , resource_database_id int , LockType nvarchar(60) ) declare @sys_partitions table ( hobt_id bigint , [object_id] int ) declare @sys_objects table ( object_id int , name varchar(256) ) declare @sys_dm_exec_sessions table ( session_id smallint , login_name nvarchar(128) , host_name nvarchar(128) ) declare @sys_dm_exec_connections table ( auth_scheme nvarchar(40) , session_id smallint , most_recent_sql_handle varbinary(64) ) /***************************************************************************************************** ** *****************************************************************************************************/ insert into @dm_tran_locks ( SPID , DBName , LockedResource , resource_associated_entity_id , resource_database_id , LockType ) select l.request_session_id , db_name(l.resource_database_id) , l.resource_type , l.resource_associated_entity_id , l.resource_database_id , l.request_mode from sys.dm_tran_locks l WHERE l.resource_database_id = db_id() insert into @sys_partitions ( hobt_id , [object_id] ) select p.hobt_id , p.object_id from sys.partitions p insert into @sys_objects ( object_id , name ) select object_id, name from sys.objects s insert into @sys_dm_exec_sessions ( session_id , login_name , host_name ) select c.session_id, c.login_name, c.host_name from sys.dm_exec_sessions c insert into @sys_dm_exec_connections ( auth_scheme , session_id , most_recent_sql_handle ) select c.auth_scheme, c.session_id, c.most_recent_sql_handle from sys.dm_exec_connections c /***************************************************************************************************** ** *****************************************************************************************************/ SELECT l.SPID , l.DBName , o.Name AS LockedObjectName , p.object_id AS LockedObjectId , l.LockedResource , l.LockType , ST.text AS SqlStatementText , ES.login_name AS LoginName , ES.host_name AS HostName , CN.auth_scheme as AuthenticationMethod FROM @dm_tran_locks l inner join @sys_partitions p on p.hobt_id = l.resource_associated_entity_id inner join @sys_objects o on o.object_id = p.object_id inner join @sys_dm_exec_sessions es on es.session_id = l.SPID inner join @sys_dm_exec_connections cn on cn.session_id = es.session_id CROSS APPLY sys.dm_exec_sql_text(cn.most_recent_sql_handle) st /***************************************************************************************************** ** *****************************************************************************************************/ SELECT l.SPID , o.Name AS LockedObjectName , l.LockedResource , l.LockType , count(1) as lock_count FROM @dm_tran_locks l inner join @sys_partitions p on p.hobt_id = l.resource_associated_entity_id inner join @sys_objects o on o.object_id = p.object_id group by l.SPID, o.name, l.LockedResource, l.LockType
Coming up: Deadlocks – Who gets the toy?
References:
http://www.sqlteam.com/article/introduction-to-locking-in-sql-server#sthash.ujDqLUPK.dpuf
https://technet.microsoft.com/en-us/library/jj856598(v=sql.110).aspx