terça-feira, 15 de dezembro de 2015

Transaction Isolation Level - Como utilizar (SQL Server)

por Wagner Aparecido Miranda - W2R Consultoria e Sistemas 

Os Níveis de Isolamento (ou Transaction Isolation Level, do Inglês) não são apenas transações criadas para validar se um processamento gerou erro ou não. Vejo muita gente usando somente o BEGIN TRAN e COMMIT/ROLLBACK pensando que estão usando transações de forma correta, afinal, se der algum problema durante o processamento ele pode voltar com um ROLLBACK e tudo estará tudo como antes.
Bom, olhar somente por essa ótica pode estar errado, justamente pelo fato do Isolation Level ter causas/efeitos mais profundos do que somente garantir que a transação que está rodando no momento dê erro ou não.
Vale lembrar que o SQL Server é um SGBD que implementa o conceito de ACID(Atomicidade, Consistência, Isolamento e Durabilidade) e os níveis de isolamento atuam diretamente neste conceito. Outra coisa que é bom lembrar é que os níveis de isolamento impactam diretamente na quantidade de LOCKs e WAITs gerados no SQL.
Por padrão, o SQL Server tem o Isolation Level configurado para READ COMMITED. Isso pode ser confirmado rodando esta linha de código abaixo, e encontrando o resultado de ISOLATION LEVEL.
DBCC USEROPTIONS
Para exemplificar estas explicações, abra uma conexão com o SQL Server e execute o código abaixo. Ele irá criar um database e duas tabelas com alguns dados, onde serão explicados os tipos de isolamentos e de quebra um exemplo de dead lock.
CREATE DROP DATABASE dbNogare
GO

USE dbNogare
GO

CREATE TABLE tbIsolationLevel
(
    Id INT IDENTITY,
    Col1 varchar(10),
    Col2 varchar(10),
    Col3 varchar(10)
)
GO

INSERT INTO tbIsolationLevel(Col1,Col2,Col3)
VALUES ('AAAAA','BBBBB','CCCCC')
GO 5

CREATE TABLE tbDeadlock ( id INT)
GO

INSERT INTO tbDeadlock(ID) values (1),(2)
GO

Quando alteramos o nivel de isolamento, ele é aplicado à conexão que o alterou. Se você abrir uma nova conexão com o SQL Server e verificar qual é o nivel de isolamento desta transação, você verá que voltou para o padrão. Read Committed.
O READ COMMITTED nos garante uma leitura somente do que já está “comitado” no banco, isso é, garante que o dado que está sendo lido foi realmente escrito no banco e não é uma leitura suja ou fantasma, causado por alguma outra transação. Pelo fato dele só ler as informações realmente escritas no banco, se uma outra transação estiver trabalhando com a tabela que você quer ler, o SQL irá esperar a transação liberar a tabela para então fazer a leitura. Isso gera LOCK (pela outra transação) e WAIT (pela sua).
Para entender na prática esse tipo de isolamento, abra duas conexões diferentes com a tabela tbNogare e execute o primeiro bloco de código em uma delas, e o segundo bloco na segunda conexão. Após iniciar a execução do primeiro, vá até a segunda conexão e execute em simultâneo, deixando ambas rodando. Você irá ver o SQL Server terminar de executar a primeira transação para então iniciar a segunda.
/*********  RODAR NA CONEXÃO 1 *********/
BEGIN TRAN
    UPDATE tbIsolationLevel set Col1 = 'DDDDD' where id = 1
    WAITFOR DELAY '00:00:10'
ROLLBACK TRAN

/*********  RODAR NA CONEXÃO 2 *********/
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
SELECT * FROM tbIsolationLevel

O READ UNCOMMITTED evita que o SQL Server dê um LOCK na tabela de leitura por causa de uma transação, isso faz com que também não gere um WAIT em outra transação. Porém, este processo permite uma leitura suja dos dados, entregando a informação “errada” em alguns cenários. Quando se utiliza este nível de isolamento, é possível ler os dados de uma tabela mesmo ela sendo utilizada dentro de uma transação longa que executa vários processos (INSERTS, UPDATES e DELETES). Estes passos intermediários que são executados antes do COMMIT no final, podem ser lidos e retornados. Quando isso acontece, chamamos de leitura suja (Dirty Read, do inglês).
Veja os códigos abaixo, e execute cada bloco simultaneamente em uma conexão diferente.
/*********  RODAR NA CONEXÃO 1 *********/
BEGIN TRAN
    UPDATE tbIsolationLevel set Col1 = 'DDDDD' where id = 1
    WAITFOR DELAY '00:00:10'
ROLLBACK TRAN
No código abaixo existem duas consultas. Ambas podem ler dados sujos, a primeira alterando o isolation level e a segundo consumindo dados com o hint WITH (NOLOCK). No final, o resultado de ambas serão os mesmos.
/*********  RODAR NA CONEXÃO 2 *********/
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT * FROM tbIsolationLevel

