Долгосрочная пессимистическая блокировка на уровне приложения в SQL Server - PullRequest
0 голосов
/ 02 мая 2018

У меня есть ресурс (например, «документ»), хранящийся в таблице SQL Server.

Пользователи должны иметь возможность получить блокировку уровня приложения (а не блокировку базы данных) для этого ресурса и удерживать эту блокировку потенциально навсегда.

Я разработал стратегию, чтобы справиться с этим, но я не уверен, что она правильная, и был бы признателен за совет.

В этом конкретном приложении количество пользователей и ресурсов будет очень низким, поэтому не будет большого количества споров. Но все равно было бы неплохо узнать наиболее подходящий или «правильный» способ справиться с этой ситуацией.

Таблица для записи блокировок достаточно проста:

CREATE TABLE [dbo].[ResourceLock]
(
    [Id]            [int]           NOT NULL IDENTITY(1,1),
    [ResourceId]    [int]           NOT NULL,
    [UserId]        [int]           NOT NULL,
    [Created]       [datetime2](7)  NOT NULL,
    CONSTRAINT [PK_OrderLock] PRIMARY KEY CLUSTERED
    (
        [Id] ASC
    )
)

А затем запрос на попытку снять блокировку ресурса:

--Incoming Parameters 
DECLARE @ResourceId INT;
DECLARE @UserId INT;

--Query Variables
DECLARE @LockId INT;
DECLARE @InsertedIds TABLE (Id INT);

BEGIN TRANSACTION;

SELECT  @LockId = [ResourceLock].[Id]
--TABLOCKX the table so it can't be accessed until after the transaction is complete.
FROM    [ResourceLock] WITH (TABLOCKX)
WHERE   [ResourceLock].[ResourceId] = @ResourceId;

IF @LockId IS NULL
BEGIN
    INSERT INTO [ResourceLock]
    (
        [ResourceId],
        [UserId],
        [Created]
    )
    OUTPUT inserted.Id INTO @InsertedIds(Id)
    VALUES
    (
        @ResourceId,
        @UserId,
        GETUTCDATE()
    );

    SELECT  @LockID = Id
    FROM    @InsertedIds;
END

SELECT  [ResourceLock].*
FROM    [ResourceLock]
WHERE   [ResourceLock].[Id] = @LockId;

COMMIT TRANSACTION;

Сначала проверяется, существует ли блокировка ресурса. В рамках этой проверки SELECT указывает TABLOCKX, запрещающий другому экземпляру этого запроса или любому другому запросу доступ к этой таблице, пока эта транзакция не будет завершена.

Если не существует существующей блокировки, создается новая блокировка для ресурса для пользователя.

Наконец, он возвращает существующую или вновь созданную блокировку.

Затем приложение просто сравнивает запрашивающего пользователя с пользователем, удерживающим блокировку, и сообщает пользователю, получил ли он блокировку, или блокировка удержана кем-то еще.

Когда другие запросы пытаются записать ресурс, они будут использовать такую ​​проверку:

--Incoming Parameters 
DECLARE @ResourceId INT;
DECLARE @UserId INT;

IF EXISTS
(
    SELECT  [ResourceLock].*
    FROM    [ResourceLock]
    WHERE   [ResourceLock].[ResourceId] = @ResourceId
    AND     [ResourceLock].[UserId] = @UserId
)
BEGIN
    --UPDATE the resource
END

Мое главное беспокойство связано с использованием TABLOCKX, так как это очень тяжелые руки. Это похоже на то, что может быть достигнуто с другим подходом, который был бы более эффективным, но я не смог найти ничего подобного при поиске.

Я думаю, что мои попытки поиска были затруднены тем фактом, что у меня здесь происходит два типа блокировки. Блокировка в SQL Server и блокировка ресурсов приложения.

Ответы [ 2 ]

0 голосов
/ 02 мая 2018

Другим способом является просмотр самого @ResourceId в качестве идентификатора виртуального ресурса, который нужно «заблокировать», чтобы вы могли свободно управлять всем, что нужно DML для записи этой долгосрочной пессимистической блокировки в ваши операционные столы. Однако в такой ситуации лучше избегать обычной блокировки / блокировки / и т. Д., Так как они не очень масштабируемы, и вы можете ввести взаимоблокировку и другие проблемы.

Вы можете использовать встроенный в SQL Server Lock Manager через sp_getapplock, чтобы временно заблокировать «ресурс», чтобы вы могли работать с этим ресурсом в течение некоторого времени. Однако на самом деле лучше не физически блокировать строки данных в таблицах, если вы можете помочь. Как вы увидите ниже, sp_getapplock может использоваться в качестве «шлюза», и если вы можете «заблокировать» виртуальный ресурс, теперь вы «в», и вы можете работать В СФЕРЕ ЭТОГО РЕСУРСА без фактической блокировки / блокировки / удержания на базовых столах. Блокировки на базовых таблицах будут не только удерживать блокировки строк, но и блокировки объектов и схем. Не хорошо.

Использование sp_getapplock позволит вам иметь собственные мьютексы, если хотите, и фактически не будет блокировать пользовательские таблицы, особенно с помощью тупых инструментов, таких как TABLOCKX. Конечно, эта стратегия зависит от вас и вашего приложения, которые ведут себя и используют sp_getapplock и sp_releaseapplock с помощью некоторой пользовательской семантики блокировки, но вы все равно уже хотите это сделать.

--Construct unique name for 'resource'      
DECLARE @resourceName nvarchar(255) = 'RESOURCEID:' + CAST(@ResourceId AS NVARCHAR(10))

DECLARE @retVal INT = 0;

EXEC @retVal = sp_getapplock
  @Resource = @resourceName
  ,  @LockMode = 'Exclusive'
  ,  @LockOwner = 'Session' 
  ,  @LockTimeout = 10000;

IF @retVal <= 1
  BEGIN
    /*
     Do whatever you want here, including logging into your "long-term" lock 
     table, etc. You now have a pessimistic short-term lock on @resourceName, 
     and you can act with impunity in any related tables while you hold this 
    "lock", but you are not actually locking the database resources such as 
    your resource locking/logging table.
    */
  END

EXEC sp_releaseapplock
  @Resource = @resourceName
  @LockOwner = 'Session';

Кроме того, если ваше приложение аварийно завершает работу, отключается или не может вызвать sp_releaseapplock, эта блокировка на уровне сеанса автоматически снимается диспетчером блокировок при завершении сеанса.

0 голосов
/ 02 мая 2018

Я не уверен, что ваше решение будет работать без HOLDLOCK. Вы проверяли это?

Я думаю, что может сделать это с UPLOCK, HOLDLOCK, ROWLOCK

Вы можете сократить его, и вам не нужна явная транзакция

INSERT INTO [ResourceLock] ([ResourceId], [UserId], [Created]) 
select @ResourceId, @UserId, GETUTCDATE()  
where not exists ( select 1 
                     from [ResourceLock] with (UPLOCK, HOLDLOCK, ROWLOCK) 
                    where [ResourceId] = @ResourceId
                  )
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...