Para este caso específico (rastreamento de switches do Load Balancer), desejamos otimizar um Upsert para que
- não apresenta nenhuma condição de corrida,
- causar qualquer violação PK, ou
- adquirir quaisquer bloqueios de tamanho excessivo.
Eu entendo que bloqueios maiores (página) podem ser mais eficientes, mas para fins de questão, o objetivo é mínimo (linha). Existem vários links sobre o assunto upsert/lock, mas as respostas são um tanto inconsistentes (esp. updlock e multi-statements ) e este caso particular envolve uma subconsulta incorporada.
Definição da tabela:
create table [User].[SessionWebServerLog] (
[SessionId] bigint not null,
[IsSSL] bit not null default ((0)),
[LastRequestUtc] datetime2(7) not null default (sysutcdatetime()),
[WebServerProcessInstanceId] bigint not null,
[RequestCount] int not null default ((1)),
[FirstRequestUtc] datetime2(7) not null default (sysutcdatetime()),
foreign key ([SessionId]) references [User].[Session] ( [SessionId] ) on delete cascade,
primary key clustered ([SessionId] asc, [IsSSL] asc, [LastRequestUtc] desc, [WebServerProcessInstanceId] asc)
with (
allow_row_locks = on,
allow_page_locks = off, -- Needed else page locks were taken
)
)
O SP deve inserir apenas se a combinação Session+IsSsl tiver alterado os IDs do servidor desde a solicitação mais recente para essa Session+IsSsl:
create proc [User].[usp_LogSessionWebServerRequest]
@pSessionId bigint,
@pWebServerProcessInstanceId bigint,
@pIsSsl bit, -- True for https, false for http
@pDebug bit = 0 -- debug flag for print statements
as
begin try
set xact_abort on;
begin transaction;
update l
set RequestCount = RequestCount + 1,
LastRequestUtc = sysutcdatetime()
from [User].SessionWebServerLog l
with (rowlock, xlock, serializable) -- row level, exclusively held, until end of xact
cross apply
(
select top(1) WebServerProcessInstanceId, LastRequestUtc
from [User].SessionWebServerLog
with (rowlock, xlock, serializable) -- row level, exclusively held, until end of xact
-- PK supports this join: SessionId, IsSsl, LastRequestUtc (desc), WebServerProcessId
where SessionId = @pSessionId
and IsSSL = @pIsSsl
order by LastRequestUtc desc
) prev -- previous request
where SessionId = @pSessionId
and IsSSL = @pIsSsl
and prev.WebServerProcessInstanceId = @pWebServerProcessInstanceId
and l.WebServerProcessInstanceId = @pWebServerProcessInstanceId
and l.LastRequestUtc = prev.LastRequestUtc;
if (@@rowcount = 0) -- if no update occurred, insert new
begin
insert into [user].SessionWebServerLog
( SessionId, WebServerProcessInstanceId, IsSSL )
values
( @pSessionId, @pWebServerProcessInstanceId, @pIsSsl );
end
commit;
end try
begin catch
if (xact_state() = -1 or @@trancount > 0)
rollback;
-- log, etc.
end catch
Essa rotina parece funcionar para casos simples, testando usando duas janelas e executando a primeira metade da transação dentro de cada janela e verificando o bloqueio.
Q1: Ele bloqueia quando a atualização não corresponde a nenhuma linha de nenhuma das janelas, mas ainda assim são chaves diferentes. O bloqueio ocorre porque o bloqueio do intervalo de chaves é mantido apenas nas chaves existentes ?
Vitória1:
declare
@pSessionId bigint = 3, -- does not exist in table
@pWebServerProcessInstanceId bigint = 100,
@pIsSsl bit = 0;
sp_lock 72:
spid dbid ObjId IndId Type Resource Mode Status
72 16 0 0 DB S GRANT
72 16 388964512 1 KEY (6c2787a590a2) RangeX-X GRANT
72 16 388964512 0 TAB IX GRANT
Vitória2:
declare
@pSessionId bigint = 4, -- does not exist in table
@pWebServerProcessInstanceId bigint = 100,
@pIsSsl bit = 0;
sp_lock 92:
spid dbid ObjId IndId Type Resource Mode Status
92 16 0 0 DB S GRANT
92 16 388964512 1 KEY (6c2787a590a2) RangeX-X WAIT
92 16 388964512 0 TAB IX GRANT
declare @pSessionId bigint = 4, @pWebServerProcessInstanceId bigint = 100,
@pIsSsl bit = 0;
Q2: Se eu permitir bloqueios de página (o padrão) no PK, por que um bloqueio de página é retirado mesmo que a dica de bloqueio de linha tenha sido especificada.
spid dbid ObjId IndId Type Resource Mode Status
72 16 0 0 DB S GRANT
72 16 388964512 1 PAG 1:444 IX GRANT
72 16 388964512 1 KEY (6c2787a590a2) RangeX-X GRANT
72 16 388964512 0 TAB IX GRANT
O nível de isolamento da transação é o padrão "leitura confirmada". Eu escolhi não alterar para este específico porque restaurá-lo parece mais confuso (para sucesso e falha e assumir/determinar o padrão) do que apenas usar bloqueios de tabela (imo).
Plano de consulta para o caso zero:
Plano de consulta quando existem várias linhas para WebSession+Ssl com datas diferentes (exatamente uma linha da ramificação para o topo, perfeita, aparentemente usando o PK por data):
Q3: Isso é um exagero - existem outras dicas que atingirão os objetivos? (Por favor, não reorganize a consulta ou tente converter em declaração de mesclagem para fins desta pergunta).
Q1: O bloqueio de intervalo provavelmente foi mantido porque
SessionId,IsSSL,LastRequestUtc
não foi declarado exclusivo. Isso está correto? Isso significa que o valor anterior e posterior ao que você está procurando deve ser bloqueado (pois você está solicitando um XLOCK na tabela) para evitar que o intervalo seja modificado durante a leitura. Se você declarasse a combinação única, acredito que esse problema deveria desaparecer. Isso também pode fazer com que o INSERT trave.Q2: Em primeiro lugar, um esclarecimento. O bloqueio de página NÃO é o padrão, o bloqueio de linha é (os bloqueios de página eram padrão no SQL Server 7.0). Dito isto, o servidor SQL ainda removerá um bloqueio de intenção Exclusivo (IX) na página. Isso não é um problema, pois esta fechadura é compatível com outras fechaduras IX. Na verdade, você pode tornar o bloqueio mais barato usando
ALLOW_PAGE_LOCKS = OFF
seus índices (isso tem efeitos colaterais se você fizer varreduras de tabela frequentes, portanto, tenha cuidado com isso).Q3 : Você não precisa forçar o rowlock ou forçar o xlock no CROSS APPLY. Você PODE precisar forçar a serialização se espera que várias instruções sejam inseridas na mesma sessão e intervalos. Caso contrário, você também pode se livrar disso (por exemplo, se cada insersor inserir apenas para a mesma sessão).