-- Usando o hint NOLOCK
SELECT * FROM tbIsolationLevel with (nolock)
Já o REPEATABLE READ garante que a transação que leia uma tabela mais do que uma vez dentro desta mesma transação, possa fazer isso sem ler dados diferentes dos registros já encontrados da primeira vez. Nenhuma alteração (UPDATE) dos dados já lidos anteriormente serão modificados na leitura (são alterados na tabela), mas novos registros (INSERT) são retornados nesta segunda leitura.
Veja nos blocos de código abaixo este processo.
/*********  RODAR NA CONEXÃO 1 *********/
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN TRAN
    SELECT * FROM tbIsolationLevel
    WAITFOR DELAY '00:00:10'
    SELECT * FROM tbIsolationLevel
ROLLBACK TRAN
Veja o SQL Server gerando um WAIT para executar o UPDATE, mas não o gera quando roda o INSERT, ele simplesmente insere os dados.
/*********  RODAR NA CONEXÃO 2 *********/
UPDATE tbIsolationLevel set Col1 = 'DDDDD' where id = 1

INSERT INTO tbIsolationLevel(Col1,Col2,Col3)
VALUES ('DDDDD','EEEEE','FFFFF')
Uma variação mais completa do REPEATABLE READ é o SERIALIZABLE que bloqueia qualquer modificação de dados nas colunas que são consultadas, independente da modificação ser um UPDATE ou um INSERT. Este nível de isolamento causa um LOCKna transação original e um WAIT na segunda transação tanto para o UPDATE quanto para INSERT.
Veja este processo nos códigos abaixo:
/*********  RODAR NA CONEXÃO 1 *********/
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRAN
    SELECT * FROM tbIsolationLevel
    WAITFOR DELAY '00:00:10'
    SELECT * FROM tbIsolationLevel
ROLLBACK TRAN

/*********  RODAR NA CONEXÃO 2 *********/
UPDATE tbIsolationLevel set Col1 = 'DDDDD' where id = 1

INSERT INTO tbIsolationLevel(Col1,Col2,Col3)
VALUES ('DDDDD','EEEEE','FFFFF')

Uma alternativa para evitar LOCK e WAIT nas tabelas, e também garantir que os dados modificados sejam escritos e não tenha leitura suja é o SNAPSHOT. Ele permite tal ação porque ele copia os dados alterados para a tempdb, possibilitando ler os dados originais durante a transação, mesmo que eles sejam alterados. Quando uma transação tem um snapshot, ela coloca uma flag em todos os registros, para garantir que não foram alterados. Caso isso ocorra, ela altera a flag.
Veja na imagem abaixo a explicação deste processo.
Com o snapshot habilitado no banco, e setado na transação, ele lê os dados originais da tabela mesmo sofrendo modificações por outra transação.
Se os dados lidos não sofrerem nenhuma alteração, nada é escrito na tempDB.



Porém, se algum dado for modificado, o SQL Server continua lendo a tabela, mas a versão original do dado que foi modificado é copiado para a tempDB e na hora da leitura o SQL se encarrega de ler a tempDB e recuperar o dado original.

Esta funcionalidade, por padrão, vem desabilitada do banco de dados, e para utilizar, é necessário habilitar fazendo uma alteração no database. No código abaixo, é possível ver essa alteração.
USE MASTER
GO
ALTER DATABASE dbNogare
SET ALLOW_SNAPSHOT_ISOLATION ON
GO
USE dbNogare
GO
Depois de habilitar o Snapshot Isolation Level no banco de dados, é hora de ver os blocos de códigos que possibilitam este nivel de isolamento.
/*********  RODAR NA CONEXÃO 1 *********/
SET TRANSACTION ISOLATION LEVEL SNAPSHOT
BEGIN TRAN
    SELECT * FROM tbIsolationLevel
    WAITFOR DELAY '00:00:10'
    SELECT * FROM tbIsolationLevel
    UPDATE tbIsolationLevel set Col1 = 'XXXXX' where id = 2
    WAITFOR DELAY '00:00:10'
COMMIT TRAN

/*********  RODAR NA CONEXÃO 1 *********/
UPDATE tbIsolationLevel set Col1 = 'NNNNNN' where id = 1
SELECT * FROM tbIsolationLevel

Por fim, o DEAD LOCK também causa um pouco de confusão com sua execução. A causa de um DEAK LOCK é quando uma transação espera uma liberação para seguir em frente, mas a transação que está segurando o processo (causando LOCK) está esperando a primeira transação terminar de processar alguma coisa. A grosso modo, éA esperando o B terminar e o B esperando o A terminar. Eles ficariam um esperando o outro até alguém “desistir”. Para isso, o SQL Server escolhe uma vítima de acordo com o início do processo e o grau de severidade que aquele processo pode ter. Normalmente a transação que começou por ultimo é a vítima, se desligando do processo (o SQL faz isso automaticamente) e dá um rollback nas alterações. Isso faz com que a transação que ganhou o processo termine suas atividades.


Para ver isso através de código, execute os blocos abaixo.
/*********  RODAR NA CONEXÃO 1 *********/
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
GO
BEGIN TRAN
    UPDATE tbDeadlock
    SET id = 12
    WHERE id = 2
    WAITFOR DELAY '00:00:10'
    UPDATE tbDeadlock
    SET id = 11
    WHERE id = 1
