7.1 Hvorfor er det nødvendig

Vi har diskutert i noen detalj alle egenskapene til ACID, deres formål og brukstilfeller. Som du kan se, tilbyr ikke alle databaser ACID-garantier, noe som ofrer dem for bedre ytelse. Derfor kan det godt hende at en database som ikke tilbyr ACID velges i prosjektet ditt, og du må kanskje implementere noe av den nødvendige ACID-funksjonaliteten på applikasjonssiden. Og hvis systemet ditt er utformet som mikrotjenester, eller en annen form for distribuert applikasjon, vil det som ville være en vanlig lokal transaksjon i en tjeneste nå bli en distribuert transaksjon - og vil selvfølgelig miste sin ACID-natur, selv om databasen med hver enkelt mikrotjeneste vil være ACID.

Jeg ønsker ikke å gi deg en uttømmende veiledning om hvordan du oppretter en transaksjonsadministrator, rett og slett fordi den er for stor og komplisert, og jeg vil bare dekke noen få grunnleggende teknikker. Hvis vi ikke snakker om distribuerte applikasjoner, så ser jeg ingen grunn til å prøve å fullt ut implementere ACID på applikasjonssiden hvis du trenger ACID-garantier - det vil tross alt være enklere og billigere på alle måter å ta en ferdig løsning ( det vil si en database med ACID).

Men jeg vil gjerne vise deg noen teknikker som vil hjelpe deg med å gjøre transaksjoner på applikasjonssiden. Tross alt kan det å kjenne til disse teknikkene hjelpe deg i en rekke scenarier, selv de som ikke nødvendigvis involverer transaksjoner, og gjøre deg til en bedre utvikler (håper jeg det).

7.2 Grunnleggende verktøy for transaksjonselskere

Optimistisk og pessimistisk blokkering. Dette er to typer låser på enkelte data som kan nås samtidig.

Optimistantar at sannsynligheten for samtidig tilgang ikke er så stor, og derfor gjør den følgende: leser ønsket linje, husker versjonsnummeret (eller tidsstempelet, eller sjekksum / hash - hvis du ikke kan endre dataskjemaet og legge til en kolonne for versjon eller tidsstempel), og før du skriver endringer til databasen for disse dataene, sjekker den om versjonen av disse dataene er endret. Hvis versjonen er endret, må du på en eller annen måte løse den opprettede konflikten og oppdatere dataene ("commit"), eller rulle tilbake transaksjonen ("rollback"). Ulempen med denne metoden er at den skaper gunstige forhold for en feil med det lange navnet "time-of-check to time-of-use", forkortet TOCTOU: tilstanden kan endre seg i tidsrommet mellom sjekk og skriving. Jeg har ingen erfaring med optimistisk låsing,

Som et eksempel fant jeg en teknologi fra en utviklers daglige liv som bruker noe som optimistisk låsing - dette er HTTP-protokollen. Svaret på den innledende HTTP GET-forespørselen KAN inkludere en ETag-header for påfølgende PUT-forespørsler fra klienten, som klienten KAN bruke i If-Match-headeren. For GET- og HEAD-metodene vil serveren sende tilbake den forespurte ressursen bare hvis den samsvarer med en av ET-taggene den kjenner. For PUT og andre usikre metoder vil den bare laste ressursen i dette tilfellet også. Hvis du ikke vet hvordan ETag fungerer, her er et godt eksempel ved å bruke "feedparser"-biblioteket (som hjelper til med å analysere RSS og andre feeds).


>>> import feedparser 
>>> d = feedparser.parse('http://feedparser.org/docs/examples/atom10.xml') 
>>> d.etag 
'"6c132-941-ad7e3080"' 
>>> d2 = feedparser.parse('http://feedparser.org/docs/examples/atom10.xml', etag=d.etag) 
>>> d2.feed 
{} 
>>> d2.debug_message 
'The feed has not changed since you last checked, so the server sent no data.  This is a feature, not a bug!' 

Pessimisten på sin side tar utgangspunkt i at transaksjoner ofte "møtes" på de samme dataene, og for å forenkle livet hans og unngå unødvendige raseforhold, blokkerer han ganske enkelt dataene han trenger. For å implementere låsemekanismen, må du enten opprettholde en databaseforbindelse for økten din (i stedet for å trekke tilkoblinger fra en pool - i så fall må du mest sannsynlig jobbe med optimistisk låsing), eller bruke en ID for transaksjonen , som kan brukes uavhengig av tilkoblingen. Ulempen med pessimistisk låsing er at bruken bremser behandlingen av transaksjoner generelt, men du kan være rolig om dataene og få reell isolasjon.

En ytterligere fare lurer imidlertid i den mulige fastlåsningen, der flere prosesser venter på ressurser låst av hverandre. For eksempel krever en transaksjon ressurs A og B. Prosess 1 har okkupert ressurs A, og prosess 2 har okkupert ressurs B. Ingen av de to prosessene kan fortsette kjøringen. Det er ulike måter å løse dette problemet på – jeg ønsker ikke å gå i detaljer nå, så les Wikipedia først, men kort fortalt er det mulighet for å lage et låshierarki. Hvis du ønsker å bli mer detaljert kjent med dette konseptet, er du invitert til å gruble over "Dining Philosophers Problem" ("spisefilosofers problem").

Her er et godt eksempel på hvordan begge låsene vil oppføre seg i samme scenario.

Angående implementeringer av låser. Jeg ønsker ikke å gå i detaljer, men det finnes låseansvarlige for distribuerte systemer, for eksempel: ZooKeeper, Redis, etcd, Consul.

7.3 Idempotens av operasjoner

