В MS SQL Server есть способ «атомарно» увеличить столбец, используемый в качестве счетчика? - PullRequest
31 голосов
/ 11 октября 2008

Предполагая параметр изоляции транзакции Read Committed Snapshot, является ли следующее утверждение «атомарным» в том смысле, что вы никогда не «потеряете» параллельное приращение?

update mytable set counter = counter + 1

Я бы предположил, что в общем случае, когда этот оператор обновления является частью более крупной транзакции, он не будет. Например, я думаю, что такой сценарий возможен:

  • обновить счетчик в транзакции # 1
  • делай другие вещи в транзакции № 1
  • обновить счетчик с транзакцией № 2
  • совершить транзакция № 2
  • совершить транзакцию # 1

В этой ситуации счетчик не будет увеличиваться только на 1? Имеет ли это значение, если это единственный оператор в транзакции?

Как сайт, такой как stackoverflow, обрабатывает это для своего счетчика просмотров? Или возможность «потерять» некоторые приращения только что считается приемлемой?

Ответы [ 4 ]

26 голосов
/ 23 апреля 2010

Согласно справке MSSQL, вы можете сделать это так:

UPDATE tablename SET counterfield = counterfield + 1 OUTPUT INSERTED.counterfield

Это обновит поле на единицу и вернет обновленное значение в виде набора записей SQL.

13 голосов
/ 11 октября 2008

Read Committed Snapshot имеет дело только с блокировками при выборе данных из таблиц.

Однако в t1 и t2 вы ОБНОВЛЯЕТЕ данные, что является другим сценарием.

Когда вы ОБНОВЛЯЕТЕ счетчик, вы переходите в блокировку записи (в строке), предотвращая появление другого обновления. t2 может читать, но t2 будет блокировать его ОБНОВЛЕНИЕ до тех пор, пока не будет выполнено t1, и t2 не сможет зафиксировать до t1 (что противоречит вашей временной шкале). Только одна из транзакций получит обновление счетчика, поэтому обе будут корректно обновлять счетчик с учетом представленного кода. (Проверено)

  • counter = 0
  • Счетчик обновлений t1 (counter => 1)
  • Счетчик обновлений t2 (заблокирован)
  • t1 commit (counter = 1)
  • t2 разблокирован (теперь можно обновить счетчик) (counter => 2)
  • t2 commit

Read Committed означает, что вы можете только читать зафиксированные значения, но это не означает, что у вас есть Repeatable Reads. Таким образом, если вы используете переменную counter и зависите от нее и намереваетесь обновить ее позже, возможно, вы выполняете транзакции с неверным уровнем изоляции.

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

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 является информативной, хотя в ней говорится о функциях, пока они еще были в бета-версии, поэтому она может быть не совсем точной.


обновление: как показывает Справедливость, если t2 является вложенной транзакцией в t1, семантика различна. Опять же, оба будут корректно обновлять счетчик (+2), потому что с точки зрения t2 внутри t1 счетчик уже обновлялся один раз. Вложенный t2 не имеет доступа к тому счетчику, который был до обновления t1.

  • counter = 0
  • Счетчик обновлений t1 (counter => 1)
  • Счетчик обновлений t2 (вложенная транзакция) (counter => 2)
  • t2 commit
  • t1 commit (counter = 2)

В случае вложенной транзакции, если t1 выдает ROLLBACK после t1 COMMIT, счетчик возвращается к своему первоначальному значению, поскольку он также отменяет фиксацию t2.

3 голосов
/ 11 октября 2008

Нет, это не так. Значение читается в режиме совместного использования, а затем обновляется в монопольном режиме, поэтому возможны множественные чтения.

Либо используйте уровень Serializable, либо используйте что-то вроде

update t
set counter = counter+1
from t with(updlock, <some other hints maybe>)
where foo = bar
1 голос
/ 11 октября 2008

В глубине души есть только одна транзакция, самая внешняя. Внутренние транзакции больше похожи на контрольные точки внутри транзакции. Уровни изоляции влияют только на внешние дочерние транзакции, а не на родительские / дочерние транзакции.

Счетчик будет увеличен на два. Следующее приводит к одной строке со значением (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
...