В этом уроке мы собираемся создать приложение для ведения дел, которое может продолжать работать, когда пользователь теряет подключение к Интернету. Вариантов использования этого типа функций бесчисленное множество, так что это хороший трюк, который нужно иметь в своем арсенале. Первое, что нужно знать при решении такого рода проблем, — это то, как работают наборы реплик с несколькими лидерами. Давайте сначала разберемся в этом.

Наборы реплик с несколькими лидерами

Чтобы понять эту концепцию, давайте вернемся к основам. Как мы сохраняем данные? Мы храним его в базе данных.

Как мы пишем в базу данных? Мы делаем вызов функции для узла, у которого есть полномочия на запись в указанную базу данных. Мы называем эти типы узлов лидерами.

Как мы читаем из базы данных? Мы делаем вызов функции узлу, у которого есть полномочия на чтение из базы данных. Все ведущие узлы могут читать из базы данных. Узлы, не являющиеся ведущими, называются последователями.

Как создать несколько узлов с доступом на чтение/запись? Мы создаем реплики базы данных и назначаем каждый узел одной копии базы данных.

Что вы называете системой, в которой есть более одного лидера? Набор реплик с несколькими лидерами.

Итак, какое отношение реплики с несколькими лидерами имеют к моему приложению?

Давайте на мгновение представим более «традиционную» архитектуру, в которой ваше приложение списков дел пингует некоторую конечную точку API для выполнения действия CRUD (создание, чтение, обновление, удаление). Что произойдет, если отсутствие подключения к Интернету помешает успешному выполнению сетевого вызова? Если мы спустимся в кроличью нору, нам могут прийти в голову такие идеи, как создание автономной системы очередей, которая продолжает попытки пакетной загрузки неудачных данных до тех пор, пока не получит успешный ответ. Даже если поначалу нам это удастся, теперь мы будем вынуждены поддерживать этот инструмент, следя за тем, чтобы все наши вызовы API имели логику аварийного переключения, которая добавляет неудачные данные в очередь. Кроме того, по мере роста сложности базы данных очереди может потребоваться применить пакетные операции к нескольким таблицам после того, как устройство снова подключится к сети. Как видите, сейчас мы, по сути, занимаемся созданием механизма хранения веб-базы данных.

Если мы удвоим тот факт, что нам нужно поддерживать копию данных, поскольку пользователь продолжает применять операции CRUD, несмотря на то, что он отключен от удаленной базы данных, почему бы просто не сохранить копию базы данных в браузере и не писать в нее. вместо удаленного?

  1. Семантика с точки зрения разработчика проще: разработчику просто нужно сосредоточиться на обеспечении записи данных в локальную базу данных.
  2. Когда интернет отключается, проблем нет. Мы все равно писали в локальную базу данных.
  3. Поскольку сейчас мы имеем дело с набором реплик с несколькими лидерами, подходы к созданию стабильного решения известны и задокументированы. Кроме того, существуют инструменты, которые могут помочь нам реализовать этот сценарий с несколькими лидерами.

Засучи рукава

Теперь, когда мы рассмотрели большую часть теории, давайте реализуем наше приложение. Наша реализация будет состоять из трех шагов:

  1. Мы настроим CRUD-приложение только с пользовательским интерфейсом в React.
  2. Мы создадим базу данных, которая будет жить на устройстве пользователя.
  3. Мы создадим удаленную базу данных, которая синхронизируется с базой данных, которая находится на устройстве пользователя.

CRUD-приложение только для пользовательского интерфейса

  1. Клонируйте этот репозиторий: https://github.com/alien35/offline-todo-app
  2. Ознакомьтесь с веткой step-1/basic-ui, также доступной здесь: https://github.com/alien35/offline-todo-app/tree/step-1/basic-ui

Я избавлю вас от подробностей. В этом шаге нет ничего особенного, поэтому давайте двигаться дальше.

Создайте базу данных, которая живет на устройстве пользователя

Ознакомьтесь с веткой step-2/add-local-db, также доступной здесь: https://github.com/alien35/offline-todo-app/tree/step-2/add- местная БД

Если вы посмотрите на package.json, вы увидите, что мы импортировали пакет npm pouchdb. PouchDB — это мощное веб-хранилище данных NoSQL, которое позволит нам достичь наших целей с несколькими лидерами. Узнайте больше о его удивительности здесь: https://pouchdb.com/

Суть логики нашей базы данных заключается в пользовательском хуке, определенном здесь: src/utils/useDatabaseMetadata.tsx.

Найдите минутку, чтобы ознакомиться с кодом, а затем давайте еще немного обсудим теорию.

Заказ событий

