7.1 Bakit kailangan

Tinalakay namin sa ilang mga detalye ang lahat ng mga katangian ng ACID, ang kanilang layunin at mga kaso ng paggamit. Tulad ng nakikita mo, hindi lahat ng database ay nag-aalok ng mga garantiya ng ACID, sinasakripisyo ang mga ito para sa mas mahusay na pagganap. Samakatuwid, maaaring mangyari na ang isang database na hindi nag-aalok ng ACID ay napili sa iyong proyekto, at maaaring kailanganin mong ipatupad ang ilan sa mga kinakailangang ACID functionality sa bahagi ng application. At kung ang iyong system ay idinisenyo bilang mga microservice, o ilang iba pang uri ng ipinamahagi na aplikasyon, kung ano ang magiging normal na lokal na transaksyon sa isang serbisyo ay magiging isang ipinamahagi na transaksyon - at, siyempre, mawawala ang pagiging ACID nito, kahit na ang database ng magiging ACID ang bawat indibidwal na microservice.

Hindi ko nais na bigyan ka ng isang kumpletong gabay sa kung paano lumikha ng isang tagapamahala ng transaksyon, dahil lamang ito ay masyadong malaki at kumplikado, at gusto ko lamang na masakop ang ilang mga pangunahing pamamaraan. Kung hindi namin pinag-uusapan ang tungkol sa mga ibinahagi na aplikasyon, kung gayon wala akong nakikitang dahilan upang subukang ganap na ipatupad ang ACID sa panig ng aplikasyon kung kailangan mo ng mga garantiya ng ACID - pagkatapos ng lahat, ito ay magiging mas madali at mas mura sa bawat kahulugan na kumuha ng isang handa na solusyon ( iyon ay, isang database na may ACID).

Ngunit nais kong ipakita sa iyo ang ilang mga diskarte na makakatulong sa iyo sa paggawa ng mga transaksyon sa panig ng aplikasyon. Pagkatapos ng lahat, ang pag-alam sa mga diskarteng ito ay makakatulong sa iyo sa iba't ibang mga sitwasyon, kahit na ang mga hindi kinakailangang kasangkot sa mga transaksyon, at gawin kang isang mas mahusay na developer (sana nga).

7.2 Mga pangunahing tool para sa mga mahilig sa transaksyon

Optimistic at pesimistikong pagharang. Ito ay dalawang uri ng mga lock sa ilang data na maaaring ma-access sa parehong oras.

OptimistIpinapalagay na ang posibilidad ng kasabay na pag-access ay hindi masyadong malaki, at samakatuwid ay ginagawa nito ang sumusunod: binabasa ang nais na linya, naaalala ang numero ng bersyon nito (o timestamp, o checksum / hash - kung hindi mo mababago ang schema ng data at magdagdag ng column para sa bersyon o timestamp), at bago isulat ang mga pagbabago sa database para sa data na ito, sinusuri nito kung nagbago ang bersyon ng data na ito. Kung nagbago ang bersyon, kailangan mong lutasin ang nilikhang salungatan at i-update ang data ("commit"), o ibalik ang transaksyon ("rollback"). Ang kawalan ng pamamaraang ito ay lumilikha ito ng mga paborableng kundisyon para sa isang bug na may mahabang pangalan na "oras ng pag-check sa oras ng paggamit", na dinaglat bilang TOCTOU: maaaring magbago ang estado sa tagal ng panahon sa pagitan ng check at write. Wala akong karanasan sa optimistic lock,

Bilang halimbawa, nakakita ako ng isang teknolohiya mula sa pang-araw-araw na buhay ng isang developer na gumagamit ng isang bagay tulad ng optimistic locking - ito ang HTTP protocol. MAAARING may kasamang ETag header ang tugon sa paunang kahilingan sa HTTP GET para sa mga kasunod na kahilingan ng PUT mula sa kliyente, na MAAARING gamitin ng kliyente sa header ng If-Match. Para sa mga pamamaraan ng GET at HEAD, ibabalik lang ng server ang hiniling na mapagkukunan kung tumutugma ito sa isa sa mga ETag na alam nito. Para sa PUT at iba pang hindi ligtas na pamamaraan, ilo-load lang nito ang mapagkukunan sa kasong ito rin. Kung hindi mo alam kung paano gumagana ang ETag, narito ang isang magandang halimbawa gamit ang library ng "feedparser" (na tumutulong sa pag-parse ng RSS at iba pang mga feed).


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

