Eu montei o script a seguir para provar esse truque que usei nos últimos anos. Se você usá-lo, precisará modificá-lo para atender às suas finalidades. Seguem comentários:
/*
CREATE TABLE Item
(
Title varchar(255) not null
,Teaser varchar(255) not null
,ContentId varchar(30) not null
,RowLocked bit not null
)
UPDATE item
set RowLocked = 1
where ContentId = 'Test01'
*/
DECLARE
@Check varchar(30)
,@pContentID varchar(30)
,@pTitle varchar(255)
,@pTeaser varchar(255)
set @pContentID = 'Test01'
set @pTitle = 'TestingTitle'
set @pTeaser = 'TestingTeasier'
set @check = null
UPDATE dbo.Item
set
@Check = ContentId
,Title = @pTitle
,Teaser = @pTeaser
where ContentID = @pContentID
and RowLocked = 0
print isnull(@check, '<check is null>')
IF @Check is null
INSERT dbo.Item (ContentID, Title, Teaser, RowLocked)
values (@pContentID, @pTitle, @pTeaser, 0)
select * from Item
O truque aqui é que você pode definir valores em variáveis locais dentro de uma instrução Update. Acima, o valor "flag" é definido apenas se a atualização funcionar (ou seja, os critérios de atualização forem atendidos); caso contrário, ele não será alterado (aqui, deixado em null), você pode verificar isso e processar de acordo.
Quanto à transação e torná-la serializável, gostaria de saber mais sobre o que deve ser encapsulado dentro da transação antes de sugerir como proceder.
-- Adendos, continuação do segundo comentário abaixo -----------
As idéias do Sr. Saffron são uma maneira completa e sólida de implementar essa rotina, já que suas chaves primárias são definidas fora e passadas para o banco de dados (ou seja, você não está usando colunas de identidade - tudo bem para mim, elas são frequentemente usadas em excesso).
Fiz mais alguns testes (adicionei uma restrição de chave primária na coluna ContentId, envolva o UPDATE e INSERT em uma transação, adicionei a dica serializável à atualização) e sim, isso deve fazer tudo o que você deseja. A atualização com falha aplica um bloqueio de intervalo nessa parte do índice e isso bloqueará todas as tentativas simultâneas de inserir esse novo valor na coluna. Obviamente, se N solicitações forem enviadas simultaneamente, a "primeira" criará a linha e será atualizada imediatamente pela segunda, terceira etc. - a menos que você defina o "bloqueio" em algum lugar ao longo da linha. Bom truque!
(Observe que sem o índice na coluna de chave, você bloquearia a tabela inteira. Além disso, o bloqueio de intervalo pode bloquear as linhas em "ambos os lados" do novo valor - ou talvez não, eu não teste esse. Não deve importar, pois a duração da operação deve ser [?] em milissegundos de um dígito.)