7.1 Защо е необходимо

Обсъдихме подробно всички свойства на ACID, тяхната цел и случаи на употреба. Както можете да видите, не всички бази данни предлагат ACID гаранции, жертвайки ги за по-добра производителност. Следователно може да се случи база данни, която не предлага ACID, да бъде избрана във вашия проект и може да се наложи да внедрите някои от необходимите ACID функционалности от страна на приложението. И ако вашата система е проектирана като микроуслуги or няHowъв друг вид разпределено приложение, това, което би било нормална локална транзакция в една услуга, сега ще се превърне в разпределена транзакция - и, разбира се, ще загуби своя ACID характер, дори ако базата данни на всяка отделна микроуслуга ще бъде ACID.

Не искам да ви давам изчерпателно ръководство за това How да създадете мениджър на транзакции, просто защото е твърде голямо и сложно и искам да покрия само няколко основни техники. Ако не говорим за разпределени applications, тогава не виждам причина да се опитвате да внедрите напълно ACID от страна на приложението, ако имате нужда от гаранции за ACID - в крайна сметка ще бъде по-лесно и по-евтино във всеки смисъл да вземете готово решение ( тоест база данни с ACID).

Но бих искал да ви покажа някои техники, които ще ви помогнат да извършвате транзакции от страна на приложението. В края на краищата познаването на тези техники може да ви помогне в различни сценарии, дори и такива, които не включват непременно транзакции, и да ви направи по-добър разработчик (надявам се).

7.2 Основни инструменти за любителите на транзакциите

Оптимистично и песимистично блокиране. Това са два типа заключения на някои данни, които могат да бъдат достъпни едновременно.

Оптимистпредполага, че вероятността за едновременен достъп не е толкова голяма и следователно прави следното: чете желания ред, запомня номера на неговата version (or времева клеймо, or контролна сума / хеш - ако не можете да промените схемата за данни и добавете колона за version or времево клеймо) и преди да напише промени в базата данни за тези данни, той проверява дали versionта на тези данни се е променила. Ако versionта се е променила, тогава трябва по няHowъв начин да разрешите създадения конфликт и да актуализирате данните („commit“) or да върнете транзакцията („rollback“). Недостатъкът на този метод е, че създава благоприятни условия за грешка с дългото име „от време на проверка до време на използване“, съкратено като TOCTOU: състоянието може да се промени в периода от време между проверката и записа. Нямам опит с оптимистично заключване,

Като пример открих една технология от ежедневието на програмиста, която използва нещо като оптимистично заключване - това е HTTP протоколът. Отговорът на първоначалната HTTP GET заявка МОЖЕ да включва ETag заглавка за последващи PUT заявки от клиента, която клиентът МОЖЕ да използва в заглавката If-Match. За методите GET и HEAD сървърът ще изпрати обратно искания ресурс само ако съвпада с един от познатите му ETags. За PUT и други несигурни методи, той ще зареди ресурса само в този случай. Ако не знаете How работи ETag, ето един добър пример за използване на библиотеката "feedparser" (която помага за анализиране на RSS и други канали).


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

Песимистът, от друга страна, изхожда от факта, че транзакциите често се „срещат“ на едни и същи данни и за да опрости живота си и да избегне ненужни условия на състезание, той просто блокира данните, от които се нуждае. За да приложите заключващия механизъм, трябва or да поддържате връзка с база данни за вашата сесия (instead of да изтегляте връзки от пул - в който случай най-вероятно ще трябва да работите с оптимистично заключване), or да използвате идентификатор за транзакция , който може да се използва независимо от връзката. Недостатъкът на песимистичното заключване е, че използването му забавя обработката на транзакциите като цяло, но можете да сте спокойни за данните и да получите истинска изолация.

Допълнителна опасност обаче дебне възможната безизходица, при която няколко процеса чакат ресурси, заключени един от друг. Например една транзакция изисква ресурси A и B. Процес 1 е заел ресурс A, а процес 2 е заел ресурс B. Нито един от двата процеса не може да продължи изпълнението. Има различни начини за решаване на този проблем - не искам да навлизам в подробности сега, така че първо прочетете Wikipedia, но накратко, има възможност за създаване на йерархия на заключване. Ако искате да се запознаете с тази концепция по-подробно, вие сте поканени да разбиете мозъка си върху „проблема с философите за хранене“ („проблемът с философите за хранене“).

