7.1 Nega bu kerak
Biz ACID ning barcha xususiyatlari, ularning vazifalari va foydalanish senariylarini batafsil muhokama qildik. Hamma ma'lumotlar bazalari ACID ni kafolatlamaydi, chunki ular ko'proq samaradorlik uchun ulardan voz kechishadi. Shu sababli, loyihangiz uchun ACID ni ta'minlamaydigan biror ma'lumotlar bazasi tanlanishi mumkin va sizga ACID funksionalligining bir qismini dasturning o'zida amalga oshirishingiz kerak bo'ladi. Agar sizning tizimingiz mikroservislar yoki boshqa taqsimlangan dastur sifatida ishlab chiqilsa, unda odatiy lokal tranzaksiya bo'lgan narsa mikroservislarda taqsimlangan tranzaksiyaga aylanadi va o'zining ACID-tabiatini yo'qotadi, hattoki har bir mikroservisning ma'lumotlar bazasi ACID bo'lsa ham.
Men tranzaksiya menejerini yaratish bo'yicha batafsil qo'llanma bermoqchi emasman, chunki bu juda murakkab va katta mavzu. Agar so'z taqsimlangan dasturlar haqida ketmasa, dasturning ichida to'liq ACID ni amalga oshirishga urinish mantiqsiz. Agar sizga ACID kafolatlari kerak bo'lsa, tayyor yechimdan foydalanish yaxshiroq, masalan, ACID ga ega ma'lumotlar bazasi.
Lekin men sizga ba'zi texnikalarni ko'rsatmoqchiman, ular sizga dasturda tranzaksiyalarni bajarishda yordam beradi. Oxir-oqibat, ushbu texnikalarni bilish sizni har xil vaziyatlarda, hatto tranzaksiyalar bilan bog'liq bo'lmagan holda ham foydali bo'lishi mumkin va sizni yaxshi dasturchiga aylantiradi (umid qilamanki).
7.2 Transaksiyalar ishqibozlari uchun asosiy vositalar
Optimistik va pessimistik blokirovka - bu bir vaqtning o'zida bir nechta foydalanuvchilar tomonidan ma'lumotlarga kirishni cheklashning ikki xil usuli.
Optimistik
Birinchi navbatda kirish ehtimolini kam deb hisoblaydi. Shuning uchun u kerakli qatorni o'qiydi, uning versiya raqamini (yoki timestamp, yoki checksum / hash) eslab qoladi va o'zgarishlarni ma'lumotlar bazasiga yozishdan oldin ushbu ma'lumotlar versiyasi o'zgarganligini tekshiradi. Agar ha, unda ziddiyatni hal qilish va ma'lumotlarni yangilash commit qilish kerak, yoki tranzaksiyani orqaga qaytarish rollback kerak. Ushbu usulning kamchiligi shundaki, u uzoq nomli time-of-check to time-of-use (TOCTOU) xatosi uchun qulay sharoit yaratadi. Tekshiruv va yozuv o'rtasidagi holat o'zgarishi mumkin. Men optimistik blokirovkani ishlatishda tajribam yo'q. «Vikipediya» ma'lumotlar bazalari kontekstida tekshiruv o'rniga exception handling ishlatishni taklif qiladi. Bu mening uchun ma'lumotlar bazalari kontekstida unchalik ma'no bermaydi, rostini aytsam.
Kundalik dasturchi hayotidan optimistik blokirovka foydalanadigan texnologiyaga misol qilib, HTTP protokolini olaylik. Dastlabki GET so'roviga javob ETag sarlavhasini o'z ichiga olishi mumkin, bu esa mijoz tomonidan keyingi PUT so'rovlarida ishlatilishi mumkin. Agar GET va HEAD metodlari uchun resurs serverga tanish bo'lgan ETaglardan biriga mos bo'lsa, u qaytarib yuboriladi. PUT va boshqa xavfsiz bo'lmagan metodlar uchun resurs faqat shu holatda yuklanadi. Masalan, "feedparser" kutubxonasi RSS va boshqa feedsni ETag dan foydalanib parslashga yordam beradi.
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.feed.synd.SyndFeedImpl;
import com.sun.syndication.io.SyndFeedInput;
import com.sun.syndication.io.SyndFeedOutput;
import java.io.IOException;
import java.net.URL;
public class FeedParserExample {
public static void main(String[] args) throws IOException {
// Atom feedni pars qilish.
SyndFeedInput input = new SyndFeedInput();
SyndFeed feed = input.read(new URL("http://feedparser.org/docs/examples/atom10.xml"));
// Feedning ETag qiymatini olish.
String etag = feed.getEtag();
// Feedni yana bir bor pars qilish, lekin bu safar ETag qiymatini berish.
SyndFeed feed2 = input.read(new URL("http://feedparser.org/docs/examples/atom10.xml"), etag);
// Feed o'zgarganligini tekshirish.
if (feed2.getFeed() == null) {
System.out.println("Feed o'zgarmagan, shuning uchun server hech qanday ma'lumot yubormadi. Bu xususiyat, xato emas!");
} else {
System.out.println("Feed o'zgardi.");
}
}
}
Pessimistik blokirovka tranzaktsiyalar tez-tez bir xil ma'lumotlarda duch kelishini taxmin qiladi va ortiqcha race conditions ni oldini olish uchun kerakli ma'lumotlarni blokirovka qilish kerak. Buning uchun ma'lumotlar bazasi bilan ulanishni qo'llab-quvvatlash yoki ulanishdan mustaqil ravishda foydalanilishi mumkin bo'lgan ID tranzaktsiyasi ni ishlatish mumkin. Pessimistik blokirovkaning kamchiligi shundaki, u tranzaktsiyalarni qayta ishlashni sekinlashtiradi, lekin ishonchli ma'lumotlarni izolyatsiya qilishni ta'minlaydi.
Qo‘shimcha xavf o‘zaro blokirovka deadlock da, yani bir necha jarayonlar bir-birini blokirovka qilgan resurslarni kutishadi. Masalan, tranzaktsiyani amalga oshirish uchun A va B resurslari kerak. Jarayon 1 A resursini band qilgan, jarayon 2 esa B resursini band qilgan. Jarayonlar ishini davom ettira olmaydi. Ushbu muammoni hal qilishning turli usullari mavjud. Masalan, blokirovka ierarxiyasini yaratish mumkin. Ushbu tushuncha bilan batafsil tanishish uchun "Ovqatlanayotgan faylasuflar muammosi" ("dining philosophers problem") haqida o'qib chiqishingiz mumkin.
Bu yerda har ikkala blokirovka bir xil senariyda qanday harakat qilishining yaxshi misoli bor.
Blokirovkalarni amalga oshirish haqida gap ketganda, taqsimlangan tizimlar uchun ZooKeeper, Redis, etcd va Consul kabi blokirovka menejerlari mavjud.
7.3 Amaliyotlarning idempotentligi
Kodni idempotent qilish yaxshi amaliyot. Bu dasturchiga ob'ektga bir xil operatsiyani qayta qo'llashda bir xil natijani olish imkonini beradi, tranzaksiyalarni ishlatadimi yoki yo'qmi, farqi yo'q. Ma'lumotlar bazasi va uchinchi tizimlar natijaga ta'sir qilishi mumkin, lekin barcha ma'lumotlar oldindan aytilgan bo'lishi kerak.
Idempotentlikning ko‘rinishlari har xil bo‘lishi mumkin. Ulardan biri - kod yozish bo'yicha tavsiya. Eng yaxshi funksiya bir narsani qiladimi? Va u uchun unit-testlar yozish yaxshimi? Agar siz bu ikki qoidaga rioya qilsangiz, funktsiyalarning idempotent bo'lish imkoniyatlarini oshirasiz. Chalg‘itmaslik uchun ta’kidlashim kerakki, idempotent funksiyalar “toza” (ya’ni “function purity” ma’nosida) bo‘lishi shart emas.
Toza funksiyalar - bu faqat kirishda olingan ma'lumotlar bilan ishlaydigan, ularni o'zgartirmasdan va qayta ishlangan natijani qaytaradigan funksiyalar. Bunday funksiyalar funksional dasturlash yordamida dasturni kengaytirishga imkon beradi. Lekin, biz umumiy ma'lumotlar va ma'lumotlar bazalari haqida gaplashayotganimiz sababli, bizning funksiyalarimiz ehtimol toza bo'lmaydi, chunki ular ma'lumotlar bazasining yoki dasturlar (xizmatlar)ning holatini o'zgartiradi.
Mana bu toza funksiya:
public static int square(int num) {
return num * num;
}
Va bu funksiya toza emas, lekin u idempotent (mening kod yozishim haqida xulosa chiqarishdan iltimos qilmang):
public static boolean insertData(String insertQuery, DbConnection dbConnection) {
dbConnection.execute(insertQuery);
return true;
}
Men ko'p gaplar o‘rniga, qanday majburiy ravishda idempotent dasturlar yozishni o‘rganganligim haqida aytib bera olaman. AWS bilan ishlayman, ya'ni AWS Lambda xizmati. U dastur kodini yuklab olishga, uni avtomatik ravishda jadval asosida yoki ba'zi voqealarga javoban ishga tushirishga imkon beradi. Masalan, xabarlar brokeridan xabarlar kelganda. AWS da bunday broker AWS SNS hisoblanadi. U xabarlarni topics kanallari orqali yuboradi. Ushbu kanallarga obuna bo'lgan mikroservislar xabarlarni oladi va ularga javob beradi.
Muammo shundaki, SNS xabarlarni “kamida bir marta yetkazib berish” ("at-least-once delivery") kafolatini beradi. Bu nimani anglatadi? Sizning Lambda funksiyangiz ikki marta chaqiriladi. Bu chindan ham sodir bo'ladi. Sizning funksiyangiz idempotent bo'lishi kerak bo'lgan bir qator holatlar mavjud: masalan, hisobdan pul echib olayotganda biz kimgadir ikki marta bir xil miqdorda pul echib olishini kutishimiz mumkin, lekin bu alohida, bir-biridan mustaqil bo'lgan ikki marta ekanligiga ishonch hosil qilishimiz kerak – ya'ni ikki xil tranzaksiya, emas bir takroriy.
APIga so'rovlar cheklanishi ("rate limiting")ning boshqa misolini keltiraman. Bizning Lambda bir user_id bilan voqeani qabul qiladi, bu ID bilan foydalanuvchi bizning API ga mumkin bo'lgan so'rovlar sonini tugatdimi yoki yo'qligini tekshirishimiz kerak. Biz AWS dan DynamoDBda amalga oshirilgan so'rovlar qiymatini saqlab, bizning funksiyamiz har chaqirilganida uni 1 ga oshirishimiz mumkin.
Lekin bir xil voqea bilan bu Lambda-funktsiya ikki marta chaqirilsa nima qilamiz? Aytgancha, lambda_handler() funksiyasining argumentlariga e'tibor qaratdingizmi. AWS Lambda da ikkinchi argument, context odat bo'lib beriladi va turli metama'lumotlarni, jumladan har bir unikal chaqiruv uchun yaratilgan request_id ni o'z ichiga oladi. Bu shuni anglatadiki, biz endi chaqiruvlar sonini jadvalda saqlash o'rniga, request_id ro'yxatini saqlashimiz mumkin va har bir chaqiriqda bizning Lambda ushbu chaqiruv allaqachon ishlanganmi yoki yo'qligini tekshiradi.
import java.util.HashMap;
import java.util.Map;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
public class Handler implements RequestHandler<Map<String, Object>, Map<String, Object> {
private static final AmazonDynamoDB DYNAMODB = AmazonDynamoDBClientBuilder.defaultClient();
private static final String TABLE_NAME = "my_table";
private static final String LIMIT = System.getenv("LIMIT");
@Override
public Map<String, Object> handleRequest(Map<String, Object> event, Context context) {
String requestId = context.getAwsRequestId();
String userId = (String) event.get("user_id");
UpdateItemRequest updateItemRequest = new UpdateItemRequest()
.withTableName(TABLE_NAME)
.withKey(new AttributeValue().withS(userId))
.withUpdateExpression("ADD requests :request_id")
.withConditionExpression("attribute_not_exists (requests) OR (size(requests) < :limit AND NOT contains(requests, :request_id))")
.withExpressionAttributeValues(new HashMap<>() {{
put(":request_id", new AttributeValue().withS(requestId));
put(":requests", new AttributeValue().withSS(new String[]{requestId}));
put(":limit", new AttributeValue().withN(LIMIT));
}});
DYNAMODB.updateItem(updateItemRequest);
// TODO: write further logic
Map<String, Object> response = new HashMap<>();
response.put("statusCode", 200);
response.put("headers", new HashMap<>() {{
put("Content-Type", "application/json");
}});
response.put("body", "{\"status\": \"success\"}");
return response;
}
}
This example was taken from the internet, I'll provide a link to the source which provides further information.
Oldin aytilgandek, tranzaksiya uchun unikal ID dan umumiy ma'lumotlar uchun blokirovka qilishda foydalanish mumkin. Endi biz bilamizki, bunday ID dan amaliyotlarning idempotentligini ta'minlash uchun foydalanish mumkin. Keling, bunday ID ni qanday qilib o'zingiz yaratishingiz mumkinligini ko'rib chiqamiz.
GO TO FULL VERSION