Ang pessimist, sa kabilang banda, ay nagpapatuloy mula sa katotohanan na ang mga transaksyon ay madalas na "matugunan" sa parehong data, at upang gawing simple ang kanyang buhay at maiwasan ang hindi kinakailangang mga kondisyon ng lahi, hinaharangan niya lamang ang data na kailangan niya. Upang maipatupad ang mekanismo ng pag-lock, kailangan mong magpanatili ng koneksyon sa database para sa iyong session (sa halip na humila ng mga koneksyon mula sa isang pool - kung saan malamang na kailangan mong magtrabaho nang may optimistikong pag-lock), o gumamit ng ID para sa transaksyon , na maaaring gamitin anuman ang koneksyon. Ang kawalan ng pessimistic locking ay ang paggamit nito ay nagpapabagal sa pagproseso ng mga transaksyon sa pangkalahatan, ngunit maaari kang maging kalmado tungkol sa data at makakuha ng tunay na paghihiwalay.

Ang karagdagang panganib, gayunpaman, ay nakatago sa posibleng deadlock, kung saan maraming proseso ang naghihintay para sa mga mapagkukunan na naka-lock ng bawat isa. Halimbawa, ang isang transaksyon ay nangangailangan ng mga mapagkukunan A at B. Ang Proseso 1 ay sumakop sa mapagkukunan A, at ang proseso 2 ay sumakop sa mapagkukunan B. Wala alinman sa dalawang proseso ang maaaring magpatuloy sa pagpapatupad. Mayroong iba't ibang mga paraan upang malutas ang isyung ito - Hindi ko nais na pumunta sa mga detalye ngayon, kaya basahin muna ang Wikipedia, ngunit sa madaling salita, may posibilidad na lumikha ng isang hierarchy ng lock. Kung gusto mong malaman ang konseptong ito nang mas detalyado, pagkatapos ay iniimbitahan kang i-rack ang iyong mga utak sa “Dinning Philosophers Problem” (“dining philosophers problem”).

Narito ang isang magandang halimbawa kung paano gagana ang parehong mga kandado sa parehong senaryo.

Tungkol sa pagpapatupad ng mga kandado. Hindi ko gustong pumunta sa mga detalye, ngunit may mga lock manager para sa mga distributed system, halimbawa: ZooKeeper, Redis, etcd, Consul.

7.3 Idempotency ng mga operasyon

Ang idempotent code sa pangkalahatan ay isang mahusay na kasanayan, at ito ay eksakto kung kailan ito ay magiging mabuti para sa isang developer na magawa ito, hindi alintana kung siya ay gumagamit ng mga transaksyon o hindi. Ang Idempotency ay ang pag-aari ng isang operasyon upang makagawa ng parehong resulta kapag ang operasyon na iyon ay inilapat muli sa isang bagay. Tinawag ang function - nagbigay ng resulta. Tumawag muli pagkatapos ng isang segundo o limang - nagbigay ng parehong resulta. Siyempre, kung ang data sa database ay nagbago, ang resulta ay iba. Ang data sa mga ikatlong system ay maaaring hindi nakadepende sa isang function, ngunit ang anumang gagawin ay dapat na predictable.

Maaaring may ilang mga pagpapakita ng idempotency. Ang isa sa mga ito ay isang rekomendasyon lamang kung paano isulat ang iyong code. Naaalala mo ba na ang pinakamahusay na function ay ang gumagawa ng isang bagay? At ano ang magandang bagay na magsulat ng mga unit test para sa function na ito? Kung susundin mo ang dalawang panuntunang ito, pinapataas mo na ang pagkakataon na ang iyong mga function ay magiging idempotent. Upang maiwasan ang pagkalito, lilinawin ko na ang mga idempotent function ay hindi kinakailangang "dalisay" (sa kahulugan ng "function na kadalisayan"). Ang mga purong function ay ang mga function na gumagana lamang sa data na kanilang natanggap sa input, nang hindi binabago ang mga ito sa anumang paraan at ibinabalik ang naprosesong resulta. Ito ang mga function na nagbibigay-daan sa iyong sukatin ang iyong aplikasyon gamit ang mga functional na diskarte sa programming. Dahil pinag-uusapan natin ang tungkol sa ilang pangkalahatang data at isang database, malamang na hindi malinis ang ating mga function,

