8.1 Transaktions-ID:n
Det är betecknat som XID eller TxID (om det finns en skillnad, berätta för mig). Tidsstämplar kan användas som TxID, vilket kan spela i händerna om vi vill återställa alla åtgärder till någon tidpunkt. Problemet kan uppstå om tidsstämpeln inte är tillräckligt detaljerad – då kan transaktioner få samma ID.
Därför är det mest pålitliga alternativet att generera unika UUID-prod-ID:n. I Python är detta väldigt enkelt:
>>> import uuid
>>> str(uuid.uuid4())
'f50ec0b7-f960-400d-91f0-c42a6d44e3d0'
>>> str(uuid.uuid4())
'd15bed89-c0a5-4a72-98d9-5507ea7bc0ba'
Det finns också ett alternativ att hasha en uppsättning transaktionsdefinierande data och använda denna hash som TxID.
8.2 Försök igen
Om vi vet att en viss funktion eller ett visst program är idempotent, betyder det att vi kan och bör försöka upprepa dess anrop i händelse av ett fel. Och vi måste bara vara beredda på att någon operation kommer att ge ett fel - med tanke på att moderna applikationer distribueras över nätverket och hårdvaran, bör felet inte betraktas som ett undantag, utan som normen. Felet kan uppstå på grund av en serverkrasch, nätverksfel, överbelastning av fjärrapplikationer. Hur ska vår applikation bete sig? Det stämmer, försök att upprepa operationen.
Eftersom ett stycke kod kan säga mer än en hel sida med ord, låt oss använda ett exempel för att förstå hur den naiva återförsöksmekanismen helst borde fungera. Jag ska demonstrera detta med hjälp av Tenacity-biblioteket (det är så väldesignat att även om du inte planerar att använda det, bör exemplet visa dig hur du kan designa upprepningsmekanismen):
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)
> För säkerhets skull säger jag: \@retry(...) är en speciell Python-syntax som kallas "decorator". Det är bara en funktion för att försöka igen(...) som omsluter en annan funktion och gör något före eller efter att den körs.
Som vi kan se kan återförsök utformas kreativt:
- Du kan begränsa försöken efter tid (10 sekunder) eller antal försök (5).
- Kan vara exponentiell (det vill säga 2 ** något ökande antal n ). eller på något annat sätt (till exempel fixat) för att öka tiden mellan separata försök. Den exponentiella varianten kallas "congestion collapse".
- Du kan bara försöka igen för vissa typer av fel (IOError).
- Försök igen kan föregås eller kompletteras av några speciella poster i loggen.
Nu när vi har genomfört den unga fighterkursen och känner till de grundläggande byggstenarna som vi behöver för att arbeta med transaktioner på applikationssidan, låt oss bekanta oss med två metoder som gör att vi kan implementera transaktioner i distribuerade system.
8.3 Avancerade verktyg för transaktionsälskare
Jag kommer bara att ge ganska generella definitioner, eftersom detta ämne är värt en separat stor artikel.
Tvåfas commit (2st) . 2pc har två faser: en förberedelsefas och en commitfas. Under förberedelsefasen kommer alla mikrotjänster att uppmanas att förbereda sig för vissa dataändringar som kan göras atomärt. När de alla är klara kommer commit-fasen att göra de faktiska ändringarna. För att koordinera processen behövs en global koordinator som låser de nödvändiga objekten – det vill säga de blir otillgängliga för ändringar tills koordinatorn låser upp dem. Om en viss mikrotjänst inte är redo för ändringar (till exempel inte svarar), kommer samordnaren att avbryta transaktionen och påbörja återställningsprocessen.
Varför är detta protokoll bra? Det ger atomicitet. Dessutom garanterar det isolering vid skrivning och läsning. Detta innebär att ändringar av en transaktion inte är synliga för andra förrän samordnaren åtar sig ändringarna. Men dessa egenskaper har också en nackdel: eftersom detta protokoll är synkront (blockerande) saktar det ner systemet (trots att själva RPC-anropet är ganska långsamt). Och återigen finns det en risk för ömsesidig blockering.
Saga . I detta mönster exekveras en distribuerad transaktion av asynkrona lokala transaktioner över alla associerade mikrotjänster. Mikrotjänster kommunicerar med varandra via en eventbuss. Om någon mikrotjänst misslyckas med att slutföra sin lokala transaktion kommer andra mikrotjänster att utföra kompenserande transaktioner för att återställa ändringarna.
Fördelen med Saga är att inga objekt blockeras. Men det finns förstås nackdelar.
Saga är svår att felsöka, speciellt när det finns många mikrotjänster inblandade. En annan nackdel med Saga-mönstret är att det saknar läsisolering. Det vill säga, om egenskaperna som anges i ACID är viktiga för oss, så är Saga inte särskilt lämplig för oss.
Vad ser vi av beskrivningen av dessa två tekniker? Det faktum att i distribuerade system ligger ansvaret för atomicitet och isolering hos applikationen. Samma sak händer när man använder databaser som inte ger ACID-garantier. Det vill säga saker som konfliktlösning, rollbacks, åtaganden och att frigöra utrymme faller på utvecklarens axlar.
8.4 Hur vet jag när jag behöver SYRA-garantier?
När det är stor sannolikhet att en viss uppsättning användare eller processer samtidigt kommer att arbeta på samma data .
Ursäkta banaliteten, men ett typiskt exempel är finansiella transaktioner.
När ordningen i vilka transaktioner utförs spelar roll.
Föreställ dig att ditt företag är på väg att byta från FunnyYellowChat messenger till FunnyRedChat messenger, eftersom FunnyRedChat låter dig skicka gifs, men FunnyYellowChat kan det inte. Men du byter inte bara budbäraren – du migrerar ditt företags korrespondens från en budbärare till en annan. Det gör du för att dina programmerare var för lata för att dokumentera program och processer någonstans centralt och istället publicerade de allt i olika kanaler i messengern. Ja, och dina säljare publicerade detaljerna om förhandlingar och avtal på samma plats. Kort sagt, hela livet för ditt företag finns där, och eftersom ingen har tid att överföra det hela till en tjänst för dokumentation, och sökningen efter snabbmeddelanden fungerar bra, bestämde du dig istället för att rensa spillrorna för att helt enkelt kopiera alla meddelanden till en ny plats. Ordningen på meddelandena är viktig
Förresten, för korrespondens i en budbärare är ordningen generellt sett viktig, men när två personer skriver något i samma chatt samtidigt, så är det i allmänhet inte så viktigt vems meddelande som visas först. Så för detta specifika scenario skulle ACID inte behövas.
Ett annat möjligt exempel är bioinformatik. Jag förstår inte det här alls, men jag antar att ordningen är viktig när man ska dechiffrera det mänskliga genomet. Jag hörde dock att bioinformatiker i allmänhet använder en del av sina verktyg till allt – kanske har de egna databaser.
När du inte kan ge en användare eller bearbeta inaktuella data.
Och återigen - finansiella transaktioner. För att vara ärlig kunde jag inte komma på något annat exempel.
När pågående transaktioner är förknippade med betydande kostnader. Föreställ dig vilka problem som kan uppstå när en läkare och en sjuksköterska både uppdaterar en patientjournal och raderar varandras ändringar samtidigt, eftersom databasen inte kan isolera transaktioner. Sjukvårdssystemet är ett annat område, förutom ekonomi, där ACID-garantier tenderar att vara kritiska.
8.5 När behöver jag inte ACID?
När användare bara uppdaterar en del av sina privata data.
En användare lämnar till exempel kommentarer eller klisterlappar på en webbsida. Eller redigerar personuppgifter på ett personligt konto hos en leverantör av någon tjänst.
När användare inte uppdaterar data alls, utan bara kompletterar med nya (bifoga).
Till exempel en löpande applikation som sparar data på dina löpningar: hur mycket du sprungit, för vilken tid, rutt, etc. Varje ny körning är ny data, och de gamla redigeras inte alls. Kanske, baserat på data, får du analyser - och bara NoSQL-databaser är bra för detta scenario.
När affärslogik inte avgör behovet av en viss ordning i vilken transaktioner utförs.
För en Youtube-bloggare som samlar in donationer för produktion av nytt material under nästa direktsändning är det förmodligen inte så viktigt vem, när och i vilken ordning som kastade pengar till honom.
När användare stannar på samma webbsida eller applikationsfönster i flera sekunder eller till och med minuter, och därför kommer de på något sätt att se inaktuella data.
Teoretiskt sett är dessa nyhetsmedier online, eller samma Youtube. Eller "Habr". När det inte spelar någon roll för dig att ofullständiga transaktioner tillfälligt kan lagras i systemet, kan du ignorera dem utan skada.
Om du aggregerar data från många källor, och data som uppdateras med hög frekvens - till exempel data om beläggning av parkeringsplatser i en stad som ändras minst var 5:e minut, så kommer det i teorin inte att vara ett stort problem för dig om någon gång transaktionen för en av parkeringsplatserna inte går igenom. Även om det naturligtvis beror på vad exakt du vill göra med denna data.