В прошлый раз я писал про оптимистическую блокировку. Сегодня я хочу описать одну разновидность прикладного применения оптимистической блокировки.

Это прикладное применение относится к области систем асинхронного обмена сообщений (таких как 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). Сервис принимая сообщение проверяет, есть ли в БД запись состояния для предыдущей ревизии. Если есть, то мы можем посчитать дельту и записать только ее. Если предыдущего состояния нет, мы пишем в БД полное состояние, ожидая, что предыдущее состояние поспеет позже (или это первая запись для этого объекта). Даже если предыдущее состояние не дойдет до сервиса мы не потеряем всю последующую историю. Более того, мы будем знать, что история по этому объекту неполная.

Figure 1

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

Figure 2

Вот такое вот прикладное применение оптимистической блокировки. Буду рад услышать ваши комментарии по этому поводу. Может кто-то использует что-то подобное?