Предполагая, что параметр изоляции транзакции зафиксирован на прочтении моментального снимка, является ли следующий оператор «атомарным» в том смысле, что вы никогда не «потеряете» параллельное приращение?
update mytable set counter = counter + 1
Я бы предположил, что в общем случае, когда этот оператор обновления является частью более крупной транзакции, этого не будет. Например, я думаю, что возможен такой сценарий:
В этой ситуации не будет ли счетчик увеличиваться только на 1? Имеет ли значение, если это единственное утверждение в транзакции?
Как такой сайт, как stackoverflow, обрабатывает это для своего счетчика просмотров вопросов? Или возможность «потерять» некоторые приращения просто считается приемлемой?


Нет, это не так. Значение читается в совместно используемом режиме, а затем обновляется в монопольном режиме, поэтому может произойти несколько чтений.
Либо используйте Serializable level, либо используйте что-то вроде
update t
set counter = counter+1
from t with(updlock, <some other hints maybe>)
where foo = bar
Read Committed Snapshot имеет дело только с блокировками при выборе данных из таблиц.
Однако в t1 и t2 вы ОБНОВЛЯЕТЕ данные, что представляет собой другой сценарий.
Когда вы ОБНОВЛЯЕТЕ счетчик, вы переходите к блокировке записи (в строке), предотвращая выполнение другого обновления. t2 может читать, но t2 будет блокировать свое ОБНОВЛЕНИЕ до тех пор, пока t1 не будет выполнено, а t2 не сможет выполнить фиксацию до t1 (что противоречит вашей временной шкале). Только одна из транзакций сможет обновить счетчик, поэтому обе будут обновлять счетчик правильно с учетом представленного кода. (проверено)
Read Committed просто означает, что вы можете читать только зафиксированные значения, но это не значит, что у вас есть Repeatable Reads. Таким образом, если вы используете и зависите от переменной счетчика и намереваетесь обновить ее позже, возможно, вы выполняете транзакции с неправильным уровнем изоляции.
Вы можете либо использовать повторяемую блокировку чтения, либо, если вы будете обновлять счетчик только изредка, вы можете сделать это самостоятельно, используя технику оптимистической блокировки. например столбец отметки времени с таблицей счетчиков или условное обновление.
DECLARE @CounterInitialValue INT
DECLARE @NewCounterValue INT
SELECT @CounterInitialValue = SELECT counter FROM MyTable WHERE MyID = 1234
-- do stuff with the counter value
UPDATE MyTable
SET counter = counter + 1
WHERE
MyID = 1234
AND
counter = @CounterInitialValue -- prevents the update if counter changed.
-- the value of counter must not change in this scenario.
-- so we rollback if the update affected no rows
IF( @@ROWCOUNT = 0 )
ROLLBACK
Эта статья devx является информативной, хотя в ней рассказывается о функциях, которые были еще на стадии бета-тестирования, поэтому она может быть не совсем точной.
update: Как указывает Джастис, если t2 является вложенной транзакцией в t1, семантика будет другой. Опять же, оба будут правильно обновлять счетчик (+2), потому что с точки зрения t2 внутри t1 счетчик уже был обновлен один раз. Вложенный t2 не имеет доступа к тому, какой счетчик был до того, как t1 обновил его.
Во вложенной транзакции, если t1 выдает ROLLBACK после t1 COMMIT, счетчик возвращается к исходному значению, потому что он также отменяет фиксацию t2.
@Travis В начальном примере есть 2 запроса в разных транзакциях, обновляющих значение (+1) в одном операторе обновления (например, без его чтения перед обновлением). N запросов, выполняющих одно и то же обновление counter=counter+1, будут заблокированы, и обновления будут выполняться последовательно, поскольку оператор обновления уже увеличивает блокировку. Существуют подсказки по блокировке и другие механизмы, если они вам потребуются. например T1, выдающий SELECT counter FROM MyTable WITH(updlock) WHERE MyId = 1234, увеличивает блокировку и предотвращает чтение T2 до тех пор, пока T1 не будет зафиксирован / откат.
@ Трэвис прав. Хотя Пользователь не читает его перед обновлением, база данных является читает его перед обновлением. Одна часть плана выполнения уходит на чтение текущего значения, затем увеличивает значение в памяти, затем записывает новое значение. Даже добавление WITH(HOLDLOCK) к обновлению означает, что он удерживает эту первоначальную блокировку читать, позволяя другому процессу также читать текущее значение. Вы проверяете возникшую в результате отсутствие атомарности, имея две партии в цикле while, постоянно выполняя UPDATE MyTAble SET Counter=Counter+1.
@IanBoyd Думаю, ты ошибаешься. Я не думаю, что когда-либо видел этот провал при использовании транзакции. Как бы то ни было, я протестировал его с весьма спорным производственным кодом и доволен, что он у меня работает. Я уверен, что другие оценят, что вы добавите ответ с собственными выводами, используя разные уровни транзакций и т. д. Спасибо
Этот ответ подтверждается MS MVP сюда.
По сути, существует только одна транзакция, самая внешняя. Внутренние транзакции больше похожи на контрольные точки внутри транзакции. Уровни изоляции влияют только на самые близкие внешние транзакции, но не на транзакции, связанные с родительскими / дочерними элементами.
Счетчик увеличится на два. Следующее дает одну строку со значением (Num = 3). (Я открыл SMSS и указал на локальный экземпляр SQL Server 2008 Express. У меня есть база данных с именем Playground для тестирования.)
use Playground
drop table C
create table C (
Num int not null)
insert into C (Num) values (1)
begin tran X
update C set Num = Num + 1
begin tran Y
update C set Num = Num + 1
commit tran Y
commit tran X
select * from C
Я не верю, что вопрос касался вложенных транзакций, которые, как вы указываете, обрабатываются по-разному.
Согласно справке MSSQL, это можно сделать так:
UPDATE tablename SET counterfield = counterfield + 1 OUTPUT INSERTED.counterfield
Это обновит поле на единицу и вернет обновленное значение в виде набора записей SQL.
Я использовал этот SP для обработки случая, когда имя изначально не имеет счетчика
ALTER PROCEDURE [dbo].[GetNext](
@name varchar(50) )
AS BEGIN SET NOCOUNT ON
DECLARE @Out TABLE(Id BIGINT)
MERGE TOP (1) dbo.Counter as Target
USING (SELECT 1 as C, @name as name) as Source ON Target.name = Source.Name
WHEN MATCHED THEN UPDATE SET Target.[current] = Target.[current] + 1
WHEN NOT MATCHED THEN INSERT (name, [current]) VALUES (@name, 1)
OUTPUT
INSERTED.[current];
END
В вашем первом примере вы упоминаете, что T2 может читать, но блокирует запись (имеет смысл). Однако приращение - это чтение и запись - вы не захотите читать, а затем блокировать при записи, иначе вы бы прочитали зафиксированные данные (0), инкремент (1), а затем блокировку при записи 1, пока T1 не записал 1. Я предполагаю, что база данных также заблокирует чтение здесь, поскольку намерение состоит в том, чтобы обновить. Это правда?