Как работает блокировка SQL Server в этом сценарии? - PullRequest
4 голосов
/ 03 ноября 2008

Считайте, что у меня есть транзакция:

BEGIN TRANSACTION
DECLARE MONEY @amount
SELECT Amount AS @amount
  FROM Deposits
  WHERE UserId = 123
UPDATE Deposits
  SET Amount = @amount + 100.0
  WHERE UserId = 123
COMMIT

И он выполняется на 2 потока, в порядке:

  1. Тема 1 - выберите
  2. нить 2 - выберите
  3. Тема 1 - обновление
  4. Тема 2 - обновление

Предположим, что перед выполнением Сумма равна 0.

Что произойдет в этом случае в различных настройках SQL Server (чтение без передачи, чтение с подтверждением, повторное чтение, сериализация), сколько будет в конце, будет ли тупик?

Ответы [ 5 ]

2 голосов
/ 03 ноября 2008

Хороший хорошо изложенный сценарий. Я решил проверить это.

Вот мой сценарий установки:

CREATE TABLE Deposits(Amount Money, UserID int)
INSERT INTO Deposits (Amount, UserID)
SELECT 0.0, 123
--Reset
UPDATE Deposits
SET Amount = 0.00
WHERE UserID = 123

Вот мой тестовый скрипт.

SET TRANSACTION ISOLATION LEVEL Serializable
----------------------------------------
-- Part 1
----------------------------------------
BEGIN TRANSACTION
DECLARE @amount MONEY
SET @amount =
(
SELECT Amount
FROM Deposits
WHERE UserId = 123
)
SELECT @amount as Amount
----------------------------------------
-- Part 2
----------------------------------------
DECLARE @amount MONEY
SET @amount =  *value from step 1*
UPDATE Deposits
SET Amount = @amount + 100.0
WHERE UserId = 123
COMMIT
SELECT *
FROM Deposits
WHERE UserID = 123

Я загрузил этот тестовый скрипт в два окна анализатора запросов и запустил каждую часть, как описано в вопросе.

Все чтение происходит перед любой записью, поэтому все потоки / сценарии будут читать значение 0 в @ amount.

Вот результаты:

Чтение совершено

1 T1.@Amount = 0.00
2 T1.@Amount = 0.00
3 Deposits.Amount = 100.00
4 Deposits.Amount = 100.00

Чтение незафиксировано

1 T1.@Amount = 0.00
2 T1.@Amount = 0.00
3 Deposits.Amount = 100.00
4 Deposits.Amount = 100.00

Повторяемое чтение

1 T1.@Amount = 0.00 (locks out changes by others on Deposit.UserID = 123)
2 T1.@Amount = 0.00 (locks out changes by others on Deposit.UserID = 123)
3 Hangs until step 4. (due to lock in step 2)
4 Deadlock!
Final result: Deposits.Amount = 100.00

Сериализуемый

1 T1.@Amount = 0.00 (locks out changes by others on Deposit)
2 T1.@Amount = 0.00 (locks out changes by others on Deposit)
3 Hangs until step 4. (due to lock in step 2)
4 Deadlock!
Final result: Deposits.Amount = 100.00

Вот объяснение каждого типа, которое можно использовать для достижения этих результатов посредством мысленного моделирования.

Read Committed и Read Uncommited , оба не блокируют данные, которые были прочитаны от изменений другими пользователями. Разница в том, что чтение без подтверждения позволит вам увидеть данные, которые еще не зафиксированы (недостаток), и не заблокирует чтение, если есть данные, заблокированные другими пользователями для чтения (вверх), что на самом деле говорит одно и то же дважды.

Повторяемое чтение и Сериализуемый , оба ведут себя как чтение, зафиксированное для чтения. Для блокировки оба блокируют данные, которые были прочитаны против модификации другими пользователями. Разница в том, что сериализуемые блоки больше, чем строка, которая была прочитана, а также блокирует вставки, которые вводят записи, которых раньше не было.

Таким образом, при повторяемом чтении вы можете увидеть новые записи (называемые фантомными записями) в последующих чтениях. С сериализуемым, вы блокируете создание этих записей, пока не сделаете коммит.

Приведенные выше объяснения получены из моей интерпретации этой msdn статьи.

2 голосов
/ 03 ноября 2008

Другие уже решили проблему использования REPEATABLE READ.

Так что я поделюсь другим советом ...

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

UPDATE Deposits
SET Amount = Amount + 100.0
WHERE UserId = 123

Кроме того, ваши реальные транзакции основаны на чем-то большем, чем UserID, верно? В противном случае вы рискуете работать с большим количеством записей, чем изначально планировали.

1 голос
/ 03 ноября 2008

В противном случае вы можете использовать подсказку о блокировке, чтобы избежать взаимоблокировок (в случае, если у вас есть сервер в режиме фиксации чтения):

BEGIN TRANSACTION
DECLARE MONEY @amount
SELECT Amount AS @amount
  FROM Deposits WITH(UPDLOCK)
  WHERE UserId = 123
UPDATE Deposits
  SET Amount = @amount + 100.0
  WHERE UserId = 123
COMMIT

В этой конкретной процедуре, конечно, предпочтительнее одно утверждение (например, опубликованное Кевином Фэйрчайлдом), которое не вызывает побочных эффектов, но в более сложных ситуациях подсказка UPDLOCK может оказаться полезной.

1 голос
/ 03 ноября 2008

Да, вы, вероятно, хотите повторить чтение.

Я бы, вероятно, справился с этим через оптимистическую блокировку, при которой вы обновляете, только если существующее значение совпадает с тем, которое было при чтении (проверка и установка). Если значение не совпадает, выведите ошибку. Это позволяет запускать операции чтения без фиксации, без блокировок и без повреждения данных.

BEGIN TRANSACTION
DECLARE MONEY @amount
SELECT Amount AS @amount
  FROM Deposits
  WHERE UserId = 123
UPDATE Deposits
  SET Amount = @amount + 100.0
  WHERE UserId = 123 AND Amount = @amount
IF @@ROWCOUNT <> 1 BEGIN ROLLBACK; RAISERROR(...) END
ELSE COMMIT END
0 голосов
/ 03 ноября 2008

Я полагаю, что вы захотите использовать Repeatable read, который будет блокировать записи, первый select получит значение, затем обновит блокирующий поток два до его завершения. Таким образом, конечный результат 200 в вашем примере

Чтение незафиксированным приведет к тому, что обе записи установят значение 100.

Зафиксированное чтение может иметь немного интересный результат, в зависимости от синхронизации двух потоков ....

Вот хорошая статья, которую я нашел о Repeatable Read, а также , которая является хорошим примером

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...