5.1 Въпросът за едновременността

Нека започнем с малко далечна теория.

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

И в почти всяко приложение по един or друг начин участват бази данни (DBs), които обикновено изпълняват две функции: съхраняват данни, когато са получени от вас и по-късно ви ги предоставят при поискване. Рядко някой се сеща да създаде собствена база данни, защото вече има много готови решения. Но How да изберете правилния за вашето приложение?

И така, нека си представим, че сте написали приложение с мобилен интерфейс, който ви позволява да заредите предварително запазен списък със задачи в къщата - тоест да прочетете от базата данни и да я допълвате с нови задачи, Howто и да приоритизирате всяка конкретна задача - от 1 (най-висока) до 3 (най-ниска). Да приемем, че вашето мобилно приложение се използва само от един човек в даден момент. Но сега се осмелихте да кажете на майка си за вашето творение и сега тя стана вторият редовен потребител. Какво се случва, ако решите по едно и също време, точно в една и съща мorсекунда, да зададете няHowва задача - "измийте прозорците" - с различна степен на приоритет?

От професионална гледна точка вашите и тези на майка ви заявки към базата данни могат да се разглеждат като 2 процеса, които са направor заявка към базата данни. Процесът е обект в компютърна програма, който може да работи в една or повече нишки. Обикновено процесът има изображение на машинен code, памет, контекст и други ресурси. С други думи, процесът може да се характеризира като изпълнение на програмни инструкции на процесора. Когато вашето приложение прави заявка към базата данни, ние говорим за факта, че вашата база данни обработва заявката, получена по мрежата от един процес. Ако има двама потребители, работещи в приложението по едно и също време, тогава може да има два процеса във всеки конкретен момент.

Когато някой процес направи заявка към базата данни, той я намира в определено състояние. Системата със състояние е система, която помни предишни събития и съхранява няHowва информация, която се нарича "състояние". Променлива, декларирана като, integerможе да има състояние 0, 1, 2 or да речем 42. Mutex (взаимно изключване) има две състояния: заключено or отключено , точно като двоичен семафор („задължителен“ срещу „освободен“) и като цяло двоичен (двоични) типове данни и променливи, които могат да имат само две състояния - 1 or 0.

Въз основа на концепцията за състояние се основават няколко математически и инженерни структури, като краен автомат - модел, който има един вход и един изход и е в едно от краен набор от състояния във всеки момент от времето - и „състоянието ” модел на проектиране, при който даден обект променя поведението си в зависимост от вътрешното състояние (например в зависимост от това Howва стойност е присвоена на една or друга променлива).

И така, повечето обекти в света на машините имат няHowво състояние, което може да се промени с времето: нашият тръбопровод, който обработва голям пакет данни, извежда грешка и става неуспешен, or свойството на обект Wallet, което съхранява сумата пари, останала в потребителския сметка, промени след разписки за заплати.

Преход („преход“) от едно състояние в друго - да речем, от в ход към неуспешно - се нарича операция. Вероятно всеки знае операциите CRUD - create, read, update, deleteor подобни HTTP методи - POST, GET, PUT, DELETE. Но програмистите често дават други имена на операциите в техния code, защото операцията може да бъде по-сложна от простото четене на определена стойност от базата данни - тя може също да провери данните и тогава нашата операция, която е приела формата на функция, ще се нарича например А validate()кой изпълнява тези операции-функции? вече описани процеси.

Още малко и ще разберете защо описвам термините толкова подробно!

Всяка операция - независимо дали е функция or в разпределените системи изпращане на заявка до друг сървър - има 2 свойства: времето за извикване и времето за завършване (време за завършване) , което ще бъде строго по-голямо от времето за извикване (изследователи от Jepsen изхождайте от теоретичните предположения, че и на двата времеви клейма ще бъдат дадени въображаеми, напълно синхронизирани, глобално налични часовници).

Нека си представим нашето приложение със списък със задачи. Правите заявка към базата данни през мобилния интерфейс в 14:00:00.014, а майка ви в 13:59:59.678(т.е. 336 мorсекунди преди това) актуализира списъка със задачи през същия интерфейс, добавяйки към него миене на чинии. Като се има предвид забавянето на мрежата и възможната опашка от задачи за вашата база данни, ако освен вас и майка ви, всички приятели на майка ви също използват вашето приложение, базата данни може да изпълни заявката на майката, след като обработи вашата. С други думи, има шанс две ваши заявки, Howто и заявки от приятелки на майка ви, да бъдат изпратени до едни и същи данни едновременно (едновременно).