ROLLBACK TRAN

/*********  RODAR NA CONEXÃO 2 *********/
BEGIN TRAN
    UPDATE tbDeadlock
    SET id = 11
    WHERE id = 1
    WAITFOR DELAY '00:00:10'
    UPDATE tbDeadlock
    SET id = 12
    WHERE id = 2
ROLLBACK TRAN
Quando ocorre o dead lock, a mensagem de erro é assim:

Msg 1205, Level 13, State 45, Line 2 Transaction (Process ID 54) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.

quinta-feira, 11 de dezembro de 2014

Apagar as conexões que ficam armazenadas no sql server management studio



por Wagner Aparecido Miranda - W2R Consultoria e Sistemas 



Quem nunca precisou editar uma conexão salva ou deletar algum usuário que fica armazenado ao abrir o SQL Management Studio.

Os dados, tanto de ip, nome de máquina quanto logins não ficam no Registro do Windows.
Eles são armazenados em no arquivo SqlStudio.bin que se encontra neste diretório:

C:\Users\USER\AppData\Roaming\Microsoft\Microsoft SQL Server\xxx\Tools\Shell
onde xxx é a versão do SQL: 90 para 2005 e 100 para 2008 e 2008R2

quinta-feira, 19 de dezembro de 2013

Monitorando bloqueios com o uso de Recursive Common Table Expressions

Nos últimos meses tenho presenciado diversas situações envolvendo bloqueios. Isso não somente no ambiente de produção do meu atual emprego, mas também em alguns clientes bem como eventuais dúvidas em sala de aula, fóruns e comunidades. A última vez foi bem recente e por conta de alguns bloqueios em conjunto com alguns BUGs de aplicação me deparei com uma base absurdamente crítica, uma quantidade enorme de bloqueios e algumas dezenas de milhões de reais envolvidas.
Já perdi a conta de quantas vezes vi ou ouvi falar de uma cadeia extensa de bloqueios em um ambiente crítico com tudo "parado". Os sintomas são bem clássicos, ou seja, CPU baixa, memória disponível e bastante lentidão. Algumas consultas na sysprocesses, sys.dm_exec_requests, sys.dm_os_waiting_tasks ou ainda as tradicionais sp_who2 e sp_lock são suficientes para diagnosticar vários bloqueios. O grande desafio é encontrar por onde começar. Em alguns ambientes, eliminar vários processos de uma vez ou reiniciar um servidor são algumas possibilidades, mas em ambientes de missão crítica resolver o problema ao invés de contorná-lo pode não ser uma opção, mas sim o único caminho. O que fazer para encontrar os verdadeiros culpados? Quais são os processos que estão "travando" tudo? Qual a "bendita" transação, usuário ou aplicação que está provocando os incidentes?
Desde o lançamento da versão 2005, o SQL Server melhorou consideravelmente as possibilidades de diagnóstico. No momento em que ele passou a disponibilizar as DMVs e "abrir" a "caixa preta" ficou muito fácil ser assertivo nos problemas e nas soluções. Outro excelente recursos foi o uso das Common Table Expressions (CTEs) em sua modalidade recursiva (Recursive CTEs). A combinação das CTEs recursivas com a natureza hierárquica das cadeias de bloqueios através das DMVs possibilita ser muito preciso em encontrar o "culpado" pela forte presença de bloqueios em um servidor SQL Server.
Um exemplo prático
Para poder simular uma contensão no ambiente utilizarei uma pequena estrutura e provocarei alguns bloqueios. O script abaixo cria cinco tabelas (T1 a T5) com apenas uma única coluna (cod) e preenche essas tabelas com números de 1 a 5.
– Muda o contexto de banco de dados
USE TempDB
– Cria uma tabela
CREATE TABLE T1 (COD TINYINT)
– Insere os dados em T1
INSERT INTO T1 VALUES (1)
INSERT INTO T1 VALUES (2)
INSERT INTO T1 VALUES (3)
INSERT INTO T1 VALUES (4)
INSERT INTO T1 VALUES (5)
– Cria réplicas
SELECT COD INTO T2 FROM T1
SELECT COD INTO T3 FROM T1
SELECT COD INTO T4 FROM T1
SELECT COD INTO T5 FROM T1
Agora que estão presentes 5 tabelas (ou melhor dizendo 5 recursos), é possível provocar uma cadeia de bloqueios entre processos que acessem esses recursos. O primeiro passo é provocar uma contensão por parte de uma conexão. A consulta abaixo (deve ser executada com o SQL Server Management Studio) bloqueará exclusivamente as tabelas recém criadas, pois, a transação ficará em aberto.
– Muda o contexto de banco de dados
USE TempDB;
– Muda o nível de isolamento (o mais rígido)
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRAN
    – Lê os dados da primeira tabela de forma exclusiva
    
SELECT COD FROM T1 WITH (XLOCK)
    – Atualiza os dados da segunda tabela
    
UPDATE T2 SET COD = COD + 1
    – Exclui os dados da terceira tabela
    
