Інтуїтивно здається: у кожної події є мітка часу, тож порядок подій очевидний. У розподіленій системі це не так.
Чому годинник ненадійний:
- годинники різних серверів розходяться - на мілісекунди чи більше навіть з NTP;
- годинник може стрибати назад при синхронізації NTP чи переході віртуальної машини;
- мітка часу ставиться в різних місцях: на клієнті, на сервері при отриманні, в базі при записі;
- мережа змінює порядок: повідомлення, відправлене раніше, може прийти пізніше.
Типова помилка:
Сервіс A о 10:00:00.120: ProfileUpdated(name="Оля")
Сервіс B о 10:00:00.100: ProfileUpdated(name="Олена") ← годинник B відстає на 50 мс
Правило «перемагає пізніша мітка часу» (last write wins) тихо втрачає справжню останню зміну.
Що використовують замість фізичного часу:
1. Версії сутності - лічильник, що збільшується з кожною зміною в джерелі істини:
CustomerRenamed { customer_id: 7, version: 15, name: "Оля" }
Споживач застосовує подію, лише якщо version більша за збережену, - запізнілі й дубльовані події ігноруються.
2. Логічні годинники Лампорта - лічильник, який кожен вузол збільшує при події й синхронізує з отриманими повідомленнями (max(local, received) + 1). Дають порядок, узгоджений з причинністю: якщо подія A спричинила B, лічильник A менший.
3. Векторні годинники - виявляють конкурентні зміни (жодна не спричинила іншу) - тоді конфлікт треба розв'язати явно, а не мовчки перезаписати.
4. Гібридні логічні годинники (HLC) - поєднують фізичний час із логічним лічильником; використовуються в розподілених базах (CockroachDB).
5. Порядок у брокері повідомлень: Kafka гарантує порядок у межах партиції - події однієї сутності публікують з тим самим ключем (order_id), і вони обробляються послідовно.
6. Одне джерело істини для порядку - послідовність у базі власника даних (автоінкремент, sequence) чи запис усіх змін однієї сутності через один сервіс.
Практичні правила:
- мітки часу - для показу людям і приблизного аналізу, а не для вирішення конфліктів;
- для конкурентних змін однієї сутності - оптимістичне блокування з версією (
WHERE version = ?); - події, що мають застосовуватися в порядку, - з ключем партиціювання за сутністю й номером версії.
Докладніше в документації: Patterns of Distributed Systems: Lamport Clock