8.1 ID-uri de tranzacție
Este desemnat ca XID sau TxID (dacă există o diferență, spuneți-mi). Timpurile pot fi folosite ca TxID, care poate juca în mâini dacă dorim să restabilim toate acțiunile la un moment dat. Problema poate apărea dacă marca temporală nu este suficient de granulară - atunci tranzacțiile pot obține același ID.
Prin urmare, cea mai fiabilă opțiune este de a genera ID-uri unice de produs UUID. În Python, acest lucru este foarte ușor:
>>> import uuid
>>> str(uuid.uuid4())
'f50ec0b7-f960-400d-91f0-c42a6d44e3d0'
>>> str(uuid.uuid4())
'd15bed89-c0a5-4a72-98d9-5507ea7bc0ba'
Există, de asemenea, o opțiune de hash a unui set de date care definesc tranzacția și de a utiliza acest hash ca TxID.
8.2 Reîncercări
Dacă știm că o anumită funcție sau program este idempotent, atunci aceasta înseamnă că putem și ar trebui să încercăm să-i repetă apelul în cazul unei erori. Și trebuie doar să fim pregătiți pentru faptul că o operațiune va da o eroare - având în vedere că aplicațiile moderne sunt distribuite în rețea și hardware, eroarea ar trebui considerată nu ca o excepție, ci ca o normă. Eroarea poate apărea din cauza unei erori de server, a unei erori de rețea, a congestionării aplicațiilor de la distanță. Cum ar trebui să se comporte aplicația noastră? Așa este, încercați să repetați operația.
Deoarece o bucată de cod poate spune mai mult decât o întreagă pagină de cuvinte, să folosim un exemplu pentru a înțelege cum ar trebui să funcționeze în mod ideal mecanismul naiv de reîncercare. Voi demonstra acest lucru folosind biblioteca Tenacity (este atât de bine concepută încât, chiar dacă nu intenționați să o utilizați, exemplul ar trebui să vă arate cum puteți proiecta mecanismul de recurență):
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)
> Pentru orice eventualitate, voi spune: \@retry(...) este o sintaxă specială Python numită „decorator”. Este doar o funcție de reîncercare (...) care include o altă funcție și face ceva înainte sau după ce este executată.
După cum putem vedea, reîncercările pot fi proiectate în mod creativ:
- Puteți limita încercările în funcție de timp (10 secunde) sau de numărul de încercări (5).
- Poate fi exponențial (adică 2 ** un număr crescător n ). sau într-un alt fel (de exemplu, fix) pentru a mări timpul dintre încercări separate. Varianta exponențială se numește „colapsul congestiei”.
- Puteți reîncerca numai pentru anumite tipuri de erori (IOError).
- Încercările de reîncercare pot fi precedate sau completate de unele intrări speciale din jurnal.
Acum că am finalizat cursul de tineri luptători și cunoaștem elementele de bază de care avem nevoie pentru a lucra cu tranzacțiile din partea aplicației, să ne familiarizăm cu două metode care ne permit să implementăm tranzacții în sisteme distribuite.
8.3 Instrumente avansate pentru iubitorii de tranzacții
Voi da doar definiții destul de generale, deoarece acest subiect merită un articol mare separat.
Comitare în două faze (2 buc) . 2pc are două faze: o fază de pregătire și o fază de commit. În timpul fazei de pregătire, tuturor microserviciilor li se va cere să se pregătească pentru unele modificări de date care pot fi făcute atomic. Odată ce sunt toate gata, faza de comitere va face modificările reale. Pentru a coordona procesul, este nevoie de un coordonator global, care blochează obiectele necesare - adică devin inaccesibile pentru modificări până când coordonatorul le deblochează. Dacă un anumit microserviciu nu este pregătit pentru modificări (de exemplu, nu răspunde), coordonatorul va anula tranzacția și va începe procesul de rollback.
De ce este bun acest protocol? Oferă atomicitate. În plus, garantează izolarea atunci când scrieți și citiți. Aceasta înseamnă că modificările aduse unei tranzacții nu sunt vizibile pentru alții până când coordonatorul comite modificările. Dar aceste proprietăți au și un dezavantaj: deoarece acest protocol este sincron (blocare), încetinește sistemul (în ciuda faptului că apelul RPC în sine este destul de lent). Și din nou, există pericolul de blocare reciprocă.
Saga . În acest model, o tranzacție distribuită este executată prin tranzacții locale asincrone în toate microserviciile asociate. Microserviciile comunică între ele printr-un autobuz de evenimente. Dacă vreun microserviciu nu reușește să își finalizeze tranzacția locală, alte microservicii vor efectua tranzacții de compensare pentru a anula modificările.
Avantajul Saga este că niciun obiect nu este blocat. Dar există, desigur, dezavantaje.
Saga este greu de depanat, mai ales când există multe microservicii implicate. Un alt dezavantaj al modelului Saga este că îi lipsește izolarea de citire. Adică, dacă proprietățile indicate în ACID sunt importante pentru noi, atunci Saga nu este foarte potrivit pentru noi.
Ce vedem din descrierea acestor două tehnici? Faptul că în sistemele distribuite, responsabilitatea pentru atomicitate și izolare revine aplicației. Același lucru se întâmplă atunci când se utilizează baze de date care nu oferă garanții ACID. Adică, lucruri precum rezolvarea conflictelor, rollback-urile, commit-urile și eliberarea spațiului cad pe umerii dezvoltatorului.
8.4 Cum știu când am nevoie de garanții ACID?
Când există o probabilitate mare ca un anumit set de utilizatori sau procese să lucreze simultan pe aceleași date .
Scuze pentru banalitate, dar un exemplu tipic sunt tranzacțiile financiare.
Când contează ordinea în care sunt executate tranzacțiile.
Imaginați-vă că compania dvs. este pe cale să treacă de la FunnyYellowChat messenger la FunnyRedChat messenger, deoarece FunnyRedChat vă permite să trimiteți gif-uri, dar FunnyYellowChat nu poate. Dar nu schimbați doar mesagerul - migrați corespondența companiei dvs. de la un mesager la altul. Faceți acest lucru pentru că programatorii dvs. au fost prea leneși să documenteze programe și procese undeva la nivel central și, în schimb, au publicat totul în diferite canale în messenger. Da, iar oamenii de vânzări au publicat detaliile negocierilor și acordurilor în același loc. Pe scurt, întreaga viață a companiei tale este acolo și, din moment ce nimeni nu are timp să transfere totul într-un serviciu de documentare, iar căutarea de mesagerie instant funcționează bine, ai decis, în loc să curățați moloz, să copiați pur și simplu toate mesaje către o nouă locație. Ordinea mesajelor este importantă
Apropo, pentru corespondența într-un messenger, ordinea este în general importantă, dar când două persoane scriu ceva în același chat în același timp, atunci în general nu este atât de important al cui mesaj va apărea primul. Deci, pentru acest scenariu special, ACID nu ar fi necesar.
Un alt exemplu posibil este bioinformatica. Nu înțeleg deloc acest lucru, dar presupun că ordinea este importantă atunci când descifrăm genomul uman. Cu toate acestea, am auzit că bioinformaticienii folosesc în general unele dintre instrumentele lor pentru orice - poate că au propriile baze de date.
Când nu puteți oferi unui utilizator sau nu puteți procesa date învechite.
Și din nou - tranzacții financiare. Sincer să fiu, nu m-am putut gândi la niciun alt exemplu.
Atunci când tranzacțiile în așteptare sunt asociate cu costuri semnificative. Imaginați-vă problemele care pot apărea atunci când un medic și o asistentă actualizează amândoi fișa pacientului și își șterg reciproc modificările în același timp, deoarece baza de date nu poate izola tranzacțiile. Sistemul de sănătate este un alt domeniu, pe lângă finanțe, în care garanțiile ACID tind să fie critice.
8.5 Când nu am nevoie de ACID?
Când utilizatorii își actualizează doar o parte din datele lor private.
De exemplu, un utilizator lasă comentarii sau note lipicioase pe o pagină web. Sau editează datele personale într-un cont personal la un furnizor de servicii.
Când utilizatorii nu actualizează deloc datele, ci doar completează cu altele noi (anexează).
De exemplu, o aplicație care rulează care salvează date despre alergările tale: cât ai alergat, pentru ce oră, traseu etc. Fiecare rulare nouă reprezintă date noi, iar cele vechi nu sunt editate deloc. Poate că, pe baza datelor, obțineți analize - și doar bazele de date NoSQL sunt bune pentru acest scenariu.
Când logica de afaceri nu determină necesitatea unei anumite ordine în care se efectuează tranzacțiile.
Probabil, pentru un blogger Youtube care strânge donații pentru producerea de material nou în cadrul următoarei transmisii live, nu este atât de important cine, când și în ce ordine, i-a aruncat banii.
Când utilizatorii vor rămâne pe aceeași pagină web sau pe aceeași fereastră de aplicație timp de câteva secunde sau chiar minute și, prin urmare, vor vedea cumva date învechite.
Teoretic, acestea sunt orice mass-media de știri online sau același Youtube. Sau „Habr”. Când nu contează pentru dvs. că tranzacțiile incomplete pot fi stocate temporar în sistem, le puteți ignora fără nicio deteriorare.
Dacă agregați date din mai multe surse și date care sunt actualizate cu o frecvență înaltă - de exemplu, date despre ocuparea locurilor de parcare dintr-un oraș care se schimbă cel puțin la fiecare 5 minute, atunci în teorie nu va fi o mare problemă pentru tine daca la un moment dat tranzactia pentru una din parcari nu se va duce. Deși, desigur, depinde ce anume vrei să faci cu aceste date.