mndev
EN/UA/IT
Усі нотатки
4 хв читання

Синхронізація 500 тисяч діамантів без жодного подвійного продажу

Архітектура маркетплейсу діамантів на 500K+ живих товарів: адаптивне сканування, 200 паралельних воркерів, версійований кеш і система резервування, що прибрала подвійні продажі.

Подвійний продаж — найгірший сценарій відмови в комерції. Не через повернення грошей, а через те, що він каже покупцю. Він обрав камінь, ввів дані картки, а потім йому повідомили, що куплена річ не існує. Жодна якість дизайну після цього не рятує.

Calavera продає діаманти через Nivoda: каталог з 500 000+ каменів від світових постачальників, де кожен унікальний і будь-який може бути проданий кимось іншим будь-якої миті. Залишок — не число, яке зменшується. Це 500 000 окремих позицій, існуванням яких керує третя сторона.

Жодного подвійного продажу з моменту запуску. Ось архітектура, яка це дала.

Чому очевидний підхід не працює

Очевидний підхід — питати API постачальника під час завантаження сторінки. Він ламається на латентності: пошук за 28 параметрами стає живим викликом до чужої інфраструктури, а люксова вітрина не може просити покупця чекати дві секунди на застосування фільтра.

Другий очевидний підхід — нічна повна синхронізація в локальну базу. Він ламається на свіжості: камінь, проданий о 9:00, лишається в списку до наступного нічного прогону. При такому розмірі каталогу це не рідкісний край — це нормальний стан системи.

Робоча відповідь — ані те, ані те, і це не компроміс між ними. Це конвеєр, який безперервно звіряє локальну канонічну копію, плюс шар резервування, який робить залишкову несвіжість нешкідливою.

Адаптивне сканування замість перебору

Перебирати 500 000 записів посторінково за фіксованим графіком — водночас надто повільно там, де важливо, і марнотратно там, де ні. Залишки діамантів розподілені нерівномірно: цінові діапазони з найбільшою кількістю каменів мають і найбільшу оборотність, тоді як рідкісний важкий сегмент змінюється повільно.

Планувальник ділить каталог на цінові діапазони й сканує за щільністю — діапазони з більшою кількістю каменів і вищою оборотністю відвідуються частіше. Робота видається повідомленнями, по одній сторінці на повідомлення, і обробляється приблизно 200 паралельними воркерами.

Дві властивості роблять це надійним, а не просто швидким:

  • Одна сторінка на повідомлення. Падіння воркера втрачає одну сторінку роботи, яка повторюється незалежно. Немає довгої задачі, яку можна втратити.
  • Паралельність — регулятор, а не константа. Її налаштовують під ліміти постачальника, і зниження погіршує свіжість, а не ламає коректність.

Трифазний конвеєр

Сирі дані постачальника ніколи не пишуться напряму в джерело правди вітрини. Вони проходять три фази, кожна з однією відповідальністю:

  1. 01Map — нормалізація полів постачальника в канонічну схему. Постачальники не згодні між собою щодо сертифікаційних органів, назв форм і форматів вимірів, і ця незгода локалізується тут.
  2. 02Price — правила націнки, конвертація валют і округлення. Логіка ціноутворення живе рівно в одному місці, і саме це робить її аудитованою.
  3. 03Rate — оцінка й ранжування кожного каменя для релевантності пошуку, щоб каталог можна було сортувати чимось кориснішим за ціну.

Саме розділення тримає систему придатною до налагодження. Коли камінь показує неправильну ціну, фаза відома ще до початку розслідування. Єдиний монолітний трансформ перетворює кожен баг на читання всього конвеєра.

Пошук: менше 50 мс на 28 фасетах

Канонічний набір даних індексується в Typesense, який віддає пошук за 28 параметрами — карат, огранка, колір, чистота, флуоресценція, сертифікація, виміри тощо — з відповідями менше 50 мс.

Пошук працює виключно з локальним індексом. Він ніколи не звертається до API постачальника. Саме це рішення робить вітрину миттєвою на відчуття, і воно виправдане лише тому, що конвеєр позаду тримає індекс чесним.

Перед цим стоїть версійований кеш із підтримкою ETag: 95% влучань і 85% умовних запитів, закритих через 304. Версія входить у ключ кешу, тож оновлення даних інвалідує атомарно, а не за строком життя.

// Версія бере участь у ключі, тож нова версія набору даних
// інвалідує всі залежні записи одразу — без розтягнутого вікна протухання.
const cacheKey = `search:v${datasetVersion}:${hash(filters)}`

Система резервування

Усе вище зменшує вікно несвіжості. Ніщо з цього його не закриває. Між останньою синхронізацією конкретного каменя і моментом, коли покупець тисне «купити», цей камінь можуть продати деінде.

Тому останній крок не намагається запобігти несвіжості — він робить її нешкідливою. Коли камінь потрапляє в кошик, він резервується, і доступність перевіряється у постачальника саме в цю мить. Резерви оновлюються щогодини. Перевірка відбувається один раз, на одній позиції, яка справді цікавить покупця, в один момент, коли коректність має значення.

У цьому весь трюк, і він добре узагальнюється далеко за межі діамантів:

Перегляд може бути в кінцевому підсумку консистентним. Оформлення замовлення — ні. Витрачайте бюджет консистентності в точці зобов'язання, а не на кожному перегляді сторінки.

Жива перевірка доступності на кожному результаті пошуку була б неприйнятною за вартістю й уповільнила б каталог до повзання. На одному камені в момент додавання в кошик це один виклик — і саме той виклик, який прибирає подвійний продаж.

Як це експлуатувати

Конвеєр, який працює без нагляду, все одно має бути спостережуваним. Адмін-панель на 15 сторінок відстежує 487 тисяч діамантів у трьох фідах із 99,7% успішності конвеєра — пропускну здатність по фазах, кількість помилок, версію й вік набору даних.

Операційно важливе число — не успішність, а вік даних: скільки минуло з останньої звірки кожного цінового діапазону. Успішність каже, що конвеєр працює. Вік каже, чи говорить вітрина правду.

Що узагальнюється

Більшість магазинів ніколи не синхронізуватиме пів мільйона чужих товарів. Структурні рішення все одно переносяться:

  • Ніколи не давайте запиту від покупця залежати від стороннього API. Синхронізуйте в те, чим володієте, і віддавайте звідти.
  • Розділяйте прийом даних на фази з однією відповідальністю, щоб збої локалізувались.
  • Робіть інвалідацію кешу функцією версії даних, а не часу.
  • Перевіряйте в точці зобов'язання. В усьому іншому приймайте кінцеву консистентність.
  • Моніторте вік даних, а не лише успішність задач.

Результат на цьому проєкті: 500K+ каменів у пошуку швидше за 50 мс, 100% точність залишків на чекауті й жодного подвійного продажу з моменту запуску — від восьми мікросервісів, які працюють без людини поруч.

Написати в Telegram