DELETE FROM T3
    – Exclui os dados da quarta tabela com o uso do truncate
    
TRUNCATE TABLE T4
    – Lê os dados da quinta tabela de forma compartilhada, mas mantendo o bloqueio
    
SELECT COD FROM T5 WITH (HOLDLOCK)
Um comando SELECT demonstra os vários bloqueios impostos por essa transação.
– Muda o contexto do Banco
USE TempDB;
SELECT
    
db_name(resource_database_id) As Banco, T.name As Tabela,
    request_mode 
As Modelo, request_session_id As Sessao
FROM
    
sys.dm_tran_locks As B
    
INNER JOIN sys.tables As T ON B.resource_associated_entity_id = T.object_id
WHERE
    resource_type = 
‘OBJECT’ AND resource_associated_entity_id > 10
ORDER BY Tabela
O resultado mostra alguns bloqueios sobre as tabelas da transação e mostra o SPID da mesma (no meu caso é 52)
Banco
Tabela
Modelo
Sessão
tempdb
T1
X
52
tempdb
T2
X
52
tempdb
T3
X
52
tempdb
T4
Sch-M
52
tempdb
T5
S
52
Uma vez que a sessão 52 (minha sessão no SSMS) esteja "segurando" os recursos, os acessos a esses recursos tendem a ser bloqueados já que a maioria dos bloqueios é exclusiva. O código abaixo em C# monta um pequeno aplicativo com 25 conexões ao banco com o intuito de acessar os recursos que estão bloqueados (T1 a T5).
using System;
using System.Collections.Generic;
using System.Threading;
using System.Data.Sql;
using System.Data.SqlClient;
namespace MB
{
    
class Program
    {
        
static void Main(string[] args)
        {
            
Program p = new Program();
            string[] strCmds = new string[25];
            strCmds[0]  = "UPDATE T1 SET COD = COD + 2";
            strCmds[1]  = 
"SELECT COD FROM T2 WHERE COD < 3";
            strCmds[2]  = 
"SELECT COD FROM T2 WHERE COD > 2";
            strCmds[3]  = 
"DELETE FROM T5";
            strCmds[4]  = 
"TRUNCATE TABLE T5";
            strCmds[5]  = 
"SELECT COD FROM T4";
            strCmds[6]  = 
"UPDATE T2 SET COD = 10 WHERE COD = 1";
            strCmds[7]  = 
"SELECT * FROM T1, T2, T3, T5";
            strCmds[8]  = 
"DELETE FROM T5 WHERE COD IN (SELECT COD FROM T3)";
            strCmds[9]  = 
"TRUNCATE TABLE T1";
            strCmds[10] = 
"SELECT * FROM T2, T4, T5";
            strCmds[11] = 
"DROP TABLE T1";
            strCmds[12] = 
"ALTER TABLE T2 ALTER COLUMN COD CHAR(3)";
            strCmds[13] = 
"UPDATE T3 SET COD = COD + 20";
            strCmds[14] = 
"DELETE FROM T2";
            strCmds[15] = 
"SELECT COD FROM T1 WHERE COD > 2";
            strCmds[16] = 
"INSERT INTO T1 VALUES (6)";
            strCmds[17] = 
"INSERT INTO T5 VALUES (7)";
            strCmds[18] = 
"INSERT INTO T5 VALUES (8)";
            strCmds[19] = 
"DELETE FROM T1";
            strCmds[20] = 
"DELETE FROM T2";
            strCmds[21] = 
"SELECT COD FROM T1 WITH (XLOCK)";
            strCmds[22] = 
"UPDATE T3 SET COD = 1 WHERE COD IN (SELECT COD FROM T4)";
            strCmds[23] = 
"SELECT COD FROM T1, T4";
            strCmds[24] = 
"INSERT INTO T3 VALUES (10)";
            Thread[] trds = new Thread[25];
            int i = 0;
            
string vApp = "";
            for (i = 0; i < 25; i++)
            {
                vApp = 
"App" + i.ToString("00");
                trds[i] = 
new Thread(delegate() { p.Manipula(strCmds[i], vApp); });
                trds[i].Start();
                
Thread.Sleep(500);
            }
        }
        public void Manipula(string strCmd, string strApp)
        {
            
try
            {
                
string strConn =
                    
"Server=localhost;Database=TempDb;Integrated Security=SSPI;" +
                    
"Connect Timeout=7200;Application Name=" + strApp + ";";
                SqlConnection cn = new SqlConnection(strConn);
                
SqlCommand cmd = cn.CreateCommand();
                cmd.CommandTimeout = 7200;
                cmd.CommandText = strCmd;
                cn.Open();
                cmd.ExecuteNonQuery();
            }
            
catch
            {
                
Console.WriteLine("Um processo foi eliminado");
            }
        }
    }
}
Como nem sempre o Visual Studio está disponível e é necessário compilar o código, se o SQL Server estiver instalado, então necessariamente o .NET Framework também estará presente juntamente com o compilador. Para compilar esse código, basta seguir as instruções:
§  Abra o Notepad
§  Cole o código
§  Salve o arquivo em algum local com a extensão .cs (Ex: H:\MB\MB.cs)
§  Abra um prompt de comando
§  Navegue até o diretório C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727 (ou superior se houver .NET Framework mais recente)
§  Digite csc /out:H:\MB\MB.exe H:\MB\MB.cs e pressione ENTER (pressupondo que o arquivo seja H:\MB\MB.cs)
O código possui uma matriz com 25 comandos que é executado a cada meio segundo. Como a transação do SQL Server Management Studio (ID 52) está bloqueando vários dos recursos, a cada meio segundo um bloqueio irá surgir. É possível verificar as conexões e os bloqueios utilizando a consulta abaixo:
– Verifica as sessões e os bloqueios
SELECT
    s.session_id 
