8.1 ID транзакцій

Позначається як XID або TxID (якщо є різниця – підкажіть). Як TxID можна використовувати timestamps, що може зіграти на руку, якщо ми захочемо відновити всі дії до якогось моменту часу. Проблема може виникнути, якщо timestamp недостатньо гранулярний - тоді транзакції можуть отримати один і той же ID.

Тому найбільш надійний варіант - це генерувати унікальні ID проде UUID. У Python це робиться дуже просто:

>>> import uuid 
>>> str(uuid.uuid4()) 
'f50ec0b7-f960-400d-91f0-c42a6d44e3d0' 
>>> str(uuid.uuid4()) 
'd15bed89-c0a5-4a72-98d9-5507ea7bc0ba' 

Також є варіант хешувати набір визначальних транзакцію даних і використовувати цей хеш як TxID.

8.2 Повторні спроби (retries)

Якщо ми знаємо, що певна функція або програма ідемпотентна, це означає, що ми можемо і повинні намагатися повторити її виклик у разі помилки. А ми просто повинні бути готові до того, що якась операція видасть помилку – враховуючи, що сучасні програми розподілені по мережі та залізу, помилка має розглядатися не як виняток, а як норма. Помилка може статися через падіння сервера, помилки мережі, перевантаження віддаленої програми. Як повинний поводитися наш додаток? Правильно, спробувати повторити операцію.

Оскільки один шматочок коду може сказати більше, ніж ціла сторінка слів, то на одному прикладі розберемо, як в ідеалі повинен працювати механізм повторення операції в дусі naive retrying. Я продемонструю це з використанням бібліотеки Tenacity (у неї настільки продуманий дизайн, що навіть якщо ви не плануєте використовувати її, приклад повинен показати, як можна спроектувати механізм повторення):

import logging
import random
import sys
from tenacity import retry, stop_after_attempt, stop_after_delay, wait_exponential, retry_if_exception_type, before_log

logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
logger = logging.getLogger(__name__)

@retry(
	stop=(stop_after_delay(10) | stop_after_attempt(5)),
	wait=wait_exponential(multiplier=1, min=4, max=10),
	retry=retry_if_exception_type(IOError),
	before=before_log(logger, logging.DEBUG)
)
def do_something_unreliable():
	if random.randint(0, 10) > 1:
    	raise IOError("Broken sauce, everything is hosed!!!111one")
	else:
    	return "Awesome sauce!"

print(do_something_unreliable.retry.statistics)

>Про всяк випадок скажу: \@retry(...) - це такий спеціальний синтаксис Python, що називається "декоратором". Це функція retry(...) , яка обертає іншу функцію і виконує деякі дії до чи після виконання.

Як бачимо, повторні спроби можна оформити креативно:

  • Можна обмежити спроби часу (10 секунд) або кількість спроб (5).
  • Можна експоненційно (тобто, 2 ** деяке число n , що збільшується). або якось ще (наприклад, фіксовано) збільшувати час між окремими спробами. Експоненційний варіант зветься "congestion collapse".
  • Можна робити повторні спроби лише деяких видів помилок (IOError).
  • Повторні спроби можна передувати чи завершувати якимись спеціальними записами в балку.

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

8.3 Розширений інструментарій для любителів транзакцій

Я лише дам досить загальні визначення, оскільки ця тема варта окремої великої статті.

Two-phase commit (2pc) . 2pc має дві фази: фазу підготовки та фазу фіксації. На етапі підготовки всім мікросервісам буде запропоновано підготуватись до деяких змін даних, які можуть бути виконані атомарно. Як тільки всі вони будуть готові, то на етапі фіксації будуть внесені фактичні зміни. Для координації процесу необхідний глобальний координатор, який блокує необхідні об'єкти, тобто вони стають недоступними для змін, поки координатор їх не розблокує. Якщо окремий мікросервіс не готовий до змін (наприклад, не відповідає), координатор перерве транзакцію і почне процес відкату.

Чим гарний цей протокол? Він забезпечує атомарність. До того ж він гарантує ізоляцію під час запису та читання. Це означає, що зміни однієї транзакції не видно іншим, поки координатор не зафіксує зміни. Але в цих властивостях криється і мінус: оскільки цей протокол синхронний (блокуючий), він уповільнює роботу системи (при тому, що виклик RPC сам по собі досить повільний). І знову-таки виникає небезпека взаємного блокування.

