paint-brush
פייתון: מבט מעמיק יותר כיצד פועלות עסקאות ג'נגועל ידי@mta
134 קריאות היסטוריה חדשה

פייתון: מבט מעמיק יותר כיצד פועלות עסקאות ג'נגו

על ידי Michael T. Andemeskel9m2025/03/02
Read on Terminal Reader

יותר מדי זמן; לקרוא

ניתן להשתמש ב-transaction.atomic() של Django ליצירת טרנזקציות לביצוע פעולות אטומיות - פעולות DB שמובטחות לביצוע ולהישמר יחד או להיכשל. ג'נגו יוצר טרנזקציות באמצעות מנגנון העסקאות של ה-DB הבסיסי, הוא מבצע את הקוד בטרנזקציה, ואם לא מתרחשות שגיאות, הוא מחייב את העסקה ל-DB.
featured image - פייתון: מבט מעמיק יותר כיצד פועלות עסקאות ג'נגו
Michael T. Andemeskel HackerNoon profile picture
0-item


תוֹכֶן

  • TLDR - סיכום של לוגיקה של עסקאות Django
  • דיאגרמת לוגיקה
  • עסקאות ופעולות ATOMIC
  • עסקאות בג'נגו
  • מצב התחייבות אוטומטית של Django - כיבוי עסקאות אוטומטיות
  • מקורות

TLDR - סיכום של לוגיקה של עסקאות Django