As Sessao, program_name As Aplicacao,
    R.status 
As StatusRequisicao, blocking_session_id As SessaoBloqueadora
FROM sys.dm_exec_sessions As S
LEFT OUTER JOIN sys.dm_exec_requests As R ON S.session_id = R.session_id
WHERE S.session_id > 50 and S.session_id != @@spid 
A consulta acima mostra todas as sessões e suas respectivas requisições (se houver) conforme a tabela abaixo:
Sessão
Aplicação
Status da Requisição
Sessão Bloqueadora
51
Microsoft SQL Server Management Studio
NULL
NULL
52
Microsoft SQL Server Management Studio – Query
NULL
NULL
54
App00
suspended
52
55
App01
suspended
52
56
App02
suspended
55
57
App03
suspended
52
58
App04
suspended
57
59
App05
suspended
52
60
App06
suspended
55
61
App07
suspended
52
62
App08
suspended
57
63
App09
suspended
54
64
App10
suspended
59
65
App11
suspended
54
66
App12
suspended
55
67
App13
suspended
62
68
App14
suspended
55
69
App15
suspended
54
70
App16
suspended
54
71
App17
suspended
57
72
App18
suspended
57
73
App19
suspended
54
74
App20
suspended
55
75
App21
suspended
54
76
App22
suspended
59
77
App23
suspended
59
78
App24
suspended
62
Para que as sessões sejam mantidas exatamente com os mesmos IDs é importante que o SQL Server Management Studio possua apenas três conexões (uma para o object explorer, uma para a transação e uma para as consultas. As conexões devem ser abertas exatamente nessa ordem e não deve haver ninguém conectado).
Como pode ser observado, algumas sessões não possuem bloqueios enquanto outras encontram-se bloqueadas. A coluna "Sessão Bloqueadora" mostra o ID da sessão que está bloqueando uma outra sessão. Como pode ser observado, algumas sessões como a 52 e a 54 são responsáveis por bloquear várias requisições que por sua vez acabam bloqueandos outras e assim é montada uma cadeia de bloqueios. É possível por exemplo visualizar que a sessão 52 bloqueia a sessão 54 que bloqueia a sessão 73 por exemplo. Isso mostra uma cadeia em três níveis embora haja mais níveis na cadeia de bloqueios.
Em um cenário desses, os iniciantes optariam por parar o serviço ou ainda eliminar aleatoriamente algumas sessões até que um nível de estabilidade fosse obtido. Tais medidas podem não ser muito efetivas para resolver o problema. Parar o serviço por exemplo provocará uma indisponibilidade nas aplicações que dependem do SQL Server. Eliminar sessões de forma aleatória provocará a reversão de transações que podem significar graves problemas futuros (imagine um usuário que mandou efetivar uma aplicação financeira ter a sua requisição cancelada). Ainda que funcionem, evitar descobrir a causa real dos bloqueios pode fazer com eles voltem mesmo após reiniciar um serviço por exemplo. Tais soluções são ainda pouco factíveis em ambientes de missão crítica.
A medida mais eficiente é identificar a sessão que está no topo da cadeia, ou seja, qual a requisição que está provocando o bloqueio de outras sessões e assim sucessivamente. Uma primeira tentativa, é identificar o maior bloqueador, pois, tratar o maior bloqueador significa pode significar liberar uma grande quantidade de sessões. A consulta abaixo mostra a lista do maior bloqueador.
SELECT blocking_session_id As Sessao, COUNT(session_id) As TotalBloqueios
FROM sys.dm_exec_requests
WHERE blocking_session_id > 0
GROUP BY blocking_session_id
ORDER BY TotalBloqueios DESC
A lista abaixo mostra as quantidades de bloqueio:
Sessão
Total de Bloqueios
54
6
55
5
52
5
57
4
59
3
62
2
A ideia de eliminar o maior bloqueador é interessante, mas será que ela é "efetiva" ? Façamos um teste com os comandos abaixo:
KILL 54
Anteriormente haviam 25 bloqueios e a sessão 54 é responsável por 6 desses 25 bloqueios (mais de 20%). Considerando que existem 28 sessões (minhas 3 e 25 da aplicação) matar uma única sessão e liberar 20% dos bloqueios parece ser uma idéia bem interessante. Após eliminar esse processo e efetuar novamente a consulta dos maiores bloqueadores, é obtido o seguinte resultado:
Sessão
Total de Bloqueios
55
5
63
5
52
4
57
4
59
3
62
2
77
1
Ao invés de restarem apenas 19 bloqueios (6 a menos em relação aos 25), restaram 24 bloqueios. Isso significa que eliminar a sessão 54 e seus seis bloqueios reduziu a quantidade de bloqueios de 25 para 24 significando muito pouco em relação ao total de bloqueios. Isso ocorre porque embora a sessões 54 tenha sido eliminada, as sessões que estavam aguardando por ela agora estão bloqueadas por outras sessões. Seguindo ainda a mesma linha, os maiores bloqueadores são as sessões 55 e 63 empatando com 5 bloqueios cada totalizando 10 bloqueios. O comando abaixo elimina essas sessões:
KILL 55
KILL 63
Após eliminar as sessões 55 e 63, a relação dos maiores bloqueadores é exibida na tabela abaixo:
Sessão
Total de Bloqueios
52
4
56
4
57
4
65
4
59
3
62
2
77
1
No início da análise havia 28 sessões e 25 bloqueios. Eliminou-se as sessões que mantinham mais bloqueios e mesmo assim o número de bloqueios diminuiu lentamente. Eliminou-se 3 sessões e o número de bloqueios caiu de 25 para 22. Se o raciocínio for mantido poderá ser necessário eliminar muitos outros bloqueios para que um resultado visível possa ser obtido. Cada sessão eliminada é uma "esperança" de que as coisas voltem a funcionar, mas pode significar uma transação incompleta e boas "dores de cabeça".
A idéia de pesquisar os maiores bloqueadores é interessante, mas pode levar a interpretações equivocadas. O interessante não é procurar os maiores bloqueadores, pois, o resultado são quantos bloqueios "diretos" cada sessão provoca. A sessões 54 provocava 6 bloqueios, mas se considerarmos que a sessão 52 bloqueava a 54 seria visível que eliminar a sessão 52 é bem mais efetivo já que mais bloqueios (quer sejam diretos ou indiretos) seriam desfeitos. Se essa sessão bloqueasse somente a sessão 54 por exemplo, teríamos 5 bloqueios diretos (os que a sessão 52 bloqueia diretamente) e mais 6 bloqueios indiretos (os bloqueios impostos pela sessão 54) totalizando 9 bloqueios.
Esse é a grande dificuldade quando se depara com cadeias de bloqueios. Como identificar exatamente quais as sessões que são responsáveis pelo maior número de bloqueios (diretos ou indiretos), ou seja, quais sessões que estão realmente "travando" o banco ? Visualmente pode ser até fácil, mas em uma longa cadeia de bloqueios, esse processo pode ser muito trabalhoso se feito de forma visual. Um detalhe que pode ser muito útil é perceber que a cadeia de bloqueios, ou seja, a relação entre as sessões e suas bloqueadoras é hierárquica. Isso é uma excelente oportunidade para utilizar o recurso de CTEs recursivas. Antes de propriamente prosseguir, é interessante que o aplicativo MB seja fechado e reaberto. Isso fará com que os processos 54, 55 e 63 que foram eliminados sejam considerados novamente.
O uso das Common Table Expressions Recursivas (Recursive CTEs)
Não vou entrar nos detalhes das CTEs recursivas uma vez que há vários assuntos sobre elas na Internet além do que as mesmas são muito bem abordadas pelo colaborador Thiago Zavaschi em Entendendo as Common Table Expressions – CTE – Parte 1. Eu já abordei um exemplo de CTE recursiva emDicas e Truques sobre consultas complexas no SQL Server, mas as apresentações do Zavaschi sobre CTEs (tanto em seus artigos quanto no SQL Server Day) são certamente superiores.
Uma vez que exista uma relação hierárquica entre as sessões, é possível utilizar uma CTE recursiva para explorar essas relações. Aconsulta abaixo demonstra a quantidade real de bloqueios de cada sessão, ou seja, tanto os bloqueios que uma sessão pode provocar diretamente em outras, quanto bloqueios provocados indiretamente, ou seja, se A bloqueia B e B bloqueia C, indiretamente A bloqueia C.
;WITH Sessoes (Sessao, Bloqueadora) As (
    
SELECT Session_Id, Blocking_Session_Id
    
FROM sys.dm_exec_requests As R
    
WHERE blocking_session_id > 0
    
UNION ALL
    
SELECT Session_Id, CAST(0 As SMALLINT)
    
FROM sys.dm_exec_sessions As S
    
WHERE EXISTS (
        
SELECT * FROM sys.dm_exec_requests As R
        
WHERE S.Session_Id = R.Blocking_Session_Id)
    
AND NOT EXISTS (
        
SELECT * FROM sys.dm_exec_requests As R
        
WHERE S.Session_Id = R.Session_Id)
),
Bloqueios As (
    SELECT
        Sessao, Bloqueadora, Sessao As Ref, 1 As Nivel
    
FROM Sessoes
    
UNION ALL
    
SELECT S.Sessao, B.Sessao, B.Ref, Nivel + 1
    
FROM Bloqueios As B
    
INNER JOIN Sessoes As S ON B.Sessao = S.Bloqueadora)
SELECT Ref As Sessao,
    