Ето един добър пример за това How двете ключалки ще се държат в един и същ сценарий.

Относно внедряването на ключалки. Не искам да навлизам в подробности, но има мениджъри за заключване за разпределени системи, например: ZooKeeper, Redis и т.н., Consul.

7.3 Идемпотентност на операциите

Идемпотентният code като цяло е добра практика и това е точно случаят, когато би било добре разработчикът да може да прави това, независимо дали използва транзакции or не. Идемпотентността е свойството на една операция да дава същия резултат, когато тази операция се приложи отново към обект. Функцията беше извикана - даде резултата. Обади се отново след секунда or пет - даде същия резултат. Разбира се, ако данните в базата данни са се променor, резултатът ще бъде различен. Данните в трети системи може да не зависят от функция, но всичко, което го прави, трябва да бъде предсказуемо.

Може да има няколко прояви на идемпотентност. Един от тях е просто препоръка How да напишете своя code. Помните ли, че най-добрата функция е тази, която прави едно нещо? И Howво би било добре да се пишат модулни тестове за тази функция? Ако се придържате към тези две правила, тогава вече увеличавате шанса вашите функции да бъдат идемпотентни. За да избегна объркване, ще изясня, че идемпотентните функции не са непременно „чисти“ (в смисъл на „чистота на функция“). Чистите функции са тези функции, които работят само с данните, които са получor на входа, без да ги променят по ниHowъв начин и да връщат обработения резултат. Това са функциите, които ви позволяват да мащабирате вашето приложение с помощта на техники за функционално програмиране. Тъй като говорим за някои общи данни и база данни, нашите функции е малко вероятно да бъдат чисти,

Това е чиста функция:


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

Но тази функция не е чиста, а идемпотентна (моля, не правете заключения за това How пиша code от тези части):


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

Вместо много думи, мога просто да говоря за това How бях принуден да се науча да пиша идемпотентни програми. Работя много с AWS, Howто можете да видите досега, и има услуга, наречена AWS Lambda. Lambda ви позволява да не се грижите за сървърите, а просто да заредите code, който ще се изпълнява в отговор на някои събития or според график. Едно събитие може да бъде съобщения, които се доставят от брокер на съобщения. В AWS този брокер е AWS SNS. Мисля, че това трябва да е ясно дори за тези, които не работят с AWS: имаме брокер, който изпраща съобщения през канали („теми“), а микроуслугите, които са абонирани за тези канали, получават съобщения и по няHowъв начин реагират на тях.

Проблемът е, че SNS доставя съобщения "поне веднъж" ("поне веднъж доставка"). Какво означава? Че рано or късно вашият ламбда code ще бъде извикан два пъти. И това наистина се случва. Има редица сценарии, при които функцията ви трябва да бъде идемпотентна: например, когато се изтеглят пари от сметка, можем да очакваме някой да изтегли същата сума два пъти, но трябва да сме сигурни, че това наистина са 2 независими времена - с други думи това са 2 различни сделки, а не повторение на една.

За промяна ще дам друг пример - ограничаване на честотата на заявките към API („ограничаване на скоростта“). Нашата Lambda получава събитие с определен user_id, за което трябва да се направи проверка, за да се види дали потребителят с този ID е изчерпал своя брой възможни заявки към някои от нашите API. Можем да съхраняваме в DynamoDB от AWS стойността на напequalsите повиквания и да я увеличаваме с всяко извикване на нашата функция с 1.

Но Howво ще стане, ако тази ламбда функция бъде извикана от едно и също събитие два пъти? Между другото, обърна ли внимание на аргументите на функцията lambda_handler(). Вторият аргумент, контекстът в AWS Lambda, е даден по подразбиране и съдържа различни метаданни, включително request_id, който се генерира за всяко уникално повикване. Това означава, че сега, instead of да съхраняваме броя на напequalsите повиквания в tableта, можем да съхраняваме списък с request_id и при всяко повикване нашата Lambda ще проверява дали дадената заявка вече е обработена:

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

Тъй като моят пример всъщност е взет от интернет, ще оставя линк към първоизточника, особено след като дава малко повече информация.

Помните ли How споменах по-рано, че нещо като уникален идентификатор на транзакция може да се използва за заключване на споделени данни? Сега научихме, че може да се използва и за превръщане на операциите в идемпотентност. Нека разберем по Howви начини можете сами да генерирате такива идентификатори.