Алгоритм создания бесшовного списка данных

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

00c820a99752fd096497a509322464c3.jpg

Проблема, которую решает алгоритм

При развитии продукта встаёт вопрос масштабирования — перехода на другую архитектуру, например, с монолитного решения на микросервисное. Когда у вас сотни тысяч клиентов их нельзя просто так взять и перенести.

Процесс миграции многомерен и длителен:

·       во-первых, существует десяток продуктов, которые могут находиться на разной стадии миграции между архитектурами;

·       во-вторых, клиенты переносятся в несколько этапов: сначала десяток избранных клиентов, затем сотня лояльных клиентов.

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

Описание алгоритма

Есть 2 подхода к бесшовной миграции данный:

  • сделать на бэке фасад, который будет объединять данные из монолита и микросервисной платформы;

  • поручить это фронту, в данном случае мобильному Flutter приложению на Dart.

Рассмотрим второй вариант. Как опорный и базовый продукт возьмём «Платежи», но держим в голове, что это только первая ласточка, на очереди ещё десяток подсистем/модулей/микросервисов.

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

То есть, информацию по платежам до какого-то момента нужно не просто брать из монолита, а после уже из микросервиса, а они так сказать перемешаны и одновременно находятся и там и там (без дублирования).

Алгоритм работы: описание и реализация

 Вот как отобразить стройный список:

  • При первичной загрузке (0 страница) загружаем определённую порцию Size из обоих сервисов Mo и Mc, при скроллинге мы определяем с какой записи стоит продолжить загрузку из Mo, а с какой из Mc;

  • Соединяем списки и сортируем (Mo + Mc).sort ();

  • Добавляем в результирующий список только первые Size записей
    Res = (Mo + Mc).sort ().take (Size);

```dart
class UtilsUnitedRecords {
	  ///Получаем из 2х списков тот, который нужно добавить в результирующий
	  List unitedRecord(List setA, List setB, int Function(T a, T b) compare) {
		var union = [...setA, ...setB];
		union.sort(compare);
		return union.take(Environment.sizePage).toList();
	  }

	  ///Получаем позицию с которой нужно продолжить загрузку
	  int getStartPosition(List setA, bool Function(T a) check) {
		return setA.where((element) => check(element)).length;
	  }
  }

class Repository {

  Repository({
    required this.api,
    required this.apiMs,
    required this.store,
    required this.permissionRepository,
  });

  final IStore store;
  final IApi api;
  final IApi apiMs;
  final IPermissionRepository permissionRepository;

  final utils = UtilsUnitedRecords

search({
    S searchParams,
    bool isFirst = true,
    int pageSize = Environment.sizePage,
  }) async {
	final userCanUseMS = await permissionRepository.getMicroservicePermission();

    //Проверяем сколько уже выкачано из МС и МОНО,
    //  т.о. определяем стартовую позицию загрузки для каждого сервиса
    var startPositionMono = isFirst
		? 0
		: userCanUseMS 
			? utils.getStartPosition(store, _isMs) 
			: 0;
    var startPositionMS = isFirst
		? 0
		: utils.getStartPosition(store, _isMono);
	
	//Заменяем стартовую позицию поиска в параметрах и ищем
	searchParams = searchParams.copyWith(startPosition: startPositionMS);
    final responseMono = await search(api: api, searchParams: searchParams);

    searchParams = searchParams.copyWith(startPosition: startPositionMS);
    final responseMs = userCanUseMS? await search(api: apiMs, searchParams: searchParams): [];

    //Соединяем списки, сортируем получившийся и возвращаем ту часть, коорую нужно добавить в стор
    final insertList = utils.unitedRecords(responseMs, responseMono, _compare);

    //В зависимости от того первая это загрузка или нет, вызывается установка или дополнение списка в стор
    final action = isFirst 
		? store.setLetters 
		: store.addLetters;
    action(letters: insertList);
  }
```

Процесс тестирования нового алгоритма на реальных данных, оценка его эффективности и точности

Проверим в Excel тестовый набор данных. Предположим, что страница при прокрутке списка будет подгружать по две записи.  Действительно видим бесшовный список из разных источников.

Пример бесшовного списка

Пример бесшовного списка

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

Это приводит к кешированию данных, если они не попали в выборку.

