В прошлый раз я писал про оптимистическую блокировку. Сегодня я хочу описать одну разновидность прикладного применения оптимистической блокировки.
Это прикладное применение относится к области систем асинхронного обмена сообщений (таких как ActiveMQ, RabbitMQ, OpenMQ, memcacheQ и другие). Очереди сообщений — это очень полезная разновидность промежуточного хранилища данных. В очередь можно положить сообщение и можно из нее взять сообщение. MQ системы позволяют частично решить проблему именуемую "Producer—Consumer problem". Кто-то ложит, а кто-то читает, — все просто. Вообще, подобную систему можно реализовать и поверх вашей любимой реляционной базы данных. Причина, почему существуют очереди сообщений как отдельный тип middleware, заключается в эффективности их реализации. Когда вы знаете, что все что может делать клиент — это писать в хвост и читать с головы, вы можете написать действительно эффективную реализацию с очень большой пропускной способностью. Предыдущее предложение метафорично. Я надеюсь, вы не будете пытаться написать свою mq-систему.
Однако, у MQ систем есть одна особенность. Они не гарантируют порядок доставки. Это значит что вы можете положить в очередь сообщение А, затем Б, а из очереди вылетит сначала Б, а затем А. Я не буду вдаваться в технические подробности почему так происходит. Гарантия порядка доставки и его последствия для приложения, — тема отдельная.
Тем не менее, порядок следования сообщений иногда бывает важен. Настолько важен, что его нарушение может привести к потери целостности данных. Представьте себе такой сценарий, вы хотите написать сервис, который позволит собирать, хранить и просматривать историю изменения каких-либо объектов в системе. Возьмем, скажем, объявления. Объявления меняются и мы хотим иметь историю того когда и как именно они менялись с течением времени. Мы в проекте пришли к схеме, в которой сервис через очередь сообщений принимает сообщения следующего формата:
<?xml version=“1.0” encoding=“UTF-8”?>
<state time=“2009-11-02T12:01:00+11:00” revision="15">
<object type=“bulletin” id=“3461432” />
<attributes>
<attribute name=“subject” type=“string”>Продам автомагнитолу</attribute>
<attribute name=“ownerId” type=“integer” value=“15” />
</attributes>
</state>
В данном XML сообщении содержится информация об объекте и о его состоянии. Сервис аккумулирует все эти изменения и позже позволяет просмотреть когда и как менялось состояние отдельно взятого объявления.
Если вы будете реализовывать нечто подобное, то скорее всего, вы очень быстро придете к тому, что надо хранить дельты между состояниями, иначе в вашей БД будет очень много дублирующихся данных, а ее размер будет очень быстро расти.
Но вы не можете отправлять на ваш сервис дельты, так как очередь не гарантирует порядок доставки. Дельты могут прийти в другом порядке, и тогда вы получите некорректную историю изменений. И вот тут то нам может помочь тот самый счетчик, который мы добавили для реализации оптимистической блокировки. Этот счетчик, являясь уникальным, монотонно возрастающим номером ревизии, позволяет восстановить порядок посылки сообщений клиентом.
Общая схема работы выглядит следующим образом. Клиенты всегда посылают в сервис полное состояние объекта на момент изменения (snapshot). Сервис принимая сообщение проверяет, есть ли в БД запись состояния для предыдущей ревизии. Если есть, то мы можем посчитать дельту и записать только ее. Если предыдущего состояния нет, мы пишем в БД полное состояние, ожидая, что предыдущее состояние поспеет позже (или это первая запись для этого объекта). Даже если предыдущее состояние не дойдет до сервиса мы не потеряем всю последующую историю. Более того, мы будем знать, что история по этому объекту неполная.

Также, если сервис получает сообщение содержащие состояние объекта с ревизией ß, то следует проверить есть ли в хранилище состояние ß+1. Если так, то можно его сократить.

Вот такое вот прикладное применение оптимистической блокировки. Буду рад услышать ваши комментарии по этому поводу. Может кто-то использует что-то подобное?
> Однако, у MQ систем есть одна особенность. Они не гарантируют порядок доставки.
ОтветитьУдалитьНе совсем так. Правильно сказать "в моём воображении все MQ системы не гарантируют порядок доставки".
Почему я в memcacheQ делаю set 1, set 2, set 3, потом три get и получаю 3, 2, 1?
Сетевые транспортные протоколы не гарантируют доставку, да? Верно для UDP. Неверно для всех.
Ещё вот здесь в презентации RabbitMQ пишут, что "Queues are stateful, ordered, and canbe persistent, transient, private, shared.Exchanges are stateless routing tables. Четвёртое слово - ordered."
По поводу.
Все системы контроля версий используют это. Вы изобрели VCS с XML протоколом. Можно было просто взять какой-нибудь subversion/git/monotone/etc.
> Почему я в memcacheQ делаю set 1, set 2, set 3, потом три get и получаю 3, 2, 1?
ОтветитьУдалитьВы наверное имели ввиду, что получаете "1, 2, 3", а не "3, 2, 1", а то это уже стек получается :)
Ну да не суть... Вы правы насчет доставки. Большинство MQ систем в штатных условиях доставляют сообщения по порядку. Так же как и TCP, как вы верно заметили. Тем не менее, большинство систем формально не гарантируют этого (документация по memcacheq ничего не говорит о гарантиях порядка доставки). Но даже те MQ системы которые гарантируют порядок доставки не меняют картины в целом, потому что порядок доставки сам по себе никому не интересен. Интересен порядок обработки, гарантировать который и при этом еще поддерживать большую пропускную способность просто невозможно.
Я собираюсь более полно проиллюстрировать свои мысли в следующей заметке. Так что, stay tuned :)