זה מיועד לאפליקציות שמריצות את Django עם PSQL או מסדי נתונים אחרים מבוססי SQL דומה (SQLite, MySQL וכו') (DBs); ייתכן שהכללים הבסיסיים סביב עסקאות לא יחולו על DBs אחרים. מתחת למכסה המנוע, קוד ניהול העסקאות של ג'נגו עושה את הדברים הבאים (זה מועתק מהמסמך של ג'נגו -גלול למטה- עם נקודות התבליטים שלי להבהרה):


  • פותח עסקה בעת כניסה לבלוק האטומי החיצוני ביותר;
    • מקבל או יוצר חיבור ל-DB ומגדיר אותו כעסקה.


  • מבקש ורוכש מנעולים כאשר נתקלים בפעולת DB;
    • כל פעולות DB דורשות מנעולים - חלק מהנעילות רופפות ויאפשרו לחיבורים אחרים לבצע פעולות על אותו משאב, אך מנעולים אחרים מחמירים יותר; הם בלעדיים ויחסמו פעולות קריאה וכתיבה כאחד. יצירה, עדכון ומחיקה של רשומות, כמו גם שינוי טבלאות ועמודות, ירכשו מנעולים מחמירים יותר שייחסמו חיבורים אחרים מביצוע פעולות. מנעולים אלה מוחזקים עד תום העסקה.


  • יוצר נקודת שמירה בעת כניסה לבלוק אטומי פנימי;
    • זה נעשה באופן אוטומטי כאשר, למשל, הכנסת רשומות לטבלה - חלק מפעולות המודל יעטפו את עצמן באופן אוטומטי בלוקים אטומיים.

    • ניתן לעשות זאת באופן ידני על ידי קריאה לפונקציה שעטופה על ידי atomic() או באמצעות atomic() במשפט with (כמנהל הקשר).

    • נקודות שמירה ישתמשו באותו חיבור כמו העסקה החיצונית - אם תסתכל על קוד ה-Django עבור נקודות שמירה, זו רק קריאה רקורסיבית ליצירת עסקה נוספת.

    • ניתן לקנן נקודות שמירה בתוך נקודות שמירה - ערימה עוקבת אחר נקודות שמירה.


  • משחרר או מתגלגל חזרה לנקודת השמירה בעת יציאה מבלוק פנימי;
    • לאחר ההכנסה, נקודת השמירה מוסרת על ידי ביצוע השינויים והפיכתם לזמינים לשאר העסקה (אך לא לחיבורים אחרים) או על ידי ביטול השינויים.


  • כאשר נתקלים בקוד שאינו נוגע ב-DB הקוד מבוצע כרגיל;
    • המנעולים ב-DB עדיין מוחזקים בזמן שהלוגיקה שאינה DB מבוצעת, למשל, אם נשלח בקשת HTTP בגוף העסקה, המנעולים יוחזקו עד שליחת הבקשה הזו והתשובה תוחזר. Django אינו משחרר את המנעולים כאשר אנו מפסיקים לבצע פעולות DB. הוא מחזיק את המנעולים עד לסיום העסקה החיצונית.

    • מנעולים שנרכשו בעסקאות מקוננות מוחזקים גם עד לסיום העסקה החיצונית או לביטול העסקה המקוננת.


  • כאשר מתרחשת RuntimeError או שהועלתה שגיאה מחוץ ל-DB (לא קשור לפעולות ה-DB);
    • העסקה נעצרת, מתגלגלת לאחור, והמנעולים משוחררים.

    • אם השגיאה מתרחשת בעסקה מקוננת, העסקה החיצונית אינה מושפעת. החזרה לאחור מתרחשת אך העסקה החיצונית יכולה להמשיך ללא הפרעה (כל עוד השגיאה שהועלתה נתפסת ומטופלת).

    • עסקאות לא תופסות שגיאות, אבל הן תלויות בהן כדי לדעת מתי להחזיר שינויים. לכן, אל תעטוף פעולות DB ב-try/למעט, במקום זאת, עטוף אותן בטרנזקציה ואז עטוף את העסקה ב-try/except. פעולה זו מאפשרת לפעולת DB להיכשל ולהתהפך אוטומטית מבלי להשפיע על העסקה החיצונית.


  • מתחייב או מבטל את העסקה בעת יציאה מהגוש החיצוני ביותר.
    • לבסוף, כל העסקה מחויבת, מה שהופך את השינויים לזמינים לכל המשתמשים/החיבורים של DB. לחלופין, הוא מוחזר, ומשאיר את ה-DB באותו מצב כפי שהיה כאשר נתקלה בעסקה.


בבלוג הבא אכתוב על מה כדאי או לא כדאי לשים בעסקה כדי למנוע הפסקות שנגרמו מהרעבת מנעולים.

דיאגרמת לוגיקה

תרשים לוגי של עסקאות ג'נגו - מלא

עסקאות ופעולות ATOMIC

עסקאות הן מנגנונים המאפשרים ל- ACID DB's (PSQL, SQL, MongoDB העדכניים וכו') להיות ATOMIC (ה-A ב-ACID). זה אומר שכל הכתיבה ל-DB מתבצעת כפעולה אחת - לא משנה כמה מורכבת. אם הכתיבה מצליחה, כל השינויים ב-DB נמשכים וזמינים לכל החיבורים בו זמנית (ללא כתיבה חלקית). אם הכתיבה נכשלת, כל השינויים יוחזרו לאחור - שוב, אין שינויים חלקיים.


עסקאות מבטיחות את הכללים הבאים:

  • הנתונים ב-DB לא ישתנו באמצע עסקה.
  • אף חיבור אחר לא יוכל לראות את השינויים שבוצעו על ידי העסקה עד לסיום.
  • ל-DB יהיו כל הנתונים החדשים או אף אחד מהם.


כללים אלו מאפשרים למשתמש ב-DB לאגד פעולות מורכבות יחד ולבצע אותן כפעולה אחת. למשל, ביצוע העברה מחשבון בנק אחד לאחר; אם לא היו לנו עסקאות, אז באמצע שתי פעולות הכתיבה הללו, יכולה להיות משיכה או אפילו פעולת סגירת חשבון שהופכת את ההעברה לבלתי חוקית. עסקאות מאפשרות למשתמש צורה כלשהי של בקרת תנועה. אנו יכולים לחסום את כל שאר הפעולות המתנגשות בזמן שהעסקה מתבצעת.


הפעולות נחסמות באמצעות סדרה של מנעולים על טבלאות ושורות. ב-PSQL ובגרסאות SQL אחרות, עסקאות נוצרות עם BEGIN; אז המנעולים נרכשים כאשר מופעלת פעולה כמו select/sert/delete/alter והעסקאות מסתיימות ב-COMMIT או ROLLBACK. המנעולים משתחררים בעת ביצוע COMMIT או ROLLBACK. למרבה המזל, Django מאפשר לנו ליצור עסקאות מבלי שנצטרך להשתמש בשלושת ההצהרות הללו (אבל אנחנו עדיין צריכים לדאוג לגבי המנעולים; עוד על כך בפוסט הבא).


 -- Start a new transaction BEGIN; SELECT … INSERT INTO … UPDATE … DELETE FROM … -- Save the changes COMMIT;


  • הבחירה רוכשת מנעולים שחוסמים עדכונים בשורות הנקראות.
  • התוספת רוכשת מנעולים שחוסמים עדכונים בשורות שנוצרות.
  • העדכון רוכש מנעולים שחוסמים עדכונים בשורות המתעדכנות.
  • המחיקה רוכשת מנעולים שחוסמים עדכונים בשורות הנמחקות.
  • אנחנו לא צריכים לבדוק במפורש שגיאות ולהתקשר ל-ROLLBACK - העסקה עושה זאת עבורנו.
  • אם הייתה שם פעולת שינוי טבלה, היא הייתה חוסמת את כל הקריאה והכתיבה בטבלה (עוד על כך בבלוג הבא).

עסקאות בג'נגו

 from django.db import transaction from app.core.models import Accounts, Fee, NotificationJob def do_migration(): overdrawn_accounts = Accounts.objects.filter(type='overdrawn') for acount in overdrawn_accounts: create_notification(acount) @transaction.atomic def create_notification(acount: Accounts): # $5 fee for overdraft - 500 because we never store money as float!!! recall = Fee.objects.create(acount=acount, description='Fee for overdraft', amount=500) NotificationJob.objects.create(recall=recall, notification_type='all') acount.status = 'awaiting_payment' acount.save() def do_migration2(): overdawn_account = Accounts.objects.filter(type='overdrawn') for account in overdawn_account: with transaction.atomic(): recall = Fee.objects.create(acount=account, description='Fee for overdraft', amount=500) NotificationJob.objects.create(recall=recall, notification_type='all') account.status = 'awaiting_payment' account.save()


Django עוטפת עבורנו באופן אוטומטי כל פעולת DB בטרנזקציה כאשר היא פועלת במצב אוטומטי (עוד על כך בהמשך). ניתן ליצור טרנזקציות מפורשות באמצעות הדקורטור atomic() או מנהל הקשר (עם atomic()) - כאשר נעשה שימוש ב-atomic(), פעולות DB בודדות אינן עוטפות בטרנזקציה או מחויבות מיד ל-DB (הן מבוצעות, אך השינויים ב-DB אינם גלויים למשתמשים אחרים). במקום זאת, הפונקציה כולה עטופה בבלוק טרנזקציה ומבוצעת בסוף (אם לא מועלות שגיאות) - COMMIT מבוצעת.


אם יש שגיאות, השינויים ב-DB מבוטלים - ROLLBACK מבוצע. זו הסיבה שעסקאות נצמדות למנעולים עד הסוף; אנחנו לא רוצים לשחרר מנעול על שולחן, שחיבור אחר ישנה את הטבלה או יקרא אותה, ואז נאלץ להחזיר לאחור את מה שזה עתה שונה או קרא.


 ANGRY_CUSTOMER_THRESHOLD = 3 @transaction.atomic def create_notification(acount: Accounts): recall = Fee.objects.create(acount=acount, description='Fee for overdraft', amount=500) NotificationJob.objects.create(recall=recall, notification_type='all') try: # when this completes successfully, the changes will be available to the outer transaction with transaction.atomic(): owner = acount.owner fees = Fee.objects.filter(owner=owner).count() if fees >= ANGRY_CUSTOMER_THRESHOLD: for fee in fees: fee.amount = 0 fee.save() owner.status = 'angry' owner.save() # as long as we catch the error, the outer transaction will not be rolled back except Exception as e: logger.error(f'Error while removings fees for account {acount.id}: {e}') acount.status = 'awaiting_payment' acount.save()


אנחנו יכולים גם לקנן טרנזקציות על ידי קריאה לפונקציה אחרת שעטופה על ידי atomic() או באמצעות מנהל ההקשרים בתוך טרנזקציה. זה מאפשר לנו ליצור נקודות שמירה בהן נוכל לנסות פעולות מסוכנות מבלי להשפיע על שאר העסקה - כאשר מזוהה עסקה פנימית, נוצרת נקודת שמירה, העסקה הפנימית מבוצעת, ואם העסקה הפנימית נכשלת, הנתונים מתגלגלים חזרה לנקודת השמירה והטרנזקציה החיצונית ממשיכה. אם העסקה החיצונית נכשלת, כל העסקאות הפנימיות מתגלגלות לאחור לצד העסקה החיצונית. ניתן למנוע קינון של עסקאות על ידי הגדרת durable=True ב-atomic() - זה יגרום לעסקה להעלות RuntimeError אם יתגלו טרנזקציות פנימיות כלשהן.


מה שאתה צריך לזכור לגבי עסקאות מקוננות:

  • עסקאות מקוננות אל תשחרר מנעולים אלא אם עסקאות מקוננות מבוטלות או שהעסקה החיצונית מחויבת או מבוטלת.
  • עסקאות מקוננות לא תופסות שגיאות DB; הם פשוט מחזירים לאחור את הפעולות בעסקה שגרמה לשגיאה. אנחנו עדיין צריכים לתפוס את שגיאת DB כדי שהעסקה החיצונית תמשיך.
  • שינויים שבוצעו בעסקאות מקוננות זמינים לעסקה החיצונית ולטרנזקציות המקוננות הבאות.

מצב התחייבות אוטומטית של Django - כיבוי עסקאות אוטומטיות

 DATABASES = { 'default': { # True by default 'AUTOCOMMIT': False, # ...rest of configs } }

כברירת מחדל, Django פועל במצב אוטומטי , כלומר כל פעולת DB עטופה בטרנזקציה ומופעלת מיד (מחויב, ומכאן אוטומטית) למעט אם הפעולה עטופה במפורש בטרנזקציה על ידי המשתמש. זה מטשטש את לוגיקית העסקאות של ה-DB הבסיסי - חלק מה-DBs, כמו PSQL, עוטפים אוטומטית את כל הפעולות בטרנזקציות בעוד שאחרים לא. אם יש לנו ש-Django עושה זאת עבורנו, אנחנו לא צריכים לדאוג לגבי ההיגיון הבסיסי של DB.


אנחנו יכולים לכבות את מצב הביצוע האוטומטי, אבל זה ישאיר אותנו להסתמך על ה-DB לנהל את הפעולות ולהבטיח שהן ATOMIC, וזה מסוכן ולא נוח למפתחים. זה לא יהיה כיף אם, תוך כדי יצירת API, היינו צריכים להבין אילו פעולות כתיבה יש בטרנזקציות, ולכן ייכתבו בסך הכל ולא באופן חלקי.


עם זאת, ייתכן שנרצה לכבות את מצב ה-autocommit אם אנו יודעים את מספר החיבורים ב-DB ואת רצף הפעולות שהם מבצעים. אם נגלה כיצד לבטל את הסכסוך בין הפעולות הללו, נוכל לכבות את מצב הביצוע האוטומטי ולהשיג כמה יעילות:

  • אנו מסירים שתי פקודות מכל פעולה: BEGIN ו-COMMIT/ROLLBACK. אבל רוב DB's עוטפים אוטומטית כל פעולה בעסקה כך שזה עשוי להיות חסר טעם.
  • מנעולים לא יישמרו יותר מדי זמן - ברגע שמבצע מסתיים, המנעולים שהוא מחזיק משתחררים, לעומת המתנה לסיום העסקה כולה.
  • סבירות נמוכה יותר למבוי סתום - מבוי סתום מתרחש כאשר שתי עסקאות זקוקות למנעול שהעסקה השנייה מחזיקה. זה משאיר את שתי העסקאות מחכות לסיום השני, אבל אף אחת מהן לא יכולה לסיים. עם זאת, מבוי סתום עדיין יכול להתרחש. חלק מפעולות DB יבצעו כתיבה מרובת, למשל, עדכון עמודה שיש עליה אינדקס יעדכן את העמודה והאינדקס (שתי כתיבה לפחות). זה יכול להוביל למבוי סתום אם חיבור אחר ינסה לפעול על השורות או העמודות המושפעות.


החסרונות הם הסיכונים הגבוהים יותר לשחיתות נתונים. זה לא שווה את זה, אבל אולי יש מקרה שימוש שאני לא יכול לחשוב עליו.

מקורות


L O A D I N G
. . . comments & more!

About Author

Michael T. Andemeskel HackerNoon profile picture
Michael T. Andemeskel@mta
I write code and, occasionally, bad poetry. Thankfully, my code isn’t as bad as my poetry.

תלו תגים

מאמר זה הוצג ב...