COUNT(DISTINCT R.Session_Id) As BloqueiosDiretos,
    
COUNT(DISTINCT B.Sessao) – 1 As BloqueiosTotal,
    
COUNT(DISTINCT B.Sessao) – COUNT(DISTINCT R.Session_Id) – 1 AsBloqueiosIndiretos
FROM Bloqueios As B
    
INNER JOIN sys.dm_exec_requests As R
        
ON B.Ref = R.blocking_session_id
GROUP BY Ref
O resultado pode ser observado logo abaixo:
Sessão
Bloqueios Diretos
Bloqueios Total
Bloqueios Indiretos
52
5
25
20
54
6
6
0
55
5
5
0
57
4
4
0
59
3
3
0
62
2
2
0
Eliminar a sessão 54 significa liberar 6 bloqueios diretos. A sessão 52 bloqueia apenas 5 sessões diretamente, mas indiretamente bloqueia outras 20 já que as 5 sessões bloqueadas diretamente bloqueam outras sessões. A eliminação da sessão 52 libera outras 25 sessões e visivelmente é a medida mais efetiva para ajudar que os bloqueios sejam resolvidos.
Uma pequena mudança no código é capaz de mostrar os comandos executados por cada uma das sessões que impõe bloqueios sobre as outras.
;WITH Sessoes (Sessao, Bloqueadora) As (
    
SELECT Session_Id, Blocking_Session_Id
    
FROM sys.dm_exec_requests As R
    
WHERE blocking_session_id > 0
    
UNION ALL
    
SELECT Session_Id, CAST(0 As SMALLINT)
    
FROM sys.dm_exec_sessions As S
    
WHERE EXISTS (
        
SELECT * FROM sys.dm_exec_requests As R
        
WHERE S.Session_Id = R.Blocking_Session_Id)
    
AND NOT EXISTS (
        
SELECT * FROM sys.dm_exec_requests As R
        
WHERE S.Session_Id = R.Session_Id)
),
Bloqueios As (
    SELECT
        Sessao, Bloqueadora, Sessao As Ref, 1 As Nivel
    
FROM Sessoes
    
UNION ALL
    
SELECT S.Sessao, B.Sessao, B.Ref, Nivel + 1
    
FROM Bloqueios As B
    
INNER JOIN Sessoes As S ON B.Sessao = S.Bloqueadora)
SELECT Ref As Sessao,
    