```dart
enum ServerDef { MONO, MS }
class UtilsUnitedRecords {
	  ///Получаем из 2х списков тот, который нужно добавить в результирующий
	  List unitedRecord(List setA, List setB, int Function(T a, T b) compare) {
		var union = [...setA, ...setB];
		union.sort(compare);
		return union.take(Environment.sizePage).toList();
	  }

	  ///Получаем позицию с которой нужно продолжить загрузку
	  int getStartPosition(List setA, bool Function(T a) check) {
		return setA.where((element) => check(element)).length;
	  }

	///Возвращает данные для кэширования, если все данные одного из наборов не попали в результирующий список
  MapEntry>? needCaching(
      {required List union,
      required MapEntry> setA,
      required MapEntry> setB}) {
    final minSet = (setA.value.length < setB.value.length) ? setA : setB;

    final isContains = union.any((element) => minSet.value.contains(element));
    return (isContains) ? null : minSet;
  }
  }

class Repository {

  Repository({
    required this.api,
    required this.apiMs,
    required this.store,
    required this.permissionRepository,
  });

  final IStore store;
  final IApi api;
  final IApi apiMs;
  final IPermissionRepository permissionRepository;

  final utils = UtilsUnitedRecords
  MapEntry>? _cacheData;

search({
    S searchParams,
    bool isFirst = true,
    int pageSize = Environment.sizePage,
  }) async {
	final userCanUseMS = await permissionRepository.getMicroservicePermission();

    //Проверяем сколько уже выкачано из МС и МОНО,
    //  то есть, определяем стартовую позицию загрузки для каждого сервиса
    var startPositionMono = isFirst
		? 0
		: userCanUseMS 
			? utils.getStartPosition(store, _isMs) 
			: 0;
    var startPositionMS = isFirst
		? 0
		: utils.getStartPosition(store, _isMono);
	if (isFirst){
cacheData = null;
}

//Заменяем стартовую позицию поиска в параметрах и ищем
searchParams = (_cacheData != null && _cacheData!.key == ServerDef.MONO)
        ? _cacheData!.value
	: searchParams.copyWith(startPosition: startPositionMS);

    final responseMono = (_cacheData != null && _cacheData!.key == ServerDef.MS)
        ? _cacheData!.value
	: await search(api: api, searchParams: searchParams);

    searchParams = searchParams.copyWith(startPosition: startPositionMS);
    final responseMs = userCanUseMS? await search(api: apiMs, searchParams: searchParams): [];

    //Соединяем списки, сортируем получившийся и возвращаем ту часть, которую нужно добавить в стор
    final insertList = utils.unitedRecords(responseMs, responseMono, _compare);

    //В зависимости от того первая это загрузка или нет, вызывается установка или дополнение списка в стор
    final action = isFirst 
		? store.setLetters 
		: store.addLetters;
    action(letters: insertList);

//Определяем, нужно ли кэшировать данные
    _cacheData = urds.needCaching(
      union: insertList,
      setA: CacheData(ServerDef.MONO, responseMono),
      setB: CacheData(ServerDef.MS, responseMs),
    );
  }

Итоги и выводы о значимости алгоритма и дальнейших перспективах его развития

Алгоритм решает проблему получения бесшовных данных из разных источников со следующими последствиями:

  • решение сделать на фронте (в данном случае мобильном приложении) позволяет реализовать локальный (в разрезе пользователя на устройстве) кэш на уровне репозитория. При реализации на бэке пришлось бы использовать дополнительные хранилища Redis или аналоги с отслеживанием сессий пользователя и принудительной чисткой;

  • в данном случае у нас имеется только 2 источника, но репозиторий и функция запросто переписывается под массив источников;

  • впереди ещё десяток продуктов ждущих переезда на микросервисы, вспомогательные функции/utils — это дженерики, а значит, ничего заново переписывать не придётся;

  • функция поиска и функции определения, откуда объект _isMs и _isMono в репозитории достаточно универсальны, нужно будет обновить функцию сравнения объектов других продуктов _compare для сортировки;

Есть ещё пара идей для улучшения, например:

  • использовать параллельный запрос данных (в dart это реализуется с помощью изолятов);

  • оптимизировать вычисление стартовой позиции;

  • кешировать неиспользованные данные, сейчас они не перезапрашиваются только если ни одна запись из ответа не использовалась.

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

© Habrahabr.ru