Дружественная к конфликтам архитектура базы данных для больших документов и внутренних массивов - PullRequest
2 голосов
/ 07 мая 2020

Контекст

У меня есть база данных с набором документов, использующих эту схему (сокращенная схема, потому что некоторые данные не имеют отношения к моей проблеме):

{
    title: string;
    order: number;
    ...
    ...
    ...
    modificationsHistory: HistoryEntry[];
    items: ListRow[];
    finalItems: ListRow[];
    ...
    ...
    ...
}

Эти документы могут легко достигать 100 или 200 КБ, в зависимости от количества элементов и finalItems, которые они содержат. Также очень важно, чтобы они обновлялись как можно быстрее с минимальным использованием полосы пропускания.

Это внутри контекста веб-приложения, используя Angular 9 и @angular/fire 6.0.0.

Проблемы

Когда конечный пользователь редактирует один элемент в массиве item объекта, например, при редактировании только свойства, отражение того, что внутри базы данных требует от меня отправки всего объекта, потому что метод update firestore не поддерживает индексы массива внутри пути к полю, единственные операции, которые могут быть выполнены с массивами, - это добавление или удаление элемента , как описано в документации .

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

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

  • Некоторые операции записи могут завершиться ошибкой из-за слишком большого количества конфликтов в документе, если два обновления выполняются за одну секунду.
  • Обновления не являются atomi c, поскольку мы отправляем весь документ сразу, так как он не использует транзакции, чтобы еще больше избежать использования полосы пропускания.

Решения, которые я уже пробовал

Подколлекции

Описание

Это было очень простое решение: создать подколлекцию для массивов items, finalItems и modificationsHistory, что упростит их редактирование, поскольку теперь у них есть собственный идентификатор так что их легко найти, чтобы обновить их.

Почему это не сработало

Наличие списка с 10 finalItems, 30 items и 50 записями внутри modificationsHistory означает, что Мне нужно открыть в общей сложности 4 слушателя, чтобы один элемент был полностью прослушан. Учитывая тот факт, что пользователь может открывать многие из этих элементов одновременно, прослушивание нескольких десятков документов создает одинаково плохую ситуацию с производительностью, возможно, даже хуже в случае полного пользователя.

Это также означает, что если Я хочу обновить большой элемент со 100 элементами, и я хочу обновить половину из них, это будет стоить мне одной операции записи на элемент, не говоря уже о количестве операций чтения, необходимых для проверки разрешений, и т. Д. c, вероятно, 3 на запись, поэтому 150 операций чтения + 50 записей только для обновления 50 элементов в массиве.

Облачная функция для обновления документа

const {
  applyPatch
} = require('fast-json-patch');

function applyOffsets(data, entries) {
  entries.forEach(customEntry => {
    const explodedPath = customEntry.path.split('/');
    explodedPath.shift();
    let pointer = data;
    for (let fragment of explodedPath.slice(0, -1)) {
      pointer = pointer[fragment];
    }
    pointer[explodedPath[explodedPath.length - 1]] += customEntry.offset;
  });
  return data;
}

exports.updateList = functions.runWith(runtimeOpts).https.onCall((data, context) => {
  const listRef = firestore.collection('lists').doc(data.uid);
  return firestore.runTransaction(transaction => {
    return transaction.get(listRef).then(listDoc => {
      const list = listDoc.data();
      try {
        const [standard, custom] = JSON.parse(data.diff).reduce((acc, entry) => {
          if (entry.custom) {
            acc[1].push(entry);
          } else {
            acc[0].push(entry);
          }
          return acc;
        }, [
          [],
          []
        ]);
        applyPatch(list, standard);
        applyOffsets(list, custom);
        transaction.set(listRef, list);
      } catch (e) {
        console.log(data.diff);
      }
    });
  });
});

Описание

Используя библиотеку различий, я проводил различие между предыдущим документом и новым обновленным и отправлял это различие в GCF, который был выполнение обновления с использованием API транзакции.

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

Почему это не сработало

На самом деле, облачная функция была очень медленной, и некоторые обновления занимали более 2 секунд, они также могли выйти из строя из-за разногласия, без ведома соединителя firestore, поэтому в этом случае нет возможности гарантировать целостность данных.

Я буду отредактирован соответствующим образом, чтобы добавить больше решений, если я найду другие вещи, которые можно попробовать

Вопрос

Мне кажется, что я что-то упускаю, например, если бы в firestore было что-то, чего я вообще не знал, что могло бы решить мой вариант использования, но я не могу понять, что это такое, возможно, мои ранее протестированные решения были плохо реализовал или пропустил что-то важное. Что я пропустил? Возможно ли вообще добиться того, чем я хочу заниматься? Я открыт для ремоделирования данных, изменения запросов, всего, что угодно, поскольку это в основном для целей обучения.

1 Ответ

0 голосов
/ 19 мая 2020

Ваш diff-подход выглядел в основном разумным, не говоря уже о деталях.

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

Каждая запись в modificationsHistory не должна описывать отдельное различие, но, по возможности, набор различий.

Применять изменения из коллекций modificationsHistory к items в пакетном режиме, с задержкой через GCF. Вы можете отложить это сколь угодно долго, и вы можете исключить модификации, выполненные только за последние несколько секунд, чтобы учесть неустановленную согласованность в Firestore. Таким образом, не возникает риска конкуренции.

Очистку из коллекции modificationsHistory необходимо отложить еще больше, пока вы не будете уверены, что ни у одного клиента нет доступа к более старой версии root документ. Особенно, если вы считаете, что от клиента не требуется строго обновлять документ root при срабатывании слушателя.

Вам может потребоваться реконструировать стек патчей на стороне клиента, если modificationsHistory изменяется неожиданным образом из-за возможных ограничений согласованности. Например, если у вас есть полный порядок в наборе патчей, вам необходимо повторно применить стек патчей из базового образа, если коллекция неожиданно внезапно содержит «более старые» патчи, ранее неизвестные клиенту.

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

Вы можете настроить, как долго клиенты могут игнорировать жесткие обновления документа root и сколько изменений они могут вносить на стороне клиента перед отправкой нового сравнения. Последнее также зависит от того, сколько документов другой клиент должен получить вначале, с учетом ограничений на максимальное количество документов на запрос.


Если вам нужна другая информация, которая может пострадать от разногласия, например, список пользователей, у которых в настоящее время открыт определенный c документ, который также должен go разделиться на подколлекции.


Если задержка для просмотра изменений другими пользователями в конечном итоге окажется равной быть неприемлемым, вы можете выбрать дополнительный канал передачи данных в режиме реального времени для распространения исправлений в конкретном документе c. ActiveMQ или какой-либо другой брокер сообщений работает на выделенных ресурсах, независимо от FireStore.

...