7.1 Miért szükséges?

Részletesen tárgyaltuk az ACID összes tulajdonságát, célját és felhasználási eseteit. Amint látható, nem minden adatbázis kínál ACID-garanciát, feláldozva azokat a jobb teljesítmény érdekében. Emiatt könnyen előfordulhat, hogy olyan adatbázis kerül kiválasztásra a projektben, amely nem kínál ACID-t, és szükség lehet néhány szükséges ACID-funkció megvalósítására az alkalmazás oldalon. És ha a rendszerét mikroszolgáltatásnak vagy valamilyen más elosztott alkalmazásnak tervezték, akkor ami egy szolgáltatásban egy normál helyi tranzakció lenne, az most elosztott tranzakció lesz - és természetesen elveszíti ACID jellegét, még akkor is, ha az adatbázis minden egyes mikroszolgáltatás ACID lesz.

Nem akarok kimerítő útmutatót adni a tranzakciókezelő létrehozásához, egyszerűen azért, mert túl nagy és bonyolult, és csak néhány alapvető technikát szeretnék ismertetni. Ha nem osztott alkalmazásokról beszélünk, akkor nem látom okát arra, hogy az ACID teljes körű bevezetését az alkalmazási oldalon próbáljuk meg, ha ACID garanciák kellenek - elvégre minden szempontból egyszerűbb és olcsóbb lesz kész megoldást venni ( azaz egy adatbázis ACID-vel).

De szeretnék néhány technikát bemutatni, amelyek segítenek a tranzakciók lebonyolításában az alkalmazási oldalon. Végül is ezeknek a technikáknak az ismerete sokféle forgatókönyvben segíthet, még olyan esetekben is, amelyek nem feltétlenül járnak tranzakciókkal, és jobb fejlesztővé teheti Önt (remélem).

7.2 Alapvető eszközök a tranzakciók szerelmeseinek

Optimista és pesszimista blokkolás. Ez kétféle zárolás egyes adatoknál, amelyek egyidejűleg elérhetők.

Optimistafeltételezi, hogy az egyidejű hozzáférés valószínűsége nem olyan nagy, ezért a következőket teszi: beolvassa a kívánt sort, megjegyzi a verziószámát (vagy időbélyeget, vagy ellenőrző összeget / hash - ha nem tudja megváltoztatni az adatsémát és hozzáadni egy oszlopot a verzióhoz vagy időbélyeg), és mielőtt változtatásokat írna az adatok adatbázisába, ellenőrzi, hogy az adatok verziója nem változott-e. Ha a verzió megváltozott, akkor valahogyan meg kell oldania a keletkezett ütközést és frissítenie kell az adatokat („commit”), vagy vissza kell állítania a tranzakciót („rollback”). Ennek a módszernek az a hátránya, hogy kedvező feltételeket teremt a hosszú elnevezésű „időtől a használatig” hibához, rövidítve TOCTOU: az állapot változhat az ellenőrzés és az írás közötti időtartamban. Nincs tapasztalatom az optimista zárolásról,

Példaként találtam egy technológiát egy fejlesztő mindennapi életéből, amely olyasmit használ, mint az optimista zárolás – ez a HTTP protokoll. A kezdeti HTTP GET kérésre adott válasz tartalmazhat egy ETag fejlécet az ügyféltől érkező további PUT kérésekhez, amelyet az ügyfél használhat az If-Match fejlécben. A GET és HEAD metódusok esetén a szerver csak akkor küldi vissza a kért erőforrást, ha az megfelel az általa ismert egyik ETagnek. A PUT és más nem biztonságos módszerek esetében ebben az esetben is csak az erőforrást tölti be. Ha nem ismeri az ETag működését, íme egy jó példa a "feedparser" könyvtár használatára (amely segít az RSS és más hírcsatornák elemzésében).


>>> 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!' 

A pesszimista viszont abból indul ki, hogy a tranzakciók gyakran ugyanazokon az adatokon „találkoznak”, és élete egyszerűsítése és a felesleges versenyfeltételek elkerülése érdekében egyszerűen blokkolja a szükséges adatokat. A zárolási mechanizmus megvalósításához vagy fenn kell tartania egy adatbázis-kapcsolatot a munkamenethez (ahelyett, hogy egy készletből vonja le a kapcsolatokat - ebben az esetben valószínűleg optimista zárolást kell használnia), vagy használjon egy azonosítót a tranzakcióhoz. , amely a csatlakozástól függetlenül használható. A pesszimista zárolás hátránya, hogy használata általában lelassítja a tranzakciók feldolgozását, de nyugodt lehet az adatokkal kapcsolatban, és valódi elszigeteltséget kaphat.

További veszély azonban leselkedik az esetleges holtpontra, amelyben több folyamat egymás által zárolt erőforrásokra vár. Például egy tranzakcióhoz A és B erőforrásokra van szükség. Az 1. folyamat az A erőforrást, a 2. folyamat pedig a B erőforrást foglalta el. A két folyamat egyike sem tudja folytatni a végrehajtást. A probléma megoldásának többféle módja van – most nem akarok részletekbe menni, ezért először olvassa el a Wikipédiát, de röviden, lehetőség van zárhierarchia létrehozására. Ha szeretné részletesebben megismerni ezt a fogalmat, akkor felkérjük Önt, hogy törje a fejét a „Dinning Philosophers Problems” („Dinning Philosophers Problems”) („Dinning Philosophers Problems”).