Saga . У цьому шаблоні розподілена транзакція виконується асинхронними локальними транзакціями у всіх пов'язаних мікросервісах. Мікросервіси зв'язуються між собою через шину подій („event bus“). Якщо мікросервіс не може завершити свою локальну транзакцію, інші мікросервіси виконають компенсаційні транзакції для відкату змін.

Плюси Saga у тому, що жодні об'єкти не блокуються. Але є, звісно, ​​і мінуси.

Saga складно налагоджувати, особливо коли задіяно багато мікросервісів. Ще один недолік шаблону Saga – у ньому відсутня ізоляція читання. Тобто, якщо нам важливі властивості, позначені ACID, то Saga нам не дуже підходить.

Що бачимо з опису цих двох технік? Те, що у розподілених системах відповідальність за атомарність та ізоляцію лягає на додаток. Те саме відбувається і при використанні БД, які не надають гарантії ACID. Тобто такі речі, як вирішення конфліктів, відкати, комміти та вивільнення місця лягають на плечі розробника.

8.4 Як зрозуміти, коли мені потрібні гарантії ACID?

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

Вибачте за банальність, але типовий приклад – фінансові транзакції.

Коли порядок виконання транзакцій має значення.

Уявіть собі, що ваша компанія зібралася переходити з месенджера FunnyYellowChat до месенджера FunnyRedChat, тому що у FunnyRedChat можна відсилати гіфки, а у FunnyYellowChat - не можна. Але ви не просто змінюєте месенджер — ви мігруєте листування вашої компанії з одного месенджера до іншого. Ви робите це, тому що ваші програмісти лінувалися документувати програми та процеси десь централізовано, і натомість усе публікували у різних каналах у месенджері. Та й ваші продажники деталі переговорів та угод публікували там же. Коротше, все життя вашої компанії - там, і оскільки ніхто не має часу переносити всю цю справу в сервіс для документації, а пошук у месенджерів працює непогано, ви вирішабо замість розгрібання завалів просто скопіювати всі повідомлення в нове місце. Черговість повідомлень важлива,

До речі, для листування в месенджері взагалі важлива черговість, але коли дві людини одночасно пишуть щось в одному чаті, то загалом не так важливо, чиє повідомлення здасться першим. Отже, саме для цього сценарію ACID був би не потрібен.

Інший можливий приклад – це біоінформатика. Я в цьому зовсім не розуміюся, але припускаю, що при розшифровці геному людини порядок важливий. Втім, я чув, що біоінформатики взагалі якісь свої інструменти для всього використовують — можливо, у них і свої БД.

Коли ви не можете видати користувачеві або процесу застарілі дані.

І знову – фінансові транзакції. Щиро кажучи, не вигадав іншого прикладу.

Коли незавершені транзакції пов'язані зі значними витратами. Уявіть собі проблеми, які можуть виникнути, коли лікар і медсестра одночасно оновлюють карту пацієнта та стирають зміни один одного, тому що БД не може ізолювати транзакції. Система охорони здоров'я — це ще одна сфера, окрім фінансової, для якої гарантії ACID, як правило, є критично важливими.

8.5 У яких випадках мені не потрібні ACID?

Коли користувачі оновлюють лише свої приватні дані.

Наприклад, користувач залишає коментарі або sticky notes до веб-сторінки. Або редагує особисті дані в особистому кабінеті провайдера будь-яких послуг.

Коли користувачі взагалі оновлюють дані, лише доповнюють новими (append).

Наприклад, додаток для бігу, який зберігає дані про ваші пробіжки: скільки пробігли, за який час, маршрут і т.д. Кожна нова пробіжка – нові дані, а старі взагалі не редагуються. Можливо, на підставі даних ви отримуєте аналітику - і якраз БД NoSQL хороші для цього сценарію.

Коли бізнес-логіка не визначає потреби певного порядку виконання транзакцій.

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

Коли користувачі будуть перебувати на одній і тій же веб-сторінці або вікні програми кілька секунд або навіть хвабон, тому вони так чи інакше бачитимуть застарілі дані.

Теоретично це будь-які новинні онлайн-медіа, або той же Youtube. Або "Хабр". Коли вам не має значення, що в системі тимчасово можуть зберігатися неповні транзакції, ви можете їх ігнорувати без жодних збитків.

Якщо ви агрегуєте дані з безлічі джерел, причому дані, які оновлюються з високою періодичністю - наприклад, дані про зайнятість паркувальних місць у місті, які змінюються щонайменше кожні 5 хвабон, то теоретично для вас не буде великої проблеми, якщо в якийсь момент транзакція для одного з паркувань не пройде. Хоча, звичайно, залежить від того, що саме ви хочете робити із цими даними.