Така стигнахме до най-важния термин в областта на базите данни и разпределените applications – паралелност. Какво точно може да означава едновременността на две операции? Ако са дадени няHowва операция T1 и няHowва операция T2, тогава:

  • T1 може да бъде стартиран преди началния час на изпълнение T2 и завършен между началния и крайния час на T2
  • T2 може да започне преди началния час на T1 и да завърши между началото и края на T1
  • T1 може да бъде стартиран и завършен между началния и крайния час на изпълнението на T1
  • и всеки друг сценарий, при който T1 и T2 имат няHowво общо време за изпълнение

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

И Howво всъщност може да се обърка, чисто теоретично?

Когато работите върху споделени данни, могат да възникнат множество проблеми, свързани с паралелността, наричани още „състезателни условия“. Първият проблем възниква, когато даден процес получи данни, които не е трябвало да получи: непълни, временни, анулирани or по друг начин „неправилни“ данни. Вторият проблем е, когато процесът получи остарели данни, тоест данни, които не съответстват на последното запазено състояние на базата данни. Да кажем, че някое приложение е изтеглило пари от акаунта на потребител с нулев баланс, тъй като базата данни е върнала състоянието на акаунта на приложението, без да взема предвид последното теглене на пари от него, което се случи само преди няколко мorсекунди. Ситуацията е така-така, нали?

5.2 Транзакциите ни спасиха

За решаването на такива проблеми се появи концепцията за транзакция - определена група от последователни операции (промени в състоянието) с база данни, която е логически единична операция. Пак ще дам пример с банка - и то неслучайно, защото понятието транзакция се появи, очевидно, точно в контекста на работата с пари. Класическият пример за транзакция е прехвърлянето на пари от една банкова сметка в друга: първо трябва да изтеглите сумата от изходната сметка и след това да я депозирате в целевата сметка.

За да бъде извършена тази транзакция, приложението ще трябва да извърши няколко действия в базата данни: проверка на баланса на подателя, блокиране на сумата по сметката на подателя, добавяне на сумата към сметката на получателя и приспадане на сумата от подателя. Ще има няколко изисквания за такава сделка. Например, приложението не може да получава остаряла or неправилна информация за баланса - например, ако в същото време паралелна транзакция е приключила с грешка по средата и средствата не са бor дебитирани от сметката - и нашето приложение вече е получило информация че средствата са отписани.

За да се реши този проблем, беше използвано такова свойство на транзакция като „изолация“: нашата транзакция се изпълнява, сякаш няма други транзакции, които да се изпълняват в същия момент. Нашата база данни извършва едновременни операции, сякаш ги изпълнява една след друга, последователно - всъщност най-високото ниво на изолация се нарича Strict Serializable . Да, най-високото, което означава, че има няколко нива.

„Спри“, казваш. Дръжте конете си, сър.

Нека си спомним How описах, че всяка операция има време за извикване и време за изпълнение. За удобство можете да разгледате извикването и изпълнението като 2 действия. След това сортираният списък на всички извиквания и действия за изпълнение може да се нарече история на базата данни. Тогава нивото на изолация на транзакцията е набор от истории. Използваме нива на изолация, за да определим кои истории са „добри“. Когато казваме, че една история „нарушава възможността за сериализиране“ or „не може да бъде сериализирана“, имаме предвид, че историята не е в набора от истории, които могат да бъдат сериализирани.

За да стане ясно за Howви истории става дума, ще дам примери. Например, има такъв вид история - междинно четене . Това се случва, когато на транзакция A е разрешено да чете данни от ред, който е бил модифициран от друга изпълнявана транзакция B и все още не е ангажиран („не е ангажиран“) – тоест всъщност промените все още не са окончателно ангажирани от транзакция B и може по всяко време да ги отмени. И, например, прекратеното четене е само нашият пример с анулирана транзакция за теглене

Има няколко възможни аномалии. Тоест, аномалиите са няHowъв вид нежелано състояние на данните, което може да възникне по време на конкурентен достъп до базата данни. И за да се избегнат определени нежелани състояния, базите данни използват различни нива на изолация – тоест различни нива на защита на данните от нежелани състояния. Тези нива (4 броя) са изброени в стандарта ANSI SQL-92.

Описанието на тези нива изглежда неясно за някои изследователи и те предлагат свои собствени, по-подробни класификации. Съветвам ви да обърнете внимание на вече споменатия Jepsen, Howто и на проекта Hermitage, който има за цел да изясни Howви точно нива на изолация предлагат конкретни СУБД, като MySQL or PostgreSQL. Ако отворите файловете от това хранorще, можете да видите Howва последователност от SQL команди използват, за да тестват базата данни за определени аномалии, и можете да направите нещо подобно за базите данни, които ви интересуват). Ето един пример от хранorщето, за да ви заинтересува:

-- Database: MySQL

-- Setup before test
create table test (id int primary key, value int) engine=innodb;
insert into test (id, value) values (1, 10), (2, 20);

-- Test the "read uncommited" isolation level on the "Intermediate Reads" (G1b) anomaly
set session transaction isolation level read uncommitted; begin; -- T1
set session transaction isolation level read uncommitted; begin; -- T2
update test set value = 101 where id = 1; -- T1
select * from test; -- T2. Shows 1 => 101
update test set value = 11 where id = 1; -- T1
commit; -- T1
select * from test; -- T2. Now shows 1 => 11
commit; -- T2

-- Result: doesn't prevent G1b

Важно е да разберете, че за една и съща база данни, като правило, можете да изберете един от няколко вида изолация. Защо не изберете най-здравата изолация? Тъй като, Howто всичко в компютърните науки, избраното ниво на изолация трябва да съответства на компромис, който сме готови да направим - в този случай, компромис в скоростта на изпълнение: колкото по-силно е нивото на изолация, толкова по-бавни ще бъдат заявките обработени. За да разберете от Howво ниво на изолация се нуждаете, трябва да разберете изискванията за вашето приложение и за да разберете дали избраната от вас база данни предлага това ниво, ще трябва да разгледате documentацията - за повечето applications това ще е достатъчно, но ако имате няHowви особено строги изисквания, по-добре е да организирате тест като това, което правят момчетата от проекта Hermitage.

5.3 "I" и други букви в ACID

Изолацията е основно това, което хората имат предвид, когато говорят за ACID като цяло. И поради тази причина започнах анализа на това съкращение с изолация, а не отидох в ред, Howто обикновено правят онези, които се опитват да обяснят това понятие. Сега нека да разгледаме останалите три букви.

Спомнете си отново нашия пример с банков превод. Транзакцията за прехвърляне на средства от една сметка в друга включва операция за теглене от първата сметка и операция за попълване на втората. Ако операцията по попълване на втората сметка е неуспешна, вероятно не искате да се извърши операцията по теглене от първата сметка. С други думи, or транзакцията е успешна напълно, or изобщо не се случва, но не може да бъде напequalsа само за няHowва част. Това свойство се нарича "атомарност" и е "A" в ACID.

Когато нашата транзакция се изпълни, тогава, Howто всяка операция, тя прехвърля базата данни от едно валидно състояние в друго. Някои бази данни предлагат така наречените ограничения - т.е. правила, които се прилагат към съхранените данни, например по отношение на първични or вторични ключове, индекси, стойности по подразбиране, типове колони и т.н. Така че, когато правим транзакция, трябва да сме сигурни, че всички тези ограничения ще бъдат изпълнени.

Тази гаранция се нарича "последователност" и буква Cв ACID (да не се бърка с последователност от света на разпределените applications, за която ще говорим по-късно). Ще дам нагледен пример за последователност по смисъла на ACID: приложение за онлайн магазин иска да добави ordersред към tableта, като ID от tableта product_idще бъде посочен в колоната - типичен .productsforeign key

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

И накрая остава D- "устойчивост" (издръжливост). Системен срив or друг срив не трябва да води до загуба на резултати от транзакции or съдържание на база данни. Тоест, ако базата данни отговори, че транзакцията е успешна, това означава, че данните са записани в енергонезависима памет - например на твърд диск. Това, между другото, не означава, че веднага ще видите данните при следващата заявка за четене.

Точно онзи ден работих с DynamoDB от AWS (Amazon Web Services) и изпратих някои данни за запазване и след като получих отговор ( HTTP 200ОК) or нещо подобно, реших да го проверя - и не видях това данни в базата данни за следващите 10 секунди. Това означава, че DynamoDB ангажира моите данни, но не всички възли се синхронизираха незабавно, за да получат последното копие на данните (въпреки че може да са бor в кеша). Тук отново се изкачихме в територията на последователността в контекста на разпределените системи, но времето да говорим за това все още не е дошло.

Така че сега знаем Howво представляват гаранциите на ACID. И дори знаем защо са полезни. Но дали наистина имаме нужда от тях във всяко приложение? И ако не, кога точно? Всички DBs предлагат ли тези гаранции и ако не, Howво предлагат instead of това?