מניעת race condition ב-DB
-
@yossiz לכאורה טריגר הוא הפיתרון הנכון כאן
אם כי אני כמעט מעולם לא השתמשתי בטריגר (למעט הכרה שלו)
יש מקרה אחד ויחיד שבו עד היום השתמשתי בטריגר, וגם זה היה מחוסר ברירה ולא מבחירה
(יכול להיות שאני טועה בזה, אבל אני לא אוהב להשתמש בזה)מה מפריע לך בעידכון של שאילתת משנה?
update orgs set lastCharge=( select max(chargeId) from charges where orgId=orges.orgeId ) where orgeId=?
@clickone אמר במניעת race condition ב-DB:
מה מפריע לך בעידכון של שאילתת משנה?
אין לי שום בעיה עם זה. זה לכאורה פתרון מצויין, אבל אני רוצה גם ללמוד פתרונות לבעיה במקומות שבהם פתרון זה לא יהיה מעשי.
(חוץ מזה אני לא יודע עדיין איך לעשות שאילתת משנה ב-SEQUELIZEאבל זה אני יכול ללמוד בקלות)
גם אשמח להבין אם פתרון זה הוא זהה (ברמת מתחת למכסה) לאחד מהפתרונות האחרות (האם יש נעילה? טרנזקציה באיזה רמת בידוד?) -
@clickone אמר במניעת race condition ב-DB:
מה מפריע לך בעידכון של שאילתת משנה?
אין לי שום בעיה עם זה. זה לכאורה פתרון מצויין, אבל אני רוצה גם ללמוד פתרונות לבעיה במקומות שבהם פתרון זה לא יהיה מעשי.
(חוץ מזה אני לא יודע עדיין איך לעשות שאילתת משנה ב-SEQUELIZEאבל זה אני יכול ללמוד בקלות)
גם אשמח להבין אם פתרון זה הוא זהה (ברמת מתחת למכסה) לאחד מהפתרונות האחרות (האם יש נעילה? טרנזקציה באיזה רמת בידוד?)@yossiz
אני לא מכיר את postgres, אבל אני חושב שזה שווה ערך ל4 והוא עושה את 2 הפקודות ביחד, ולכאורה זה נועל את הDBאני מנסה קודם להבין את הצורך
כמו שכתב @dovid , בד"כ משתמשים במיספור אוטומטי, למה זה לא המצב אצלך?
אא"כ (שזה מה ששאני מריח) שזה טבלה כמו של חשבוניות / קבלות ואתה צריך לדעת את המספר האחרון שיצא
ועדיין, לא הבנתי למה אתה שומר את הנתון הזה סטטי בorgs, במקום להביא אותו מחושב באותו הרגע שתצטרך?
ז"א, במקום למשוך את זה מהטבלה, וללכת ולחזור, אתה פשוט מוסיף את הנתונים בinsert ואז מיד את המס' אתה מכניס עם sub queryINSERT INTO charge ( docNumber, name, amount ) VALUES ( (select max(chargeId) from charges where orgId=?) as docNumber, "yossiz" as name, 100 as amount )
שים לב שאני כותב את השאילתא ישר לכאן ללא בדיקה אז יכול להיות טעויות
-
@yossiz
אני לא מכיר את postgres, אבל אני חושב שזה שווה ערך ל4 והוא עושה את 2 הפקודות ביחד, ולכאורה זה נועל את הDBאני מנסה קודם להבין את הצורך
כמו שכתב @dovid , בד"כ משתמשים במיספור אוטומטי, למה זה לא המצב אצלך?
אא"כ (שזה מה ששאני מריח) שזה טבלה כמו של חשבוניות / קבלות ואתה צריך לדעת את המספר האחרון שיצא
ועדיין, לא הבנתי למה אתה שומר את הנתון הזה סטטי בorgs, במקום להביא אותו מחושב באותו הרגע שתצטרך?
ז"א, במקום למשוך את זה מהטבלה, וללכת ולחזור, אתה פשוט מוסיף את הנתונים בinsert ואז מיד את המס' אתה מכניס עם sub queryINSERT INTO charge ( docNumber, name, amount ) VALUES ( (select max(chargeId) from charges where orgId=?) as docNumber, "yossiz" as name, 100 as amount )
שים לב שאני כותב את השאילתא ישר לכאן ללא בדיקה אז יכול להיות טעויות
@clickone נראה לי שאנחנו לא משדרים באותו תדר...
מספר ה-charge שמור בטבלת ה-charges לא עם ה-org.
כדי לקבל את המספר הבא אני מושך את המספר הגבוהה ביותר של ארגון זה ומוסיף עליו עוד אחד.ועדיין, לא הבנתי למה אתה שומר את הנתון הזה סטטי בorgs, במקום להביא אותו מחושב באותו הרגע שתצטרך?
ז"א, במקום למשוך את זה מהטבלה, וללכת ולחזור, אתה פשוט מוסיף את הנתונים בinsert ואז מיד את המס' אתה מכניס עם sub query
זה בדיוק מה שהתכוונתי בפתרון מס' 4. (וזה מה שחשבתי שאתה מתכוון בפוסט הראשון שלך, עכשיו קראתי שוב ואני רואה שלא הבנת את מבנה ה-DB נכון)
ואני עדיין רוצה להבין מה רמת הנעילה שזה עושה, האם זה נועל את כל הטבלה למשך הרצת הפקודה או שרק הוספת שורות עבור אותו ארגון. -
@clickone נראה לי שאנחנו לא משדרים באותו תדר...
מספר ה-charge שמור בטבלת ה-charges לא עם ה-org.
כדי לקבל את המספר הבא אני מושך את המספר הגבוהה ביותר של ארגון זה ומוסיף עליו עוד אחד.ועדיין, לא הבנתי למה אתה שומר את הנתון הזה סטטי בorgs, במקום להביא אותו מחושב באותו הרגע שתצטרך?
ז"א, במקום למשוך את זה מהטבלה, וללכת ולחזור, אתה פשוט מוסיף את הנתונים בinsert ואז מיד את המס' אתה מכניס עם sub query
זה בדיוק מה שהתכוונתי בפתרון מס' 4. (וזה מה שחשבתי שאתה מתכוון בפוסט הראשון שלך, עכשיו קראתי שוב ואני רואה שלא הבנת את מבנה ה-DB נכון)
ואני עדיין רוצה להבין מה רמת הנעילה שזה עושה, האם זה נועל את כל הטבלה למשך הרצת הפקודה או שרק הוספת שורות עבור אותו ארגון. -
@yossiz
ואין לך דרך לדעת מי קשור לאותו ORG?
אם כן, למה זה ככה? (מוזר לי שאתה מתבסס על הORG אבל אין עמודה שלו בטבלה השנייה)כנראה אני אכן מפספס כאן משהו מאד מהותי
תוכל להסביר יותר מה הקשרים בין הטבלאות?@clickone עזוב טבלאות אחרות, בטעות הזכרתי שיש טבלה של org.
יש רק טבלה אחת של charges עם 3 עמודות (שקשורות לנושא שלנו): id, orgId, chargeNumber
ה-chargenumber אמור להיות מספר ה-charge לפי הארגון. זה לא יכול להיות autoincrement כי אפשר שתיים עם אותו מספר כאשר הם משוייכים לארגון אחר -
@clickone עזוב טבלאות אחרות, בטעות הזכרתי שיש טבלה של org.
יש רק טבלה אחת של charges עם 3 עמודות (שקשורות לנושא שלנו): id, orgId, chargeNumber
ה-chargenumber אמור להיות מספר ה-charge לפי הארגון. זה לא יכול להיות autoincrement כי אפשר שתיים עם אותו מספר כאשר הם משוייכים לארגון אחר -
@yossiz אמר במניעת race condition ב-DB:
ה-chargenumber אמור להיות מספר ה-charge של הארגון.
שזה תמיד האחרון שנכנסם + 1?
או שזה מספר קבוע?
כעת אני כן רואה שאתה כותב שיש בטבלה orgid -
@clickone אמר במניעת race condition ב-DB:
שזה תמיד האחרון שנכנסם + 1?
בדיוק, אבל האחרון של ארגון זה, לא של כל הטבלה
@yossiz
אז מצויין, זה מה שכתבתי בדוגמא הזו
(תחליף את docNumber ב chargeNumber)INSERT INTO charge ( docNumber, name, amount ) VALUES ( (select max(chargeId) from charges where orgId=?) as docNumber, "yossiz" as name, 100 as amount )
ואתה התכוונת אליו למעלה, לא?
לכאורה בשיטה הזו הוא ינעל את השינויים כי זה נעשה בפעם אחת -
כאן
https://stackoverflow.com/questions/39498675/how-to-create-an-auto-increment-column-that-is-segmented-by-an-other-column
לכאורה הוא פתר את זה עם טריגר ולכאורה זה הנכון
(זה נראה MSSQL אבל לא התעמקתי) -
סליחה שאני שם כאן כ"כ הרבה מראי מקומות
מאד תמוה בעצם שאין דבר כזה מובנה כי זה דבר די נפוץ מה שאתה מנסה לעשות
אבל ראיתי שזה קיים בmysql אבל רק עם המנוע MyISAM
https://dba.stackexchange.com/questions/19019/mysql-get-next-unique-value-without-auto-increment/19020#19020
הרעיון הוא שאתה מגדיר שדה שהערך שלו יגדל ב1 על סמך שדה אחר בטבלהPRIMARY KEY (SiteID,SiteWorkorderNum)
והנה כל הדוגמא משם
mysql> CREATE DATABASE david; Query OK, 1 row affected (0.00 sec) mysql> USE david Database changed mysql> CREATE TABLE site_workorder_seq -> ( -> SiteID int not null, -> SiteWorkorderNum int not null auto_increment, -> PRIMARY KEY (SiteID,SiteWorkorderNum) -> ) ENGINE=MyISAM; Query OK, 0 rows affected (0.01 sec) mysql> INSERT INTO site_workorder_seq (SiteID) VALUES -> (1),(1),(2),(3),(3),(3),(3),(4),(4),(4), -> (5),(5),(4),(2),(2),(2); Query OK, 16 rows affected (0.00 sec) Records: 16 Duplicates: 0 Warnings: 0 mysql> SELECT * FROM site_workorder_seq; +--------+------------------+ | SiteID | SiteWorkorderNum | +--------+------------------+ | 1 | 1 | | 1 | 2 | | 2 | 1 | | 2 | 2 | | 2 | 3 | | 2 | 4 | | 3 | 1 | | 3 | 2 | | 3 | 3 | | 3 | 4 | | 4 | 1 | | 4 | 2 | | 4 | 3 | | 4 | 4 | | 5 | 1 | | 5 | 2 | +--------+------------------+ 16 rows in set (0.00 sec)
-
סליחה שאני שם כאן כ"כ הרבה מראי מקומות
מאד תמוה בעצם שאין דבר כזה מובנה כי זה דבר די נפוץ מה שאתה מנסה לעשות
אבל ראיתי שזה קיים בmysql אבל רק עם המנוע MyISAM
https://dba.stackexchange.com/questions/19019/mysql-get-next-unique-value-without-auto-increment/19020#19020
הרעיון הוא שאתה מגדיר שדה שהערך שלו יגדל ב1 על סמך שדה אחר בטבלהPRIMARY KEY (SiteID,SiteWorkorderNum)
והנה כל הדוגמא משם
mysql> CREATE DATABASE david; Query OK, 1 row affected (0.00 sec) mysql> USE david Database changed mysql> CREATE TABLE site_workorder_seq -> ( -> SiteID int not null, -> SiteWorkorderNum int not null auto_increment, -> PRIMARY KEY (SiteID,SiteWorkorderNum) -> ) ENGINE=MyISAM; Query OK, 0 rows affected (0.01 sec) mysql> INSERT INTO site_workorder_seq (SiteID) VALUES -> (1),(1),(2),(3),(3),(3),(3),(4),(4),(4), -> (5),(5),(4),(2),(2),(2); Query OK, 16 rows affected (0.00 sec) Records: 16 Duplicates: 0 Warnings: 0 mysql> SELECT * FROM site_workorder_seq; +--------+------------------+ | SiteID | SiteWorkorderNum | +--------+------------------+ | 1 | 1 | | 1 | 2 | | 2 | 1 | | 2 | 2 | | 2 | 3 | | 2 | 4 | | 3 | 1 | | 3 | 2 | | 3 | 3 | | 3 | 4 | | 4 | 1 | | 4 | 2 | | 4 | 3 | | 4 | 4 | | 5 | 1 | | 5 | 2 | +--------+------------------+ 16 rows in set (0.00 sec)
-
@yossiz
כעת מצאתי משהו דומה על mssql
שזה יצירת אינדקס מותאם אישית, שעושה בדיוק את הפעולה הזו
https://stackoverflow.com/questions/12152790/sql-server-autoincrement-varying-by-value-of-another-field
עכשיו נשאר לבדוק האם יש אפשרות כזו גם בפוסטגרס, אם כי אולי תעדיף כבר להשאר עם הפיתרון הרגיל, שבכל מקרה זה מה שממומש מאחרי הקלעים.... -
@clickone עזוב טבלאות אחרות, בטעות הזכרתי שיש טבלה של org.
יש רק טבלה אחת של charges עם 3 עמודות (שקשורות לנושא שלנו): id, orgId, chargeNumber
ה-chargenumber אמור להיות מספר ה-charge לפי הארגון. זה לא יכול להיות autoincrement כי אפשר שתיים עם אותו מספר כאשר הם משוייכים לארגון אחר@yossiz אמר במניעת race condition ב-DB:
@clickone עזוב טבלאות אחרות, בטעות הזכרתי שיש טבלה של org.
יש רק טבלה אחת של charges עם 3 עמודות (שקשורות לנושא שלנו): id, orgId, chargeNumber
ה-chargenumber אמור להיות מספר ה-charge לפי הארגון. זה לא יכול להיות autoincrement כי אפשר שתיים עם אותו מספר כאשר הם משוייכים לארגון אחרלא שיש לי פתרון. אבל הרי הפרנציפ נמצא בכל טבלה עם מספור ID אוטומטי, רק שכאן אתה צריך לממש את זה ידנית, אבל מסתמא אפשר להעתיק את המימוש הזה משם?
-
לא הבנתי למה לא נעילה ברמת הקוד?
גם את הנעילה הזאת אתה מקבל בחינם מהקוד..
ולגופו של ענין, הרי אתה לא ניגש לdb מחוץ לקוד, אז אני לא רואה סיבה למה לא להשתמש בזה.
אתה גם יכול לעשות נעילה בקוד פר ארגון, כך שלא יהיו לך הרבה התנגשויות.אני מממש דבר כזה בחנות אינטרנטית, כשיש צורך להגביל מוצר לפי כמות במלאי, ואני לא רוצה שיווצר מצב דומה למה שתיארת.
-
@yossiz אמר במניעת race condition ב-DB:
@clickone עזוב טבלאות אחרות, בטעות הזכרתי שיש טבלה של org.
יש רק טבלה אחת של charges עם 3 עמודות (שקשורות לנושא שלנו): id, orgId, chargeNumber
ה-chargenumber אמור להיות מספר ה-charge לפי הארגון. זה לא יכול להיות autoincrement כי אפשר שתיים עם אותו מספר כאשר הם משוייכים לארגון אחרלא שיש לי פתרון. אבל הרי הפרנציפ נמצא בכל טבלה עם מספור ID אוטומטי, רק שכאן אתה צריך לממש את זה ידנית, אבל מסתמא אפשר להעתיק את המימוש הזה משם?
-
יש לי טבלה של
charges
. כלcharge
משוייך ל-org
על ידי עמודתorgId
. אני אמור לתת לכלcharge
מספר. המספר הזה הוא לפי ארגון.
היום כאשר אני מכניס שורה חדשה אני עושה ככה:- מושך מה-DB את מספר ה-
charge
הגבוה ביותר של הארגון. - מוסיף עליו 1.
- שומר
charge
חדש עם מספר המעודכן.
עכשיו, ברור לכל שיש פה race condition אם מתווסף
charge
חדש לטבלה בין שלב 1 לשלב 3.יש לי כרגע 5 רעיונות איך לפתור את הבעיה. אשמח לשמוע את דעתכם. יצויין שמדובר ב-postgres, ואני משתמש ב-Sequelize ORM לכן יש עדיפות לפתרון שאפשר לממש ב-Sequelize.
- לעשות את התהליך בתוך טרנזקציה כאשר רמת הבידוד של הטרנזקציה הוא
SERIALIZABLE
. (רמת הבידוד ברירת המחדל לכאורה לא יפתור את הבעיה). - לעשות נעילה על הטבלה לפני ביצוע שלב 1.
- לעשות עבור כל ארגון
SEQUENCE
ולקבל את מספר ה-charge
הנוכחי מה-SEQUENCE
. - לעשות את כל התהליך בפקודת SQL אחד.
- לממש את הנעילה ברמת התוכנה בלי להזדקק לשירותי ה-DB.
אני לא אוהב את פתרון 1 כי זה מסבך את הקוד, כי זה לא מונע בעיית מקביליות אלא מכשיל את הטרנזקציה כאשר יש בעיה ואז אני צריך לטפל בזה בקוד ולנסות שוב.
לגבי פתרון 2, אני לא יודע עדיין על סוגי הנעילה שקיימים אבל ברור שאני רוצה נעילה הכי זולה. האם יש אפשרות לנעול רק הכנסת שורות חדשות עבור ארגון זו?
פתרון 3 דורש עשיית טריגר או הוספת קוד לייצרSEQUENCE
עובר כל ארגון חדש.
פתרון 4, האם זה זהה ל-1 או ל-2? או שיש הבדלים?
פתרון 5: אני לא אוהב את הרעיון של נעילה ברמת התוכנה כאשר אני מקבל את הנעילה בחינם מה-DB (אם רק הייתי יודע איך אני אמור לממש את זה) אבל אולי זה הפתרון הכי פשוט ונקי?אשמח לדעתכם.
@yossiz אמר במניעת race condition ב-DB:
- לעשות את כל התהליך בפקודת SQL אחד.
@dovid אמר במניעת race condition ב-DB:
אני יודע שבSQL SERVER נכון להיום אתה מוגן ב4
אתה בטוח בזה? יש לך מקור?
@clickone אמר במניעת race condition ב-DB:
לכאורה בשיטה הזו הוא ינעל את השינויים כי זה נעשה בפעם אחת
מכיון שהנושא מאוד סקרן אותי והתחלתי לחשוש שזה לא נכון עשיתי נסיון,
- יצרתי טבלה פשוטה:
create table test (id SERIAL, i INTEGER);
ואז הרצתי בלולאה במקביל בשתי סשנים את הפקודה הזאת:
INSERT INTO test (i) VALUES ( (SELECT max FROM (SELECT MAX(i) AS max, pg_sleep(1) FROM test) AS i) + 1 );
(הוספתי את ה-
pg_sleep
כדי למקסם את הסיכויים להתנגשות)התוצאה היתה שהיו מספר שורות עם אותו מספר, בניגוד לצפיה שכל שורה תהיה מספר אחד מעל לשורה הקודמת, כלומר שהיה race condition.
אח"כ שניתי את הפרמטרים של הקליינטים:SET default_transaction_isolation TO serializable;
והרצתי את זה שוב, וחלק מהפעולות נכשלו בגלל התנגשויות
לכאורה זה מלמד שרמת הבידוד בתוך פקודה בודדת שווה לרמת הבידוד של טרנזקציה - לא יותר. ואם כן אני לא מרוויח הרבה מפתרון 4 חוץ מזה שמשך הזמן שחשוף לבעיות התנגשות אולי יהיה קצת יותר קצר, אבל זה לא מגן לגמרי מהתנגשויות.
אני חושש שזה המצב בכל המנועים ואם כן זו נקודה חשובה להיות מודע לזה.
- מושך מה-DB את מספר ה-
-
@yossiz אמר במניעת race condition ב-DB:
- לעשות את כל התהליך בפקודת SQL אחד.
@dovid אמר במניעת race condition ב-DB:
אני יודע שבSQL SERVER נכון להיום אתה מוגן ב4
אתה בטוח בזה? יש לך מקור?
@clickone אמר במניעת race condition ב-DB:
לכאורה בשיטה הזו הוא ינעל את השינויים כי זה נעשה בפעם אחת
מכיון שהנושא מאוד סקרן אותי והתחלתי לחשוש שזה לא נכון עשיתי נסיון,
- יצרתי טבלה פשוטה:
create table test (id SERIAL, i INTEGER);
ואז הרצתי בלולאה במקביל בשתי סשנים את הפקודה הזאת:
INSERT INTO test (i) VALUES ( (SELECT max FROM (SELECT MAX(i) AS max, pg_sleep(1) FROM test) AS i) + 1 );
(הוספתי את ה-
pg_sleep
כדי למקסם את הסיכויים להתנגשות)התוצאה היתה שהיו מספר שורות עם אותו מספר, בניגוד לצפיה שכל שורה תהיה מספר אחד מעל לשורה הקודמת, כלומר שהיה race condition.
אח"כ שניתי את הפרמטרים של הקליינטים:SET default_transaction_isolation TO serializable;
והרצתי את זה שוב, וחלק מהפעולות נכשלו בגלל התנגשויות
לכאורה זה מלמד שרמת הבידוד בתוך פקודה בודדת שווה לרמת הבידוד של טרנזקציה - לא יותר. ואם כן אני לא מרוויח הרבה מפתרון 4 חוץ מזה שמשך הזמן שחשוף לבעיות התנגשות אולי יהיה קצת יותר קצר, אבל זה לא מגן לגמרי מהתנגשויות.
אני חושש שזה המצב בכל המנועים ואם כן זו נקודה חשובה להיות מודע לזה.
@yossiz אמר במניעת race condition ב-DB:
אתה בטוח בזה? יש לך מקור?
כעת בדקתי, זה לא נכון (נוצר לי כפילות..) אז אני לא מחפש מקור
מהלינקים שהבאתי כבר למעלה עולה שחובה לעשות נעילה ברמת SERIALIZABLE בדיוק כפי שכתבת.
יש פתרון פה לא הבנתי אותו, מריצים את זה באותו פקודה של ההכנסה?
סתם ככה אשמח לדעת מה הרקע של הצורך.