COUNT(DISTINCT R.Session_Id) As BloqueiosDiretos,
    
COUNT(DISTINCT B.Sessao) – 1 As BloqueiosTotal,
    
COUNT(DISTINCT B.Sessao) – COUNT(DISTINCT R.Session_Id) – 1 AsBloqueiosIndiretos,
    (SELECT TEXT FROM sys.dm_exec_sql_text(
        (
SELECT most_recent_sql_handle FROM sys.dm_exec_connections
        
WHERE session_id = B.Ref))) As Comando
FROM Bloqueios As B
    
INNER JOIN sys.dm_exec_requests As R
        
ON B.Ref = R.blocking_session_id
GROUP BY Ref
O resultado pode ser observado logo abaixo:
Sessão
Bloqueios Diretos
Bloqueios Total
Bloqueios Indiretos
Comando
52
5
25
20
– Muda o contexto de banco de dados  USE TempDB;     — M
54
6
6
0
(@1 int)UPDATE [T1] set [COD] = [COD]+@1
55
5
5
0
(@1 tinyint)SELECT [COD] FROM [T2] WHERE [COD]<@1
57
4
4
0
DELETE FROM T5
59
3
3
0
SELECT COD FROM T4
62
2
2
0
DELETE FROM T5 WHERE COD IN (SELECT COD FROM T3)
Como era de se esperar, a sessão 52 (a aberta pelo SSMS) é a sessão que está no topo da cadeia de bloqueios. O interessante dessa última consulta é que não somente foi possível visualizar a quantidade de bloqueios aberta, mas principalmente o comando que proporcionou que a cadeia se formasse.
O uso das Recursive CTEs não se limita apenas a descobrir qual é a sessão que impõe mais bloqueios diretos e indiretos. Com o uso da CTE é possível exibir as cadeias de bloqueios e visualizar o que acontecerá em um primeiro momento quando a sessão 52 for eliminada. A consulta abaixo mostra as cadeias de bloqueio provocadas pela sessão 52 de forma indireta (exatamente 20 registros conforme a tabela acima).
;WITH Sessoes (Sessao, Bloqueadora) As (
    
SELECT Session_Id, Blocking_Session_Id
    
FROM sys.dm_exec_requests As R
    
WHERE blocking_session_id > 0
    
UNION ALL
    
SELECT Session_Id, CAST(0 As SMALLINT)
    
FROM sys.dm_exec_sessions As S
    
WHERE EXISTS (
        
SELECT * FROM sys.dm_exec_requests As R
        
WHERE S.Session_Id = R.Blocking_Session_Id)
    
AND NOT EXISTS (
        
SELECT * FROM sys.dm_exec_requests As R
        
WHERE S.Session_Id = R.Session_Id)
),
Bloqueios As (
    SELECT
        
CAST(Sessao As VARCHAR(200)) As Cadeia,
        Sessao, Bloqueadora, 1 
As Nivel
    
FROM Sessoes
    
UNION ALL
    
SELECT CAST(B.Cadeia + ‘ -> ‘ + CAST(S.Sessao As VARCHAR(5)) As VARCHAR(200)),
        S.Sessao, B.Sessao, Nivel + 1
    
FROM Bloqueios As B
    
INNER JOIN Sessoes As S ON B.Sessao = S.Bloqueadora)
SELECT Cadeia FROM Bloqueios
WHERE Nivel = (SELECT MAX(Nivel) FROM Bloqueios)
ORDER BY Cadeia
O resultado é obtido na tabela abaixo (a formatação foi proposital):
Linha
Cadeia
Linha
Cadeia
01
52 -> 54 -> 63
11
52 -> 55 -> 74
02
52 -> 54 -> 65
12
52 -> 57 -> 58
03
52 -> 54 -> 69
13
52 -> 57 -> 61
04
52 -> 54 -> 70
14
52 -> 57 -> 71
05
52 -> 54 -> 73
15
52 -> 57 -> 72
06
52 -> 54 -> 75
16
52 -> 59 -> 64
07
52 -> 55 -> 56
17
52 -> 59 -> 76
08
52 -> 55 -> 60
18
52 -> 59 -> 77
09
52 -> 55 -> 66
19
52 -> 62 -> 67
10
52 -> 55 -> 68
20
52 -> 62 -> 78
A exemplo da linha 01 pode-se ver claramente que a sessão 52 bloqueia a sessão 54 que por sua vez bloqueia a sessão 63. A tabela exibe os 20 bloqueios indiretos. Eliminar por exemplo a sessão 52, irá liberar a sessão 54 que irá liberar a sessão 63. A consulta mostra todas as liberações que irão ocorrer se a sessão 52 for eliminada. Não é necessário eliminá-la ainda.
As consultas anteriores juntamente com essa última mostram o poder que as CTEs podem realizar no monitoramento e resolução dos bloqueios já que esses são apresentados em uma relação hierárquica. Ainda assim, se o objetivo era apenas encontrar a sessão que mais promove bloqueios, há uma maneira bem mais direta. O raciocínio é bem simples. A sessão que forma a hierarquia será a primeira a efetuar um bloqueio. A sessão que é bloqueada primeiro irá naturalmente ser a que estará esperando por mais tempo, pois, à medida que ela é bloqueada é que os demais bloqueios se formam. A consulta abaixo demonstra esse raciocínio
SELECT TOP 10 Session_Id, Blocking_Session_Id
FROM sys.dm_exec_requests
WHERE Blocking_Session_Id > 0
ORDER BY Wait_Time DESC
A consulta exibe o seguinte resultado:
Session_Id
Blocking_Session_Id
54
52
55
52
56
55
57
52
58
57
59
52
60
55
61
57
62
52
63
54
A consulta retorna todas as requisições que não estão bloqueadas (blocking_session_id > 0) e as ordena pelo maior tempo de espera. A sessão 54 está bloqueada pela sessão 52 e é a sessão que espera a mais tempo. Isso é um forte indício de que a sessão 52 possui forte influência na cadeia de bloqueios. A resposta não é tão precisa quanto o uso das CTEs, mas normalmente essa alternativa é muito certeira por conta da lógica.
A idéia desse artigo é demonstrar como abordar longas cadeias de bloqueios em ambientes de produção. Normalmente quando o bloqueio é formado não há um aviso formal ao responsável pelo banco de dados. Isso é natural, pois, os bloqueios são necessários para garantir a integridade dos dados (principalmente na ótica pessimista presença na maioria dos bancos de dados). A demora no processo de resolução desses bloqueios (seja por conexões órfãs, transações longas, etc) pode formar um situação bem crítica. Um bloqueio de 10ms talvez passe despercebido, mas uma cadeia de bloqueios formada por várias sessões e imposta há alguns minutos provavelmente será descoberta e o DBA não terá tempo para tentar analisar qual a sessão que provoca tantos bloqueios. Abordei aqui as principais técnicas que costumo utilizar para encontrá-los.
Os códigos abordam um cenário muito simplista. Nem sempre há uma única cadeia de bloqueios. É possível que haja vários processos montado extensas cadeias. É possível também que as cadeias possuam mais de três níveis de aninhamento e assim por diante. Os códigos mostram o caminho das pedras, mas certamente podem ser otimizados. Espero que esse artigo sirva de subsídio para entender e lidar com os bloqueios quando eles incomodarem. Por fim, matar a sessão 52, irá fazer com que todas as sessões sejam liberadas e o SQL Server se resolva. Aos curiosos, vale a pena efetuar um KILL e conferir o resultado das consultas repassadas até então.