Ito ay isang purong function:


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

Ngunit ang function na ito ay hindi dalisay, ngunit idempotent (mangyaring huwag gumawa ng mga konklusyon tungkol sa kung paano ako sumulat ng code mula sa mga piraso):


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

Sa halip na maraming salita, maaari ko na lamang pag-usapan kung paano ako napilitang matuto kung paano magsulat ng mga programang idempotent. Marami akong ginagawa sa AWS, gaya ng nakikita mo ngayon, at mayroong serbisyong tinatawag na AWS Lambda. Pinapayagan ka ng Lambda na huwag pangalagaan ang mga server, ngunit i-load lamang ang code na tatakbo bilang tugon sa ilang mga kaganapan o ayon sa isang iskedyul. Ang isang kaganapan ay maaaring mga mensahe na inihahatid ng isang broker ng mensahe. Sa AWS, ang broker na ito ay AWS SNS. Sa tingin ko, dapat itong maging malinaw kahit na para sa mga hindi nagtatrabaho sa AWS: mayroon kaming broker na nagpapadala ng mga mensahe sa pamamagitan ng mga channel ("mga paksa"), at ang mga microservice na naka-subscribe sa mga channel na ito ay tumatanggap ng mga mensahe at kahit papaano ay tumutugon sila.

Ang problema ay ang SNS ay naghahatid ng mga mensahe "kahit isang beses" ("hindi bababa sa isang beses na paghahatid"). Ano ang ibig sabihin nito? Na maaga o huli ang iyong Lambda code ay tatawagin nang dalawang beses. At talagang nangyayari ito. Mayroong ilang mga sitwasyon kung saan ang iyong function ay kailangang maging idempotent: halimbawa, kapag ang pera ay na-withdraw mula sa isang account, maaari naming asahan ang isang tao na mag-withdraw ng parehong halaga nang dalawang beses, ngunit kailangan naming tiyakin na ito ay talagang 2 independiyenteng mga oras - sa madaling salita, ito ay 2 magkaibang transaksyon, at hindi pag-uulit ng isa.

Para sa isang pagbabago, magbibigay ako ng isa pang halimbawa - nililimitahan ang dalas ng mga kahilingan sa API ("paglilimita sa rate"). Ang aming Lambda ay tumatanggap ng isang kaganapan na may isang partikular na user_id kung saan ang isang pagsusuri ay dapat gawin upang makita kung ang user na may ID na iyon ay naubos na ang kanyang bilang ng mga posibleng kahilingan sa ilan sa aming mga API. Maaari naming iimbak sa DynamoDB mula sa AWS ang halaga ng mga tawag na ginawa, at dagdagan ito sa bawat tawag sa aming function ng 1.

Ngunit paano kung ang Lambda function na ito ay tinawag ng parehong kaganapan nang dalawang beses? Sa pamamagitan ng paraan, binigyan mo ba ng pansin ang mga argumento ng lambda_handler() function. Ang pangalawang argumento, ang konteksto sa AWS Lambda ay ibinibigay bilang default at naglalaman ng iba't ibang metadata, kasama ang request_id na nabuo para sa bawat natatanging tawag. Nangangahulugan ito na ngayon, sa halip na iimbak ang bilang ng mga tawag na ginawa sa talahanayan, maaari kaming mag-imbak ng isang listahan ng request_id at sa bawat tawag ay titingnan ng aming Lambda kung ang ibinigay na kahilingan ay naproseso na:

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

Dahil ang aking halimbawa ay talagang kinuha mula sa Internet, mag-iiwan ako ng isang link sa orihinal na pinagmulan, lalo na dahil nagbibigay ito ng kaunti pang impormasyon.

Tandaan kung paano ko nabanggit kanina na ang isang bagay tulad ng isang natatanging transaction ID ay maaaring gamitin upang i-lock ang nakabahaging data? Nalaman na natin ngayon na maaari rin itong gamitin para gawing idempotent ang mga operasyon. Alamin natin kung paano ka makakabuo ng mga ganoong ID sa iyong sarili.