Íme egy jó példa arra, hogy mindkét zár hogyan fog viselkedni ugyanabban a forgatókönyvben.

A zárak megvalósításával kapcsolatban. Nem akarok belemenni a részletekbe, de vannak zárkezelők az elosztott rendszerekhez, pl.: ZooKeeper, Redis, etcd, Consul.

7.3 A műveletek identitása

Az Idempotens kód általában bevált gyakorlat, és pontosan ez az az eset, amikor jó lenne, ha egy fejlesztő ezt megteheti, függetlenül attól, hogy használ-e tranzakciókat vagy sem. Az idempotencia egy művelet azon tulajdonsága, hogy ugyanazt az eredményt hozza, amikor a műveletet ismét alkalmazzák egy objektumra. A függvényt hívták - adta meg az eredményt. Egy vagy öt másodperc múlva újra hívták - ugyanazt az eredményt adta. Természetesen, ha az adatbázisban lévő adatok megváltoztak, akkor az eredmény más lesz. A harmadik rendszerekben lévő adatok nem függhetnek egy függvénytől, de bárminek, ami függ, előre láthatónak kell lennie.

Az idempotenciának számos megnyilvánulása lehet. Az egyik csak egy ajánlás a kód megírására. Emlékszel, hogy a legjobb funkció az, amelyik egy dolgot csinál? És mire lenne jó egységteszteket írni ehhez a függvényhez? Ha betartja ezt a két szabályt, máris növeli annak esélyét, hogy funkciói idempotensek legyenek. A félreértések elkerülése végett tisztázom, hogy az idempotens funkciók nem feltétlenül „tiszták” (a „funkciótisztaság” értelmében). A tiszta függvények azok a függvények, amelyek csak a bemeneten kapott adatokkal működnek, anélkül, hogy azokat bármilyen módon megváltoztatnák és visszaadnák a feldolgozott eredményt. Ezek azok a funkciók, amelyek lehetővé teszik az alkalmazás méretezését funkcionális programozási technikák segítségével. Mivel néhány általános adatról és adatbázisról beszélünk, a funkcióink valószínűleg nem lesznek tiszták,

Ez egy tiszta funkció:


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

De ez a függvény nem tiszta, hanem idempotens (kérem, ne vonjon le következtetéseket arra vonatkozóan, hogyan írok kódot ezekből a darabokból):


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

Sok szó helyett csak arról tudok beszélni, hogy kénytelen voltam megtanulni idempotens programokat írni. Sokat dolgozom az AWS-sel, ahogy mostanra is látható, és van egy AWS Lambda nevű szolgáltatás. A Lambda lehetővé teszi, hogy ne gondoskodjon a szerverekről, hanem egyszerűen olyan kódot töltsön be, amely bizonyos eseményekre válaszul vagy ütemezés szerint fut. Egy esemény lehet üzenet, amelyet egy üzenetközvetítő kézbesít. Az AWS-ben ez a bróker az AWS SNS. Szerintem ez azoknak is világos legyen, akik nem dolgoznak az AWS-szel: van egy közvetítőnk, aki csatornákon ("témákon") küld üzeneteket, és az ezekre a csatornákra előfizetett mikroszolgáltatások fogadják az üzeneteket, és valahogy reagálnak rájuk.

A probléma az, hogy az SNS "legalább egyszer" ("least-one szállítás") kézbesíti az üzeneteket. Mit jelent? Hogy előbb-utóbb kétszer hívják a Lambda kódot. És tényleg megtörténik. Számos forgatókönyv létezik, amikor a funkciójának idempotensnek kell lennie: például amikor pénzt vesznek fel egy számláról, akkor azt várhatjuk, hogy valaki kétszer vegye ki ugyanazt az összeget, de meg kell győződnünk arról, hogy ez valóban 2 független alkalom - más szóval, ez 2 különböző tranzakció, és nem egy megismétlése.

A változtatás kedvéért adok egy másik példát - a kérések gyakoriságának korlátozását az API-ra („sebességkorlátozás”). Lambdánk egy bizonyos user_id-vel rendelkező eseményt kap, amelynél ellenőrizni kell, hogy az ezzel az azonosítóval rendelkező felhasználó kimerítette-e a lehetséges kérések számát néhány API-nkhoz. A DynamoDB-ben tárolhatjuk az AWS-ből a kezdeményezett hívások értékét, és minden függvényhívásnál 1-gyel növelhetjük.

De mi van akkor, ha ezt a lambda-függvényt ugyanaz az esemény kétszer hívja meg? Egyébként odafigyeltél a lambda_handler() függvény argumentumaira? A második argumentum, az AWS Lambda kontextusa alapértelmezés szerint adott, és különféle metaadatokat tartalmaz, beleértve az egyes egyedi hívásokhoz generált request_id-t. Ez azt jelenti, hogy most a hívások számának táblázatban való tárolása helyett tárolhatunk egy request_id listát, és minden hívásnál a Lambda ellenőrzi, hogy az adott kérést feldolgozták-e már:

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"
    	})
	}

Mivel a példám valójában az internetről származik, hagyok egy hivatkozást az eredeti forrásra, főleg, hogy egy kicsit több információt ad.

Emlékszel, hogyan említettem korábban, hogy például egy egyedi tranzakcióazonosító használható a megosztott adatok zárolására? Most megtudtuk, hogy a műveletek idempotenssé tételére is használható. Nézzük meg, milyen módon hozhat létre ilyen azonosítókat saját maga.