Атомарные операции в лазурном хранилище таблиц

Я хочу реализовать счетчик просмотров страниц в хранилище таблиц Azure. Если, скажем, два пользователя посещают страницу одновременно, а текущее значение PageViews = 100, гарантируется ли, что PageViews = 102 после операции обновления?


person States    schedule 07.08.2012    source источник
comment
Я не знаю вопроса, но почему бы просто не добавить базу данных SQL на 100 МБ за 5 долларов в месяц. SQL работает с блокировками.   -  person paparazzo    schedule 08.08.2012


Ответы (4)


Ответ зависит от того, как вы реализуете свой счетчик. :-)

Хранилище таблиц не имеет оператора «инкремент», поэтому вам нужно будет прочитать текущее значение (100) и обновить его до нового значения (101). Хранилище таблиц использует оптимистичный параллелизм, поэтому, если вы делаете то, что естественно при использовании клиентской библиотеки хранилища .NET, вы, вероятно, увидите исключение, когда два процесса попытаются сделать это одновременно. Это будет поток:

  1. Процесс A считывает значение PageViews и получает 100.
  2. Процесс B считывает значение PageViews и получает 100.
  3. Процесс A выполняет условное обновление для PageViews, что означает «установить для PageViews значение 101, если в настоящее время оно равно 100». Это удается.
  4. Процесс B выполняет те же операции и терпит неудачу, потому что предварительное условие (PageViews == 100) ложно.

Очевидно, что при появлении ошибки необходимо повторить процесс. (Прочитайте текущее значение, которое сейчас равно 101, и обновите его до 102.) Это всегда (в конечном итоге) приведет к тому, что ваш счетчик будет иметь правильное значение.

Есть и другие возможности, и мы сделали целый выпуск Cloud Cover о том, как реализовать по-настоящему масштабируемый счетчик: http://channel9.msdn.com/Shows/Cloud+Cover/Cloud-Cover-Episode-43.-Масштабируемые-счетчики-с-Windows-Azure.

То, что описано в этом видео, вероятно, излишне, если столкновения маловероятны. То есть, если ваша частота обращений равна одному в секунду, обычный шаблон «чтение, увеличение, запись» будет безопасным и эффективным. Если, с другой стороны, вы получаете 1000 обращений в секунду, вы захотите сделать что-то умнее.

ИЗМЕНИТЬ

Просто хотел уточнить для людей, которые читают это, чтобы понять оптимистичный параллелизм ... условная операция на самом деле не «установить для PageViews значение 101, если в настоящее время оно равно 100». Это больше похоже на «установите для PageViews значение 101, если оно не изменилось с тех пор, как я последний раз просматривал его». (Это достигается с помощью ETag, возвращенного в HTTP-запросе.)

person user94559    schedule 07.08.2012
comment
Я бы предложил использовать AutoRenewLease.DoOnce (наш хороший друг smarx, blog.smarx.com/posts/)) :) - person Sandrino Di Mattia; 08.08.2012
comment
Как бы мне ни нравились люди, использующие мой код :-), я не думаю, что понимаю, как это поможет здесь? - person user94559; 08.08.2012
comment
К сожалению, DoOnce был неправильным, но я бы использовал AutoRenewLease для большого двоичного объекта с именем = PageView.PartitionKey + _ + PageView.RowKey. Как только он заблокирует большой двоичный объект, он получит эту запись и увеличит количество. Таким образом, вы можете быть уверены, что каждый просмотр страницы учитывается. И все это с использованием временной обработки ошибок, чтобы убедиться, что в случае проблем код повторяет попытку, пока не сможет зарегистрировать просмотр страницы. - person Sandrino Di Mattia; 08.08.2012
comment
Но почему? Вы предлагаете получить блокировку (повторить по мере необходимости), прочитать значение, записать значение, снять блокировку вместо значения чтения, попытаться записать значение, повторить (только при возникновении конфликта). Для небольшого объема простое использование табличного хранилища приведет к меньшему количеству транзакций. Для большого объема вам действительно следует сделать что-то вроде счетчика осколков. Когда предпочтительнее аренда больших двоичных объектов? - person user94559; 08.08.2012
comment
Лично я бы сделал значение чтения и пытался до тех пор, пока не приблизится к успеху. Меня беспокоит только то, не увеличит ли это значительно время отклика? Интересно, могу ли я использовать какие-либо асинхронные методы для этого... хм. - person States; 08.08.2012
comment
Правда, сценарий блокировки на самом деле не применим к вашему решению. Я объяснил, что я имел в виду в своем ответе. PS: Не думаете ли вы, что ваш сценарий не сломается при высокой нагрузке? Если у вас 1000 просмотров страниц в секунду, это вызовет много обновлений в вашей записи, а также приведет к сбою многих условных операций (ETag будет постоянно меняться). - person Sandrino Di Mattia; 08.08.2012
comment
@Sandrino Да, именно поэтому вы выбираете масштабируемое решение, такое как сегментирование счетчика. - person user94559; 08.08.2012
comment
@States Да, я предполагаю, что обновление вашего счетчика страниц будет асинхронным (неблокирующим). Если вы делаете достаточно обновлений, чтобы задержка вызывала беспокойство, вам может потребоваться масштабирование, как в эпизоде ​​с облачным покровом. - person user94559; 08.08.2012
comment
Хорошо, думаю, мне просто нужно их оценить и посмотреть, как все пойдет. - person States; 08.08.2012

Вы также можете переосмыслить часть «счет». Почему бы не превратить это в двухэтапный процесс?

Шаг 1. Запись просмотров страниц

Каждый раз, когда кто-то просматривает страницу, добавляйте запись в таблицу (назовем ее PageViews). Информация, которую вы добавите в один из этих магазинов, будет следующей:

  • Ключ раздела = Имя страницы
  • RowKey = Случайный GUID

После нескольких просмотров у вас будет что-то вроде этого:

  • MyPage.aspx — SomeGuid
  • MyPage.aspx — SomeGuid
  • SomePage.aspx — SomeGuid
  • MyPage.aspx — SomeGuid

Шаг 2. Подсчет просмотров страниц

Сейчас мы хотим получить все эти записи, подсчитать их, увеличить где-нибудь счетчик и удалить все записи. Предположим, у вас запущено несколько рабочих процессов. У обоих ваших рабочих цикл будет случайным образом работать от 1 до 10 минут. Каждый раз, когда истекает время рабочего процесса, он будет брать в аренду большой двоичный объект, если аренда еще не была взята (это всегда должен быть один и тот же большой двоичный объект, вы можете использовать AutoRenewLease).

Первый рабочий, получивший замок, может продолжить подсчет:

  1. Получить все записи из таблицы PageViewRecordings или из кеша
  2. Подсчет всех просмотров страниц на странице
  3. Обновить счетчик где-то
  4. Удалить записи, которые учитывались при подсчете

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

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

  • Ключ раздела = Имя страницы
  • RowKey = Guid.Empty (только не используйте случайный guid, так мы узнаем разницу между записанным просмотром страницы и записью, содержащей общее количество).
  • Count = количество просмотров текущей страницы.

Это вполне возможно, потому что в Table Storage меньше схемы. И зачем мы это делаем? Потому что у нас есть транзакции, если мы ограничимся одной и той же таблицей + разделом с максимальным количеством объектов 100. Что мы можем с этим сделать?

  1. Используя Take, мы получаем 100 записей из этой таблицы + раздел.
  2. Первая запись, которую мы получим, — это «встречная» запись. Почему? Поскольку его rowkey — Guid.Empty, а сортировка — лексикографическая.
  3. Подсчитайте эти записи (-1, потому что первая запись не является просмотром страницы, это просто заполнитель нашего счетчика)
  4. Обновите свойство Count записи счетчика.
  5. Удалите 99 (или меньше) других записей
  6. Сохранить изменения с помощью пакетной обработки.
  7. Повторяйте, пока не останется только 1 запись (запись счетчика).

И каждые X минут ваши рабочие процессы будут видеть, нет ли аренды большого двоичного объекта, получить аренду и перезапустить процесс.

Достаточно ли ясен этот ответ или мне нужно добавить код?

person Sandrino Di Mattia    schedule 07.08.2012
comment
Теперь я понимаю, что вы говорите об аренде больших двоичных объектов. Это разумно, но я все же предпочитаю что-то вроде сегментирования (как в эпизоде ​​с облачным покровом). - person user94559; 08.08.2012
comment
Мне нравится эта идея. Код не нужен, я его четко понимаю. Но вы имели в виду, что если нет аренды, рабочий берет аренду и перезапускает процесс, верно? - person States; 08.08.2012

Я пришла с тем же вопросом. С библиотекой Azure Python я разрабатываю простое увеличение счетчика, используя eTag и If-Match вместо блокировки. Основная идея состоит в том, чтобы повторить попытку увеличения счетчика до тех пор, пока обновление не будет успешно запущено в соответствии с определенными критериями, то есть никакие другие обновления не будут мешать этому запущенному обновлению. Если запрос обновлений большой, следует вызвать сегментирование.

https://github.com/flyakite/simple-scalable-datastore/blob/master/datastore/azuretable.py

person Shih-Wen Su    schedule 28.02.2014

Если вы используете веб-сайты Azure, то очереди Azure и веб-задания — еще один вариант. Однако в одном из моих сценариев я фактически собираюсь использовать подход сегментирования и периодически обновлять агрегаты с помощью веб-заданий. Таблица хранилища таблиц Azure с UserPageViews с PartitionKey = User и RowKey = Page . Два одновременных пользователя с одним и тем же идентификатором пользователя не допускаются.

person Sentinel    schedule 07.08.2014