В предыдущей статье — часть 1, обзорная — я рассказал о том, зачем нужны распределенные структуры данных (далее — РСД) и разобрал несколько вариантов, предлагаемых распределенным кешем Apache Ignite. Сегодня же я хочу рассказать о подробностях реализации конкретных РСД, а также провести небольшой ликбез по распределенным кешам.  Итак: Начнем с того, что, как минимум в случае с Apache Ignite, РСД не реализованы с нуля, а являются надстройкой над распределенным кешем. Распределенный кеш — система хранения данных, в которой информация хранится более чем на одном сервере, но при этом обеспечивается доступ ко всему объему данных сразу. Основным преимуществом подобного рода систем является возможность впихнуть невпихуемое хранить огромные объемы данных без разбиения их на куски, ограниченные объемами конкретных накопителей или даже целых серверов. Чаще всего такие системы позволяют динамически наращивать объемы хранения посредством добавления новых серверов в систему распределенного хранения. Чтобы иметь возможность изменять топологию кластера (добавлять и удалять серверы), а также балансировать данные, используется принцип партиционирования (секционирования) данных.  При создании распределенного кеша указывается число партиций, на которые будут разделены данные, например, 1024. При добавлении данных выбирается партиция, ответственная за их хранение, например по хешу ключа. Каждая партиция может храниться на одном или нескольких серверах, в зависимости от конфигурации кеша. Для каждой конкретной топологии (набора серверов), сервер, где будет храниться партиция, вычисляется по заранее заданному алгоритму. Например, при старте кеша укажем что: Запустим четыре data node [JVM 1-4] (ответственные за хранение данных) и одну client node [Client JVM] (ответственную только за предоставление доступа к данным). Каждая из четырёх data node может быть использована как client node (то есть предоставлять доступ ко всем данным). Например, JVM 1 смогла получить данные по партициям A,C,D, хотя, локально, располагает только A (Primary) и D (Backup). Любая data node распределенного кеша для конкретной партиции может являться Primary или Backup, либо вообще не содержать партицию.  Primary node отличается от Backup тем, что именно она обрабатывает запросы в рамках партиции и, по необходимости, реплицирует результаты на Backup node. В случае выхода Primary node из строя, одна из Backup node становится Primary. В случае выхода Primary node из строя, при отсутствии Backup node, партиция считается утерянной. Некоторые распределенные кеши предоставляют возможность локально кешировать данные, расположенные на других node. Например Client JVM локально закешировала партицию B и не будет запрашивать дополнительные данные, пока они не изменятся. Рапределенные кеши разделяют на Partitioned и Replicated.  Разница состоит в том, что Partitioned-кеш хранит один (или один + N бекапов) экзепляр партиции в рамках кластера, а Replicated хранит по одному экземпляру партиции на каждой data node. Partitioned-кеш имеет смысл использовать для хранения данных, чьи объемы превышают возможности отдельных серверов, а Replicated — для хранения одних и тех же данных «везде». Хорошим примером для понимания является связка Сотрудник — Организация. Сотрудников много, и они довольно часто меняются, так что лучше хранить их в Partitioned-кеше. Организаций же мало, и меняются они редко, значит имеет смысл хранить их в Replicated-кеше, чтение из него гораздо быстрее. Итак, перейдем к подробностям реализации.  Хочу еще раз обозначить, что речь идет о реализации в рамках исходного кода Apache Ignite, в других распределенных кешах реализация может отличаться. Для обеспечения работы РСД используются два кеша: один Replicated и один Partitioned.  Replicated-кеш — в данном случае это системный кеш, (ignite-sys-cache) отвечающий, в том числе, за хранение информации об РСД, зарегистрированных в системе.  Partitioned-кеш (ignite-atomics-sys-cache) хранит данные, необходимые для работы РСД, и их состояние. Итак, большинство РСД создается следующим образом:  При первом запросе на создание РСД создается новый экземпляр, а последующие запросы получают уже ранее созданный. Третий шаг инициализации для обоих типов сводится к добавлению в ignite-atomics-sys-cache объекта типа GridCacheAtomicReferenceValue или GridCacheAtomicLongValue. Оба класса содержат одно единственное поле val.  Соответственно, любое изменение IgniteAtomicReference: … это запуск EntryProcessor со следующим кодом метода process: EntryProcessor — это функция, позволяющая атомарно выполнять сложные операции над объектами в кеше. Метод process принимает MutableEntry (объект в кеше) и может изменить его значение.  EntryProcessor, по сути, является альтернативой транзакции по одному ключу (иногда даже реализуется как транзакция).  Как следствие, гарантируется, что над одним объектом в кеше будет выполняться только один EntryProcessor в единицу времени. IgniteAtomicLong является дефакто расширением IgniteAtomicReference, поэтому и его метод compareAndSet реализован аналогичным образом.  Метод incrementAndGet не имеет проверок на ожидаемое значение, а просто добавляет единицу. При создании каждого экземпляра IgniteAtomicSequence... … ему выделяется пул идентификаторов. Соответственно, вызов... … просто инкрементирует локальный счетчик до достижения верхней границы пула значений.  При достижении границы происходит выделение нового пула идентификаторов, аналогично тому, как это происходит при создании нового экземпляра IgniteAtomicSequence. Декремент счетчика: … реализуется следующим образом: Ожидание декрементации счетчика до 0... … реализуется через механизм Continuous Queries, то есть при каждом изменении GridCacheCountDownLatchValue в кеше все экземпляры IgniteCountDownLatch уведомляются об этих изменениях.  Каждый экземпляр IgniteCountDownLatch имеет локальный: Каждое уведомление декрементирует internalLatch до актуального значения. Поэтому latch.await() реализуется очень просто: Получение разрешения... … происходит следующим образом: Возврат разрешения... … происходит аналогичным образом, за исключением того, что новое значение больше текущего. В отличие от остальных РСД, IgniteQueue не использует ignite-atomics-sys-cache. Используемый кеш описывается через параметр colCfg. В зависимости от указанного Atomicity Mode (TRANSACTIONAL, ATOMIC) можно получить разные варианты IgniteQueue. В обоих случаях состояние IgniteQueue контролируется с помощью: Для добавления элемента используется AddProcessor... … который, по сути, просто перемещает указатель на хвост очереди.  После этого... … в очередь добавляется новый элемент: Удаление элемента происходит аналогично, но указатель меняется не на tail, а на head... … и элемент удаляется. Разница между GridAtomicCacheQueueImpl и GridTransactionalCacheQueueImpl состоит в том, что: GridAtomicCacheQueueImpl при добавлении элемента сначала атомарно инкрементирует hdr.tail(), а потом уже добавляет по полученному индексу элемент в кеш.  Как следствие, GridAtomicCacheQueueImpl работает быстрее, но может возникнуть проблема консистентности данных: если информация о размере очереди и сами данные сохраняются не одновременно, то и вычитаться они могут не одновременно.  Вполне вероятна ситуация, когда внутри метода poll видно, что очередь содержит новые элементы, но самих элементов еще нет. Такое крайне редко, но всё же возможно. Эта проблема решается таймаутом ожидания значения. Бывали реальные случаи, когда не хватало и пятисекундного таймаута, что приводило к потерям данных в очереди. Я хотел бы еще раз отметить, что распределенный кеш — это, по сути, ConcurrentHashMap в рамках множества компьютеров, объединенных в кластер. Распределенный кеш может быть использован для реализации множества важных, сложных, но надежных систем.  Частным случаем реализации являются распределенные структуры данных, а в целом они применяются для хранения и обработки колоссальных объемов данных в реальном времени, с возможностью увеличения объемов или скорости обработки простым добавлением новых узлов. Java developer