Точно так же, как согласованность в конечном итоге является актуальной темой при чтении данных из реплицированной базы данных, причинно-следственная согласованность является важной темой для записи данных в реплицированную базу данных. В нашем приложении для ведения дел достаточно тривиально построить решение на основе идеи о том, что пользователь будет использовать только одно устройство одновременно, но что произойдет, если он использует два (или более) устройства одновременно? Сосредоточение внимания на генерации идентификатора для записей о текущих делах с использованием процесса генерации идентификатора на основе времени может предсказуемо привести к тому, что два устройства случайно попытаются создать две новые записи с одним и тем же идентификатором. Это возможно, особенно в нашем веб-решении, поскольку объект Date Javascript имеет разрешение только в миллисекундах.

Логические часы

Стандартный подход к управлению генерацией идентификаторов в параллельной системе заключается в использовании логических часов, а не часов, основанных на времени. Логические часы измеряют относительный порядок событий, чтобы помочь нам достичь причинно-следственной согласованности. В частности, Временная метка Лампорта — это простой, но надежный подход к реализации логических часов в распределенных системах.

Временные метки Лэмпорта: Определено

В сценарии с одним устройством вы можете просто сохранить счетчик на устройстве, который увеличивается с каждым событием записи. Однако, если бы вы запускали это приложение на нескольких устройствах, вы бы быстро столкнулись с конфликтами, поскольку второе устройство не знает значение счетчика первого устройства, и поэтому вы получили бы перекрывающиеся идентификаторы. Временная метка Лампорта учитывает это, назначая каждому узлу (помните, что в нашем случае каждое устройство является узлом) идентификатор, а затем идентификаторы записей базы данных генерируются путем объединения идентификатора узла со счетчиком событий.

Так как же нам это построить?

Давайте еще раз посмотрим на src/utils/useDatabaseMetadata.tsx.

В fetchMetadata() мы сначала проверяем, было ли устройство уже зарегистрировано как узел. Если это не так, мы создаем идентификатор для устройства, а затем сохраняем метаданные вокруг идентификатора узла, а также «счетчик событий», который мы будем использовать для наших логических часов в _local/database_info. . Префикс «_local» — это просто техническая возможность предотвратить репликацию метаданных, специфичных для устройства, после того, как мы настроим облачную версию базы данных.

Если вы посмотрите на src/Todos.tsx, вы увидите, что мы разработали наше приложение CRUD для обновления нашей базы данных PouchDB соответствующими действиями записи/чтения/обновления/удаления. Вы можете увидеть, как наши временные метки Лампорта генерируются на лету здесь https://github.com/alien35/offline-todo-app/blob/step-2/add-local-db/src/Todos.tsx#L46

Синхронизация нашей локальной базы данных с удаленной базой данных

Первое, что нам нужно сделать, это настроить удаленную базу данных. Мы будем использовать CouchDB, так как есть встроенные функции для синхронизации с нашей копией базы данных PouchDB. Инструкции можно найти здесь https://pouchdb.com/guides/setup-couchdb.html

Совет: Обязательно следуйте инструкциям CORS на этой странице, а также войдите в систему по адресу http://localhost:5984/, чтобы обеспечить доступ нашего браузерного приложения к базе данных.

Последний отрезок

Как только у вас будет эта настройка, мы официально подойдем к финальному этапу создания приложения. Проверьте ветку main.

Примечательным моментом в этой ветке является то, что теперь мы синхронизируем нашу локальную базу данных с удаленной. Логику для этого можно найти здесь:

https://github.com/alien35/offline-todo-app/blob/main/src/utils/useDatabaseMetadata.tsx#L17

PouchDB позволяет нам просто сказать, что наша исходная база данных, локальная, должна быть реплицирована в целевую базу данных, удаленную.

Отключить подключение к Интернету

Чтобы увидеть, что мы сделали в действии, в инструментах разработчика браузера перейдите в раздел Сеть и выберите Офлайн.

Добавьте пару дел и следите за сетью. Теперь сеть должна начать забиваться неудачными запросами:

Это потому, что, несмотря на то, что мы записали в нашу локальную базу данных, наша локальная база данных пытается синхронизироваться с удаленной и не может подключиться. Снова включите подключение к Интернету, и теперь запросы должны снова начать выполняться успешно:

Не связанная с рекламой реклама: Ищете новую высокооплачиваемую работу по разработке программного обеспечения работу? Отправьте мне свое резюме на адрес [email protected], и я свяжусь с вами!

Заключение

В этом руководстве мы рассмотрели концепции репликации с несколькими лидерами и применили их для создания приложения, которое работает, даже когда пользователь находится в автономном режиме, и снова синхронизируется с облаком при восстановлении подключения к Интернету. Мы рассмотрели логические часы — в частности, временные метки Лампорта — как простой, но надежный подход к управлению идентификаторами в распределенной системе. Как обычно, весь исходный код доступен здесь: