Я хочу реализовать счетчик просмотров страниц в хранилище таблиц Azure. Если, скажем, два пользователя посещают страницу одновременно, а текущее значение PageViews = 100, гарантируется ли, что PageViews = 102 после операции обновления?
Атомарные операции в лазурном хранилище таблиц
Ответы (4)
Ответ зависит от того, как вы реализуете свой счетчик. :-)
Хранилище таблиц не имеет оператора «инкремент», поэтому вам нужно будет прочитать текущее значение (100) и обновить его до нового значения (101). Хранилище таблиц использует оптимистичный параллелизм, поэтому, если вы делаете то, что естественно при использовании клиентской библиотеки хранилища .NET, вы, вероятно, увидите исключение, когда два процесса попытаются сделать это одновременно. Это будет поток:
- Процесс A считывает значение PageViews и получает 100.
- Процесс B считывает значение PageViews и получает 100.
- Процесс A выполняет условное обновление для PageViews, что означает «установить для PageViews значение 101, если в настоящее время оно равно 100». Это удается.
- Процесс 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-запросе.)
Вы также можете переосмыслить часть «счет». Почему бы не превратить это в двухэтапный процесс?
Шаг 1. Запись просмотров страниц
Каждый раз, когда кто-то просматривает страницу, добавляйте запись в таблицу (назовем ее PageViews). Информация, которую вы добавите в один из этих магазинов, будет следующей:
- Ключ раздела = Имя страницы
- RowKey = Случайный GUID
После нескольких просмотров у вас будет что-то вроде этого:
- MyPage.aspx — SomeGuid
- MyPage.aspx — SomeGuid
- SomePage.aspx — SomeGuid
- MyPage.aspx — SomeGuid
Шаг 2. Подсчет просмотров страниц
Сейчас мы хотим получить все эти записи, подсчитать их, увеличить где-нибудь счетчик и удалить все записи. Предположим, у вас запущено несколько рабочих процессов. У обоих ваших рабочих цикл будет случайным образом работать от 1 до 10 минут. Каждый раз, когда истекает время рабочего процесса, он будет брать в аренду большой двоичный объект, если аренда еще не была взята (это всегда должен быть один и тот же большой двоичный объект, вы можете использовать AutoRenewLease).
Первый рабочий, получивший замок, может продолжить подсчет:
- Получить все записи из таблицы PageViewRecordings или из кеша
- Подсчет всех просмотров страниц на странице
- Обновить счетчик где-то
- Удалить записи, которые учитывались при подсчете
Проблема здесь в том, что очень сложно превратить это в идемпотентный процесс. Что произойдет, если ваш экземпляр выйдет из строя между подсчетом и удалением? У вас будет увеличено количество страниц, но поскольку элементы не были удалены, они будут добавлены к общему количеству при следующей обработке.
Именно поэтому я бы предложил следующее. В той же таблице (PageViews) вы также будете записывать общее количество просмотров страниц в том же разделе. Но данные будут немного другими (это будет единственная запись в этом разделе, содержащая общее количество):
- Ключ раздела = Имя страницы
- RowKey = Guid.Empty (только не используйте случайный guid, так мы узнаем разницу между записанным просмотром страницы и записью, содержащей общее количество).
- Count = количество просмотров текущей страницы.
Это вполне возможно, потому что в Table Storage меньше схемы. И зачем мы это делаем? Потому что у нас есть транзакции, если мы ограничимся одной и той же таблицей + разделом с максимальным количеством объектов 100. Что мы можем с этим сделать?
- Используя Take, мы получаем 100 записей из этой таблицы + раздел.
- Первая запись, которую мы получим, — это «встречная» запись. Почему? Поскольку его rowkey — Guid.Empty, а сортировка — лексикографическая.
- Подсчитайте эти записи (-1, потому что первая запись не является просмотром страницы, это просто заполнитель нашего счетчика)
- Обновите свойство Count записи счетчика.
- Удалите 99 (или меньше) других записей
- Сохранить изменения с помощью пакетной обработки.
- Повторяйте, пока не останется только 1 запись (запись счетчика).
И каждые X минут ваши рабочие процессы будут видеть, нет ли аренды большого двоичного объекта, получить аренду и перезапустить процесс.
Достаточно ли ясен этот ответ или мне нужно добавить код?
Я пришла с тем же вопросом. С библиотекой Azure Python я разрабатываю простое увеличение счетчика, используя eTag
и If-Match
вместо блокировки. Основная идея состоит в том, чтобы повторить попытку увеличения счетчика до тех пор, пока обновление не будет успешно запущено в соответствии с определенными критериями, то есть никакие другие обновления не будут мешать этому запущенному обновлению. Если запрос обновлений большой, следует вызвать сегментирование.
https://github.com/flyakite/simple-scalable-datastore/blob/master/datastore/azuretable.py
Если вы используете веб-сайты Azure, то очереди Azure и веб-задания — еще один вариант. Однако в одном из моих сценариев я фактически собираюсь использовать подход сегментирования и периодически обновлять агрегаты с помощью веб-заданий. Таблица хранилища таблиц Azure с UserPageViews с PartitionKey = User и RowKey = Page . Два одновременных пользователя с одним и тем же идентификатором пользователя не допускаются.