בדף הזה מוסבר על רמות בידוד שונות ואיך הן פועלות ב-Spanner.
רמת הבידוד היא מאפיין של מסד נתונים שמגדיר אילו נתונים גלויים לעסקאות מקבילות. Spanner תומך בשתי רמות בידוד שמוגדרות בתקן ANSI/ISO SQL: serializable ו-repeatable read. כשיוצרים טרנזקציה, צריך לבחור את רמת הבידוד המתאימה ביותר לטרנזקציה. רמת הבידוד שנבחרה מאפשרת לעסקאות בודדות לתת עדיפות לגורמים שונים, כמו זמן האחזור, שיעור הביטולים והאם האפליקציה רגישה להשפעות של אנומליות בנתונים. הבחירה הטובה ביותר תלויה בדרישות הספציפיות של עומס העבודה.
בידוד ניתן לסריאליזציה
בידוד ניתן לסדרות הוא רמת הבידוד שמוגדרת כברירת מחדל ב-Spanner. בבידוד ניתן לסדר את העסקאות בסדר מסוים, ו-Spanner מספק לכם את ההבטחות המחמירות ביותר לגבי בקרת מקביליות של עסקאות, שנקראת עקביות חיצונית. ההתנהגות של Spanner היא כאילו כל העסקאות בוצעו ברצף, למרות שבפועל Spanner מריץ אותן בכמה שרתים (ואולי בכמה מרכזי נתונים) כדי להשיג ביצועים וזמינות טובים יותר מאשר במסדי נתונים של שרת יחיד. בנוסף, אם עסקה אחת מסתיימת לפני שעסקה אחרת מתחילה להתחייב, Spanner מבטיח שהלקוחות תמיד יראו את תוצאות העסקאות בסדר עוקב. באופן אינטואיטיבי, Spanner דומה למסד נתונים במכונה יחידה.
החיסרון הוא ש-Spanner עשוי לבטל עסקאות אם עומס העבודה כולל הרבה קריאות וכתיבות בו-זמניות, שבהן הרבה עסקאות קוראות נתונים שעסקה אחרת מעדכנת, בגלל האופי הבסיסי של עסקאות שניתנות לסדרות. עם זאת, זוהי הגדרת ברירת מחדל טובה למסד נתונים תפעולי. כך אפשר להימנע מבעיות מורכבות שקשורות לתזמון, שבדרך כלל מתרחשות רק כשמבצעים הרבה פעולות בו-זמנית. קשה לשחזר את הבעיות האלה ולפתור אותן. לכן, בידוד ניתן לסדרות מספק את ההגנה החזקה ביותר מפני אנומליות בנתונים. אם צריך לנסות שוב לבצע עסקה, יכול להיות שיהיה גידול בחביון בגלל ניסיונות חוזרים לבצע את העסקה.
בידוד קריאות חוזרות
ב-Spanner, בידוד של קריאות חוזרות מיושם באמצעות טכניקה שנקראת בדרך כלל בידוד snapshot. בידוד קריאה חוזרת ב-Spanner מבטיח שכל פעולות הקריאה בתוך עסקה יראו תמונת מצב עקבית או חזקה של מסד הנתונים כפי שהיה בתחילת העסקה. היא גם מבטיחה שפעולות כתיבה בו-זמניות לאותם נתונים יצליחו רק אם אין התנגשויות. הגישה הזו מועילה בתרחישים של קונפליקטים רבים של קריאה וכתיבה, שבהם מספר רב של טרנזקציות קוראות נתונים שטרנזקציות אחרות עשויות לשנות. השימוש בתמונת מצב קבועה מאפשר לקריאה חוזרת להימנע מההשפעות על הביצועים של רמת הבידוד המגבילה יותר של סדרת פעולות. פעולות קריאה יכולות להתבצע בלי לקבל נעילות ובלי לחסום פעולות כתיבה מקבילות, וכך יש פחות עסקאות שבוטלו וצריך לנסות לבצע אותן מחדש בגלל סכסוכי סריאליזציה פוטנציאליים. בתרחישי שימוש שבהם הלקוחות כבר מריצים הכול בעסקת קריאה-כתיבה, וקשה לעצב מחדש ולהשתמש בעסקאות לקריאה בלבד, אפשר להשתמש בבידוד קריאה חוזרת כדי לשפר את זמן האחזור של עומסי העבודה.
בניגוד לבידוד שניתן לסדר, קריאה חוזרת עשויה להוביל לחריגות בנתונים אם האפליקציה שלכם מסתמכת על קשרים או אילוצים ספציפיים בנתונים שלא נאכפים על ידי סכימת מסד הנתונים, במיוחד אם סדר הפעולות חשוב. במקרים כאלה, יכול להיות שטרנזקציה תקרא נתונים, תקבל החלטות על סמך הנתונים האלה ואז תכתוב שינויים שמפירים את האילוצים הספציפיים לאפליקציה, גם אם האילוצים של סכימת מסד הנתונים עדיין מתקיימים. הסיבה לכך היא שבידוד קריאה חוזרת מאפשר לעסקאות מקבילות להתבצע ללא סריאליזציה קפדנית. אנומליה פוטנציאלית אחת נקראת write skew, והיא נובעת מסוג מסוים של עדכון מקביל, שבו כל עדכון מתקבל באופן עצמאי, אבל ההשפעה המשולבת שלהם פוגעת בשלמות הנתונים של האפליקציה. לדוגמה, נניח שיש מערכת של בית חולים שבה לפחות רופא אחד צריך להיות בכוננות בכל רגע נתון, ורופאים יכולים לבקש להסיר אותם מהכוננות במשמרת מסוימת. בבידוד של קריאה חוזרת, אם גם ד"ר ריצ'רדס וגם ד"ר סמית' מתוזמנים להיות בכוננות באותה משמרת ומנסים בו-זמנית לבקש להסיר אותם מהכוננות, כל בקשה מצליחה במקביל. הסיבה לכך היא ששתי העסקאות קוראות שיש לפחות רופא אחד נוסף שנקבע לו תור להיות בכוננות בתחילת העסקה, מה שגורם לחריגה בנתונים אם העסקאות מצליחות. לעומת זאת, שימוש בבידוד ניתן לסדר ימנע מהעסקאות האלה להפר את האילוץ, כי עסקאות שניתנות לסדר יזהו אנומליות פוטנציאליות בנתונים ויבטלו את העסקה. כך אפשר להבטיח עקביות של האפליקציה על ידי קבלת שיעורי ביטול גבוהים יותר.
בדוגמה הקודמת, אפשר להשתמש בסעיף SELECT FOR UPDATE בבידוד של קריאה חוזרת.
הפסקה SELECT…FOR UPDATE מאמתת אם הנתונים שהיא קראה בתמונת המצב שנבחרה נשארו ללא שינוי בזמן השמירה. באופן דומה, פקודות DML ושינויים שקוראים נתונים באופן פנימי כדי לוודא את תקינות הכתיבה, גם מאמתים שהנתונים נשארים ללא שינוי בזמן השמירה.
מידע נוסף זמין במאמר בנושא שימוש בבידוד קריאה חוזרת.
תרחיש שימוש לדוגמה
בדוגמה הבאה אפשר לראות את היתרון של שימוש בבידוד של קריאה חוזרת כדי למנוע תקורה של נעילה. הפעולות Transaction 1 ו-Transaction 2 מופעלות בבידוד של קריאה חוזרת.
Transaction 1 יוצרת חותמת זמן של תמונת מצב כשההצהרה SELECT מופעלת.
GoogleSQL
-- Transaction 1
BEGIN;
-- Snapshot established at T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;
/*-----------+------------------*
| AlbumId | MarketingBudget |
+------------+------------------+
| 1 | 50000 |
| 2 | 100000 |
| 3 | 70000 |
| 4 | 80000 |
*------------+------------------*/
PostgreSQL
-- Transaction 1
BEGIN;
-- Snapshot established at T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;
/*-----------+------------------*
| albumid | marketingbudget |
+------------+------------------+
| 1 | 50000 |
| 2 | 100000 |
| 3 | 70000 |
| 4 | 80000 |
*------------+------------------*/
לאחר מכן, Transaction 2 יוצר חותמת זמן של תמונת מצב אחרי ש-Transaction 1 מתחיל אבל לפני שהוא מתבצע. מכיוון ש-Transaction 1 לא עדכן את הנתונים, השאילתה SELECT ב-Transaction 2 קוראת את אותם הנתונים כמו Transaction 1.
GoogleSQL
-- Transaction 2
BEGIN;
-- Snapshot established at T2 > T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;
INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 50000);
COMMIT;
PostgreSQL
-- Transaction 2
BEGIN;
-- Snapshot established at T2 > T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;
INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 50000);
COMMIT;
הפעולה Transaction 1 נמשכת אחרי ש-Transaction 2 מתבצעת.
GoogleSQL
-- Transaction 1 continues
SELECT SUM(MarketingBudget) as UsedBudget
FROM Albums
WHERE SingerId = 1;
/*-----------*
| UsedBudget |
+------------+
| 300000 |
*------------*/
PostgreSQL
-- Transaction 1 continues
SELECT SUM(marketingbudget) AS usedbudget
FROM albums
WHERE singerid = 1;
/*-----------*
| usedbudget |
+------------+
| 300000 |
*------------*/
הערך UsedBudget שמוחזר על ידי Spanner הוא סכום התקציב שנקרא על ידי Transaction 1. הסכום הזה משקף רק את הנתונים שמופיעים בתמונת המצב של T1. התקציב שTransaction 2 הוסיף לא נכלל, כי Transaction 2 התחייב אחרי שTransaction 1 יצר את תמונת המצב T1. שימוש ב-repeatable read פירושו ש-Transaction 1 לא נאלץ לבטל את הפעולה גם אם Transaction 2 שינה את הנתונים שנקראו על ידי Transaction 1. עם זאת,
יכול להיות שהתוצאה ש-Spanner מחזיר תהיה התוצאה הרצויה ויכול להיות שלא.
קונפליקטים של קריאה-כתיבה ותקינות
בדוגמה הקודמת, אם הנתונים שנשאלו על ידי הצהרות SELECT ב-Transaction 1 שימשו לקבלת החלטות לגבי תקציב השיווק בהמשך, יכול להיות שיהיו בעיות בדיוק.
לדוגמה, נניח שהתקציב הכולל הוא 400,000. על סמך התוצאה של ההצהרה SELECT ב-Transaction 1, יכול להיות שנחשוב שנותרו 100,000 בתקציב ונחליט להקצות את כולם ל-AlbumId = 4.
GoogleSQL
-- Transaction 1 continues..
UPDATE Albums
SET MarketingBudget = MarketingBudget + 100000
WHERE SingerId = 1 AND AlbumId = 4;
COMMIT;
PostgreSQL
-- Transaction 1 continues..
UPDATE albums
SET marketingbudget = marketingbudget + 100000
WHERE singerid = 1 AND albumid = 4;
COMMIT;
Transaction 1 מתבצעת בהצלחה, למרות ש-Transaction 2 כבר הקצה 50,000 מהתקציב שנותר לאלבום חדש AlbumId = 5.100,000
אתם יכולים להשתמש בתחביר SELECT...FOR UPDATE כדי לוודא שקריאות מסוימות של טרנזקציה לא משתנות במהלך משך החיים של הטרנזקציה, וכך להבטיח את התקינות שלה. בדוגמה הבאה, שבה נעשה שימוש ב-SELECT...FOR UPDATE, הפעולה Transaction 1 מבוטלת בזמן השמירה.
GoogleSQL
-- Transaction 1 continues..
SELECT SUM(MarketingBudget) AS TotalBudget
FROM Albums
WHERE SingerId = 1
FOR UPDATE;
/*-----------*
| TotalBudget |
+------------+
| 300000 |
*------------*/
COMMIT;
PostgreSQL
-- Transaction 1 continues..
SELECT SUM(marketingbudget) AS totalbudget
FROM albums
WHERE singerid = 1
FOR UPDATE;
/*-------------*
| totalbudget |
+-------------+
| 300000 |
*-------------*/
COMMIT;
מידע נוסף זמין במאמר בנושא שימוש ב-SELECT FOR UPDATE בבידוד של קריאה חוזרת.
קונפליקטים של כתיבה-כתיבה ונכונות
שימוש ברמת הבידוד של קריאה חוזרת מאפשר כתיבה בו-זמנית של נתונים זהים רק אם אין התנגשויות.
בדוגמה הבאה, הפקודה Transaction 1 יוצרת חותמת זמן של תמונת מצב בהצהרה הראשונה SELECT.
GoogleSQL
-- Transaction 1
BEGIN;
-- Snapshot established at T1
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;
PostgreSQL
-- Transaction 1
BEGIN;
-- Snapshot established at T1
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;
הפקודה הבאה Transaction 2 קוראת את אותם נתונים כמו הפקודה Transaction 1 ומוסיפה פריט חדש. הפעולה Transaction 2 מתבצעת בהצלחה בלי המתנה או ביטול.
GoogleSQL
-- Transaction 2
BEGIN;
-- Snapshot established at T2 (> T1)
SELECT AlbumId, MarketingBudget
FROM Albums
WHERE SingerId = 1;
INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 50000);
COMMIT;
PostgreSQL
-- Transaction 2
BEGIN;
-- Snapshot established at T2 (> T1)
SELECT albumid, marketingbudget
FROM albums
WHERE singerid = 1;
INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 50000);
COMMIT;
הפעולה Transaction 1 נמשכת אחרי ש-Transaction 2 מתבצעת.
GoogleSQL
-- Transaction 1 continues
INSERT INTO Albums (SingerId, AlbumId, MarketingBudget) VALUES (1, 5, 30000);
-- Transaction aborts
COMMIT;
PostgreSQL
-- Transaction 1 continues
INSERT INTO albums (singerid, albumid, marketingbudget) VALUES (1, 5, 30000);
-- Transaction aborts
COMMIT;
Transaction 1 aborts since Transaction 2 already committed an insertion
to the AlbumId = 5 row.
המאמרים הבאים
מידע נוסף על סריאליזציה של Spanner ועקביות חיצונית זמין במאמר TrueTime ועקביות חיצונית.