Idempotent kode er generelt sett en god praksis, og dette er akkurat tilfellet når det vil være bra for en utvikler å kunne gjøre dette, uavhengig av om han bruker transaksjoner eller ikke. Idempotens er egenskapen til en operasjon for å produsere det samme resultatet når den operasjonen brukes på et objekt igjen. Funksjonen ble kalt - ga resultatet. Ringte igjen etter et sekund eller fem - ga samme resultat. Selvfølgelig, hvis dataene i databasen har endret seg, vil resultatet bli annerledes. Data i tredje systemer er kanskje ikke avhengig av en funksjon, men alt som gjør det må være forutsigbart.

Det kan være flere manifestasjoner av idempotens. En av dem er bare en anbefaling om hvordan du skriver koden din. Husker du at den beste funksjonen er den som gjør én ting? Og hva ville være en god ting å skrive enhetstester for denne funksjonen? Hvis du følger disse to reglene, øker du allerede sjansen for at funksjonene dine blir idempotente. For å unngå forvirring vil jeg presisere at idempotente funksjoner ikke nødvendigvis er "rene" (i betydningen "funksjonsrenhet"). Rene funksjoner er de funksjonene som kun opererer på dataene de mottok ved inngangen, uten å endre dem på noen måte og returnere det behandlede resultatet. Dette er funksjonene som lar deg skalere applikasjonen din ved hjelp av funksjonelle programmeringsteknikker. Siden vi snakker om noen generelle data og en database, er det usannsynlig at funksjonene våre er rene,

Dette er en ren funksjon:


def square(num: int) -> int: 
	return num * num 

Men denne funksjonen er ikke ren, men idempotent (vennligst ikke trekk konklusjoner om hvordan jeg skriver kode fra disse stykkene):


def insert_data(insert_query: str, db_connection: DbConnectionType) -> int: 
  db_connection.execute(insert_query) 
  return True 

I stedet for mange ord, kan jeg bare snakke om hvordan jeg ble tvunget til å lære å skrive idempotente programmer. Jeg jobber mye med AWS, som du kan se nå, og det er en tjeneste som heter AWS Lambda. Lambda lar deg ikke ta vare på servere, men bare laste inn kode som vil kjøre som svar på enkelte hendelser eller i henhold til en tidsplan. En hendelse kan være meldinger som leveres av en meldingsmegler. I AWS er ​​denne megleren AWS SNS. Jeg mener at dette burde være klart selv for de som ikke jobber med AWS: Vi har en megler som sender meldinger gjennom kanaler («topics»), og mikrotjenester som abonnerer på disse kanalene mottar meldinger og reagerer på en eller annen måte.

Problemet er at SNS leverer meldinger «minst én gang» («at-least-once delivery»). Hva betyr det? At Lambda-koden din før eller siden blir oppringt to ganger. Og det skjer virkelig. Det er en rekke scenarier der funksjonen din må være idempotent: for eksempel når penger trekkes fra en konto, kan vi forvente at noen tar ut det samme beløpet to ganger, men vi må sørge for at dette virkelig er 2 uavhengige ganger - med andre ord, dette er 2 forskjellige transaksjoner, og ikke en repetisjon av en.

For en endring vil jeg gi et annet eksempel - å begrense frekvensen av forespørsler til API (“rate limiting”). Lambdaen vår mottar en hendelse med en bestemt user_id som det bør sjekkes for å se om brukeren med den IDen har brukt opp antallet mulige forespørsler til noen av APIene våre. Vi kunne lagre verdien av de foretatte samtalene i DynamoDB fra AWS, og øke den med 1 for hvert kall til funksjonen vår.

Men hva om denne Lambda-funksjonen kalles opp av samme hendelse to ganger? Forresten, la du merke til argumentene til lambda_handler()-funksjonen. Det andre argumentet, kontekst i AWS Lambda er gitt som standard og inneholder ulike metadata, inkludert request_id som genereres for hvert unikt kall. Dette betyr at vi nå, i stedet for å lagre antall anrop gjort i tabellen, kan lagre en liste over request_id og på hver samtale vil Lambdaen vår sjekke om den gitte forespørselen allerede er behandlet:

import json
import os
from typing import Any, Dict

from aws_lambda_powertools.utilities.typing import LambdaContext  # needed only for argument type annotation
import boto3

limit = os.getenv('LIMIT')

def handler_name(event: Dict[str: Any], context: LambdaContext):

	request_id = context.aws_request_id

	# We find user_id in incoming event
	user_id = event["user_id"]

	# Our table for DynamoDB
	table = boto3.resource('dynamodb').Table('my_table')

	# Doing update
	table.update_item(
    	Key={'pkey': user_id},
    	UpdateExpression='ADD requests :request_id',
    	ConditionExpression='attribute_not_exists (requests) OR (size(requests) < :limit AND NOT contains(requests, :request_id))',
    	ExpressionAttributeValues={
        	':request_id': {'S': request_id},
        	':requests': {'SS': [request_id]},
        	':limit': {'N': limit}
    	}
	)

	# TODO: write further logic

	return {
    	"statusCode": 200,
    	"headers": {
        	"Content-Type": "application/json"
    	},
    	"body": json.dumps({
        	"status ": "success"
    	})
	}

Siden eksemplet mitt faktisk er hentet fra Internett, vil jeg legge igjen en lenke til originalkilden, spesielt siden den gir litt mer informasjon.

Husker du hvordan jeg nevnte tidligere at noe som en unik transaksjons-ID kan brukes til å låse delte data? Vi har nå erfart at det også kan brukes til å gjøre operasjoner idempotente. La oss finne ut på hvilke måter du kan generere slike IDer selv.